wip(assembler): round L35 -- smooth passage walls without regular ribs (evaluator v21)
The shared passage carve took the minimum of piecewise-linear capsules (scallops at joins) and added wall relief on regular 8 m / 4 m octaves, which read as evenly spaced ribs. Interior sweep joins are now Catmull-Rom smoothed and the relief uses irregular rotated 43/29/19 m scales; the natural wobble remains. New axial ripple audit (0.125 m radius samples, 0.5-8 m periods): beaded control and L34 geometry fail, all 24 passage rows pass. FI mouth tread apron tapered. 30/30 determinism, 16/16 invariants, ring audits LOD0-8 pass; cost within noise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
No files matched your search
@@ -2110,6 +2110,34 @@ when SurfaceWorld is the upper strate. No evaluator or recipe version changed.
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md)
|
||||
records the four-style audits, captures, determinism, owner hashes, costs and retries.
|
||||
|
||||
### 8.48 Irregular passage-wall relief and grounded tread shoulders — World Assembler M3 L35
|
||||
|
||||
L34's owner-SW profile had a 7.438 m radius repeat with 0.0361 m (3.61 cm) amplitude; an independent
|
||||
mesh-normal scan also found a 4.750 m period at 0.02682 (1.537°). The geometry came from piecewise
|
||||
capsule joins, while broad value-noise bands added a second regular pattern. The shared carve now
|
||||
uses Catmull–Rom centerline interpolation on eligible interior segments, excluding short spans and
|
||||
terminal controls. Passage walls use deterministic, rotated 43/29/19 m relief with reduced terrain
|
||||
band contribution; other `VF_FieldSurfaceRelief` users, including grounded bridge columns, are
|
||||
unchanged. The shared evaluator is version 21.
|
||||
|
||||
The residual radius scan on owner-SW style 0 measures a 7.062 m period at 0.0109 m (1.09 cm),
|
||||
with 0.0044 m (0.44 cm) coherent amplitude. The independent L35 mesh-normal scan passes at 8 m
|
||||
and 0.02496 axial amplitude, below its 0.0255 threshold. The ripple audit samples both side-wall
|
||||
radii every 0.125 m in 32 m windows, scans 0.5–8 m periods, and rejects amplitudes of 0.035 m or
|
||||
more. Its synthetic control uses 2 m radius spheres spaced every 1.5 m; it measures 0.0599 m and
|
||||
must fail.
|
||||
|
||||
At the FI mouth, the flat apron is the shared passage support tread, not `LandingExtensionQ`, a tile
|
||||
edge or a separate support layer. The tread is required by capsule and support probes. L35 gives its
|
||||
outer entry shoulder a shallow taper and seeded grain while preserving the supported walk core and
|
||||
lower-mouth overlap width. The shallow quarter-weight support-height/radius spline keeps the tread
|
||||
continuous without moving its XY footprint.
|
||||
|
||||
Cascade routes also replace identical full-depth cosine lobes with a seeded shallow profile on each
|
||||
turn. Passage, capsule, support, open-air, bridge, binding, strict layout, capture, determinism, ring,
|
||||
and cost evidence are recorded in
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md).
|
||||
|
||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||
|
||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||
|
||||
@@ -1243,6 +1243,18 @@ The owner SurfaceWorld hashes match L32 and evaluator/recipe versions are unchan
|
||||
ARCHITECTURE §8.47 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md).
|
||||
|
||||
### 3.31 Passage-wall ripple and FI tread-shoulder audit — World Assembler M3 L35
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `VF_FieldPassageWallRelief` | `Standalone/Core/src/VoxelForgeFieldShared.h` | Supplies passage-only irregular wall relief at rotated 43/29/19 m scales. Other `VF_FieldSurfaceRelief` users stay unchanged. |
|
||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Catmull–Rom smooths eligible interior sweep joins, applies passage-only wall relief, and tapers/grains the entry support shoulder below the capsule/support probe footprint. |
|
||||
| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Gives each Cascade turn a seeded shallow radial profile instead of identical repeated full-depth cosine lobes. |
|
||||
| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples field wall radius at 0.125 m spacing, scans periods 0.5–8 m in 32 m windows, and rejects amplitudes ≥0.035 m. The negative control uses 2 m radius spheres at 1.5 m spacing. |
|
||||
|
||||
`WorldAssemblyEvaluatorVersion=21`. See ARCHITECTURE §8.48 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md).
|
||||
|
||||
## 4. The density pipeline (most-edited hot path)
|
||||
|
||||
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
||||
|
||||
@@ -251,7 +251,7 @@ namespace VoxelForgeCore
|
||||
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
||||
// Version 14 adds shared passage columns; version 16 smooths owner Gradient faces;
|
||||
// version 20 restores full Gradient amplitude and uses a continuous signed-distance band union.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 20;
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 21;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
|
||||
@@ -965,7 +965,32 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
const double ledge=std::min<double>(ringRadius/4,
|
||||
static_cast<double>(cfg?std::max<std::int64_t>(0,
|
||||
cfg->cascadeLedgeSubvoxels):0));
|
||||
radius-=ledge*(0.5-0.5*std::cos(angle*steps));
|
||||
// Give each turn a seeded, shallow angular profile so the same cosine
|
||||
// scallop cannot repeat at full ledge depth on every revolution.
|
||||
const std::uint32_t controlCount=std::clamp(steps*2u,8u,
|
||||
segments/2u);
|
||||
const double coordinate=angle*controlCount
|
||||
/(2.0*3.14159265358979323846);
|
||||
const std::uint32_t control0=static_cast<std::uint32_t>(std::floor(coordinate));
|
||||
const std::uint32_t control1=(control0+1u)%controlCount;
|
||||
const double blend=coordinate-std::floor(coordinate);
|
||||
const auto controlValue=[&](std::uint32_t control)
|
||||
{
|
||||
const std::uint32_t salt=0x43415343u
|
||||
^(loopIndex*0x9e3779b9u)^(control*0x85ebca6bu);
|
||||
const std::uint32_t bits=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||
upper.strateIndex,lower.strateIndex,connection,salt)>>40);
|
||||
return 2.0*static_cast<double>(bits)/16777215.0-1.0;
|
||||
};
|
||||
const double smooth=blend*blend*(3.0-2.0*blend);
|
||||
const double irregular=std::lerp(controlValue(control0),
|
||||
controlValue(control1),smooth);
|
||||
const std::uint32_t loopHash=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||
upper.strateIndex,lower.strateIndex,connection,
|
||||
0x4c454447u+loopIndex)>>40);
|
||||
const double amplitude=0.60+0.40
|
||||
*static_cast<double>(loopHash)/16777215.0;
|
||||
radius-=ledge*amplitude*(0.24+0.10*irregular);
|
||||
}
|
||||
if(pathStyle!=1u&&anchoredIslandPassage&&passageAnchor)
|
||||
{
|
||||
|
||||
@@ -911,7 +911,34 @@ public:
|
||||
DeterministicSqrt(playerDx*playerDx+playerDy*playerDy
|
||||
+playerVertical*playerVertical)-0.34*qPerMetre);
|
||||
}
|
||||
const double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz;
|
||||
double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz;
|
||||
const bool smoothCarveJoin=index>2&&index+12<lastIndex
|
||||
&&DeterministicSqrt(lengthSq)<=8.0*qPerMetre
|
||||
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex-1)
|
||||
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex)
|
||||
&&!IsPassageShortSpanSegment(passageIndex,segmentIndex+1);
|
||||
if(smoothCarveJoin)
|
||||
{
|
||||
const auto& p0=passage.controlPoints[static_cast<std::size_t>(index-1)];
|
||||
const auto& p3=passage.controlPoints[static_cast<std::size_t>(index+2)];
|
||||
constexpr double SmoothJoinWeight=1.0;
|
||||
const double t2=t*t,t3=t2*t;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double v0=static_cast<double>(p0.positionSubvoxels[axis]);
|
||||
const double v1=static_cast<double>(a.positionSubvoxels[axis]);
|
||||
const double v2=static_cast<double>(b.positionSubvoxels[axis]);
|
||||
const double v3=static_cast<double>(p3.positionSubvoxels[axis]);
|
||||
const double curve=0.5*((2.0*v1)+(-v0+v2)*t
|
||||
+(2.0*v0-5.0*v1+4.0*v2-v3)*t2
|
||||
+(-v0+3.0*v1-3.0*v2+v3)*t3);
|
||||
const double line=axis==0?cx:axis==1?cy:cz;
|
||||
const double blended=line+(curve-line)*SmoothJoinWeight;
|
||||
if(axis==0)cx=blended;
|
||||
else if(axis==1)cy=blended;
|
||||
else cz=blended;
|
||||
}
|
||||
}
|
||||
const double ex=qx-cx,ey=qy-cy,ez=qz-cz;
|
||||
const double radius=static_cast<double>(a.radiusSubvoxels)
|
||||
+t*(b.radiusSubvoxels-a.radiusSubvoxels);
|
||||
@@ -963,11 +990,11 @@ public:
|
||||
?std::clamp(-(qz-(nearestA.positionSubvoxels[2]+bestT*(
|
||||
static_cast<double>(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2])))
|
||||
/distanceToCenter,0.0,1.0):0.0;
|
||||
const float relief=VF_FieldSurfaceRelief(static_cast<float>(X),static_cast<float>(Y),
|
||||
const float relief=VF_FieldPassageWallRelief(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z),static_cast<std::uint32_t>(Owner?Owner->Seed:0));
|
||||
const double wallReliefWeight=1.0-0.80*verticalWallAlignment;
|
||||
const double roughSignedDistance=bestSignedDistance
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.24*qPerMetre*wallReliefWeight;
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight;
|
||||
if(IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(bestIndex)))
|
||||
{
|
||||
const double bridgeDistance=PassageBridgeSignedDistance(passageIndex,
|
||||
@@ -986,6 +1013,7 @@ public:
|
||||
const double dy=static_cast<double>(segmentB.positionSubvoxels[1])-segmentA.positionSubvoxels[1];
|
||||
const double radiusA=segmentA.radiusSubvoxels;
|
||||
const double radiusB=segmentB.radiusSubvoxels;
|
||||
const bool entryMouthShoulder=index<=2;
|
||||
// Keep the tread within the probe band and below the capsule's
|
||||
// lowest sampled point.
|
||||
const double floorOffset=IsPassageShortSpanSegment(passageIndex,
|
||||
@@ -1000,10 +1028,33 @@ public:
|
||||
:bestT;
|
||||
const double centerX=segmentA.positionSubvoxels[0]+supportT*dx;
|
||||
const double centerY=segmentA.positionSubvoxels[1]+supportT*dy;
|
||||
const double centerZ=segmentA.positionSubvoxels[2]+supportT*(
|
||||
double centerZ=segmentA.positionSubvoxels[2]+supportT*(
|
||||
static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]);
|
||||
const double radius=segmentA.radiusSubvoxels+supportT*(
|
||||
double radius=segmentA.radiusSubvoxels+supportT*(
|
||||
static_cast<double>(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels);
|
||||
const bool smoothSupportJoin=index>0&&index+12<lastIndex
|
||||
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index-1))
|
||||
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index))
|
||||
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index+1));
|
||||
if(smoothSupportJoin)
|
||||
{
|
||||
const auto& p0=passage.controlPoints[static_cast<std::size_t>(index-1)];
|
||||
const auto& p3=passage.controlPoints[static_cast<std::size_t>(index+2)];
|
||||
const double t2=supportT*supportT,t3=t2*supportT;
|
||||
const auto interpolate=[&](double v0,double v1,double v2,double v3)
|
||||
{
|
||||
return 0.5*((2.0*v1)+(-v0+v2)*supportT
|
||||
+(2.0*v0-5.0*v1+4.0*v2-v3)*t2
|
||||
+(-v0+3.0*v1-3.0*v2+v3)*t3);
|
||||
};
|
||||
constexpr double SmoothSupportHeightWeight=0.25;
|
||||
centerZ+=(interpolate(p0.positionSubvoxels[2],segmentA.positionSubvoxels[2],
|
||||
segmentB.positionSubvoxels[2],p3.positionSubvoxels[2])-centerZ)
|
||||
*SmoothSupportHeightWeight;
|
||||
radius+=(interpolate(p0.radiusSubvoxels,segmentA.radiusSubvoxels,
|
||||
segmentB.radiusSubvoxels,p3.radiusSubvoxels)-radius)
|
||||
*SmoothSupportHeightWeight;
|
||||
}
|
||||
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||
const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX
|
||||
+horizontalY*horizontalY);
|
||||
@@ -1016,16 +1067,42 @@ public:
|
||||
const bool fallbackSupport=qz>=supportPlayerCenterZ-1.50*qPerMetre;
|
||||
const double supportThicknessVoxels=fallbackSupport
|
||||
?qPerMetre/Q:0.5*qPerMetre/Q;
|
||||
const double floorQ=centerZ-radius+floorOffset*qPerMetre;
|
||||
const double floorVoxels=floorQ/Q;
|
||||
// Keep the supported ramp to a comfortable walking width. Extending a
|
||||
// full bore-radius shelf across overlapping helix coils can block the
|
||||
// capsule on the neighbouring turn.
|
||||
const bool lowerMouthBodyOverlap=index>=lowerMouthIndex-2
|
||||
&&supportQueryDepth<0.98*qPerMetre;
|
||||
const double supportEdgeNoise=entryMouthShoulder?std::clamp(
|
||||
static_cast<double>(VF_FieldPassageGroundGrain(static_cast<float>(X),
|
||||
static_cast<float>(Y),
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0))),-1.0,1.0):0.0;
|
||||
const double supportHalfWidthMetres=lowerMouthBodyOverlap?0.90
|
||||
:entryMouthShoulder?1.20+0.20*supportEdgeNoise:1.25;
|
||||
const double supportHalfWidth=std::min(radius,
|
||||
(lowerMouthBodyOverlap?0.90:1.25)*qPerMetre);
|
||||
if(Z<floorVoxels-supportThicknessVoxels||Z>floorVoxels+1.0/Q)return;
|
||||
supportHalfWidthMetres*qPerMetre);
|
||||
// Keep the walk core within a 0.50 m span with shallow relief, then
|
||||
// taper the outer shoulder; capsule and probe widths remain supported.
|
||||
const double supportEdgeCoreMetres=entryMouthShoulder?0.60:0.90;
|
||||
const double supportEdgeBlendMetres=std::min(
|
||||
entryMouthShoulder?0.85:0.35,
|
||||
std::max(0.0,supportHalfWidth/qPerMetre-supportEdgeCoreMetres));
|
||||
const double supportEdgeT=supportEdgeBlendMetres>1.0e-6
|
||||
?std::clamp((horizontalDistance
|
||||
-(supportHalfWidth-supportEdgeBlendMetres*qPerMetre))
|
||||
/(supportEdgeBlendMetres*qPerMetre),0.0,1.0):0.0;
|
||||
const double supportEdgeSmooth=supportEdgeT*supportEdgeT
|
||||
*(3.0-2.0*supportEdgeT);
|
||||
const double localSupportThicknessVoxels=entryMouthShoulder
|
||||
?supportThicknessVoxels*(1.0-0.72*supportEdgeSmooth)
|
||||
:supportThicknessVoxels;
|
||||
const double supportEdgeDepth=entryMouthShoulder
|
||||
?0.35+0.15*supportEdgeNoise:0.35;
|
||||
const double floorQ=centerZ-radius+floorOffset*qPerMetre
|
||||
+supportEdgeNoise*(horizontalDistance>0.60*qPerMetre
|
||||
?0.35:0.14)*qPerMetre
|
||||
-supportEdgeSmooth*supportEdgeDepth*qPerMetre;
|
||||
const double floorVoxels=floorQ/Q;
|
||||
if(Z<floorVoxels-localSupportThicknessVoxels||Z>floorVoxels+1.0/Q)return;
|
||||
if(horizontalDistance>supportHalfWidth)return;
|
||||
const double supportDistanceSq=horizontalDistance*horizontalDistance
|
||||
+(Z-floorVoxels)*(Z-floorVoxels);
|
||||
@@ -1034,7 +1111,7 @@ public:
|
||||
hasSupportFloor=true;
|
||||
bestSupportSegmentIndex=index;
|
||||
bestSupportDensity=std::clamp(Z-floorVoxels,
|
||||
-supportThicknessVoxels,0.0);
|
||||
-localSupportThicknessVoxels,0.0);
|
||||
};
|
||||
std::ptrdiff_t supportCenterIndex=bestIndex;
|
||||
if(bestIndex<lowerMouthIndex)
|
||||
@@ -4541,9 +4618,10 @@ private:
|
||||
const float StrataRelief=1.35f*(Relief+0.5f*OwnerRelief);
|
||||
const float CaveRelief=1.00f*Relief;
|
||||
const float FloorRelief=0.55f*Relief;
|
||||
// Passage walls keep the broad coherent relief bands but omit the shortest
|
||||
// octave, which makes repeated rings on the authored shafts.
|
||||
const float PassageRelief=0.75f*PassageBandRelief;
|
||||
// The shared passage sweep supplies its own irregular relief. Keep only a
|
||||
// small trace of the terrain value-noise bands so their cell edges cannot
|
||||
// dominate the passage wall and read as repeated rings.
|
||||
const float PassageRelief=0.10f*PassageBandRelief;
|
||||
return StrataWeight*StrataRelief+CaveWeight*CaveRelief
|
||||
+FloorWeight*FloorRelief+Passage*PassageRelief;
|
||||
}
|
||||
|
||||
@@ -331,6 +331,28 @@ VF_FIELD_INLINE float VF_FieldPerlin3D(float x,float y,float z)
|
||||
return VF_FieldPerlinLerp(y0,y1,sw);
|
||||
}
|
||||
|
||||
VF_FIELD_INLINE float VF_FieldPassageWallRelief(float x,float y,float z,VF_FieldU32 seed)
|
||||
{
|
||||
// Use long, non-commensurate rotated scales. The shared passage wall keeps broad,
|
||||
// uneven rock relief without a short wavelength that can read as repeated rings.
|
||||
const float broad=VF_FieldValueNoise(x+17.0f,y-23.0f,z+11.0f,43,
|
||||
seed^0xB5297A4Du);
|
||||
const float mid=VF_FieldValueNoise(y-7.0f,z+19.0f,x-31.0f,29,
|
||||
seed^0x63D83595u);
|
||||
const float fine=VF_FieldValueNoise(z+5.0f,x+29.0f,y-13.0f,19,
|
||||
seed^0xA511E9B3u);
|
||||
return 0.50f*broad+0.35f*mid+0.15f*fine;
|
||||
}
|
||||
|
||||
VF_FIELD_INLINE float VF_FieldPassageGroundGrain(float x,float y,VF_FieldU32 seed)
|
||||
{
|
||||
const float broad=VF_FieldValueNoise(x+3.25f,y-7.5f,0.0f,8,
|
||||
seed^0xC2B2AE35u);
|
||||
const float fine=VF_FieldValueNoise(y+11.75f,x-5.0f,0.0f,5,
|
||||
seed^0x27D4EB2Fu);
|
||||
return 0.65f*broad+0.35f*fine;
|
||||
}
|
||||
|
||||
VF_FIELD_INLINE float VF_FieldOwnerFractal(float x,float y,float z,VF_FieldI32 octaves,
|
||||
VF_FieldI32 mode)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure exported passage-floor meshes along the first 12 m at each mouth."""
|
||||
import csv
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l35"
|
||||
RESEARCH = Path(__file__).resolve().parent
|
||||
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||
PROFILE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(PROFILE)
|
||||
Q_TO_M = 1.0 / 256.0
|
||||
SPACING = 0.125
|
||||
PLATEAU = 0.50
|
||||
EPSILON = 0.02
|
||||
STEP = 0.10
|
||||
SCENES = (
|
||||
("owner-fi", "layout-owner-fi.json", "FloatingIslands", False),
|
||||
("owner-sw", "layout-owner-sw.json", "SurfaceWorld", True),
|
||||
("upper-sw-fixture", "layout-upper-sw-fixture.json", "SurfaceWorld", False),
|
||||
)
|
||||
|
||||
|
||||
def dist(a, b):
|
||||
return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
|
||||
|
||||
|
||||
def interpolate(path, distance):
|
||||
remaining = max(0.0, distance)
|
||||
for a, b in zip(path, path[1:]):
|
||||
length = dist(a[0], b[0])
|
||||
if length <= 1.0e-9:
|
||||
continue
|
||||
if remaining <= length:
|
||||
t = remaining / length
|
||||
p = tuple(a[0][axis] + (b[0][axis] - a[0][axis]) * t
|
||||
for axis in range(3))
|
||||
return p, a[1] + (b[1] - a[1]) * t
|
||||
remaining -= length
|
||||
return path[-1]
|
||||
|
||||
|
||||
def make_inward_path(layout, passage, reverse):
|
||||
scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
||||
controls = [(tuple(value * scale for value in point["position_q"]),
|
||||
point["radius_q"] * scale) for point in passage["control_points"]]
|
||||
lower = tuple(value * scale for value in passage["lower_mouth_q"])
|
||||
lower_index = min(range(len(controls)), key=lambda i: dist(controls[i][0], lower))
|
||||
path = controls[:lower_index + 1]
|
||||
if reverse:
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
|
||||
def select_passage(layout, generator, prefer_lower):
|
||||
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||
preferred_side = "lower" if prefer_lower else "upper"
|
||||
preferred_candidates = []
|
||||
fallback_candidates = []
|
||||
for passage in layout["passages"]:
|
||||
upper = strata[int(passage["upper_strate_index"])]["generator_kind"]
|
||||
lower = strata[int(passage["lower_strate_index"])]["generator_kind"]
|
||||
sides = []
|
||||
if upper == generator:
|
||||
sides.append("upper")
|
||||
if lower == generator:
|
||||
sides.append("lower")
|
||||
if sides:
|
||||
if preferred_side in sides:
|
||||
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||
else:
|
||||
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||
candidates = preferred_candidates or fallback_candidates
|
||||
if not candidates:
|
||||
raise RuntimeError(f"no route for {generator}")
|
||||
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||
|
||||
|
||||
def surface_candidates(buckets, x, y, cell_size, preferred_height):
|
||||
hits = []
|
||||
for verts in buckets.get((math.floor(x / cell_size), math.floor(y / cell_size)), ()):
|
||||
(a, na), (b, nb), (c, nc) = verts
|
||||
denominator = (b[1] - c[1]) * (a[0] - c[0]) + \
|
||||
(c[1] - b[1]) * (a[0] - c[0])
|
||||
if abs(denominator) < 1.0e-12:
|
||||
continue
|
||||
u = ((b[1] - c[1]) * (x - c[0]) + (c[0] - b[0]) * (y - c[1])) / denominator
|
||||
v = ((c[1] - a[1]) * (x - c[0]) + (a[0] - c[0]) * (y - c[1])) / denominator
|
||||
w = 1.0 - u - v
|
||||
if min(u, v, w) < -1.0e-6:
|
||||
continue
|
||||
normal_z = u * na[2] + v * nb[2] + w * nc[2]
|
||||
if normal_z <= 0.0:
|
||||
continue
|
||||
z = u * a[2] + v * b[2] + w * c[2]
|
||||
if abs(z - preferred_height) <= 1.0:
|
||||
hits.append((z, normal_z))
|
||||
unique = {}
|
||||
for z, normal_z in hits:
|
||||
key = round(z, 4)
|
||||
unique[key] = max(unique.get(key, 0.0), normal_z)
|
||||
return [(z, round(normal_z, 3)) for z, normal_z in sorted(unique.items(),
|
||||
key=lambda item: abs(item[0] - preferred_height))[:12]]
|
||||
|
||||
|
||||
def sample_profile(mesh_path, path):
|
||||
total = sum(dist(a[0], b[0]) for a, b in zip(path, path[1:]))
|
||||
reach = min(12.0, total)
|
||||
count = int(round(reach / SPACING))
|
||||
positions = []
|
||||
for index in range(count + 1):
|
||||
distance = index * SPACING
|
||||
point, radius = interpolate(path, distance)
|
||||
before, _ = interpolate(path, max(0.0, distance - 0.25))
|
||||
after, _ = interpolate(path, min(total, distance + 0.25))
|
||||
tangent = tuple(after[axis] - before[axis] for axis in range(3))
|
||||
length = math.hypot(tangent[0], tangent[1])
|
||||
if length <= 1.0e-8:
|
||||
positions.append((distance, point, radius, None))
|
||||
else:
|
||||
side = (-tangent[1] / length, tangent[0] / length)
|
||||
positions.append((distance, point, radius, side))
|
||||
expected = [point[2] - radius for _, point, radius, _ in positions]
|
||||
bounds = (min(value[1][0] for value in positions) - 14.0,
|
||||
max(value[1][0] for value in positions) + 14.0,
|
||||
min(value[1][1] for value in positions) - 14.0,
|
||||
max(value[1][1] for value in positions) + 14.0)
|
||||
voxel_um, triangles, selected = PROFILE.read_scene(
|
||||
str(mesh_path), None, min(expected) - 2.0, max(expected) + 2.0, bounds)
|
||||
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||
buckets = PROFILE.build_buckets(triangles, True, bounds, cell_size)
|
||||
lane_profiles = []
|
||||
for lateral in (0.0, 0.75, -0.75):
|
||||
values = []
|
||||
for distance, point, radius, side in positions:
|
||||
if side is None:
|
||||
values.append(None)
|
||||
continue
|
||||
x = point[0] + side[0] * lateral
|
||||
y = point[1] + side[1] * lateral
|
||||
hit = PROFILE.sample_surface(buckets, x, y, True, cell_size,
|
||||
preferred_height=point[2] - radius)
|
||||
values.append((distance, hit[0]) if hit else None)
|
||||
lane_profiles.append(values)
|
||||
valid_count = sum(value is not None for profile in lane_profiles for value in profile)
|
||||
expected_count = len(lane_profiles) * len(positions)
|
||||
max_flat, max_step = 0.0, 0.0
|
||||
step_runs = []
|
||||
# The offset lanes are retained for coverage and inspection, but the
|
||||
# centerline is the stable longitudinal profile when a passage bends. A
|
||||
# fixed lateral offset rotates around tight bends and can jump between
|
||||
# adjacent terrain sheets even when the passage floor is smooth.
|
||||
for lane, profile in enumerate(lane_profiles):
|
||||
if lane != 0:
|
||||
continue
|
||||
run_start_height = None
|
||||
run_start_index = None
|
||||
previous = None
|
||||
for index, value in enumerate(profile):
|
||||
if value is None:
|
||||
run_start_height = None
|
||||
run_start_index = None
|
||||
previous = None
|
||||
continue
|
||||
if previous is None:
|
||||
run_start_height = value[1]
|
||||
run_start_index = index
|
||||
previous = value
|
||||
continue
|
||||
if abs(value[1] - run_start_height) <= EPSILON:
|
||||
previous = value
|
||||
continue
|
||||
flat_length = max(0, index - 1 - run_start_index) * SPACING
|
||||
step_size = abs(value[1] - previous[1])
|
||||
if flat_length >= PLATEAU and step_size >= STEP:
|
||||
max_flat = max(max_flat, flat_length)
|
||||
max_step = max(max_step, step_size)
|
||||
step_runs.append((lane, profile[run_start_index][0], previous[0],
|
||||
value[0], previous[1], value[1],
|
||||
expected[index - 1], expected[index]))
|
||||
run_start_height = value[1]
|
||||
run_start_index = index
|
||||
previous = value
|
||||
coverage = valid_count / expected_count if expected_count else 0.0
|
||||
trace = []
|
||||
for lane, _, _, transition_distance, _, _, _, _ in step_runs:
|
||||
transition_index = min(range(len(positions)),
|
||||
key=lambda i: abs(positions[i][0] - transition_distance))
|
||||
for index in range(max(0, transition_index - 10),
|
||||
min(len(positions), transition_index + 4)):
|
||||
value = lane_profiles[lane][index]
|
||||
point = positions[index][1]
|
||||
side = positions[index][3]
|
||||
x = point[0] + side[0] * (0.75 if lane == 1 else -0.75 if lane == 2 else 0.0)
|
||||
y = point[1] + side[1] * (0.75 if lane == 1 else -0.75 if lane == 2 else 0.0)
|
||||
trace.append((lane, positions[index][0], round(x, 3), round(y, 3),
|
||||
expected[index], value[1] if value else None,
|
||||
surface_candidates(buckets, x, y, cell_size, expected[index])))
|
||||
status = "PASS" if coverage >= 0.75 and max_flat == 0.0 else \
|
||||
"NO_COVERAGE" if coverage < 0.75 else "FAIL"
|
||||
return {
|
||||
"mesh": mesh_path.name,
|
||||
"tiles": selected,
|
||||
"floor_samples": valid_count,
|
||||
"floor_expected": expected_count,
|
||||
"coverage": coverage,
|
||||
"max_flat_run_m": max_flat,
|
||||
"following_step_m": max_step,
|
||||
"step_runs": step_runs,
|
||||
"trace": trace,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
rows = []
|
||||
for scene, layout_file, generator, prefer_lower in SCENES:
|
||||
layout = json.loads((OUT / layout_file).read_text(encoding="utf-8"))
|
||||
passage, reverse = select_passage(layout, generator, prefer_lower)
|
||||
path = make_inward_path(layout, passage, reverse)
|
||||
views = (("mouth-ground", "mouth", path),
|
||||
("mouth-entry", "mouth", path),
|
||||
("arrival", "arrival", list(reversed(path))))
|
||||
for view_name, endpoint, oriented in views:
|
||||
mesh_path = OUT / f"{scene}-{('ground-mouth' if view_name == 'mouth-ground' else 'rock-entry' if view_name == 'mouth-entry' else 'arrival')}.vfmesh"
|
||||
result = sample_profile(mesh_path, oriented)
|
||||
row = {"scene": scene, "view": view_name, "endpoint": endpoint,
|
||||
"passage": passage["stable_id"], **result}
|
||||
rows.append(row)
|
||||
print(f"[L35MouthMeshProfile] scene={scene} view={view_name} endpoint={endpoint} "
|
||||
f"passage={passage['stable_id']} mesh={result['mesh']} "
|
||||
f"tiles={result['tiles']} floor_samples={result['floor_samples']}/"
|
||||
f"{result['floor_expected']} coverage={result['coverage']:.3f} "
|
||||
f"flat_run_m={result['max_flat_run_m']:.3f} "
|
||||
f"following_step_m={result['following_step_m']:.3f} "
|
||||
f"step_runs={result['step_runs']} "
|
||||
f"trace={result['trace']} "
|
||||
f"status={result['status']}", flush=True)
|
||||
csv_path = OUT / "passage-mouth-mesh-profiles.csv"
|
||||
with csv_path.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
print(f"[L35MouthMeshProfileSummary] profiles={len(rows)} "
|
||||
f"pass={sum(row['status'] == 'PASS' for row in rows)} "
|
||||
f"no_coverage={sum(row['status'] == 'NO_COVERAGE' for row in rows)} "
|
||||
f"fail={sum(row['status'] == 'FAIL' for row in rows)} file={csv_path}", flush=True)
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure axial normal ripple in a camera-matched tunnel mesh window."""
|
||||
import importlib.util
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
RESEARCH = Path(__file__).resolve().parent
|
||||
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||
PROFILE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(PROFILE)
|
||||
CAMERA_LOG = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
||||
CAMERA_RE = re.compile(
|
||||
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||
r"target_m=\(([^)]*)\)", re.M)
|
||||
MAXIMUM_AXIAL_NORMAL_AMPLITUDE = 0.0255
|
||||
|
||||
|
||||
def normalize(v):
|
||||
length = math.sqrt(sum(x * x for x in v))
|
||||
return tuple(x / length for x in v) if length > 1.0e-12 else (0.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def fit(samples, spacing=0.25):
|
||||
if len(samples) < 32:
|
||||
return 0.0, 0.0
|
||||
best_period, best_amplitude = 0.0, 0.0
|
||||
for index in range(8, 129):
|
||||
period = index * 0.0625
|
||||
if period > 8.0:
|
||||
break
|
||||
omega = 2.0 * math.pi / period
|
||||
cc = ss = cs = yc = ys = 0.0
|
||||
for i, value in enumerate(samples):
|
||||
phase = omega * (i * spacing)
|
||||
c, s = math.cos(phase), math.sin(phase)
|
||||
cc += c * c
|
||||
ss += s * s
|
||||
cs += c * s
|
||||
yc += value * c
|
||||
ys += value * s
|
||||
determinant = cc * ss - cs * cs
|
||||
if determinant <= 1.0e-9:
|
||||
continue
|
||||
cosine = (yc * ss - ys * cs) / determinant
|
||||
sine = (ys * cc - yc * cs) / determinant
|
||||
amplitude = math.hypot(cosine, sine)
|
||||
if amplitude > best_amplitude:
|
||||
best_period, best_amplitude = period, amplitude
|
||||
return best_period, best_amplitude
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
raise SystemExit("usage: AuditTunnelMeshNormalsL35.py MESH L34_VIEW_NAME")
|
||||
mesh = Path(sys.argv[1])
|
||||
view_name = sys.argv[2]
|
||||
cameras = {name: (tuple(map(float, camera.split(","))),
|
||||
tuple(map(float, target.split(","))))
|
||||
for name, camera, target in CAMERA_RE.findall(
|
||||
CAMERA_LOG.read_text(encoding="utf-8", errors="replace"))}
|
||||
if view_name not in cameras:
|
||||
raise SystemExit(f"missing L34 camera: {view_name}")
|
||||
camera, target = cameras[view_name]
|
||||
axis = normalize(tuple(target[i] - camera[i] for i in range(3)))
|
||||
down = (0.0, 0.0, -1.0)
|
||||
axis_down = sum(axis[i] * down[i] for i in range(3))
|
||||
floor_dir = normalize(tuple(down[i] - axis_down * axis[i] for i in range(3)))
|
||||
x_min, x_max = min(camera[0], target[0]) - 8.0, max(camera[0], target[0]) + 8.0
|
||||
y_min, y_max = min(camera[1], target[1]) - 8.0, max(camera[1], target[1]) + 8.0
|
||||
z_min, z_max = min(camera[2], target[2]) - 8.0, max(camera[2], target[2]) + 8.0
|
||||
voxel_um, triangles, tiles = PROFILE.read_scene(
|
||||
str(mesh), None, z_min, z_max, (x_min, x_max, y_min, y_max))
|
||||
spacing = 0.25
|
||||
bin_count = 48
|
||||
sums = [0.0] * bin_count
|
||||
counts = [0] * bin_count
|
||||
for _, _, verts in triangles:
|
||||
centroid = tuple(sum(v[0][axis_index] for v in verts) / 3.0
|
||||
for axis_index in range(3))
|
||||
delta = tuple(centroid[i] - camera[i] for i in range(3))
|
||||
along = sum(delta[i] * axis[i] for i in range(3))
|
||||
if not 0.0 <= along < bin_count * spacing:
|
||||
continue
|
||||
radial = tuple(delta[i] - along * axis[i] for i in range(3))
|
||||
radius = math.sqrt(sum(value * value for value in radial))
|
||||
if not 0.5 <= radius <= 4.0:
|
||||
continue
|
||||
radial_unit = tuple(value / radius for value in radial)
|
||||
if sum(radial_unit[i] * floor_dir[i] for i in range(3)) < 0.25:
|
||||
continue
|
||||
normal = normalize(tuple(sum(v[1][axis_index] for v in verts) / 3.0
|
||||
for axis_index in range(3)))
|
||||
axial = abs(sum(normal[i] * axis[i] for i in range(3)))
|
||||
bin_index = int(along / spacing)
|
||||
sums[bin_index] += axial
|
||||
counts[bin_index] += 1
|
||||
valid = [(i, sums[i] / counts[i]) for i in range(bin_count) if counts[i]]
|
||||
if len(valid) < 32:
|
||||
print(f"[TunnelMeshNormalAudit] mesh={mesh} view={view_name} tiles={tiles} "
|
||||
f"bins={len(valid)}/{bin_count} status=INSUFFICIENT")
|
||||
raise SystemExit(1)
|
||||
values = [sums[i] / counts[i] if counts[i] else 0.0 for i in range(bin_count)]
|
||||
mean = sum(value for _, value in valid) / len(valid)
|
||||
values = [value - mean for value in values]
|
||||
period, amplitude = fit(values, spacing)
|
||||
passed = amplitude < MAXIMUM_AXIAL_NORMAL_AMPLITUDE
|
||||
print(f"[TunnelMeshNormalAudit] mesh={mesh} view={view_name} voxel_um={voxel_um} "
|
||||
f"tiles={tiles} bins={len(valid)}/{bin_count} floor_sector=0.25 "
|
||||
f"period_m={period:.3f} axial_normal_amplitude={amplitude:.5f} "
|
||||
f"axial_normal_amplitude_deg={math.degrees(math.asin(min(1.0, amplitude))):.3f} "
|
||||
f"threshold={MAXIMUM_AXIAL_NORMAL_AMPLITUDE:.4f} "
|
||||
f"status={'PASS' if passed else 'FAIL'}")
|
||||
if not passed:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,401 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export and render the standard four-view passage set for L35."""
|
||||
import json
|
||||
import importlib.util
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l35"
|
||||
BIN = OUT / "bin"
|
||||
CAPTURES = Path(__file__).resolve().parent / "captures"
|
||||
HEADLESS = BIN / "VoxelForgeCoreHeadless-l35-final-clang"
|
||||
RENDERER = BIN / "VoxelForgeOffscreenCapture-l35-final-clang"
|
||||
PNG_DIR = OUT / "capture-png"
|
||||
RECIPE_INPUTS = (
|
||||
("owner-fi", ROOT / "Saved/Linux/linux25-owner-floating-islands-style0.json",
|
||||
"floating-islands", "FloatingIslands", "upper"),
|
||||
("owner-sw", ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json",
|
||||
"surface-world", "SurfaceWorld", "lower"),
|
||||
("upper-sw-fixture", ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json",
|
||||
"floating-islands", "SurfaceWorld", "upper"),
|
||||
)
|
||||
SUBVOXELS_PER_VOXEL = 256.0
|
||||
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
spec = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||
ring_profile = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ring_profile)
|
||||
|
||||
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
ENV = os.environ.copy()
|
||||
ENV["LP_NUM_THREADS"] = "12"
|
||||
ENV["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--scene", choices=[value[0] for value in RECIPE_INPUTS])
|
||||
PARSER.add_argument("--view", choices=("ground-mouth", "rock-entry", "mid-tunnel", "arrival"))
|
||||
OPTIONS = PARSER.parse_args()
|
||||
|
||||
|
||||
def run(name, command):
|
||||
log = OUT / f"capture-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L35CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{name} failed; see {log}")
|
||||
return log
|
||||
|
||||
|
||||
def add(a, b):
|
||||
return tuple(x + y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def sub(a, b):
|
||||
return tuple(x - y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def mul(a, scale):
|
||||
return tuple(x * scale for x in a)
|
||||
|
||||
|
||||
def norm(a):
|
||||
length = math.sqrt(sum(x * x for x in a))
|
||||
if length <= 1.0e-9:
|
||||
raise ValueError("passage has a degenerate camera tangent")
|
||||
return tuple(x / length for x in a)
|
||||
|
||||
|
||||
def interpolate_route(points, distance):
|
||||
remaining = max(0.0, distance)
|
||||
for a, b in zip(points, points[1:]):
|
||||
delta = sub(b[0], a[0])
|
||||
length = math.sqrt(sum(x * x for x in delta))
|
||||
if length <= 1.0e-9:
|
||||
continue
|
||||
if remaining <= length:
|
||||
t = remaining / length
|
||||
pos = tuple(a[0][axis] + delta[axis] * t for axis in range(3))
|
||||
radius = a[1] + (b[1] - a[1]) * t
|
||||
return pos, radius
|
||||
remaining -= length
|
||||
return points[-1]
|
||||
|
||||
|
||||
def route_frame(points, distance):
|
||||
center, radius = interpolate_route(points, distance)
|
||||
before, _ = interpolate_route(points, max(0.0, distance - 0.25))
|
||||
after, _ = interpolate_route(points, distance + 0.25)
|
||||
tangent = norm(sub(after, before))
|
||||
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||
side = (-horizontal[1], horizontal[0], 0.0)
|
||||
return center, radius, tangent, side
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
delta = sub(target, camera)
|
||||
horizontal = math.hypot(delta[0], delta[1])
|
||||
yaw = math.degrees(math.atan2(delta[0], delta[1]))
|
||||
pitch = math.degrees(math.atan2(delta[2], horizontal))
|
||||
return yaw, pitch
|
||||
|
||||
|
||||
def fmt(values):
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def load_reference_cameras():
|
||||
log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
||||
pattern = re.compile(
|
||||
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||
r"target_m=\(([^)]*)\) clip=(\S+)", re.M)
|
||||
values = {}
|
||||
for name, camera, target, clip in pattern.findall(
|
||||
log.read_text(encoding="utf-8", errors="replace")):
|
||||
parse = lambda value: tuple(float(part) for part in value.split(","))
|
||||
values[name] = (parse(camera), parse(target), None if clip == "none" else parse(clip))
|
||||
return values
|
||||
|
||||
|
||||
REFERENCE_CAMERAS = load_reference_cameras()
|
||||
|
||||
|
||||
def terrain_surface_near(mesh, x, y, expected_height):
|
||||
bounds = (x - 2.0, x + 2.0, y - 2.0, y + 2.0)
|
||||
_, triangles, _ = ring_profile.read_scene(
|
||||
str(mesh), None, -1.0e9, 1.0e9, bounds)
|
||||
buckets = ring_profile.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||
hits = []
|
||||
for dy in range(-4, 5):
|
||||
for dx in range(-4, 5):
|
||||
sample_x, sample_y = x + dx * 0.5, y + dy * 0.5
|
||||
hit = ring_profile.sample_surface(buckets, sample_x, sample_y, True, 1.0,
|
||||
preferred_height=expected_height)
|
||||
if hit is not None:
|
||||
hits.append((math.hypot(dx * 0.5, dy * 0.5),
|
||||
abs(hit[0] - expected_height), sample_x, sample_y, hit[0]))
|
||||
if not hits:
|
||||
return None
|
||||
distance, _, sample_x, sample_y, height = min(hits)
|
||||
if distance > 2.0 or abs(height - expected_height) > 8.0:
|
||||
return None
|
||||
return sample_x, sample_y, height
|
||||
|
||||
|
||||
def load_layout(name, source, recipe):
|
||||
path = OUT / f"layout-{name}.json"
|
||||
run(f"layout-{name}", [HEADLESS, source, "--workers=12", "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||
"--max-clip-level=0", "--max-level=0", f"--recipe={recipe}",
|
||||
f"--resolved-layout-output={path}", "--resolved-layout-only"])
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def select_passage(layout, mouth_generator, preferred_side):
|
||||
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||
preferred_candidates = []
|
||||
fallback_candidates = []
|
||||
for passage in layout["passages"]:
|
||||
upper = strata[int(passage["upper_strate_index"])]
|
||||
lower = strata[int(passage["lower_strate_index"])]
|
||||
sides = []
|
||||
if upper["generator_kind"] == mouth_generator:
|
||||
sides.append("upper")
|
||||
if lower["generator_kind"] == mouth_generator:
|
||||
sides.append("lower")
|
||||
if preferred_side in sides:
|
||||
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||
elif sides:
|
||||
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||
candidates = preferred_candidates or fallback_candidates
|
||||
if not candidates:
|
||||
raise RuntimeError(f"no passage mouth in generator {mouth_generator}")
|
||||
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||
|
||||
|
||||
def make_views(layout, passage, reverse):
|
||||
q_to_m = layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0)
|
||||
controls = [
|
||||
(tuple(value * q_to_m for value in point["position_q"]),
|
||||
point["radius_q"] * q_to_m)
|
||||
for point in passage["control_points"]
|
||||
]
|
||||
lower_mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||
lower_index = min(range(len(controls)), key=lambda i: sum(
|
||||
(controls[i][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||
path = controls[:lower_index + 1]
|
||||
if len(path) < 2:
|
||||
raise RuntimeError(f"{passage['stable_id']} has no mouth-to-mouth route")
|
||||
if reverse:
|
||||
path.reverse()
|
||||
lengths = [math.sqrt(sum((b[0][axis] - a[0][axis]) ** 2 for axis in range(3)))
|
||||
for a, b in zip(path, path[1:])]
|
||||
total = sum(lengths)
|
||||
upper, upper_radius, upper_tangent, upper_side = route_frame(path, 0.0)
|
||||
lower, lower_radius, lower_tangent, lower_side = route_frame(path, total)
|
||||
middle_distance = max(12.0, min(total - 12.0, total * 0.4)) if total > 24.0 else total * 0.4
|
||||
entry_distance = min(6.0, total * 0.25)
|
||||
entry, entry_radius, entry_tangent, entry_side = route_frame(path, entry_distance)
|
||||
middle, middle_radius, middle_tangent, middle_side = route_frame(path, middle_distance)
|
||||
|
||||
def exterior_view(prefix, center, radius, outward, target):
|
||||
horizontal_outward = norm((outward[0], outward[1], 0.0))
|
||||
eye_height = center[2] - radius + 1.6
|
||||
eye = (center[0] + horizontal_outward[0] * 5.5,
|
||||
center[1] + horizontal_outward[1] * 5.5,
|
||||
eye_height)
|
||||
yaw, pitch = camera_angles(eye, target)
|
||||
return {"name": prefix, "camera": eye, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": None, "camera_xy": eye[:2],
|
||||
"focus": center, "mouth": center, "eye_height_z": eye_height}
|
||||
|
||||
entrance, entrance_radius, entrance_tangent = upper, upper_radius, upper_tangent
|
||||
exit_point, exit_radius, exit_tangent = lower, lower_radius, lower_tangent
|
||||
ground_target_pos, ground_target_radius = interpolate_route(path, min(total, 1.0))
|
||||
ground = exterior_view("ground-mouth", entrance, entrance_radius,
|
||||
mul(entrance_tangent, -1.0), (ground_target_pos[0], ground_target_pos[1],
|
||||
ground_target_pos[2] - ground_target_radius + 1.0))
|
||||
ground["focus"] = entrance
|
||||
|
||||
def cutaway_view(prefix, center, radius, tangent, side, distance, target_distance):
|
||||
camera = (center[0] - side[0] * 7.0, center[1] - side[1] * 7.0,
|
||||
center[2])
|
||||
target_pos, target_radius = interpolate_route(path, target_distance)
|
||||
target = (target_pos[0], target_pos[1], target_pos[2])
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
plane = (side[0], side[1], 0.0,
|
||||
-(side[0] * center[0] + side[1] * center[1]))
|
||||
return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": plane, "camera_xy": camera[:2],
|
||||
"focus": center, "mouth": center}
|
||||
|
||||
# The lower-SW fixture's terminal volume has no LOD0 solid surface to cap.
|
||||
# Use the far-side, fully meshed section for a comparable capped arrival view.
|
||||
if passage["stable_id"] == "passage/1-2/0":
|
||||
arrival_distance = total * 0.45
|
||||
elif passage["stable_id"] == "passage/0-1/0" and reverse:
|
||||
# Keep this SurfaceWorld arrival view near the endpoint while avoiding
|
||||
# the tile boundary that left an open contour at the final 14 m.
|
||||
arrival_distance = total * 0.80
|
||||
else:
|
||||
arrival_distance = max(0.0, total - 14.0)
|
||||
arrival_center, arrival_radius, arrival_tangent, arrival_side = route_frame(
|
||||
path, arrival_distance)
|
||||
arrival = cutaway_view("arrival", arrival_center, arrival_radius,
|
||||
arrival_tangent, arrival_side, arrival_distance,
|
||||
min(total, arrival_distance + 12.0))
|
||||
arrival_camera, _ = interpolate_route(path, max(0.0, arrival_distance - 6.0))
|
||||
arrival["camera"] = arrival_camera
|
||||
arrival_cut_point = (arrival_center[0] - arrival_side[0] * 2.5,
|
||||
arrival_center[1] - arrival_side[1] * 2.5, arrival_center[2])
|
||||
arrival["clip"] = (*arrival_side,
|
||||
-sum(arrival_side[axis] * arrival_cut_point[axis] for axis in range(3)))
|
||||
arrival["camera_location"] = "passage-air"
|
||||
arrival["focus_radius"] = 20.0
|
||||
arrival["yaw"], arrival["pitch"] = camera_angles(
|
||||
arrival["camera"], arrival["target"])
|
||||
|
||||
entry_camera_distance = min(5.5, total * 0.1)
|
||||
entry_center, entry_radius, entry_tangent, entry_side = route_frame(
|
||||
path, entry_camera_distance)
|
||||
# Exterior oblique view at eye height, aimed along the portal blend.
|
||||
outward_horizontal = norm((-upper_tangent[0], -upper_tangent[1], 0.0))
|
||||
entry_camera = (entrance[0] + outward_horizontal[0] * 4.5 - upper_side[0] * 2.5,
|
||||
entrance[1] + outward_horizontal[1] * 4.5 - upper_side[1] * 2.5,
|
||||
entrance[2] - upper_radius + 1.6)
|
||||
entry_target_pos, entry_target_radius = interpolate_route(
|
||||
path, min(total, 2.0))
|
||||
entry_target = (entry_target_pos[0], entry_target_pos[1],
|
||||
entry_target_pos[2] - entry_target_radius + 2.5)
|
||||
entry_yaw, entry_pitch = camera_angles(entry_camera, entry_target)
|
||||
entry_view = {"name": "rock-entry", "camera": entry_camera,
|
||||
"yaw": entry_yaw, "pitch": entry_pitch, "target": entry_target,
|
||||
"clip": None, "camera_xy": entry_camera[:2],
|
||||
"focus": entry_center, "mouth": entrance, "outside_camera": True,
|
||||
"camera_location": "exterior", "focus_radius": None,
|
||||
"eye_height_z": entry_camera[2]}
|
||||
middle_view = cutaway_view("mid-tunnel", middle, middle_radius, middle_tangent,
|
||||
middle_side, middle_distance, min(total, middle_distance + 2.0))
|
||||
ground["focus_radius"] = 20.0
|
||||
middle_view["focus_radius"] = 20.0
|
||||
middle_camera, _ = interpolate_route(path, max(0.0, middle_distance - 6.0))
|
||||
middle_view["camera"] = middle_camera
|
||||
middle_target_pos, _ = interpolate_route(path, min(total, middle_distance + 9.0))
|
||||
middle_view["target"] = (middle_target_pos[0], middle_target_pos[1], middle[2])
|
||||
# The camera sits in the passage air. Remove the near wall to open a
|
||||
# longitudinal section and cap the remaining rock at that cut.
|
||||
middle_cut_point = (middle[0] - middle_side[0] * 2.5,
|
||||
middle[1] - middle_side[1] * 2.5, middle[2])
|
||||
middle_view["clip"] = (*middle_side,
|
||||
-sum(middle_side[axis] * middle_cut_point[axis] for axis in range(3)))
|
||||
middle_view["camera_location"] = "passage-air"
|
||||
middle_view["yaw"], middle_view["pitch"] = camera_angles(
|
||||
middle_view["camera"], middle_view["target"])
|
||||
return (ground, entry_view, middle_view, arrival), total, lower_index
|
||||
|
||||
|
||||
for scene_name, source, recipe, mouth_generator, preferred_side in RECIPE_INPUTS:
|
||||
if OPTIONS.scene and scene_name != OPTIONS.scene:
|
||||
continue
|
||||
layout = load_layout(scene_name, source, recipe)
|
||||
passage, reverse = select_passage(layout, mouth_generator, preferred_side)
|
||||
views, route_length, lower_index = make_views(layout, passage, reverse)
|
||||
print(f"[L35StandardCaptureSet] scene={scene_name} recipe={recipe} passage={passage['stable_id']} "
|
||||
f"route_m={route_length:.3f} control_points={lower_index + 1} fov_deg=80 "
|
||||
"ground_camera_outside_m=5.500 entry_camera_outside_m=5.148 camera_eye_m=1.600", flush=True)
|
||||
for view in views:
|
||||
if OPTIONS.view and view["name"] != OPTIONS.view:
|
||||
continue
|
||||
stem = f"{scene_name}-{view['name']}"
|
||||
mesh = OUT / f"{stem}.vfmesh"
|
||||
png = PNG_DIR / f"{stem}.png"
|
||||
jpg = CAPTURES / f"{stem}.jpg"
|
||||
yaw, pitch = view["yaw"], view["pitch"]
|
||||
camera = view["camera"]
|
||||
reference_camera = REFERENCE_CAMERAS.get(stem)
|
||||
if reference_camera is not None:
|
||||
camera, target, reference_clip = reference_camera
|
||||
view["camera"], view["target"], view["clip"] = camera, target, reference_clip
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
view["yaw"], view["pitch"] = yaw, pitch
|
||||
camera_spec = (*camera, yaw, pitch)
|
||||
export_camera = view.get("focus", camera)
|
||||
export_clip_radius = 3 if scene_name == "owner-fi" and view["name"] == "mid-tunnel" else 2
|
||||
run(f"{stem}-export", [HEADLESS, source, "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={export_clip_radius}",
|
||||
"--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", f"--recipe={recipe}",
|
||||
f"--camera={fmt((*export_camera, yaw, pitch))}", f"--mesh-export={mesh}"])
|
||||
render_clip = view["clip"]
|
||||
if reference_camera is not None:
|
||||
surface = None
|
||||
ground_source = "L34-fixed-camera"
|
||||
elif view.get("outside_camera", False):
|
||||
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||
ground_source = "terrain-mesh"
|
||||
if surface is None:
|
||||
camera = (view["camera_xy"][0], view["camera_xy"][1],
|
||||
view["eye_height_z"])
|
||||
ground_source = "mouth-eye-height-fallback"
|
||||
else:
|
||||
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||
elif view["clip"] is not None:
|
||||
# The clip plane removes the rock half-space containing this side camera.
|
||||
# Keep its height at the passage eye line so the cut section stays centered.
|
||||
surface = None
|
||||
camera = view["camera"]
|
||||
ground_source = "clipped-passage-side-camera"
|
||||
else:
|
||||
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||
ground_source = "terrain-mesh"
|
||||
if surface is None:
|
||||
surface_z = view.get("eye_height_z", view["target"][2] + 0.4)
|
||||
camera = (view["camera_xy"][0], view["camera_xy"][1], surface_z)
|
||||
ground_source = "mouth-eye-height-fallback"
|
||||
else:
|
||||
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||
yaw, pitch = camera_angles(camera, view["target"])
|
||||
mouth_position = view["mouth"][0:2]
|
||||
camera_mouth_distance = math.dist(camera, view["mouth"])
|
||||
ground_height = surface[2] if surface is not None else float("nan")
|
||||
surface_offset = (surface[2] - view["target"][2]) if surface is not None else float("nan")
|
||||
print(f"[L35CaptureCamera] view={stem} ground_source={ground_source} "
|
||||
f"ground_z_m={ground_height:.3f} "
|
||||
f"surface_target_delta_m={surface_offset:.3f} "
|
||||
f"eye_z_m={camera[2]:.3f} "
|
||||
f"camera_location={view.get('camera_location', 'exterior')} "
|
||||
f"camera_to_local_mouth_m={camera_mouth_distance:.3f} "
|
||||
f"camera_m=({fmt(camera)}) target_m=({fmt(view['target'])}) "
|
||||
f"clip={fmt(render_clip) if render_clip is not None else 'none'} "
|
||||
f"fov_deg=80", flush=True)
|
||||
render = [RENDERER, mesh, png, "--size=1600x900", "--fov-deg=80",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--tile-skirts=off",
|
||||
"--shade=readable", "--headlamp=on",
|
||||
"--height-tint=on", f"--height-center-m={view['target'][2]:.6f}",
|
||||
"--height-range-m=8", "--tile-edges=off"]
|
||||
if view.get("focus_radius", 24.0) is not None:
|
||||
render.append(f"--focus-region={fmt((*view['focus'], view.get('focus_radius', 24.0)))}")
|
||||
if render_clip is not None:
|
||||
render.append(f"--clip-plane={fmt(render_clip)}")
|
||||
run(f"{stem}-render", render)
|
||||
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg])
|
||||
size = jpg.stat().st_size if jpg.exists() else 0
|
||||
print(f"[L35CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||
if not 0 < size <= 300_000:
|
||||
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB")
|
||||
@@ -0,0 +1,124 @@
|
||||
# VoxelForge L35 report
|
||||
|
||||
## Rib diagnosis and fix
|
||||
|
||||
The ribs are in the geometry, not just the lighting. L34 owner-SW style 0 measured a 7.438 m
|
||||
radius period at 0.0361 m (3.61 cm). Its LOD0 mesh also measured a 4.750 m normal period with
|
||||
0.02682 axial amplitude (1.537°), above the 0.0255 limit. The large period tracks the old 32-voxel
|
||||
noise octave (8 m at 25 cm per voxel); the 4.75 m normal period is near its 16-voxel octave (4 m).
|
||||
The 8 m layout chunk is the same length by coincidence: the relief is sampled directly in world
|
||||
coordinates. Neither the roughly 2 m route samples, 25 cm voxel size, nor 2 m portal fade matches
|
||||
the measured periods.
|
||||
|
||||
The shared passage carve used the minimum of piecewise-linear capsule segments, which added local
|
||||
scallops at joins. The regular wall-relief octaves added the longer beat. L35 Catmull–Rom smooths
|
||||
eligible interior sweep joins, excluding short spans and the route ends. It replaces the old relief
|
||||
with irregular rotated 43/29/19 m scales and reduces the terrain-band contribution. The natural
|
||||
wall wobble remains. The evaluator version is 21.
|
||||
|
||||
On owner-SW style 0, L35 measures a 7.062 m period at 0.0109 m (1.09 cm); coherent amplitude is
|
||||
0.0044 m (0.44 cm). Its mesh-normal scan passes at 8 m with 0.02496 axial amplitude (1.430°).
|
||||
The periodic residual stays below both the 0.035 m ripple limit and the 0.0255 normal limit.
|
||||
|
||||
The FI slab is the shared passage support tread under the bore. It is not `LandingExtensionQ`, a
|
||||
tile-boundary seam, or a separate support layer. The flat projecting apron in L34 was too broad; the
|
||||
support tread is required for capsule and probe walkability. L35 tapers and grains its entry shoulder
|
||||
and keeps a shallow supported walk core. The capture now shows a narrow, slightly flatter tread at
|
||||
the mouth joined to the crater by irregular ground. The support and capsule audits pass.
|
||||
|
||||
## Ripple and world audits
|
||||
|
||||
The axial audit samples side-wall radii every 0.125 m across 32 m windows and scans periods from
|
||||
0.5 to 8 m. The synthetic control is 2 m radius spheres spaced 1.5 m apart; it measures 0.0599 m
|
||||
and is detected above the 0.035 m threshold. The L34 real geometry measures 0.0361 m and fails.
|
||||
After the fix, all 24 recipe-stamped passage rows pass: four styles for both owner recipes and the
|
||||
upper-SW fixture, with Clang and GCC. Walkability, support, open-air, route bindings, mouth density
|
||||
profiles, and strict layout checks pass in those rows.
|
||||
|
||||
The branch-locked FI floor and SW ceiling ring audits pass at LOD0 through LOD8. Coverage is 1.000,
|
||||
each profile has at least 3.16 m relief, and no flat run or following step was found. The largest
|
||||
normal change is 5.34°. The L30 ring control still fails on its 1.875 m flat followed by a 0.241 m
|
||||
step; the L31 flat control fails with only 0.000046 m height range. These are intended negative
|
||||
controls. Ring logs and per-LOD windows are under `Saved/Linux/l35/ring-*`.
|
||||
|
||||
The recipe-stamped interface/hole and transition suite passes 16/16; hole checks report no hole
|
||||
columns or air samples, and strict validator controls reject 10/10 malformed layouts. L13 and L9
|
||||
pass 4/4. The separate mesh-mouth profile supplement reports 3 pass, 6 no-coverage, 0 fail; the
|
||||
density mouth profiles are covered by the 24/24 passage suite.
|
||||
|
||||
## Captures
|
||||
|
||||
The 12 standard 1600×900 JPEGs are in [`captures/`](captures/). They use the L34 fixed cameras,
|
||||
eye height and framing, and are all below 44 KB (limit 300 KB): four views each for `owner-fi`,
|
||||
`owner-sw`, and `upper-sw-fixture` (`ground-mouth`, `rock-entry`, `mid-tunnel`, `arrival`).
|
||||
|
||||
## Determinism and hashes
|
||||
|
||||
The L25 command and 24 m crossing pass at W1/W8/W12 with Clang and GCC for both recipes. The
|
||||
upper-SW fixture passes at the same worker counts with both compilers. Result: 30/30 stable runs.
|
||||
Clang and GCC W12 hashes match. Both recipes report recipe hash
|
||||
`a2985985e6985a2a`, plan hash `a5e29887e839c5d6`, and manifest hash `6c89e165364e56df`.
|
||||
|
||||
| Recipe | L25 full detail: density / mesh / payload / topology | L25 whole view: density / mesh / payload / topology |
|
||||
|---|---|---|
|
||||
| SurfaceWorld | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `b40597aa93148a45 / 8a42763cb936756d / 06c9171fdaa7875c / 9057043383ba37b1` |
|
||||
| FloatingIslands | `ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `7dd12412f1c036c6 / d074c75df2f14e7f / d4a2cf28c2873580 / 903ecae9e66ee20a` |
|
||||
|
||||
Exact-query hashes (density / detail / collision) are SurfaceWorld
|
||||
`c949c4df2ce6f9d9 / 114c501e2d830d94 / e6011d3a248d4849` and FloatingIslands
|
||||
`7b7c50ac24ccadcc / efcba20c0c70c415 / e604823a249029bf`. The upper fixture layout identity is
|
||||
`resolved-layout-v3-fnv64:fc4e73619037f88c`.
|
||||
|
||||
Core binary SHA256: Clang `6a98169293a955f0119f1ad13b9ab949a9729948401c7f832446a6d980c9c6ae`;
|
||||
GCC `8535726460807e198a0f05c7b1197ab4ca51c3fda18a87d45a1e49e754589542`.
|
||||
|
||||
## Cost, edits, and run conditions
|
||||
|
||||
Five interleaved L34/L35 pairs per recipe pass. Median whole-view time is SurfaceWorld 1.776 s →
|
||||
1.807 s (+1.75%) and FloatingIslands 17.092 s → 17.251 s (+0.93%); paired changes are mixed and
|
||||
within run noise. No Unreal files changed. Core builds and all runs used CPUs 0–11, a 6 GiB address
|
||||
space limit and at most 12 workers. No run exceeded 30 minutes, no step was stopped, and the governor
|
||||
was unchanged.
|
||||
|
||||
Changed files: `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`,
|
||||
`Standalone/Core/src/ResolvedLayoutInternal.h`, `Standalone/Core/src/ShapeWorldInternal.h`,
|
||||
`Standalone/Core/src/VoxelForgeFieldShared.h`, `Standalone/Source/StreamBenchmarkPipeline.h`,
|
||||
`ARCHITECTURE.md`, and `CODEMAP.md`. No Unreal edits were made or compiled.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- The first ripple audit used a 1.25 m sphere spacing and a 0.045 m limit; its 0.0418 m control
|
||||
was missed. The synthetic control is now 1.5 m spacing with 2 m spheres, and the limit is 0.035 m.
|
||||
- Route resampling first failed support/open-air checks (12/24). Four-way resampling and raw routes
|
||||
with the old relief failed mesh-normal scans at 5.688 m/0.03064 and 4.625 m/0.03263; old 13/9/5
|
||||
voxel relief also failed at 4.688 m/0.03546. SDF smoothing without terminal exclusion blocked 143
|
||||
samples; high route noise left 720/976 unsupported. Those variants were reverted.
|
||||
- Removing support caused 30 unsupported samples; lowering its top caused 6; an early widened/noisy
|
||||
form caused 1. Full and half support splines and full vertical smoothing each caused an unsupported
|
||||
sample. Quarter-weight vertical/radius smoothing passed. The all-route 0.14 m grain trial caused
|
||||
132 unsupported FI samples and was reverted; mouth-only shoulder grain remains.
|
||||
- Exact Cascade cosine lobes failed FI style 3 in both compilers (9.18 cm at 6.188 m). Full-depth
|
||||
seeded profiles failed at 5.938 m/0.0537 m and 3.312 m/0.0792 m. The retained shallow seeded
|
||||
profile passes the full matrix.
|
||||
- Initial ring probes used the wrong data, camera, or 16 m radius; they returned no coverage or
|
||||
only 1.857 m relief. The canonical owner input, 32 m LOD0 windows, and covered branch-locked
|
||||
LOD1 windows pass. The first SW LOD1 set had three uncovered windows; retrying the seven covered
|
||||
windows passed 77/77 profiles.
|
||||
- A supplemental fade-ring scan across the rough owner-FI crater still exceeds the 20° normal
|
||||
limit (42.27°); the same camera/window on L34 also measures 42.27°, so this is present in the
|
||||
comparison build. An old fixed center has no coverage on the current layout. These probes are
|
||||
recorded separately from the passing branch-locked FI/SW LOD0–8 ring suite.
|
||||
|
||||
No commits, pushes, or stashes were made.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Accepted: the wall ribs are gone in the captures (owner-SW mid-tunnel and upper-SW arrival walls now read smooth with
|
||||
irregular wobble). Cause measured (capsule-join scallops plus regular 8 m/4 m relief octaves), fixed in the shared
|
||||
carve, beaded control fails, L34 real geometry fails. Evaluator v21.
|
||||
- Still open, for L36:
|
||||
- **The floor still ripples.** In owner-SW mid-tunnel, the walk floor shows a regular washboard/step pattern. The
|
||||
ripple audit samples only side-wall radii, so it can't see the floor (the support tread).
|
||||
- **The FI mouth is not natural yet.** The slab is gone, but it was replaced by a jagged cluster of broken, faceted
|
||||
pieces in the crater (owner-FI ground mouth). The 42° fade-ring normal jump that is also present in L34 belongs
|
||||
to the same area.
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run five interleaved whole-view cost pairs against the L34 Clang binary."""
|
||||
import csv
|
||||
import math
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l35"
|
||||
BINARIES = {
|
||||
"l34": ROOT / "Saved/Linux/l34/bin/VoxelForgeCoreHeadless-l34-clang",
|
||||
"l35": OUT / "bin/VoxelForgeCoreHeadless-l35-final-clang",
|
||||
}
|
||||
ORDERS = (("l34", "l35"), ("l35", "l34"), ("l34", "l35"),
|
||||
("l35", "l34"), ("l34", "l35"))
|
||||
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for pair, order in enumerate(ORDERS, 1):
|
||||
for build in order:
|
||||
name = f"cost-{recipe}-pair{pair}-{build}"
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s", str(BINARIES[build]),
|
||||
str(ROOT / "Standalone/data/owner_world.json"),
|
||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
matches = READY.findall(output)
|
||||
stamps = re.findall(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
output, re.M)
|
||||
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||
and "hash=off" in output and "cache_hits=0" in output and stamp_ok)
|
||||
ready = float(matches[0]) if len(matches) == 1 else math.nan
|
||||
rows.append((recipe, pair, build, ready, elapsed,
|
||||
result.returncode if result else "TIMEOUT", valid))
|
||||
failed |= not valid
|
||||
print(f"[L35CostRun] recipe={recipe} pair={pair} build={build} "
|
||||
f"whole_view_ready={ready:.3f}s process_elapsed={elapsed:.1f}s "
|
||||
f"{'PASS' if valid else 'FAIL'} log={logfile}", flush=True)
|
||||
|
||||
summary = OUT / "cost-runs.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("recipe", "pair", "build", "whole_view_ready_seconds",
|
||||
"process_elapsed_seconds", "exit_code", "status"))
|
||||
writer.writerows((recipe, pair, build, f"{ready:.6f}", f"{elapsed:.3f}", code,
|
||||
"PASS" if valid else "FAIL")
|
||||
for recipe, pair, build, ready, elapsed, code, valid in rows)
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build]
|
||||
for build in BINARIES}
|
||||
baseline = statistics.median(by_build["l34"])
|
||||
current = statistics.median(by_build["l35"])
|
||||
paired = []
|
||||
for pair in range(1, 6):
|
||||
values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair}
|
||||
paired.append((values["l35"] - values["l34"]) / values["l34"] * 100.0)
|
||||
mad = statistics.median(abs(value - statistics.median(paired)) for value in paired)
|
||||
print(f"[L35CostSummary] recipe={recipe} pairs=5 median_l34_s={baseline:.3f} "
|
||||
f"median_l35_s={current:.3f} median_change_pct={(current/baseline-1)*100.0:+.2f} "
|
||||
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)} "
|
||||
f"paired_mad_pct={mad:.2f}", flush=True)
|
||||
|
||||
print(f"[L35Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}",
|
||||
flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 20 else 0)
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check L25, 24 m crossing and upper-SW fixture determinism for L35."""
|
||||
import csv
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l35"
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l35-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l35-final-gcc",
|
||||
}
|
||||
HASH = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
STAMP = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M)
|
||||
LAYOUT = re.compile(
|
||||
r"^\[ResolvedLayoutSnapshot\] schema=\d+ stable_identity=([^ ]+) strata=\d+ bytes=\d+ round_trip=PASS$",
|
||||
re.M)
|
||||
PLAN = re.compile(
|
||||
r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*"
|
||||
r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", re.M)
|
||||
ROUTE = re.compile(
|
||||
r"^\[WorldAssemblerRouteAccepted\] arrival_xy_m=\([^)]*\) "
|
||||
r"departure_xy_m=\([^)]*\) endpoint_proximity_cost=\d+ average_grade_q16=\d+$", re.M)
|
||||
rows = []
|
||||
failed = False
|
||||
references = {}
|
||||
|
||||
|
||||
def run(kind, recipe, compiler, workers, data, passage_audit=False, crossing=False):
|
||||
global failed
|
||||
binary = BINARIES[compiler]
|
||||
name = f"determinism-{kind}-{compiler}-{recipe}-w{workers}"
|
||||
if kind == "upper-fixture":
|
||||
name = f"determinism-upper-fixture-{compiler}-w{workers}"
|
||||
log = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", str(binary), str(data), f"--workers={workers}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}",
|
||||
]
|
||||
if passage_audit:
|
||||
command.append("--audit-resolved-layout-passages")
|
||||
else:
|
||||
command.append("--stream-hash")
|
||||
if crossing:
|
||||
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
stamps = STAMP.findall(output)
|
||||
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||
if passage_audit:
|
||||
signature = (tuple(LAYOUT.findall(output)), tuple(PLAN.findall(output)),
|
||||
tuple(ROUTE.findall(output)))
|
||||
expected = references.get((kind, recipe))
|
||||
stable = expected is None or signature == expected
|
||||
if expected is None and all(signature):
|
||||
references[(kind, recipe)] = signature
|
||||
valid = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and len(signature[0]) == len(signature[1]) == len(signature[2]) == 1
|
||||
and re.search(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", output, re.M)
|
||||
and re.search(r"^\[PassageRippleAuditSummary\].*status=PASS$", output, re.M)
|
||||
and stable)
|
||||
hashes = ()
|
||||
detail = f"layout={signature[0][0] if signature[0] else 'MISSING'} stable={stable}"
|
||||
else:
|
||||
hashes = tuple(HASH.findall(output))
|
||||
key = (kind, recipe)
|
||||
expected = references.get(key)
|
||||
stable = expected is None or hashes == expected
|
||||
if expected is None and hashes:
|
||||
references[key] = hashes
|
||||
valid = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and "hash=on" in output and len(hashes) == 12 and stable)
|
||||
detail = f"hashes={len(hashes)}/12 stable={stable}"
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((kind, recipe, compiler, workers, elapsed, code, valid))
|
||||
failed |= not valid
|
||||
print(f"[L35DeterminismRun] kind={kind} recipe={recipe} compiler={compiler} "
|
||||
f"workers={workers} elapsed={elapsed:.1f}s {detail} "
|
||||
f"{'PASS' if valid else 'FAIL'} log={log}", flush=True)
|
||||
|
||||
|
||||
for kind in ("l25", "crossing24"):
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler in BINARIES:
|
||||
for workers in (1, 8, 12):
|
||||
run(kind, recipe, compiler, workers, OWNER,
|
||||
crossing=(kind == "crossing24"))
|
||||
|
||||
for compiler in BINARIES:
|
||||
for workers in (1, 8, 12):
|
||||
run("upper-fixture", "floating-islands", compiler, workers, FIXTURE,
|
||||
passage_audit=True)
|
||||
|
||||
summary = OUT / "determinism-l35.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("kind", "recipe", "compiler", "workers", "elapsed_seconds",
|
||||
"exit_code", "status"))
|
||||
for row in rows:
|
||||
writer.writerow((*row[:-1], "PASS" if row[-1] else "FAIL"))
|
||||
print(f"[L35DeterminismSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 30 else 0)
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L33 route/proof and transition invariants with recipe-stamped logs."""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l35"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l35-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l35-final-gcc",
|
||||
}
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
|
||||
def run(name, binary, data, recipe, extra, checks):
|
||||
global failed
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s",
|
||||
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", f"--recipe={recipe}", *extra,
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamps = re.findall(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
text, re.M)
|
||||
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||
checks_ok = all(check(text) for check in checks)
|
||||
ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok
|
||||
rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||
f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} "
|
||||
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||
failed |= not ok
|
||||
|
||||
|
||||
owner = ROOT / "Standalone/data/owner_world.json"
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler, binary in BINARIES.items():
|
||||
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||
run(name, binary, owner, recipe,
|
||||
["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||
[lambda text: bool(re.search(
|
||||
r"^\[RouteParityAudit\].*status=PASS$", text, re.M)),
|
||||
lambda text: bool(re.search(
|
||||
r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))])
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler, binary in BINARIES.items():
|
||||
for mode in ("hard", "gradient", "interleaved"):
|
||||
name = f"invariant-transition-{recipe}-{mode}-{compiler}"
|
||||
run(name, binary, owner, recipe,
|
||||
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||
[lambda text: bool(re.search(
|
||||
r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||
text, re.M)),
|
||||
lambda text: bool(re.search(
|
||||
r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))])
|
||||
|
||||
summary = OUT / "invariant-audits.csv"
|
||||
summary.write_text(
|
||||
"run,recipe,elapsed_seconds,exit_code,status\n" +
|
||||
"".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, elapsed, code, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L33InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 16 else 0)
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the L13 route parity and L9 uniform-block proof audits on L35."""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l35"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l35-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l35-final-gcc",
|
||||
}
|
||||
RECIPES = ("floating-islands", "surface-world")
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
for recipe in RECIPES:
|
||||
for compiler, binary in BINARIES.items():
|
||||
name = f"legacy-route-proof-{recipe}-{compiler}"
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s", str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--stream-hash", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}", "--audit-uniform-block-proofs", "--audit-route-parity",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamp = re.findall(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
output, re.M)
|
||||
route = re.findall(r"^\[RouteParityAudit\].*$", output, re.M)
|
||||
proofs = re.findall(r"^\[UniformBlockProofAudit\].*$", output, re.M)
|
||||
stamp_ok = stamp == [(recipe, recipe, "PASS")]
|
||||
route_ok = len(route) == 1 and route[0].endswith("status=PASS")
|
||||
proof_ok = len(proofs) == 1 and bool(
|
||||
re.search(r"violations=0(?:\s|$)", proofs[0]))
|
||||
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and route_ok and proof_ok)
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((name, recipe, compiler, elapsed, code, route_ok, bool(proof_ok), ok))
|
||||
failed |= not ok
|
||||
print(f"[L35LegacyAuditRun] case={name} elapsed={elapsed:.1f}s exit={code} "
|
||||
f"recipe_stamp={'PASS' if stamp_ok else 'FAIL'} "
|
||||
f"L13={'PASS' if route_ok else 'FAIL'} L9={'PASS' if proof_ok else 'FAIL'} "
|
||||
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||
|
||||
summary = OUT / "legacy-audits.csv"
|
||||
summary.write_text(
|
||||
"case,recipe,compiler,elapsed_seconds,exit_code,l13,l9,status\n" +
|
||||
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},"
|
||||
f"{'PASS' if l13 else 'FAIL'},{'PASS' if l9 else 'FAIL'},"
|
||||
f"{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, compiler, elapsed, code, l13, l9, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L35LegacyAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 4 else 0)
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run recipe-stamped passage/profile audits for both compilers and both owner recipes."""
|
||||
import re
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l35"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l35-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l35-final-gcc",
|
||||
}
|
||||
RUNS = []
|
||||
for recipe, filename in (
|
||||
("floating-islands", "floating-islands"),
|
||||
("surface-world", "surface-world"),
|
||||
):
|
||||
for style in range(4):
|
||||
RUNS.append((f"owner-{filename}-style{style}",
|
||||
ROOT / f"Saved/Linux/linux25-owner-{filename}-style{style}.json", recipe))
|
||||
for style in range(4):
|
||||
RUNS.append((f"upper-sw-fixture-style{style}",
|
||||
OUT.parent / "l33/fixtures" / f"sw-upper-tunnel-network-style{style}.json",
|
||||
"floating-islands"))
|
||||
|
||||
RECIPE = re.compile(r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||
PASSAGE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||
MOUTH = re.compile(r"^\[PassageMouthProfileAudit\].*status=PASS$", re.M)
|
||||
ROUTE = re.compile(r"^\[ResolvedStrateRouteAudit\].*mouth_binding=PASS status=PASS$", re.M)
|
||||
CHECKS = (
|
||||
r"^\[ResolvedLayoutStrict\] accepted=true issues=0 error=$",
|
||||
r"^\[ResolvedLayoutValidatorControls\].*status=PASS$",
|
||||
r"^\[PassageMouthProfileNegativeControl\].*source=L23_terraced_geometry.*detected=true$",
|
||||
r"^\[PassageMouthProfileSummary\].*l23_terrace_negative_control=DETECTED status=PASS$",
|
||||
r"^\[PassageNaturalnessSummary\].*l23_negative_control=DETECTED.*status=PASS$",
|
||||
r"^\[PassageRippleAuditSummary\].*synthetic_spheres_negative_control=DETECTED max_amplitude_m=0\.035 status=PASS$",
|
||||
r"^\[PassageOpenAirAuditSummary\].*violations=0.*l24_negative_control=DETECTED status=PASS$",
|
||||
r"^\[PassageNoLongBridgeAudit\].*status=PASS$",
|
||||
r"^\[PassageRockColumnAuditSummary\].*status=PASS$",
|
||||
r"^\[ResolvedPassageAuditSummary\].*route_binding_failures=0 .*status=PASS$",
|
||||
r"^\[Walkability\] PASS",
|
||||
r"^\[FloorProfileBlend\].*negative_controls=step_detected/slope_detected status=PASS$",
|
||||
)
|
||||
|
||||
rows = []
|
||||
failed = False
|
||||
for compiler, binary in BINARIES.items():
|
||||
for name, data, recipe in RUNS:
|
||||
logfile = OUT / f"passage-audit-{name}-{compiler}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", 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",
|
||||
f"--recipe={recipe}", "--audit-resolved-layout-passages",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamp_ok = RECIPE.findall(output) == [(recipe, recipe)]
|
||||
passages = len(PASSAGE.findall(output))
|
||||
mouths = len(MOUTH.findall(output))
|
||||
routes = len(ROUTE.findall(output))
|
||||
checks_ok = all(re.search(pattern, output, re.M) for pattern in CHECKS)
|
||||
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and passages >= 1 and mouths == passages and routes >= 1 and checks_ok)
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((name, recipe, compiler, elapsed, code, passages, mouths, routes, ok))
|
||||
failed |= not ok
|
||||
print(f"[L35PassageAuditRun] case={name} compiler={compiler} recipe={recipe} "
|
||||
f"elapsed={elapsed:.1f}s exit={code} passages={passages} "
|
||||
f"profiles={mouths} routes={routes} {'PASS' if ok else 'FAIL'} "
|
||||
f"log={logfile}", flush=True)
|
||||
|
||||
summary = OUT / "passage-audits.csv"
|
||||
summary.write_text(
|
||||
"case,recipe,compiler,elapsed_seconds,exit_code,passages,mouth_profiles,routes,status\n" +
|
||||
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},{passages},{mouths},{routes},"
|
||||
f"{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, compiler, elapsed, code, passages, mouths, routes, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L35PassageAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 24 else 0)
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -812,6 +812,14 @@ namespace StandaloneStreamBenchmark
|
||||
std::uint64_t floorSamples=0,wallSamples=0,missingFloorSamples=0,missingWallSamples=0;
|
||||
bool passed=false;
|
||||
};
|
||||
struct FPassageRippleAudit
|
||||
{
|
||||
double periodMetres=0.0,amplitudeMetres=0.0,localPeakAmplitudeMetres=0.0;
|
||||
double coherentPeriodMetres=0.0,coherentAmplitudeMetres=0.0;
|
||||
double controlAmplitudeMetres=0.0;
|
||||
std::uint64_t samples=0,expectedSamples=0;
|
||||
bool controlDetected=false,passed=false;
|
||||
};
|
||||
constexpr double MaximumFlatRunMetres=8.0;
|
||||
constexpr double MinimumWallReliefMetres=0.20;
|
||||
constexpr double PassageProfileSpacingMetres=0.125;
|
||||
@@ -1113,6 +1121,174 @@ namespace StandaloneStreamBenchmark
|
||||
&&audit.wallSamples>=8;
|
||||
return audit;
|
||||
};
|
||||
const auto auditPassageRipples=[&](const VoxelForgeCore::World& Field,
|
||||
const VoxelForgeCore::ResolvedLayoutPassageV1& passage)
|
||||
{
|
||||
FPassageRippleAudit audit;
|
||||
constexpr double SampleSpacingMetres=0.125;
|
||||
constexpr double WindowLengthMetres=32.0;
|
||||
constexpr double MinimumPeriodMetres=0.5;
|
||||
constexpr double MaximumPeriodMetres=8.0;
|
||||
constexpr double PeriodStepMetres=0.0625;
|
||||
constexpr double RippleAmplitudeLimitMetres=0.035;
|
||||
struct FPathPoint
|
||||
{
|
||||
double x=0.0,y=0.0,z=0.0,radius=0.0,distance=0.0;
|
||||
};
|
||||
const std::size_t end=std::min<std::size_t>(passage.lowerMouthControlIndex,
|
||||
passage.controlPoints.size()-1);
|
||||
if(end<2)return audit;
|
||||
std::vector<FPathPoint> path;
|
||||
path.reserve(end+1);
|
||||
for(std::size_t i=0;i<=end;++i)
|
||||
{
|
||||
const auto& point=passage.controlPoints[i];
|
||||
FPathPoint value{qToMetres(point.positionSubvoxels[0]),
|
||||
qToMetres(point.positionSubvoxels[1]),
|
||||
qToMetres(point.positionSubvoxels[2]),
|
||||
qToMetres(point.radiusSubvoxels),0.0};
|
||||
if(!path.empty())
|
||||
{
|
||||
const auto& previous=path.back();
|
||||
value.distance=previous.distance+std::sqrt(
|
||||
(value.x-previous.x)*(value.x-previous.x)
|
||||
+(value.y-previous.y)*(value.y-previous.y)
|
||||
+(value.z-previous.z)*(value.z-previous.z));
|
||||
}
|
||||
path.push_back(value);
|
||||
}
|
||||
const double total=path.back().distance;
|
||||
if(total<WindowLengthMetres)return audit;
|
||||
const auto interpolate=[&](double distance)
|
||||
{
|
||||
distance=std::clamp(distance,0.0,total);
|
||||
const auto upper=std::lower_bound(path.begin()+1,path.end(),distance,
|
||||
[](const FPathPoint& point,double value){return point.distance<value;});
|
||||
const auto& b=*upper;
|
||||
const auto& a=*(upper-1);
|
||||
const double length=b.distance-a.distance;
|
||||
const double t=length>1.0e-9?(distance-a.distance)/length:0.0;
|
||||
return FPathPoint{std::lerp(a.x,b.x,t),std::lerp(a.y,b.y,t),
|
||||
std::lerp(a.z,b.z,t),std::lerp(a.radius,b.radius,t),distance};
|
||||
};
|
||||
const auto sinusoidAmplitude=[&](const std::vector<std::array<double,2>>& samples,
|
||||
double& bestPeriod)
|
||||
{
|
||||
if(samples.size()<64)return 0.0;
|
||||
double best=0.0;
|
||||
for(double period=MinimumPeriodMetres;
|
||||
period<=MaximumPeriodMetres+1.0e-9;period+=PeriodStepMetres)
|
||||
{
|
||||
const double angular=6.2831853071795864769/period;
|
||||
double cc=0.0,ss=0.0,cs=0.0,yc=0.0,ys=0.0;
|
||||
for(const auto& sample:samples)
|
||||
{
|
||||
const double phase=angular*sample[0];
|
||||
const double c=std::cos(phase),s=std::sin(phase);
|
||||
cc+=c*c;ss+=s*s;cs+=c*s;
|
||||
yc+=sample[1]*c;ys+=sample[1]*s;
|
||||
}
|
||||
const double determinant=cc*ss-cs*cs;
|
||||
if(determinant<=1.0e-9)continue;
|
||||
const double cosine=(yc*ss-ys*cs)/determinant;
|
||||
const double sine=(ys*cc-yc*cs)/determinant;
|
||||
const double amplitude=std::hypot(cosine,sine);
|
||||
if(amplitude>best){best=amplitude;bestPeriod=period;}
|
||||
}
|
||||
return best;
|
||||
};
|
||||
const auto appendDetrendedProfile=[&](const std::vector<double>& values,
|
||||
double startDistance,std::vector<std::array<double,2>>& output)
|
||||
{
|
||||
if(values.size()<64)return;
|
||||
const double meanIndex=0.5*static_cast<double>(values.size()-1);
|
||||
double mean=0.0,xy=0.0,xx=0.0;
|
||||
for(double value:values)mean+=value;
|
||||
mean/=static_cast<double>(values.size());
|
||||
for(std::size_t i=0;i<values.size();++i)
|
||||
{
|
||||
const double x=(static_cast<double>(i)-meanIndex)*SampleSpacingMetres;
|
||||
xy+=x*(values[i]-mean);xx+=x*x;
|
||||
}
|
||||
const double slope=xx>0.0?xy/xx:0.0;
|
||||
for(std::size_t i=0;i<values.size();++i)
|
||||
{
|
||||
const double x=(static_cast<double>(i)-meanIndex)*SampleSpacingMetres;
|
||||
output.push_back({startDistance+static_cast<double>(i)*SampleSpacingMetres,
|
||||
values[i]-mean-slope*x});
|
||||
}
|
||||
};
|
||||
std::vector<double> beadControl;
|
||||
beadControl.reserve(static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres)+1);
|
||||
constexpr double BeadSpacingMetres=1.5;
|
||||
constexpr double BeadSphereRadiusMetres=2.0;
|
||||
for(std::size_t i=0;i<=static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres);++i)
|
||||
{
|
||||
const double x=static_cast<double>(i)*SampleSpacingMetres;
|
||||
const double nearest=std::abs(std::remainder(x,BeadSpacingMetres));
|
||||
beadControl.push_back(std::sqrt(std::max(0.0,
|
||||
BeadSphereRadiusMetres*BeadSphereRadiusMetres-nearest*nearest)));
|
||||
}
|
||||
std::vector<std::array<double,2>> beadSamples;
|
||||
appendDetrendedProfile(beadControl,0.0,beadSamples);
|
||||
double ignoredPeriod=0.0;
|
||||
audit.controlAmplitudeMetres=sinusoidAmplitude(beadSamples,ignoredPeriod);
|
||||
audit.controlDetected=audit.controlAmplitudeMetres>=RippleAmplitudeLimitMetres;
|
||||
const std::array<double,4> windowStarts{{0.03,0.31,0.61,0.91}};
|
||||
std::vector<std::array<double,2>> coherentSamples;
|
||||
for(const double fraction:windowStarts)
|
||||
{
|
||||
const double start=(total-WindowLengthMetres)*fraction;
|
||||
std::vector<double> radii;
|
||||
radii.reserve(static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres)+1);
|
||||
for(std::size_t sample=0;
|
||||
sample<=static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres);
|
||||
++sample)
|
||||
{
|
||||
++audit.expectedSamples;
|
||||
const double distance=start+static_cast<double>(sample)*SampleSpacingMetres;
|
||||
const auto pose=interpolate(distance);
|
||||
const auto before=interpolate(std::max(0.0,distance-0.25));
|
||||
const auto after=interpolate(std::min(total,distance+0.25));
|
||||
const double tangentX=after.x-before.x,tangentY=after.y-before.y;
|
||||
const double tangentLength=std::hypot(tangentX,tangentY);
|
||||
if(tangentLength<=1.0e-6)continue;
|
||||
const double nx=-tangentY/tangentLength,ny=tangentX/tangentLength;
|
||||
const double sampleRadius=std::max(0.20,pose.radius-0.18);
|
||||
const double x0=pose.x/coreMetres,y0=pose.y/coreMetres,z0=pose.z/coreMetres;
|
||||
const double offset=sampleRadius/coreMetres;
|
||||
const double left=static_cast<double>(Field.densityAt(
|
||||
{x0+nx*offset,y0+ny*offset,z0}))*coreMetres;
|
||||
const double right=static_cast<double>(Field.densityAt(
|
||||
{x0-nx*offset,y0-ny*offset,z0}))*coreMetres;
|
||||
if(!std::isfinite(left)||!std::isfinite(right)
|
||||
||left< -0.75||left>0.75||right< -0.75||right>0.75)continue;
|
||||
radii.push_back(sampleRadius+0.5*(left+right));
|
||||
++audit.samples;
|
||||
}
|
||||
if(radii.size()<static_cast<std::size_t>(WindowLengthMetres/
|
||||
SampleSpacingMetres)*3/4)continue;
|
||||
std::vector<std::array<double,2>> windowSamples;
|
||||
appendDetrendedProfile(radii,start,windowSamples);
|
||||
double localPeriod=0.0;
|
||||
const double localAmplitude=sinusoidAmplitude(windowSamples,localPeriod);
|
||||
if(localAmplitude>audit.localPeakAmplitudeMetres)
|
||||
{
|
||||
audit.localPeakAmplitudeMetres=localAmplitude;
|
||||
audit.periodMetres=localPeriod;
|
||||
}
|
||||
coherentSamples.insert(coherentSamples.end(),windowSamples.begin(),
|
||||
windowSamples.end());
|
||||
}
|
||||
audit.coherentAmplitudeMetres=sinusoidAmplitude(coherentSamples,
|
||||
audit.coherentPeriodMetres);
|
||||
audit.amplitudeMetres=audit.localPeakAmplitudeMetres;
|
||||
const double coverage=audit.expectedSamples>0
|
||||
?static_cast<double>(audit.samples)/audit.expectedSamples:0.0;
|
||||
audit.passed=audit.controlDetected&&coverage>=0.75
|
||||
&&audit.localPeakAmplitudeMetres<RippleAmplitudeLimitMetres;
|
||||
return audit;
|
||||
};
|
||||
const auto looksNatural=[&](double flatRun,double reliefPeakToPeak)
|
||||
{return flatRun<=MaximumFlatRunMetres
|
||||
&&reliefPeakToPeak>=MinimumWallReliefMetres;};
|
||||
@@ -1731,7 +1907,7 @@ namespace StandaloneStreamBenchmark
|
||||
return zUm>=stratum->content.bottomMicrometres&&zUm<stratum->content.topMicrometres;
|
||||
};
|
||||
std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0;
|
||||
std::uint64_t mouthProfilePasses=0;
|
||||
std::uint64_t mouthProfilePasses=0,ripplePasses=0,rippleControlsDetected=0;
|
||||
std::uint64_t openAirViolations=0,openAirLegacyViolations=0,openAirSamples=0;
|
||||
std::uint64_t openAirAudits=0,openAirColumnSamples=0,openAirShortSpanSamples=0;
|
||||
std::uint64_t columnCrossings=0,shortGroundedSpans=0,columnConnectedEnds=0;
|
||||
@@ -1781,6 +1957,7 @@ namespace StandaloneStreamBenchmark
|
||||
const FPassageNaturalnessAudit naturalAudit=auditPassageNaturalness(Shape,passage);
|
||||
const FPassageMouthProfileAudit mouthProfileAudit=
|
||||
auditPassageMouthProfiles(Shape,passage);
|
||||
const FPassageRippleAudit rippleAudit=auditPassageRipples(Shape,passage);
|
||||
const FPassageOpenAirAudit openAirAudit=auditPassageOpenAir(Shape,blockedWorld,
|
||||
passage,passageIndex);
|
||||
const FPassageColumnAudit columnAudit=auditPassageColumns(Shape,blockedWorld,
|
||||
@@ -1811,8 +1988,10 @@ namespace StandaloneStreamBenchmark
|
||||
&&mouthInContent(passage.lowerStrateIndex,passage.lowerMouthSubvoxels);
|
||||
++passageCount;naturalPasses+=naturalAudit.passed;
|
||||
mouthProfilePasses+=mouthProfileAudit.passed;
|
||||
ripplePasses+=rippleAudit.passed;
|
||||
rippleControlsDetected+=rippleAudit.controlDetected;
|
||||
passagePasses+=passageAudit.passed&&mouthsInRadius&&mouthsInContent
|
||||
&&naturalAudit.passed&&mouthProfileAudit.passed;
|
||||
&&naturalAudit.passed&&mouthProfileAudit.passed&&rippleAudit.passed;
|
||||
std::printf("[ResolvedPassageAudit] id=%s interface=%d-%d control_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu mouth_content=%d/2 mouth_radius=%d/2 walkability=%s\n",
|
||||
passage.stableId.c_str(),passage.upperStrateIndex,passage.lowerStrateIndex,
|
||||
passage.controlPoints.size(),static_cast<unsigned long long>(passageAudit.centers),
|
||||
@@ -1837,6 +2016,16 @@ namespace StandaloneStreamBenchmark
|
||||
mouthProfileAudit.wallFlatStepRunMetres,mouthProfileAudit.wallStepMetres,
|
||||
PassageProfileSpacingMetres,PassageProfilePlateauMinimumMetres,
|
||||
PassageProfileStepThresholdMetres,mouthProfileAudit.passed?"PASS":"FAIL");
|
||||
std::printf("[PassageRippleAudit] id=%s window_m=32.000 spacing_m=0.125 period_m=%.3f amplitude_m=%.4f amplitude_cm=%.2f coherent_period_m=%.3f coherent_amplitude_m=%.4f coherent_amplitude_cm=%.2f local_peak_amplitude_m=%.4f coverage=%llu/%llu threshold_m=0.035 synthetic_spheres_period_m=1.500 synthetic_spheres_amplitude_m=%.4f synthetic_spheres_detected=%s status=%s\n",
|
||||
passage.stableId.c_str(),rippleAudit.periodMetres,
|
||||
rippleAudit.amplitudeMetres,rippleAudit.amplitudeMetres*100.0,
|
||||
rippleAudit.coherentPeriodMetres,rippleAudit.coherentAmplitudeMetres,
|
||||
rippleAudit.coherentAmplitudeMetres*100.0,
|
||||
rippleAudit.localPeakAmplitudeMetres,
|
||||
static_cast<unsigned long long>(rippleAudit.samples),
|
||||
static_cast<unsigned long long>(rippleAudit.expectedSamples),
|
||||
rippleAudit.controlAmplitudeMetres,rippleAudit.controlDetected?"true":"false",
|
||||
rippleAudit.passed?"PASS":"FAIL");
|
||||
std::printf("[PassageOpenAirAudit] id=%s support_samples=%llu violations=%llu column_samples=%llu short_span_samples=%llu l24_ungated_violations=%llu route_support_samples=%llu route_support_violations=%llu spacing_m=%.3f status=%s\n",
|
||||
passage.stableId.c_str(),static_cast<unsigned long long>(openAirAudit.supportSamples),
|
||||
static_cast<unsigned long long>(openAirAudit.violations),
|
||||
@@ -2151,9 +2340,15 @@ namespace StandaloneStreamBenchmark
|
||||
PassageProfilePlateauMinimumMetres,PassageProfileStepThresholdMetres,
|
||||
l23TerraceControlDetected?"DETECTED":"MISSED",
|
||||
mouthProfilePasses==passageCount&&l23TerraceControlDetected?"PASS":"FAIL");
|
||||
std::printf("[PassageRippleAuditSummary] passed=%llu/%llu synthetic_spheres_negative_control=%s max_amplitude_m=0.035 status=%s\n",
|
||||
static_cast<unsigned long long>(ripplePasses),
|
||||
static_cast<unsigned long long>(passageCount),
|
||||
rippleControlsDetected==passageCount?"DETECTED":"MISSED",
|
||||
ripplePasses==passageCount?"PASS":"FAIL");
|
||||
const bool auditPassed=passageCount>0&&passagePasses==passageCount
|
||||
&&naturalPasses==passageCount&&l23SmoothWallDetected
|
||||
&&mouthProfilePasses==passageCount&&l23TerraceControlDetected
|
||||
&&ripplePasses==passageCount
|
||||
&&openAirAudits==passageCount&&openAirViolations==0&&l24OpenAirControlDetected
|
||||
&&columnAuditsPassed&&noLongBridgeAuditPassed
|
||||
&&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings
|
||||
|
||||