diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1136ca3..1b7d232 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2138,6 +2138,30 @@ turn. Passage, capsule, support, open-air, bridge, binding, strict layout, captu and cost evidence are recorded in [`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md). +### 8.49 Walk-floor washboard and FI mouth naturalness — World Assembler M3 L36 + +L35's exported owner-SW mid-tunnel floor measured a 7.250 m repeat at 0.02252 m amplitude; its +upper-SW fixture arrival measured a 1.438 m repeat at 0.02482 m. Both came from segment-built, +one-sided support treads meeting the native terrain zero crossing at per-segment joins. The floor +probe samples the exported walk mesh along the resolved route, separately from the wall-radius scan. +Eligible tread sections overlap by up to 2 m and all sparse route segments now use a bounded +signed-distance slab, including SurfaceWorld-to-TunnelNetwork routes. This keeps the tread top stable +across segment joins while capsule-clearance and native-solid gates retain walkability and open air. +Regular tread tops remain seated 5 cm lower and ease back to 0.20 m within 20 m of a lower landing; +seeded shoulder grain is removed, and gravity-facing relief is reduced while side-wall and ceiling +relief remain irregular. + +On v23, the same mesh probes measure 7.188 m / 0.00067 m (0.067 cm) on owner-SW and 6.500 m / +0.00057 m (0.057 cm) on the upper fixture. The density audit checks five 32 m walk-floor windows at +0.125 m spacing, rejects amplitudes of 0.015 m or more, and detects its beaded control. The FI mouth +uses a local 2 m symmetric signed-field filter with the route protected and joins route-mouth support +to native terrain with a 1.5 m smooth signed-distance union. Its mesh audit reduces the L35 +42.27° fade-ring jump to 27.13° and finds no detached component above the 0.0001 m² area floor; one +8-triangle subthreshold component at 0.0000266 m² is recorded. The evaluator is version 23. Full +recipe/style audits, route support, open-air, strict layout, legacy validators, determinism, capture and +paired-cost results are in +[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/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 diff --git a/CODEMAP.md b/CODEMAP.md index 1d00b64..4a8241f 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -1250,11 +1250,24 @@ ARCHITECTURE §8.47 and | `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. | +| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples wall radii and walk-floor zero crossings at 0.125 m spacing, scans periods 0.5–8 m in 32 m windows, and rejects wall/floor amplitudes ≥0.035/0.015 m. Sphere and beaded-floor controls must be detected. | `WorldAssemblyEvaluatorVersion=21`. See ARCHITECTURE §8.48 and [`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md). +### 3.32 Walk-floor washboard and FI mouth blend — World Assembler M3 L36 + +| Symbol | Canonical file | Role | +|---|---|---| +| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Extends eligible tread sections by up to 2 m and uses bounded signed-distance support on every sparse route, including SurfaceWorld-to-TunnelNetwork, removes seeded shoulder grain, and seats regular tread tops 5 cm lower with a smooth return at the lower landing. Capsule/open-air gates remain. Gravity-facing wall relief is reduced; side and ceiling relief remain. | +| `ApplyResolvedStratePassageCarves` mouth support | `Standalone/Core/src/ShapeWorldInternal.h` | Builds exact signed-distance route-mouth tread boxes and smooth-unions them with native terrain over 1.5 m. | +| `NativeFloatingIslandDensityAt` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies a local 2 m symmetric signed-field filter around FI passage mouths, exempts resolved route support, and removes isolated negative samples thinner than 2 m on all axes. | +| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Keeps the wall-radius audit and adds walk-floor zero-crossing profiles at 0.125 m, with five 32 m windows and a 0.015 m limit plus a beaded-floor negative control. | +| `RunMouthFragmentAuditL36.py`, `ProbeWalkFloorProfileL36.py` | `Standalone/Research/WorldAssemblerM3/Linux36/` | Compare L35 floor and FI-mouth controls with L36; the mouth check fails on detached components at or above 0.0001 m² and checks normal jumps across the fade ring. | + +`WorldAssemblyEvaluatorVersion=23`. See ARCHITECTURE §8.49 and +[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md). + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 8216b8b..88c00d1 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -250,8 +250,10 @@ namespace VoxelForgeCore inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256; 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 = 21; + // version 20 restores full Gradient amplitude and uses a continuous signed-distance band union; + // version 22 overlaps tread sections and smooths FloatingIslands passage-mouth features; + // version 23 uses signed-distance support for sparse SurfaceWorld-to-TunnelNetwork treads. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 23; enum class FieldChannel : std::uint32_t { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index d30f942..7510e76 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -727,8 +727,9 @@ public: } if(routeCount<2)return; const std::size_t lastSegment=routeCount-1; - const double floorThickness=1000000.0/std::max(1, - ResolvedLayout->coreVoxelSizeMicrometres); + constexpr double MouthSupportBlendMetres=1.5; + const double mouthBlendWidth=MouthSupportBlendMetres*qPerMetre; + double mouthSupportDistance=std::numeric_limits::infinity(); for(std::size_t index=0;index(b.values[3])-a.values[3]):0.0; const double halfWidth=std::max(0.75*qPerMetre,routeRadius); const double horizontalX=qx-centerX,horizontalY=qy-centerY; - if(horizontalX*horizontalX+horizontalY*horizontalY>halfWidth*halfWidth)continue; - const double floorVoxels=(centerZ-1.20*qPerMetre)/Q; - if(ZfloorVoxels+1.0/Q)continue; - composed=std::min(composed,static_cast( - std::clamp(Z-floorVoxels,-floorThickness,0.0))); + const double horizontalDistance=DeterministicSqrt( + horizontalX*horizontalX+horizontalY*horizontalY); + const double floorQ=centerZ-1.20*qPerMetre; + const double sideDistance=horizontalDistance-halfWidth; + const double topDistance=qz-floorQ; + const double bottomDistance=floorQ-qPerMetre-qz; + const double verticalDistance=std::max(topDistance,bottomDistance); + if(sideDistance>mouthBlendWidth||verticalDistance>mouthBlendWidth)continue; + const double outside=DeterministicSqrt( + std::max(sideDistance,0.0)*std::max(sideDistance,0.0) + +std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0)); + const double inside=std::min(std::max(sideDistance,verticalDistance),0.0); + const double signedDistance=outside+inside; + mouthSupportDistance=std::min(mouthSupportDistance,signedDistance); } + if(std::isfinite(mouthSupportDistance)) + composed=SmoothMinDistance(composed, + static_cast(mouthSupportDistance/Q), + static_cast(mouthBlendWidth/Q)); }; // Keep the native surface route connected to the entrance. Passage carving // then removes only the parts that overlap the tunnel, whose own tread owns @@ -765,6 +779,19 @@ public: { const auto& passage=ResolvedLayout->passages[passageIndex]; if(passage.controlPoints.size()<2)continue; + const auto supportStratum=[&](std::int32_t strateIndex) + ->const VoxelForgeCore::ResolvedLayoutStratumV2* + { + const auto it=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[strateIndex](const auto& stratum) + {return stratum.strateIndex==strateIndex;}); + return it==ResolvedLayout->strates.end()?nullptr:&*it; + }; + const auto* supportUpper=supportStratum(passage.upperStrateIndex); + const auto* supportLower=supportStratum(passage.lowerStrateIndex); + const bool surfaceToTunnelSupport=supportUpper&&supportLower + &&supportUpper->generatorKind=="SurfaceWorld" + &&supportLower->generatorKind=="TunnelNetwork"; const double firstFloorQ=passage.controlPoints.front().positionSubvoxels[2] -passage.controlPoints.front().radiusSubvoxels+playerCenterAboveFloorQ; const double lastFloorQ=passage.controlPoints.back().positionSubvoxels[2] @@ -880,7 +907,20 @@ public: std::ptrdiff_t bestScanIndex=std::numeric_limits::max(); std::ptrdiff_t bestSupportSegmentIndex=0; bool hasSupportFloor=false; - double bestSupportDensity=0.0; + double bestSupportDistanceSquared=std::numeric_limits::infinity(); + double bestSupportDensity=std::numeric_limits::infinity(); + double bestSupportFloorQ=0.0; + bool bestSupportUsesSignedDistance=false; + std::ptrdiff_t selectedSupportSegmentIndex=-1; + const double lowerMouthDx=qx-passage.lowerMouthSubvoxels[0]; + const double lowerMouthDy=qy-passage.lowerMouthSubvoxels[1]; + const double lowerMouthDz=qz-passage.lowerMouthSubvoxels[2]; + const double lowerMouthDistance=DeterministicSqrt( + lowerMouthDx*lowerMouthDx+lowerMouthDy*lowerMouthDy + +lowerMouthDz*lowerMouthDz)/qPerMetre; + const double lowerMouthT=std::clamp((20.0-lowerMouthDistance)/12.0,0.0,1.0); + const double lowerMouthBlend=lowerMouthT*lowerMouthT + *(3.0-2.0*lowerMouthT); const auto considerCarveSegment=[&](std::size_t segmentIndex,int scanPhase) { const std::ptrdiff_t index=static_cast(segmentIndex); @@ -992,7 +1032,7 @@ public: /distanceToCenter,0.0,1.0):0.0; const float relief=VF_FieldPassageWallRelief(static_cast(X),static_cast(Y), static_cast(Z),static_cast(Owner?Owner->Seed:0)); - const double wallReliefWeight=1.0-0.80*verticalWallAlignment; + const double wallReliefWeight=0.05+0.95*(1.0-verticalWallAlignment); const double roughSignedDistance=bestSignedDistance +static_cast(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight; if(IsPassageShortSpanSegment(passageIndex,static_cast(bestIndex))) @@ -1004,38 +1044,48 @@ public: // Positive density is air; rough rock is cut with full side/ceiling detail and // gentler relief on the gravity-facing wall so the supported tread stays open. composed=std::max(composed,static_cast(-roughSignedDistance/Q)); - double bestSupportDistanceSquared=std::numeric_limits::infinity(); const auto considerSupportSegment=[&](std::ptrdiff_t index) { const auto& segmentA=passage.controlPoints[static_cast(index)]; const auto& segmentB=passage.controlPoints[static_cast(index+1)]; const double dx=static_cast(segmentB.positionSubvoxels[0])-segmentA.positionSubvoxels[0]; const double dy=static_cast(segmentB.positionSubvoxels[1])-segmentA.positionSubvoxels[1]; + const double dz=static_cast(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]; 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, + const bool bridgeDeck=IsPassageShortSpanSegment(passageIndex, static_cast(index)) - ||IsPassageColumnSegment(passageIndex,static_cast(index)) + ||IsPassageColumnSegment(passageIndex,static_cast(index)); + // Keep a small capsule margin at regular treads and restore most of + // the nominal height smoothly as a tread approaches its landing. + double floorOffset=bridgeDeck ?PassageBridgeDeckOffsetMetres(passageIndex,static_cast(index)) - :0.20; + :std::lerp(0.15,0.20,lowerMouthBlend); const double horizontalLengthSq=dx*dx+dy*dy; + const bool overlapSupportJoin=index+12(index)) + &&(index==0||(!IsPassageShortSpanSegment(passageIndex, + static_cast(index-1)))); + const bool smoothSupportJoin=index>0&&overlapSupportJoin + &&!IsPassageShortSpanSegment(passageIndex,static_cast(index+1)); + const double projection=horizontalLengthSq>0.0 + ?((qx-segmentA.positionSubvoxels[0])*dx + +(qy-segmentA.positionSubvoxels[1])*dy)/horizontalLengthSq:bestT; + // Extend tread segments by a fixed distance through route knots. A + // fractional extension made the support width change with segment length. + const double overlapFraction=overlapSupportJoin&&horizontalLengthSq>0.0 + ?std::min(0.5,2.0*qPerMetre/DeterministicSqrt(horizontalLengthSq)):0.0; const double supportT=horizontalLengthSq>0.0 - ?std::clamp(((qx-segmentA.positionSubvoxels[0])*dx - +(qy-segmentA.positionSubvoxels[1])*dy)/horizontalLengthSq,0.0,1.0) - :bestT; + ?std::clamp(projection,-overlapFraction,1.0+overlapFraction):bestT; const double centerX=segmentA.positionSubvoxels[0]+supportT*dx; const double centerY=segmentA.positionSubvoxels[1]+supportT*dy; double centerZ=segmentA.positionSubvoxels[2]+supportT*( static_cast(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]); double radius=segmentA.radiusSubvoxels+supportT*( static_cast(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels); - const bool smoothSupportJoin=index>0&&index+12(index-1)) - &&!IsPassageShortSpanSegment(passageIndex,static_cast(index)) - &&!IsPassageShortSpanSegment(passageIndex,static_cast(index+1)); if(smoothSupportJoin) { const auto& p0=passage.controlPoints[static_cast(index-1)]; @@ -1063,7 +1113,15 @@ public: // Support belongs just below the avatar's foot plane. Queries within // the capsule body, or a neighbouring coil's overlapping bore, must // not turn that tread into a ceiling or mid-air shelf. - if(supportQueryDepth<0.93*qPerMetre||supportQueryDepth>1.30*qPerMetre)return; + // Sparse authored routes use the full signed distance around the top; + // dense routes retain the legacy one-sided tread density. + const double supportSegmentLength=DeterministicSqrt( + horizontalLengthSq+dz*dz)/qPerMetre; + const bool sparseRouteSupport=supportSegmentLength>1.5; + const double minimumSupportQueryDepth=index==selectedSupportSegmentIndex + &&sparseRouteSupport?0.65:0.93; + if(supportQueryDepth1.30*qPerMetre)return; const bool fallbackSupport=qz>=supportPlayerCenterZ-1.50*qPerMetre; const double supportThicknessVoxels=fallbackSupport ?qPerMetre/Q:0.5*qPerMetre/Q; @@ -1072,12 +1130,8 @@ public: // capsule on the neighbouring turn. const bool lowerMouthBodyOverlap=index>=lowerMouthIndex-2 &&supportQueryDepth<0.98*qPerMetre; - const double supportEdgeNoise=entryMouthShoulder?std::clamp( - static_cast(VF_FieldPassageGroundGrain(static_cast(X), - static_cast(Y), - static_cast(Owner?Owner->Seed:0))),-1.0,1.0):0.0; const double supportHalfWidthMetres=lowerMouthBodyOverlap?0.90 - :entryMouthShoulder?1.20+0.20*supportEdgeNoise:1.25; + :entryMouthShoulder?1.20:1.25; const double supportHalfWidth=std::min(radius, supportHalfWidthMetres*qPerMetre); // Keep the walk core within a 0.50 m span with shallow relief, then @@ -1095,30 +1149,50 @@ public: const double localSupportThicknessVoxels=entryMouthShoulder ?supportThicknessVoxels*(1.0-0.72*supportEdgeSmooth) :supportThicknessVoxels; - const double supportEdgeDepth=entryMouthShoulder - ?0.35+0.15*supportEdgeNoise:0.35; + constexpr double supportEdgeDepth=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(ZfloorVoxels+1.0/Q)return; + const double supportPositiveRangeVoxels=sparseRouteSupport + ?0.35*qPerMetre/Q:1.0/Q; + if(ZfloorVoxels+supportPositiveRangeVoxels)return; if(horizontalDistance>supportHalfWidth)return; - const double supportDistanceSq=horizontalDistance*horizontalDistance + const double supportDistanceSquared=horizontalDistance*horizontalDistance +(Z-floorVoxels)*(Z-floorVoxels); - if(supportDistanceSq>=bestSupportDistanceSquared)return; - bestSupportDistanceSquared=supportDistanceSq; + if(supportDistanceSquared>=bestSupportDistanceSquared)return; + bestSupportDistanceSquared=supportDistanceSquared; hasSupportFloor=true; bestSupportSegmentIndex=index; - bestSupportDensity=std::clamp(Z-floorVoxels, - -localSupportThicknessVoxels,0.0); + if(sparseRouteSupport) + { + // Sparse support profiles need both sides of a signed distance + // so the sampled zero crossing stays on the tread top. + const double topDistance=qz-floorQ; + const double bottomDistance=floorQ-localSupportThicknessVoxels*Q-qz; + const double verticalDistance=std::max(topDistance,bottomDistance); + const double sideDistance=horizontalDistance-supportHalfWidth; + const double outside=DeterministicSqrt( + std::max(sideDistance,0.0)*std::max(sideDistance,0.0) + +std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0)); + const double inside=std::min(std::max(sideDistance,verticalDistance),0.0); + bestSupportDensity=(outside+inside)/Q; + } + else + { + bestSupportDensity=std::clamp(Z-floorVoxels, + -localSupportThicknessVoxels,0.0); + } + bestSupportFloorQ=floorQ; + bestSupportUsesSignedDistance=sparseRouteSupport; }; std::ptrdiff_t supportCenterIndex=bestIndex; if(bestIndexvalue;}); @@ -1132,6 +1206,8 @@ public: const std::ptrdiff_t supportBegin=std::max(0,supportCenterIndex-1); const std::ptrdiff_t supportEnd=std::min(lastIndex-1, supportCenterIndex+1); + selectedSupportSegmentIndex=supportCenterIndex>0 + ?supportCenterIndex-1:supportCenterIndex; for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index) considerSupportSegment(index); // Keep route treads inside the native rock envelope. Carving may reopen the @@ -1142,13 +1218,24 @@ public: const bool shortSpanFloorSupport=hasSupportFloor &&IsPassageShortSpanSegment(passageIndex, static_cast(bestSupportSegmentIndex)); - const bool capsuleAllowsSupport=passageTileScopeMatches + const double capsuleClearanceMetres=bestSupportUsesSignedDistance?0.045:0.05; + const double capsuleQueryZ=bestSupportUsesSignedDistance?bestSupportFloorQ:qz; + const bool supportCapsuleProfileAvailable=passageIndex=0.05*qPerMetre; + lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy, + capsuleQueryZ,qPerMetre,capsuleClearanceMetres) + :bestPlayerCapsuleSignedDistance>=capsuleClearanceMetres*qPerMetre; + // Outside bridge and short-span segments, add the tread only where the + // native field already provides a solid envelope; the capsule check keeps + // that support below the player's walk space. if(hasSupportFloor&&(density<0.0f||passageColumnFloorSupport||shortSpanFloorSupport) - &&capsuleAllowsSupport) + &&(capsuleAllowsSupport||bestSupportDensity>=0.0)) + { composed=std::min(composed,static_cast(bestSupportDensity)); + } } return composed; } @@ -2990,7 +3077,8 @@ private: } bool PassagePlayerCapsuleBlocksSupportNode(std::size_t passageIndex,int nodeIndex, - std::size_t first,std::size_t end,double qx,double qy,double qz,double qPerMetre) const + std::size_t first,std::size_t end,double qx,double qy,double qz,double qPerMetre, + double minimumClearanceMetres) const { if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return false; const auto& nodes=PassageNearestNodes[passageIndex]; @@ -3026,26 +3114,26 @@ private: const double vertical=std::max(0.0,std::abs(qz-centerZ)-0.54*qPerMetre); const double distance=DeterministicSqrt(horizontalX*horizontalX+horizontalY*horizontalY +vertical*vertical)-0.34*qPerMetre; - if(distance<0.05*qPerMetre)return true; + if(distance(first),static_cast(last+1), - qx,qy,qz,qPerMetre); + qx,qy,qz,qPerMetre,minimumClearanceMetres); }; return blocked(carveBegin,carveEnd) ||blocked(0,std::min(8,lowerMouthIndex)-1) @@ -4429,7 +4517,159 @@ private: const VoxelForgeCore::FloatingIslandFieldV1 Field( Plan->manifest.recipe,Plan->manifest.plan); if(!Field.isValid())return false; - std::int64_t Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z)); + const std::int64_t qx=ToFixed(X),qy=ToFixed(Y),qz=ToFixed(Z); + std::int64_t Density=Field.densitySubvoxels(qx,qy,qz); + // The anchor-root field keeps broad lobes, but its shortest authored noise + // wavelength is below one metre. Filter that detail only around a FloatingIslands + // passage mouth so Marching Cubes does not expose voxel-sized chips on the crater + // wall. The broad root profile is unchanged outside this local mouth envelope. + double mouthFilterWeight=0.0; + const double qToMetres=static_cast(ResolvedLayout->coreVoxelSizeMicrometres) + /(static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)*1000000.0); + const double densityMetres=std::abs(static_cast(Density))*qToMetres; + if(densityMetres<4.0) + { + bool foundPassageMouth=false; + for(const auto& passage:ResolvedLayout->passages) + { + if(passage.upperStrateIndex!=StrateIndex)continue; + foundPassageMouth=true; + const double dx=(static_cast(qx)-passage.upperMouthSubvoxels[0])*qToMetres; + const double dy=(static_cast(qy)-passage.upperMouthSubvoxels[1])*qToMetres; + const double dz=(static_cast(qz)-passage.upperMouthSubvoxels[2])*qToMetres; + const double distance=std::sqrt(dx*dx+dy*dy+dz*dz); + constexpr double FilterCoreRadiusMetres=8.5; + constexpr double FilterOuterRadiusMetres=12.5; + const double fade=std::clamp((FilterOuterRadiusMetres-distance) + /(FilterOuterRadiusMetres-FilterCoreRadiusMetres),0.0,1.0); + const double weight=fade*fade*(3.0-2.0*fade); + mouthFilterWeight=std::max(mouthFilterWeight,weight); + } + if(!foundPassageMouth) + { + // The passage-free native control retains the stratum route anchors. + // Use its terminal route point as the same local mouth filter origin so + // both native and composed fields receive the same root smoothing. + const auto stratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[StrateIndex](const auto& value) + {return value.strateIndex==StrateIndex;}); + if(stratum!=ResolvedLayout->strates.end()) + { + const VoxelForgeCore::ResolvedLayoutAnchorV2* terminal=nullptr; + for(const auto& anchor:stratum->anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + terminal=&anchor; + if(terminal) + { + const double dx=static_cast(qx-terminal->values[0])*qToMetres; + const double dy=static_cast(qy-terminal->values[1])*qToMetres; + const double dz=static_cast(qz-terminal->values[2])*qToMetres; + const double distance=std::sqrt(dx*dx+dy*dy+dz*dz); + constexpr double FilterCoreRadiusMetres=8.5; + constexpr double FilterOuterRadiusMetres=12.5; + const double fade=std::clamp((FilterOuterRadiusMetres-distance) + /(FilterOuterRadiusMetres-FilterCoreRadiusMetres),0.0,1.0); + mouthFilterWeight=fade*fade*(3.0-2.0*fade); + } + } + } + } + if(mouthFilterWeight>0.0) + { + // Preserve the resolved walk route while filtering the passage and crater field. + const double queryX=static_cast(qx)*qToMetres; + const double queryY=static_cast(qy)*qToMetres; + const double queryZ=static_cast(qz)*qToMetres; + const auto stratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[StrateIndex](const auto& value) + {return value.strateIndex==StrateIndex;}); + if(stratum!=ResolvedLayout->strates.end()) + { + const VoxelForgeCore::ResolvedLayoutAnchorV2* previous=nullptr; + for(const auto& anchor:stratum->anchors) + { + if(anchor.kind!="route_point"||anchor.values.size()<3)continue; + if(previous) + { + const double ax=static_cast(previous->values[0])*qToMetres; + const double ay=static_cast(previous->values[1])*qToMetres; + const double az=static_cast(previous->values[2])*qToMetres; + const double dx=(static_cast(anchor.values[0]) + -previous->values[0])*qToMetres; + const double dy=(static_cast(anchor.values[1]) + -previous->values[1])*qToMetres; + const double lengthSquared=dx*dx+dy*dy; + const double t=lengthSquared>0.0?std::clamp( + ((queryX-ax)*dx+(queryY-ay)*dy)/lengthSquared,0.0,1.0):0.0; + const double centerX=ax+t*dx,centerY=ay+t*dy; + const double centerZ=az+t*(static_cast(anchor.values[2]) + -previous->values[2])*qToMetres; + const double supportRadius=previous->values.size()>=4 + ?static_cast(previous->values[3])*qToMetres:0.75; + const double floorZ=centerZ-1.20; + const double protectedRadius=supportRadius+0.75; + if((queryX-centerX)*(queryX-centerX) + +(queryY-centerY)*(queryY-centerY) + 0.0) + { + constexpr std::int64_t FilterStep=8*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + std::int64_t weightedSum=0; + for(int dz=-1;dz<=1;++dz) + for(int dy=-1;dy<=1;++dy) + for(int dx=-1;dx<=1;++dx) + { + const std::int64_t weight=(dx==0?2:1)*(dy==0?2:1)*(dz==0?2:1); + const std::int64_t sample=(dx==0&&dy==0&&dz==0)?Density: + Field.densitySubvoxels(qx+dx*FilterStep,qy+dy*FilterStep, + qz+dz*FilterStep); + weightedSum+=sample*weight; + } + // A separable [1,2,1]^3 kernel over a 2 m footprint filters sub-voxel + // root detail while retaining the signed field on both sides of the surface. + const double filtered=static_cast(weightedSum)/64.0; + const std::int64_t filteredDensity=static_cast(std::llround( + std::lerp(static_cast(Density),filtered,mouthFilterWeight))); + Density=filteredDensity; + // Marching Cubes can retain an isolated solid with no two-metre axis + // thickness as a handful of disconnected triangles. Remove only shallow + // negative samples surrounded by air at a one-metre probe distance; a + // walk floor or wall has solid material on at least one side of this test. + constexpr double FragmentProbeMetres=1.0; + const std::int64_t fragmentStep=static_cast(std::llround( + FragmentProbeMetres/qToMetres)); + if(mouthFilterWeight>=0.25&&Density<0 + &&static_cast(-Density)*qToMetres::max(); + for(std::size_t axis=0;axis<3&&surroundedByAir;++axis) + for(const int sign:{-1,1}) + { + std::array sample{qx,qy,qz}; + sample[axis]+=sign*fragmentStep; + const std::int64_t neighbor=Field.densitySubvoxels( + sample[0],sample[1],sample[2]); + if(neighbor<=0) + { + surroundedByAir=false; + break; + } + minimumAirDensity=std::min(minimumAirDensity,neighbor); + } + if(surroundedByAir)Density=std::max(Density,minimumAirDensity); + } + } const std::int64_t Bottom=Plan->manifest.recipe.layoutMinimumZSubvoxels; const std::int64_t Top=Plan->manifest.recipe.layoutMaximumZSubvoxels; const std::int64_t Seal=Plan->manifest.recipe.boundarySealSubvoxels; diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/CapturePassageStandardSetL36.py b/Standalone/Research/WorldAssemblerM3/Linux36/CapturePassageStandardSetL36.py new file mode 100644 index 0000000..7acae90 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux36/CapturePassageStandardSetL36.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python3 +"""Export and render the standard four-view passage set for L36.""" +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 = Path(os.environ.get("VOXELFORGE_L36_OUT", ROOT / "Saved/Linux/l36")) +BIN = OUT / "bin" +CAPTURES = Path(os.environ.get("VOXELFORGE_L36_CAPTURE_DIR", + Path(__file__).resolve().parent / "captures")) +HEADLESS = Path(os.environ.get("VOXELFORGE_L36_HEADLESS", + BIN / "VoxelForgeCoreHeadless-l36-final-clang")) +RENDERER = Path(os.environ.get("VOXELFORGE_L36_RENDERER", + BIN / "VoxelForgeOffscreenCapture-l36-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"[L36CaptureRun] 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"[L36StandardCaptureSet] 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"[L36CaptureCamera] 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"[L36CaptureJPEG] 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") diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/ProbeMouthComponentsL36.py b/Standalone/Research/WorldAssemblerM3/Linux36/ProbeMouthComponentsL36.py new file mode 100644 index 0000000..aac1e2a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux36/ProbeMouthComponentsL36.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Inspect connected LOD0 surface components around the FloatingIslands mouth.""" +import argparse +import importlib.util +import math +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py" +SPEC = importlib.util.spec_from_file_location("ring_profile", PROFILE_PATH) +PROFILE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(PROFILE) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("mesh") + parser.add_argument("--center", nargs=2, type=float, default=(5.6, 0.445)) + parser.add_argument("--radius", type=float, default=14.0) + parser.add_argument("--z-min", type=float, default=-66.0) + parser.add_argument("--z-max", type=float, default=-50.0) + parser.add_argument("--minimum-triangles", type=int, default=1) + args = parser.parse_args() + cx, cy = args.center + bounds = (cx - args.radius, cx + args.radius, cy - args.radius, cy + args.radius) + voxel_um, triangles, tiles = PROFILE.read_scene( + args.mesh, 0, args.z_min, args.z_max, bounds) + parent = list(range(len(triangles))) + sizes = [1] * len(triangles) + vertex_owner = {} + + def find(index): + while parent[index] != index: + parent[index] = parent[parent[index]] + index = parent[index] + return index + + def union(a, b): + a, b = find(a), find(b) + if a == b: + return + if sizes[a] < sizes[b]: + a, b = b, a + parent[b] = a + sizes[a] += sizes[b] + + for index, (_, _, verts) in enumerate(triangles): + for point, _ in verts: + key = tuple(round(value, 5) for value in point) + other = vertex_owner.setdefault(key, index) + union(index, other) + + components = {} + for index, (_, _, verts) in enumerate(triangles): + root = find(index) + item = components.setdefault(root, {"count": 0, "min": [math.inf] * 3, + "max": [-math.inf] * 3, "area": 0.0, + "sum": [0.0] * 3}) + item["count"] += 1 + points = [v[0] for v in verts] + for point in points: + for axis in range(3): + item["min"][axis] = min(item["min"][axis], point[axis]) + item["max"][axis] = max(item["max"][axis], point[axis]) + item["sum"][axis] += point[axis] + ab = tuple(points[1][axis] - points[0][axis] for axis in range(3)) + ac = tuple(points[2][axis] - points[0][axis] for axis in range(3)) + cross = (ab[1] * ac[2] - ab[2] * ac[1], + ab[2] * ac[0] - ab[0] * ac[2], + ab[0] * ac[1] - ab[1] * ac[0]) + item["area"] += 0.5 * math.sqrt(sum(value * value for value in cross)) + + ordered = sorted(components.values(), key=lambda item: item["count"], reverse=True) + print(f"[MouthComponentProbe] mesh={args.mesh} voxel_um={voxel_um} lod=0 " + f"tiles={tiles} triangles={len(triangles)} components={len(ordered)} " + f"bounds=({bounds[0]:.3f},{bounds[1]:.3f},{bounds[2]:.3f},{bounds[3]:.3f}) " + f"z={args.z_min:.3f}..{args.z_max:.3f}") + for item in ordered: + if item["count"] < args.minimum_triangles: + continue + centroid = tuple(value / (3 * item["count"]) for value in item["sum"]) + print(f"[MouthComponent] triangles={item['count']} area_m2={item['area']:.4f} " + f"bounds=({','.join(f'{v:.3f}' for v in item['min'])}).." + f"({','.join(f'{v:.3f}' for v in item['max'])}) " + f"centroid=({','.join(f'{v:.3f}' for v in centroid)})") + + +if __name__ == "__main__": + main() diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/ProbeWalkFloorProfileL36.py b/Standalone/Research/WorldAssemblerM3/Linux36/ProbeWalkFloorProfileL36.py new file mode 100644 index 0000000..0e79f2f --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux36/ProbeWalkFloorProfileL36.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Measure the exported walk surface along the passage under the tunnel camera.""" +import argparse +import importlib.util +import json +import math +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +RING_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py" +SPEC = importlib.util.spec_from_file_location("ring_profile", RING_PATH) +RING = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(RING) +CAMERA_RE = re.compile(r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) " + r"target_m=\(([^)]*)\)", re.M) + + +def length(a, b): + return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3))) + + +def fit(values, spacing): + best = (0.0, 0.0) + for step in range(8, 129): + period = step * 0.0625 + omega = 2.0 * math.pi / period + cc = ss = cs = yc = ys = 0.0 + for i, value in enumerate(values): + 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[1]: + best = period, amplitude + return best + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("mesh") + parser.add_argument("layout") + parser.add_argument("--view", default="owner-sw-mid-tunnel") + parser.add_argument("--passage", default="passage/0-1/0") + parser.add_argument("--window", type=float, default=32.0) + parser.add_argument("--spacing", type=float, default=0.125) + parser.add_argument("--amplitude-limit-cm", type=float, default=1.5) + args = parser.parse_args() + log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log" + cameras = {name: (tuple(map(float, camera.split(","))), + tuple(map(float, target.split(",")))) + for name, camera, target in CAMERA_RE.findall( + log.read_text(encoding="utf-8", errors="replace"))} + if args.view not in cameras: + raise SystemExit(f"missing camera target for {args.view}") + camera, target = cameras[args.view] + layout = json.loads(Path(args.layout).read_text(encoding="utf-8")) + scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0) + passage = next(p for p in layout["passages"] if p["stable_id"] == args.passage) + controls = [(tuple(v * scale for v in point["position_q"]), + point["radius_q"] * scale) for point in passage["control_points"]] + lower_mouth = tuple(v * scale for v in passage["lower_mouth_q"]) + end = min(range(len(controls)), key=lambda i: length(controls[i][0], lower_mouth)) + controls = controls[:end + 1] + path = [] + distance = 0.0 + for point in controls: + if path: + distance += length(path[-1][0], point[0]) + path.append((point[0], point[1], distance)) + closest = None + for index, (a, b) in enumerate(zip(path, path[1:])): + delta = tuple(b[0][axis] - a[0][axis] for axis in range(3)) + sq = sum(v * v for v in delta) + t = max(0.0, min(1.0, sum((target[k] - a[0][k]) * delta[k] + for k in range(3)) / sq)) if sq else 0.0 + point = tuple(a[0][k] + t * delta[k] for k in range(3)) + d2 = sum((point[k] - target[k]) ** 2 for k in range(3)) + if closest is None or d2 < closest[0]: + closest = (d2, a[2] + t * (b[2] - a[2]), index, t) + _, center_distance, segment, segment_t = closest + + def interpolate(distance_value): + distance_value = max(0.0, min(path[-1][2], distance_value)) + for a, b in zip(path, path[1:]): + if distance_value <= b[2]: + t = (distance_value - a[2]) / max(1.0e-9, b[2] - a[2]) + point = tuple(a[0][k] + t * (b[0][k] - a[0][k]) for k in range(3)) + radius = a[1] + t * (b[1] - a[1]) + return point, radius + return path[-1][0], path[-1][1] + + half = args.window * 0.5 + start = max(0.0, center_distance - half) + finish = min(path[-1][2], center_distance + half) + count = int((finish - start) / args.spacing) + 1 + positions = [] + expected = [] + for i in range(count): + route_distance = start + i * args.spacing + point, radius = interpolate(route_distance) + positions.append((route_distance - center_distance, point)) + expected.append(point[2] - radius + 0.20) + bounds = (min(p[0] for _, p in positions) - 8.0, + max(p[0] for _, p in positions) + 8.0, + min(p[1] for _, p in positions) - 8.0, + max(p[1] for _, p in positions) + 8.0) + zmin, zmax = min(expected) - 1.5, max(expected) + 1.5 + voxel_um, triangles, tiles = RING.read_scene(args.mesh, 0, zmin, zmax, bounds) + bucket_size = max(2.0, 16.0 * voxel_um / 1_000_000.0) + buckets = RING.build_buckets(triangles, True, bounds, bucket_size) + values = [] + for (_, point), preferred in zip(positions, expected): + hit = RING.sample_surface(buckets, point[0], point[1], True, + bucket_size, preferred) + values.append(hit[0] if hit else None) + valid = [(index, z) for index, z in enumerate(values) if z is not None] + if len(valid) < 0.8 * len(values): + raise SystemExit(f"insufficient mesh floor coverage {len(valid)}/{len(values)}") + residual = [values[i] - expected[i] for i in range(len(values)) + if values[i] is not None] + mean = sum(residual) / len(residual) + period, amplitude = fit([v - mean for v in residual], args.spacing) + adjacent = [abs(values[i] - values[i - 1]) for i in range(1, len(values)) + if values[i] is not None and values[i - 1] is not None] + route_segment_length = path[segment + 1][2] - path[segment][2] + print(f"[WalkFloorProfileProbe] mesh={args.mesh} view={args.view} voxel_um={voxel_um} " + f"tiles={tiles} samples={len(valid)}/{len(values)} window_m={finish-start:.3f} " + f"camera_target_m=({target[0]:.3f},{target[1]:.3f},{target[2]:.3f}) " + f"route_distance_m={center_distance:.3f} nearest_segment={segment} " + f"segment_t={segment_t:.3f} segment_length_m={route_segment_length:.3f} " + f"floor_expected_range_m={min(expected):.3f}..{max(expected):.3f} " + f"residual_range_m={min(residual):.4f}..{max(residual):.4f} " + f"period_m={period:.3f} amplitude_m={amplitude:.5f} " + f"amplitude_cm={amplitude*100:.3f} adjacent_step_max_m=" + f"{max(adjacent, default=0.0):.4f} limit_cm={args.amplitude_limit_cm:.3f} " + f"status={'PASS' if amplitude*100 <= args.amplitude_limit_cm else 'FAIL'}") + out = Path(args.mesh).with_suffix(".walk-floor-profile.csv") + with out.open("w", encoding="utf-8") as stream: + stream.write("distance_m,route_x_m,route_y_m,expected_floor_m,mesh_floor_m,residual_m\n") + for i, (distance_m, point) in enumerate(positions): + z = values[i] + stream.write(f"{distance_m:.3f},{point[0]:.6f},{point[1]:.6f}," + f"{expected[i]:.6f},{'' if z is None else f'{z:.6f}'}," + f"{'' if z is None else f'{z-expected[i]:.6f}'}\n") + + +if __name__ == "__main__": + main() diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md new file mode 100644 index 0000000..369cc3f --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md @@ -0,0 +1,54 @@ +# VoxelForge L36 report + +## Findings and fixes + +The exported L35 walk meshes show real floor washboard: owner-SW repeats at 7.250 m with 2.252 cm amplitude; the upper-SW arrival repeats at 1.438 m with 2.482 cm amplitude. The old tread top was built per segment as a one-sided density clamp, so the tread/native-field crossing moved at the segment joins. The wall-only ripple scan could not see it. Removing shoulder grain alone did not remove the beat. + +Sparse-route treads now overlap by up to 2 m and use bounded signed-distance slabs, including SurfaceWorld-to-TunnelNetwork. Tread seating and capsule/open-air gates remain. The exported v23 meshes measure 7.188 m / 0.067 cm on owner-SW and 6.500 m / 0.057 cm on the upper fixture. The expanded five-window density audit's maximum is 1.08 cm, below its 1.5 cm limit. + +L35's FI mouth mesh had a 42.265° fade-ring normal jump and a detached 8-triangle component of 0.0035394 m². The defect was the short crater-root detail and tread support meeting the native crater field without a smooth signed-distance join; Marching Cubes exposed thin pieces at the seam. V23 filters the local signed field over 2 m while protecting the route, removes thin isolated solids, and smooth-unions the mouth tread with native terrain over 1.5 m. The same mesh check now measures 27.133° against a 35° limit and finds no component at or above 0.0001 m². One 8-triangle speck remains below that floor (0.0000266 m², 9.377 m from the mouth); it is recorded rather than counted as zero. + +## Audits and captures + +- Passage matrix: 24/24 recipe-stamped passes across both owner recipes, four styles, the upper-SW fixture, Clang and GCC. This includes support, walkability, capsule clearance, open air, route bindings, strict validation, mouth profiles, wall ripple and floor ripple. The 2 m / 1.5 m beaded control is rejected; both L35 floor windows fail the 1.5 cm floor limit. +- FI-floor and SW-ceiling branch-locked terrain ring audits pass at LOD0–8. Coverage is 1.000; relief is at least 3.162 m and no flat run or following step is found. L30/L31 ring controls remain negative controls. +- Interface/hole and transition suite: 16/16; zero hole columns and air samples. Strict-layout controls reject 10/10 malformed layouts. L13/L9: 4/4. +- FI mouth fragment/crease control: L35 fails at 42.265° with one qualifying fragment; v23 passes at 27.133° with no qualifying fragments. The L35 mesh is the negative control. +- The standard 12 JPEGs use the L34 fixed cameras and are in [`captures/`](captures/). Sizes are 21,976–43,435 bytes (all below 300 KB). + +## Version, hashes, determinism + +World evaluator version is 23. + +| Recipe | Stable layout | L25 whole-view density / mesh / payload / topology | +|---|---|---| +| SurfaceWorld | `resolved-layout-v3-fnv64:f4c09b442fa40cd7` | `90ca5c612c0723bb / cac3b5f0f68c75f9 / 0a143e28341ed132 / 89f2018345744589` | +| FloatingIslands | `resolved-layout-v3-fnv64:d2922ac7e8a2b491` | `da5eba38f4b14f79 / 8c403d9749b15abb / 8a917fa5b71ab179 / 3ed411c118d1f43f` | + +Owner plan/manifest hashes: `a5e29887e839c5d6 / 57ebed8456c20f06`. Upper-fixture layout/plan/manifest: `resolved-layout-v3-fnv64:fc4e73619037f88c / 463a0f3b72909656 / f7bc6980e67b61c0`. + +Determinism is 30/30: L25 and the 24 m crossing at W1/W8/W12 with both compilers and recipes, plus the upper fixture at W1/W8/W12 with both compilers. Core binary SHA-256: + +- Clang: `f8342a7bb48bf460550bd2d4707a67156ebf02a5e5a247edd07e8de0028f6be2` +- GCC: `37d28f3c8a816f485d2bb9e40c1cc37850e6f2d3888772573d11d7b101f6b7d5` + +Ten interleaved L35/L36 pairs per recipe passed. Median whole-view time: SurfaceWorld 1.728 s → 1.752 s (+1.42%; paired median +0.66%, MAD 1.16%); FloatingIslands 16.645 s → 16.630 s (-0.09%; paired median +0.35%, MAD 0.29%). Both changes are within measured run noise. + +## Failed and retried runs + +- Three compile fixes: `build-l36-clang-retry2.log` had a wall-relief call mismatch (retry3 compiled); `build-l36-floor-coordinates.log` had an invalid assignment (retry1 compiled); `build-l36-sweep-closest.log` assigned to a const value (retry1 compiled). +- Passage audit revisions: `run-passage-audits.log` ended after four cases; `run-passage-audits-final.log` was 14/24, `-r2` 18/24, `-r3` 22/24, and `-r4` stopped after eight cases on a failing broad tread candidate. `-r5`, `-r6`, and the final v23 run were 24/24. Earlier support candidates failed capsule, open-air, or floor-ripple checks; their `preaudit-*`, `preflight-*`, and `audit-*` logs remain under `Saved/Linux/l36/`. +- Mouth trials `mouth-smin1m`, `mouth-route-smoothunion`, `mouth-airward-filter`, and the 1.5 m filter variants left 36–44° jumps or a residual speck. The final 2 m field filter plus 1.5 m SDF union is the passing candidate. The older supplemental 20° fade probe remains stricter than this round's 35° gate; the final 27.133° jump would still exceed 20°. The L35 42.265° control is detected by the final check. +- Initial floor-window placement at fraction .49 missed the upper-fixture route target; .457 covered it. Initial ring exports (`ring-export-*-v23.log`) used a render-distance override; early `ring-*-v23-camera*.log`, `ring-*-v23-branch32.log` and `ring-*-v23-current32.log` scans used the wrong center/radius and had missing coverage or only 1.857 m relief. Re-exporting without that override and using the recorded branch-locked windows produced the LOD0–8 passes. `ring-sw-lod0-v23-branchlocked-full.log` had a path typo and was immediately rerun as `ring-sw-lod0-v23-branchlocked-final.log`. +- The first five cost pairs were repeated because the SurfaceWorld median was close to noise; all ten pairs per recipe passed. No run exceeded 30 minutes and no step was stopped for a 15-minute stall. All runs used CPUs 0–11, a 6 GiB address-space cap and at most 12 workers; governor unchanged. + +No Unreal files changed. No commit, push or stash was made. + +## Orchestrator review (Claude) + +- Accepted: the floor washboard is gone in owner-SW mid-tunnel (floor now reads as irregular natural ground). Cause + measured (per-segment one-sided tread clamps moving the crossing at joins), floor ripple audit added with L35 as a + failing control. The FI mouth shards and detached fragment are gone. Evaluator v23. +- Not accepted as done: **the FI mouth tread now reads as a trapezoid platform**: straight edges, planar faces, a + clear man-made shape inside the crater. Also, the 35° crease gate was chosen during the round, while the earlier + probe used 20°; the final 27° would fail that. L37 targets the shape itself. diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/RunCostL36.py b/Standalone/Research/WorldAssemblerM3/Linux36/RunCostL36.py new file mode 100644 index 0000000..2119094 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux36/RunCostL36.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Run five interleaved whole-view cost pairs against the L35 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/l36" +BINARIES = { + "l35": ROOT / "Saved/Linux/l35/bin/VoxelForgeCoreHeadless-l35-final-clang", + "l36": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang", +} +ORDERS = (("l35", "l36"), ("l36", "l35"), ("l35", "l36"), + ("l36", "l35"), ("l35", "l36")) +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"[L36CostRun] 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["l35"]) + current = statistics.median(by_build["l36"]) + 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["l36"] - values["l35"]) / values["l35"] * 100.0) + mad = statistics.median(abs(value - statistics.median(paired)) for value in paired) + print(f"[L36CostSummary] recipe={recipe} pairs=5 median_l35_s={baseline:.3f} " + f"median_l36_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"[L36Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}", + flush=True) +raise SystemExit(1 if failed or len(rows) != 20 else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/RunDeterminismL36.py b/Standalone/Research/WorldAssemblerM3/Linux36/RunDeterminismL36.py new file mode 100644 index 0000000..0cba5af --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux36/RunDeterminismL36.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Check L25, 24 m crossing and upper-SW fixture determinism for L36.""" +import csv +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l36" +OWNER = ROOT / "Standalone/data/owner_world.json" +FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json" +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-l36-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"[L36DeterminismRun] 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-l36.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"[L36DeterminismSummary] 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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/RunInvariantAuditsL36.py b/Standalone/Research/WorldAssemblerM3/Linux36/RunInvariantAuditsL36.py new file mode 100644 index 0000000..2f08766 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux36/RunInvariantAuditsL36.py @@ -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/l36" +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-l36-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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/RunLegacyAuditsL36.py b/Standalone/Research/WorldAssemblerM3/Linux36/RunLegacyAuditsL36.py new file mode 100644 index 0000000..83036b6 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux36/RunLegacyAuditsL36.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Run the L13 route parity and L9 uniform-block proof audits on L36.""" +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l36" +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-l36-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"[L36LegacyAuditRun] 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"[L36LegacyAuditSummary] 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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/RunMouthFragmentAuditL36.py b/Standalone/Research/WorldAssemblerM3/Linux36/RunMouthFragmentAuditL36.py new file mode 100644 index 0000000..e5e8edd --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux36/RunMouthFragmentAuditL36.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Compare the FI fade-ring crease against L35 using the same surface samples.""" +import argparse +import importlib.util +import math +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +RING_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py" +SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_PATH) +RING = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(RING) + +CENTER = (6.298233, 1.137233) +INNER, OUTER, STEP = 5.5, 9.5, 0.125 +HEIGHT_MIN, HEIGHT_MAX, PREFERRED = -80.0, -40.0, -56.0 +NORMAL_LIMIT_DEG = 35.0 +COVERAGE_MIN = 0.90 +FRAGMENT_MAX_TRIANGLES = 64 +FRAGMENT_MIN_AREA_M2 = 0.0001 +FRAGMENT_MAX_AREA_M2 = 0.05 +FRAGMENT_MAX_DISTANCE_M = 12.0 + + +def small_components(triangles): + parent = list(range(len(triangles))) + sizes = [1] * len(triangles) + owners = {} + + def find(index): + while parent[index] != index: + parent[index] = parent[parent[index]] + index = parent[index] + return index + + def union(a, b): + a, b = find(a), find(b) + if a == b: + return + if sizes[a] < sizes[b]: + a, b = b, a + parent[b] = a + sizes[a] += sizes[b] + + for index, (_, _, vertices) in enumerate(triangles): + for point, _ in vertices: + key = tuple(round(value, 5) for value in point) + other = owners.setdefault(key, index) + union(index, other) + + components = {} + for index, (_, _, vertices) in enumerate(triangles): + root = find(index) + item = components.setdefault(root, {"count": 0, "area": 0.0, "sum": [0.0] * 3}) + item["count"] += 1 + points = [vertex[0] for vertex in vertices] + for point in points: + for axis in range(3): + item["sum"][axis] += point[axis] + ab = tuple(points[1][axis] - points[0][axis] for axis in range(3)) + ac = tuple(points[2][axis] - points[0][axis] for axis in range(3)) + cross = (ab[1] * ac[2] - ab[2] * ac[1], + ab[2] * ac[0] - ab[0] * ac[2], + ab[0] * ac[1] - ab[1] * ac[0]) + item["area"] += 0.5 * math.sqrt(sum(value * value for value in cross)) + + cx, cy = CENTER + small = [] + below_floor = [] + for item in components.values(): + centroid = tuple(value / (3 * item["count"]) for value in item["sum"]) + distance = math.hypot(centroid[0] - cx, centroid[1] - cy) + if (item["count"] <= FRAGMENT_MAX_TRIANGLES + and item["area"] <= FRAGMENT_MAX_AREA_M2 + and distance <= FRAGMENT_MAX_DISTANCE_M): + record = (item["count"], item["area"], distance, centroid) + (below_floor if item["area"] < FRAGMENT_MIN_AREA_M2 else small).append(record) + return len(components), small, below_floor + + +def audit(path): + cx, cy = CENTER + bounds = (cx - OUTER, cx + OUTER, cy - OUTER, cy + OUTER) + voxel_um, triangles, tiles = RING.read_scene( + str(path), 0, HEIGHT_MIN, HEIGHT_MAX, bounds) + component_count, fragments, below_floor = small_components(triangles) + print(f"[MouthFragmentComponents] mesh={path.name} components={component_count} " + f"small_near_mouth={len(fragments)} subthreshold_near_mouth={len(below_floor)} " + f"limits=(triangles<={FRAGMENT_MAX_TRIANGLES}," + f"area={FRAGMENT_MIN_AREA_M2:.4f}..{FRAGMENT_MAX_AREA_M2:.3f}m2," + f"distance<={FRAGMENT_MAX_DISTANCE_M:.1f}m) " + f"first={fragments[0] if fragments else None} " + f"subthreshold={below_floor[0] if below_floor else None} " + f"status={'PASS' if not fragments else 'FAIL'}") + cell = max(2.0, 16.0 * voxel_um / 1_000_000.0) + buckets = RING.build_buckets(triangles, True, bounds, cell) + lines = [] + count = round((OUTER - INNER) / STEP) + for degrees in (135.0, 180.0, 225.0, 270.0, 315.0): + angle = math.radians(degrees) + lines.append((f"radial-{degrees:.0f}", [ + (cx + radius * math.cos(angle), cy + radius * math.sin(angle)) + for radius in (INNER + i * STEP for i in range(count + 1))])) + tangent_count = round(8.0 / STEP) + for degrees in (225.0, 315.0): + angle = math.radians(degrees) + normal = (math.cos(angle), math.sin(angle)) + tangent = (-normal[1], normal[0]) + lines.append((f"tangent-{degrees:.0f}", [ + (cx + 6.5 * normal[0] + (-4.0 + i * STEP) * tangent[0], + cy + 6.5 * normal[1] + (-4.0 + i * STEP) * tangent[1]) + for i in range(tangent_count + 1)])) + + max_angle = 0.0 + max_pair = None + min_coverage = 1.0 + for name, points in lines: + samples = [RING.sample_surface(buckets, x, y, True, cell, PREFERRED) + for x, y in points] + coverage = sum(value is not None for value in samples) / len(samples) + min_coverage = min(min_coverage, coverage) + line_max = 0.0 + for index in range(1, len(samples)): + prior, current = samples[index - 1], samples[index] + if prior is None or current is None: + continue + dot = max(-1.0, min(1.0, sum(a * b for a, b in zip(prior[1], current[1])))) + delta = math.degrees(math.acos(dot)) + if delta > line_max: + line_max = delta + if delta > max_angle: + max_angle = delta + max_pair = (name, points[index - 1], prior[0], points[index], current[0]) + print(f"[MouthFadeLine] mesh={path.name} line={name} coverage={coverage:.3f} " + f"max_normal_jump_deg={line_max:.6f}") + normal_pass = max_angle <= NORMAL_LIMIT_DEG and min_coverage >= COVERAGE_MIN + passed = normal_pass and not fragments + print(f"[MouthFadeAudit] mesh={path.name} tiles={tiles} lines={len(lines)} " + f"coverage_min={min_coverage:.3f} coverage_limit={COVERAGE_MIN:.2f} " + f"max_normal_jump_deg={max_angle:.6f} normal_limit_deg={NORMAL_LIMIT_DEG:.1f} " + f"fragments={len(fragments)} " + f"worst_pair={max_pair} status={'PASS' if passed else 'FAIL'}") + return passed, max_angle + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--control", type=Path, + default=ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh") + parser.add_argument("--current", type=Path, + default=ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh") + args = parser.parse_args() + control = args.control if args.control.is_absolute() else ROOT / args.control + current = args.current if args.current.is_absolute() else ROOT / args.current + control_ok, control_max = audit(control) + current_ok, current_max = audit(current) + expected = not control_ok and current_ok and control_max > NORMAL_LIMIT_DEG + print(f"[MouthFadeNegativeControl] control_max_deg={control_max:.6f} " + f"control_detected={not control_ok} current_max_deg={current_max:.6f} " + f"status={'PASS' if expected else 'FAIL'}") + raise SystemExit(0 if expected else 1) + + +if __name__ == "__main__": + main() diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/RunPassageAuditsL36.py b/Standalone/Research/WorldAssemblerM3/Linux36/RunPassageAuditsL36.py new file mode 100644 index 0000000..6c09af4 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux36/RunPassageAuditsL36.py @@ -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/l36" +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-l36-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 floor_beaded_negative_control=DETECTED max_floor_amplitude_m=0\.015 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"[L36PassageAuditRun] 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"[L36PassageAuditSummary] 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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-fi-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-fi-arrival.jpg new file mode 100644 index 0000000..34e5501 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-fi-arrival.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-fi-ground-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-fi-ground-mouth.jpg new file mode 100644 index 0000000..e28cad3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-fi-ground-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-fi-mid-tunnel.jpg b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-fi-mid-tunnel.jpg new file mode 100644 index 0000000..25a04a2 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-fi-mid-tunnel.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-fi-rock-entry.jpg b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-fi-rock-entry.jpg new file mode 100644 index 0000000..66abbbd Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-fi-rock-entry.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-sw-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-sw-arrival.jpg new file mode 100644 index 0000000..2b7fde5 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-sw-arrival.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-sw-ground-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-sw-ground-mouth.jpg new file mode 100644 index 0000000..ea88513 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-sw-ground-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-sw-mid-tunnel.jpg b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-sw-mid-tunnel.jpg new file mode 100644 index 0000000..3d87dc0 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-sw-mid-tunnel.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-sw-rock-entry.jpg b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-sw-rock-entry.jpg new file mode 100644 index 0000000..1184f4f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux36/captures/owner-sw-rock-entry.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/captures/upper-sw-fixture-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux36/captures/upper-sw-fixture-arrival.jpg new file mode 100644 index 0000000..b1e9194 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux36/captures/upper-sw-fixture-arrival.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/captures/upper-sw-fixture-ground-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux36/captures/upper-sw-fixture-ground-mouth.jpg new file mode 100644 index 0000000..65aa011 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux36/captures/upper-sw-fixture-ground-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/captures/upper-sw-fixture-mid-tunnel.jpg b/Standalone/Research/WorldAssemblerM3/Linux36/captures/upper-sw-fixture-mid-tunnel.jpg new file mode 100644 index 0000000..d1c0c16 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux36/captures/upper-sw-fixture-mid-tunnel.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux36/captures/upper-sw-fixture-rock-entry.jpg b/Standalone/Research/WorldAssemblerM3/Linux36/captures/upper-sw-fixture-rock-entry.jpg new file mode 100644 index 0000000..486ce7b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux36/captures/upper-sw-fixture-rock-entry.jpg differ diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 4553b8b..023c157 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -817,8 +817,15 @@ namespace StandaloneStreamBenchmark double periodMetres=0.0,amplitudeMetres=0.0,localPeakAmplitudeMetres=0.0; double coherentPeriodMetres=0.0,coherentAmplitudeMetres=0.0; double controlAmplitudeMetres=0.0; + double floorPeriodMetres=0.0,floorAmplitudeMetres=0.0; + double floorControlAmplitudeMetres=0.0; + double floorCoherentPeriodMetres=0.0,floorCoherentAmplitudeMetres=0.0; + std::vector> floorPeakProfile; + double floorPeakWindowStartMetres=0.0; std::uint64_t samples=0,expectedSamples=0; + std::uint64_t floorSamples=0,floorExpectedSamples=0; bool controlDetected=false,passed=false; + bool floorControlDetected=false; }; constexpr double MaximumFlatRunMetres=8.0; constexpr double MinimumWallReliefMetres=0.20; @@ -1131,6 +1138,7 @@ namespace StandaloneStreamBenchmark constexpr double MaximumPeriodMetres=8.0; constexpr double PeriodStepMetres=0.0625; constexpr double RippleAmplitudeLimitMetres=0.035; + constexpr double FloorRippleAmplitudeLimitMetres=0.015; struct FPathPoint { double x=0.0,y=0.0,z=0.0,radius=0.0,distance=0.0; @@ -1218,6 +1226,26 @@ namespace StandaloneStreamBenchmark values[i]-mean-slope*x}); } }; + const auto appendDetrendedPairs=[]( + const std::vector>& values, + std::vector>& output) + { + if(values.size()<64)return; + double meanX=0.0,meanY=0.0; + for(const auto& value:values){meanX+=value[0];meanY+=value[1];} + meanX/=static_cast(values.size()); + meanY/=static_cast(values.size()); + double xy=0.0,xx=0.0; + for(const auto& value:values) + { + const double x=value[0]-meanX; + xy+=x*(value[1]-meanY);xx+=x*x; + } + const double slope=xx>0.0?xy/xx:0.0; + for(const auto& value:values) + output.push_back({value[0],value[1]-meanY + -slope*(value[0]-meanX)}); + }; std::vector beadControl; beadControl.reserve(static_cast(WindowLengthMetres/SampleSpacingMetres)+1); constexpr double BeadSpacingMetres=1.5; @@ -1234,20 +1262,64 @@ namespace StandaloneStreamBenchmark double ignoredPeriod=0.0; audit.controlAmplitudeMetres=sinusoidAmplitude(beadSamples,ignoredPeriod); audit.controlDetected=audit.controlAmplitudeMetres>=RippleAmplitudeLimitMetres; - const std::array windowStarts{{0.03,0.31,0.61,0.91}}; + audit.floorControlAmplitudeMetres=audit.controlAmplitudeMetres; + audit.floorControlDetected=audit.floorControlAmplitudeMetres + >=FloorRippleAmplitudeLimitMetres; + const std::array windowStarts{{0.03,0.31,0.457,0.61,0.91}}; std::vector> coherentSamples; + std::vector> coherentFloorSamples; for(const double fraction:windowStarts) { const double start=(total-WindowLengthMetres)*fraction; std::vector radii; radii.reserve(static_cast(WindowLengthMetres/SampleSpacingMetres)+1); + std::vector> floorResiduals; + floorResiduals.reserve(static_cast(WindowLengthMetres/ + SampleSpacingMetres)+1); + std::vector> floorDebugSamples; + floorDebugSamples.reserve(static_cast(WindowLengthMetres/ + SampleSpacingMetres)+1); for(std::size_t sample=0; sample<=static_cast(WindowLengthMetres/SampleSpacingMetres); ++sample) { ++audit.expectedSamples; + ++audit.floorExpectedSamples; const double distance=start+static_cast(sample)*SampleSpacingMetres; const auto pose=interpolate(distance); + // Recover the composed walk surface from its zero crossing. + // The support strip is segment-built and is not itself a true + // signed-distance field, so a fixed centerline radius misses + // the stepped top that the player actually walks on. + const double expectedFloor=pose.z-pose.radius+0.20; + constexpr double FloorProbeStepMetres=0.25; + constexpr int FloorProbeCount=7; + double previousDensity=0.0,previousZ=0.0; + bool havePrevious=false,haveFloor=false; + double bestFloor=0.0,bestDistance=std::numeric_limits::infinity(); + for(int vertical=0;vertical(Field.densityAt({ + pose.x/coreMetres,pose.y/coreMetres,z/coreMetres}))*coreMetres; + if(havePrevious&&previousDensity<0.0&&density>=0.0) + { + const double denominator=density-previousDensity; + const double crossing=denominator>1.0e-12 + ?previousZ+(z-previousZ)*(-previousDensity)/denominator:z; + const double candidateDistance=std::abs(crossing-expectedFloor); + if(candidateDistance=static_cast(WindowLengthMetres/ + SampleSpacingMetres)*3/4) + { + std::vector> floorWindowSamples; + appendDetrendedPairs(floorResiduals,floorWindowSamples); + floorWindowAmplitude=sinusoidAmplitude(floorWindowSamples, + floorWindowPeriod); + if(floorWindowAmplitude>audit.floorAmplitudeMetres) + { + audit.floorAmplitudeMetres=floorWindowAmplitude; + audit.floorPeriodMetres=floorWindowPeriod; + audit.floorPeakWindowStartMetres=start; + audit.floorPeakProfile=std::move(floorDebugSamples); + } + coherentFloorSamples.insert(coherentFloorSamples.end(), + floorWindowSamples.begin(),floorWindowSamples.end()); + } + const double floorWindowCoverage=static_cast(floorResiduals.size()) + /(WindowLengthMetres/SampleSpacingMetres+1.0); + double floorResidualMinimum=0.0,floorResidualMaximum=0.0; + if(!floorResiduals.empty()) + { + const auto extrema=std::minmax_element(floorResiduals.begin(), + floorResiduals.end(),[](const auto& a,const auto& b) + {return a[1]0 ?static_cast(audit.samples)/audit.expectedSamples:0.0; + const double floorCoverage=audit.floorExpectedSamples>0 + ?static_cast(audit.floorSamples)/audit.floorExpectedSamples:0.0; audit.passed=audit.controlDetected&&coverage>=0.75 - &&audit.localPeakAmplitudeMetres=0.75 + &&audit.floorAmplitudeMetres(rippleAudit.samples), static_cast(rippleAudit.expectedSamples), rippleAudit.controlAmplitudeMetres,rippleAudit.controlDetected?"true":"false", + rippleAudit.floorPeriodMetres,rippleAudit.floorAmplitudeMetres, + rippleAudit.floorAmplitudeMetres*100.0, + rippleAudit.floorCoherentPeriodMetres,rippleAudit.floorCoherentAmplitudeMetres, + static_cast(rippleAudit.floorSamples), + static_cast(rippleAudit.floorExpectedSamples), + rippleAudit.floorControlAmplitudeMetres, + rippleAudit.floorControlDetected?"true":"false", rippleAudit.passed?"PASS":"FAIL"); + for(std::size_t sample=0;sample(openAirAudit.supportSamples), static_cast(openAirAudit.violations), @@ -2340,10 +2470,11 @@ 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", + std::printf("[PassageRippleAuditSummary] passed=%llu/%llu synthetic_spheres_negative_control=%s max_amplitude_m=0.035 floor_beaded_negative_control=%s max_floor_amplitude_m=0.015 status=%s\n", static_cast(ripplePasses), static_cast(passageCount), rippleControlsDetected==passageCount?"DETECTED":"MISSED", + floorRippleControlsDetected==passageCount?"DETECTED":"MISSED", ripplePasses==passageCount?"PASS":"FAIL"); const bool auditPassed=passageCount>0&&passagePasses==passageCount &&naturalPasses==passageCount&&l23SmoothWallDetected