From f23ab7b9fa3bbcddb854973d229608f83deca0d8 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Wed, 21 Oct 2026 07:42:16 +0200 Subject: [PATCH] wip(L28): passage columns/routes culled per tile (BVH); cost back near L24, bit-identical to L27 GenerateMesh scopes passage queries to the tile's sample bounds with a feature AABB BVH plus per-passage segment BVHs; L9 proof boxes use a passage-bound BVH. Whole view vs L24: FI ~0%, SW +3.7% (reviewer interleaved). All hashes equal L27; all passage/route/proof audits pass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX --- ARCHITECTURE.md | 17 + CODEMAP.md | 9 + Standalone/Core/src/ShapeWorldInternal.h | 906 ++++++++++++++++-- Standalone/Core/src/VoxelForgeCore.cpp | 6 + .../WorldAssemblerM3/Linux28/REPORT.md | 125 +++ 5 files changed, 976 insertions(+), 87 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a913b24..d12f2a8 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2034,6 +2034,23 @@ adapter edit is required. The L26 report contains the eight-style table, hashes, [`Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md) records the final evidence. +### 8.45 Tile-bounded passage evaluation — World Assembler M3 Linux L28 + +Each mesh tile scopes passage composition to the inclusive density-sample AABB, including the +mesher's normal halo. A conservative feature AABB BVH selects route segments, columns and mouth +support for that tile. A per-passage nearest-segment BVH bounds the long helix search; a separate +passage-bound BVH rejects far L9 proof boxes. An unindexed native-only proof object uses a conservative +linear fallback. No field, union order, serialized layout or recipe version changes. + +The column's exact complex SDF is evaluated only in XY/Z feature candidates. A cheap density-aware +radial guard remains for samples sharing the column's Z slab: a positive column distance can still +lower positive base density, so skipping that guard needs an exact bound on the base field. This is +the remaining SurfaceWorld cost noted in the Linux L28 report. The passage candidate lists and +proof bounds preserve the L27 output. + +[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md) +records counts, parity, costs and audit caveats. + ## 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 092e38d..5c22097 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -1208,6 +1208,15 @@ The shared evaluator is version 13; FloatingIslands geometry remains version 7 a schema V3 is unchanged. See ARCHITECTURE §8.44 and [`Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md). +### 3.28 Tile-bounded passage evaluation — World Assembler M3 Linux L28 + +| Symbol | Canonical file | Role | +|---|---|---| +| FScopedPassageTileQueries / BuildPassageFeatureTileList / ResolvedPassageIntersectsBox | Standalone/Core/src/ShapeWorldInternal.h; scope entered by GenerateMesh in Standalone/Core/src/VoxelForgeCore.cpp | Builds conservative per-tile route, column and mouth-support candidates from an AABB BVH; long paths use a nearest-segment BVH and L9 box checks use a separate passage-bound BVH. Empty native-only proof indexes fall back conservatively. The density arithmetic and serialized versions stay unchanged. | + +See ARCHITECTURE §8.45 and +[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md). + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index cbd207d..67302ce 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -44,6 +44,65 @@ public: const std::vector& GetPassageVoidCrossings() const {return PassageVoidCrossings;} + class FScopedPassageTileQueries + { + public: + FScopedPassageTileQueries(const FStandaloneShapeWorld& shape, + const std::array& minimumSubvoxels, + const std::array& maximumSubvoxels) + :shape_(&shape),minimumSubvoxels_(minimumSubvoxels), + maximumSubvoxels_(maximumSubvoxels),previous_(ActivePassageTileQueries) + { + featureIndices_=shape.BuildPassageFeatureTileList( + minimumSubvoxels_,maximumSubvoxels_); + passageRoutes_.assign(shape.ResolvedLayout?shape.ResolvedLayout->passages.size():0,0); + passageColumns_=passageRoutes_; + passageMouthSupports_=passageRoutes_; + for(const auto& crossing:shape.PassageVoidCrossings) + if(crossing.needsColumn + &&crossing.bottomZSubvoxels<=maximumSubvoxels_[2] + &&crossing.topZSubvoxels>=minimumSubvoxels_[2]) + columnZOverlap_=true; + for(const std::uint32_t featureIndex:featureIndices_) + { + if(featureIndex>=shape.PassageTileFeatures.size())continue; + const auto& feature=shape.PassageTileFeatures[featureIndex]; + if(feature.passageIndex>=passageRoutes_.size())continue; + if(feature.bColumn)passageColumns_[feature.passageIndex]=1; + else if(feature.bMouthSupport)passageMouthSupports_[feature.passageIndex]=1; + else + { + passageRoutes_[feature.passageIndex]=1; + routeFeatureOverlap_=true; + } + } + ActivePassageTileQueries=this; + } + ~FScopedPassageTileQueries(){ActivePassageTileQueries=previous_;} + FScopedPassageTileQueries(const FScopedPassageTileQueries&)=delete; + FScopedPassageTileQueries& operator=(const FScopedPassageTileQueries&)=delete; + + bool matches(const FStandaloneShapeWorld& shape) const{return shape_==&shape;} + bool hasFeature(std::size_t passageIndex,bool columnFeature) const + { + const auto& candidates=columnFeature?passageColumns_:passageRoutes_; + return passageIndex minimumSubvoxels_,maximumSubvoxels_; + std::vector featureIndices_; + std::vector passageRoutes_,passageColumns_,passageMouthSupports_; + bool routeFeatureOverlap_=false,columnZOverlap_=false; + const FScopedPassageTileQueries* previous_; + }; + bool IsResolvedPassageColumnSample(std::size_t PassageIndex,std::size_t SegmentIndex, const VoxelForgeCore::Vec3d& Position) const { @@ -126,6 +185,12 @@ public: { if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X) ||!std::isfinite(Y)||!std::isfinite(Z))return density; + const auto* passageTileQueries=ActivePassageTileQueries; + const bool passageTileScopeMatches=passageTileQueries + &&passageTileQueries->matches(*this); + if(passageTileScopeMatches&&density>=0.0f + &&!passageTileQueries->hasRouteFeatureOverlap() + &&!passageTileQueries->hasColumnZOverlap())return density; constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double qx=static_cast(X)*Q,qy=static_cast(Y)*Q,qz=static_cast(Z)*Q; const double qPerMetre=Q*1000000.0/std::max(1,ResolvedLayout->coreVoxelSizeMicrometres); @@ -190,9 +255,14 @@ public: // Keep the native surface route connected to the entrance. Passage carving // then removes only the parts that overlap the tunnel, whose own tread owns // the interior ramp. - for(const auto& passage:ResolvedLayout->passages) - composeRouteMouthSupport(passage.upperStrateIndex, - passage.upperMouthSubvoxels,true); + for(std::size_t passageIndex=0;passageIndexpassages.size();++passageIndex) + { + const auto& passage=ResolvedLayout->passages[passageIndex]; + const bool tileHasSupport=passageTileScopeMatches; + if(!tileHasSupport||ActivePassageTileQueries->hasMouthSupport(passageIndex)) + composeRouteMouthSupport(passage.upperStrateIndex, + passage.upperMouthSubvoxels,true); + } for(std::size_t passageIndex=0;passageIndexpassages.size();++passageIndex) { const auto& passage=ResolvedLayout->passages[passageIndex]; @@ -205,9 +275,31 @@ public: { if(!crossing.needsColumn||crossing.passageIndex!=passageIndex ||qz>crossing.topZSubvoxels||qzhasFeature(passageIndex,true); + if(tileProvesColumnExterior) + { + if(composed<0.0f)continue; + const double dx=qx-crossing.centerXSubvoxels; + const double dy=qy-crossing.centerYSubvoxels; + const double horizontalSquared=dx*dx+dy*dy; + const double maximumRadius=static_cast(crossing.maximumRadiusSubvoxels); + const double exteriorLimit=maximumRadius + +static_cast(composed)*Q+1.0e-4; + if(horizontalSquared>exteriorLimit*exteriorLimit)continue; + columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z); + } + else + columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z); composed=std::min(composed,static_cast(columnDistance/Q)); } + const bool tilePassageFree=passageTileScopeMatches + &&!ActivePassageTileQueries->hasFeature(passageIndex,false); + if(composed>=0.0f&&tilePassageFree) + { + continue; + } if(qz>firstFloorQ+2.0*passage.mouthRadiusSubvoxels ||qz(lowerMouthIndex-1, (carveLow-passage.controlPoints.begin())+1); + const std::ptrdiff_t mouthSearchBegin=std::max(0,lowerMouthIndex-8); + const std::ptrdiff_t mouthSearchEnd=std::min(lastIndex, + lowerMouthIndex+8); + if(density<0.0f&&tilePassageFree + &&!PassageMayAffectDensity(passageIndex,carveBegin,carveEnd, + lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz, + -static_cast(composed)*Q+0.24*qPerMetre)) + { + continue; + } + // For a positive source sample, the tube can only change it within relief reach; + // the supported column tread is within 2.25 m of its centerline. A short-span + // bridge's positive exterior SDF is not bounded this way, so keep its full query. + // Pour un échantillon source positif, le tube ne change le champ qu'à portée du relief ; + // le plancher du pilier reste à 2,25 m de son axe. Le SDF extérieur d'une courte passerelle + // n'a pas cette borne, donc sa requête complète est conservée. + const bool canCullPositiveRoute=passageTileScopeMatches&&density>=0.0f + &&!PassageHasShortSpanSegments(passageIndex); + if(canCullPositiveRoute&&!PassageMayAffectDensity(passageIndex,carveBegin,carveEnd, + lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz, + std::max(2.25*qPerMetre,-static_cast(composed)*Q + +0.24*qPerMetre)))continue; double bestSignedDistance=std::numeric_limits::infinity(); double bestPlayerCapsuleSignedDistance=std::numeric_limits::infinity(); double bestT=0.0; std::ptrdiff_t bestIndex=0; + int bestScanPhase=std::numeric_limits::max(); + std::ptrdiff_t bestScanIndex=std::numeric_limits::max(); std::ptrdiff_t bestSupportSegmentIndex=0; bool hasSupportFloor=false; double bestSupportDensity=0.0; - const auto considerCarveSegment=[&](std::ptrdiff_t index) + const auto considerCarveSegment=[&](std::size_t segmentIndex,int scanPhase) { + const std::ptrdiff_t index=static_cast(segmentIndex); const auto& a=passage.controlPoints[static_cast(index)]; const auto& b=passage.controlPoints[static_cast(index+1)]; const double ax=static_cast(a.positionSubvoxels[0]); @@ -254,45 +371,64 @@ public: const double lengthSq=dx*dx+dy*dy+dz*dz; if(lengthSq<=0.0)return; const double t=std::clamp(((qx-ax)*dx+(qy-ay)*dy+(qz-az)*dz)/lengthSq,0.0,1.0); + if(!passageTileScopeMatches) + { + const double horizontalLengthSq=dx*dx+dy*dy; + const double playerT=horizontalLengthSq>0.0 + ?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):t; + const double playerDx=qx-(ax+playerT*dx); + const double playerDy=qy-(ay+playerT*dy); + const double playerRadius=static_cast(a.radiusSubvoxels) + +playerT*(b.radiusSubvoxels-a.radiusSubvoxels); + const double playerCenterZ=az+playerT*dz-playerRadius+playerCenterAboveFloorQ; + const double playerVertical=std::max(0.0, + std::abs(qz-playerCenterZ)-0.54*qPerMetre); + bestPlayerCapsuleSignedDistance=std::min(bestPlayerCapsuleSignedDistance, + DeterministicSqrt(playerDx*playerDx+playerDy*playerDy + +playerVertical*playerVertical)-0.34*qPerMetre); + } const double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz; const double ex=qx-cx,ey=qy-cy,ez=qz-cz; const double radius=static_cast(a.radiusSubvoxels) +t*(b.radiusSubvoxels-a.radiusSubvoxels); - const double horizontalLengthSq=dx*dx+dy*dy; - const double playerT=horizontalLengthSq>0.0 - ?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):t; - const double playerDx=qx-(ax+playerT*dx); - const double playerDy=qy-(ay+playerT*dy); - const double playerRadius=static_cast(a.radiusSubvoxels) - +playerT*(b.radiusSubvoxels-a.radiusSubvoxels); - const double playerCenterZ=az+playerT*dz-playerRadius+playerCenterAboveFloorQ; - const double playerVertical=std::max(0.0, - std::abs(qz-playerCenterZ)-0.54*qPerMetre); - const double playerCapsuleDistance=DeterministicSqrt( - playerDx*playerDx+playerDy*playerDy+playerVertical*playerVertical) - -0.34*qPerMetre; - bestPlayerCapsuleSignedDistance=std::min(bestPlayerCapsuleSignedDistance, - playerCapsuleDistance); const double signedDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius; - if(signedDistance(8,lowerMouthIndex);++index) - considerCarveSegment(index); - const std::ptrdiff_t mouthSearchBegin=std::max(0,lowerMouthIndex-8); - const std::ptrdiff_t mouthSearchEnd=std::min(lastIndex, - lowerMouthIndex+8); - for(std::ptrdiff_t index=mouthSearchBegin;index(8,lowerMouthIndex)-1,qx,qy,qz, + 1,bestSignedDistance,considerCarveSegment); + VisitNearestPassageSegmentRange(passageIndex,mouthSearchBegin,mouthSearchEnd-1, + qx,qy,qz,2,bestSignedDistance,considerCarveSegment); + VisitNearestPassageSegmentRange(passageIndex,lowerMouthIndex,lastIndex-1, + qx,qy,qz,3,bestSignedDistance,considerCarveSegment); + } + else + { + for(std::ptrdiff_t index=carveBegin;index<=carveEnd;++index) + considerCarveSegment(static_cast(index),0); + for(std::ptrdiff_t index=0;index(8,lowerMouthIndex);++index) + considerCarveSegment(static_cast(index),1); + for(std::ptrdiff_t index=mouthSearchBegin;index(index),2); + for(std::ptrdiff_t index=lowerMouthIndex;index(index),3); + } if(!std::isfinite(bestSignedDistance))continue; const auto& nearestA=passage.controlPoints[static_cast(bestIndex)]; const auto& nearestB=passage.controlPoints[static_cast(bestIndex+1)]; @@ -405,8 +541,12 @@ public: const bool shortSpanFloorSupport=hasSupportFloor &&IsPassageShortSpanSegment(passageIndex, static_cast(bestSupportSegmentIndex)); - if(hasSupportFloor&&bestPlayerCapsuleSignedDistance>=0.05*qPerMetre - &&(density<0.0f||passageColumnFloorSupport||shortSpanFloorSupport)) + const bool capsuleAllowsSupport=passageTileScopeMatches + ?!PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin,carveEnd, + lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz,qPerMetre) + :bestPlayerCapsuleSignedDistance>=0.05*qPerMetre; + if(hasSupportFloor&&(density<0.0f||passageColumnFloorSupport||shortSpanFloorSupport) + &&capsuleAllowsSupport) composed=std::min(composed,static_cast(bestSupportDensity)); } return composed; @@ -418,51 +558,94 @@ public: bool ResolvedPassageIntersectsBox(const int32 minimum[3],const int32 maximum[3]) const { if(!ResolvedLayout||ResolvedLayout->passages.empty())return false; - constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); - const double voxelsPerMetre=Owner&&Owner->CoreVoxelSizeMicrons>0 - ?1000000.0/Owner->CoreVoxelSizeMicrons:4.0; - const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre; - const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre; - for(const auto& passage:ResolvedLayout->passages) + if(PassageProofNodes.empty()) { - for(std::size_t i=1;i(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + const double voxelsPerMetre=Owner&&Owner->CoreVoxelSizeMicrons>0 + ?1000000.0/Owner->CoreVoxelSizeMicrons:4.0; + const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre; + const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre; + const auto overlaps=[&](const double low[3],const double high[3]) { - const auto& a=passage.controlPoints[i-1];const auto& b=passage.controlPoints[i]; - const double radius=static_cast(std::max(a.radiusSubvoxels,b.radiusSubvoxels))/Q; - const double floorThicknessVoxels=Owner&&Owner->CoreVoxelSizeMicrons>0 - ?1000000.0/Owner->CoreVoxelSizeMicrons:4.0; - // Short support spans can extend to a 3.5 m half-width and almost 5 m - // below the bore floor. Keep those cells shape-dependent for classification. - const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels); - const double footDepth=std::max(floorThicknessVoxels,bridgeFootDepthVoxels); - const double low[3]={ - std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-horizontalExtent, - std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-horizontalExtent, - std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius-footDepth}; - const double high[3]={ - std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent, - std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent, - std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius}; - bool overlap=true; for(int axis=0;axis<3;++axis) - overlap&=high[axis]>=minimum[axis]&&low[axis]<=maximum[axis]; - if(overlap)return true; + if(high[axis]maximum[axis])return false; + return true; + }; + for(const auto& passage:ResolvedLayout->passages) + { + for(std::size_t i=1;i(std::max(a.radiusSubvoxels,b.radiusSubvoxels))/Q; + const double floorThicknessVoxels=voxelsPerMetre; + const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels); + const double footDepth=std::max(floorThicknessVoxels,bridgeFootDepthVoxels); + const double low[3]={ + std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-horizontalExtent, + std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-horizontalExtent, + std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius-footDepth}; + const double high[3]={ + std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent, + std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent, + std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius}; + if(overlaps(low,high))return true; + } + const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&passage](const auto& value) + {return value.strateIndex==passage.upperStrateIndex;}); + if(upperStratum!=ResolvedLayout->strates.end()) + { + std::array route{}; + std::size_t routeCount=0; + for(const auto& anchor:upperStratum->anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + { + if(routeCount=2) + { + const double mouthReach=13.0*voxelsPerMetre*Q; + double minimumZ=std::numeric_limits::infinity(); + double maximumZ=-std::numeric_limits::infinity(); + for(std::size_t index=0;index(route[index]->values[2])); + maximumZ=std::max(maximumZ,static_cast(route[index]->values[2])); + } + const double mouthX=passage.upperMouthSubvoxels[0]; + const double mouthY=passage.upperMouthSubvoxels[1]; + const double low[3]={(mouthX-mouthReach)/Q,(mouthY-mouthReach)/Q, + (minimumZ-3.0*voxelsPerMetre*Q)/Q}; + const double high[3]={(mouthX+mouthReach)/Q,(mouthY+mouthReach)/Q, + (maximumZ+3.0*voxelsPerMetre*Q)/Q}; + if(overlaps(low,high))return true; + } + } } + for(const auto& crossing:PassageVoidCrossings) + { + if(!crossing.needsColumn)continue; + const double radius=static_cast(crossing.maximumRadiusSubvoxels)/Q; + const double low[3]={crossing.centerXSubvoxels/Q-radius, + crossing.centerYSubvoxels/Q-radius,crossing.bottomZSubvoxels/Q}; + const double high[3]={crossing.centerXSubvoxels/Q+radius, + crossing.centerYSubvoxels/Q+radius,crossing.topZSubvoxels/Q}; + if(overlaps(low,high))return true; + } + return false; } - for(const auto& crossing:PassageVoidCrossings) - { - if(!crossing.needsColumn)continue; - const double radius=static_cast(crossing.maximumRadiusSubvoxels)/Q; - const double low[3]={crossing.centerXSubvoxels/Q-radius, - crossing.centerYSubvoxels/Q-radius,crossing.bottomZSubvoxels/Q}; - const double high[3]={crossing.centerXSubvoxels/Q+radius, - crossing.centerYSubvoxels/Q+radius,crossing.topZSubvoxels/Q}; - bool overlap=true; - for(int axis=0;axis<3;++axis) - overlap&=high[axis]>=minimum[axis]&&low[axis]<=maximum[axis]; - if(overlap)return true; - } - return false; + const double low[3]={static_cast(minimum[0]),static_cast(minimum[1]), + static_cast(minimum[2])}; + const double high[3]={static_cast(maximum[0]),static_cast(maximum[1]), + static_cast(maximum[2])}; + return PassageProofBoundsOverlapNode(0,low,high); } static bool MakeResolvedStratumBounds(double BottomZ,double TopZ,double SealThickness, @@ -1723,20 +1906,557 @@ public: } private: + struct FPassageTileFeature + { + std::size_t passageIndex=0,segmentIndex=0; + bool bColumn=false,bMouthSupport=false; + }; + struct FPassageNearestNode + { + std::array minimum{},maximum{}; + double maximumRadius=0.0; + std::vector segments; + int left=-1,right=-1; + }; + struct FPassageProofBound + { + std::array minimum{},maximum{}; + }; + struct FPassageProofNode + { + std::array minimum{},maximum{}; + std::size_t first=0,end=0; + int left=-1,right=-1; + }; + struct FPassageFeatureNode + { + std::array minimum{},maximum{}; + std::size_t first=0,end=0; + int left=-1,right=-1; + }; + + int BuildPassageNearestNode(std::size_t passageIndex,std::vector segments) + { + const auto& points=ResolvedLayout->passages[passageIndex].controlPoints; + FPassageNearestNode node; + node.minimum.fill(std::numeric_limits::infinity()); + node.maximum.fill(-std::numeric_limits::infinity()); + for(const std::size_t segment:segments) + { + const auto& a=points[segment];const auto& b=points[segment+1]; + for(std::size_t axis=0;axis<3;++axis) + { + node.minimum[axis]=std::min({node.minimum[axis], + static_cast(a.positionSubvoxels[axis]), + static_cast(b.positionSubvoxels[axis])}); + node.maximum[axis]=std::max({node.maximum[axis], + static_cast(a.positionSubvoxels[axis]), + static_cast(b.positionSubvoxels[axis])}); + } + node.maximumRadius=std::max({node.maximumRadius, + static_cast(a.radiusSubvoxels),static_cast(b.radiusSubvoxels)}); + } + node.segments=segments; + std::sort(node.segments.begin(),node.segments.end()); + const int nodeIndex=static_cast(PassageNearestNodes[passageIndex].size()); + PassageNearestNodes[passageIndex].push_back(std::move(node)); + if(segments.size()>4) + { + std::size_t splitAxis=0; + for(std::size_t axis=1;axis<3;++axis) + if(PassageNearestNodes[passageIndex][nodeIndex].maximum[axis] + -PassageNearestNodes[passageIndex][nodeIndex].minimum[axis] + >PassageNearestNodes[passageIndex][nodeIndex].maximum[splitAxis] + -PassageNearestNodes[passageIndex][nodeIndex].minimum[splitAxis])splitAxis=axis; + const std::size_t middle=segments.size()/2; + const auto center=[&](std::size_t segment) + {return static_cast(points[segment].positionSubvoxels[splitAxis]) + +static_cast(points[segment+1].positionSubvoxels[splitAxis]);}; + std::nth_element(segments.begin(),segments.begin()+middle,segments.end(), + [&](std::size_t a,std::size_t b) + {const double ca=center(a),cb=center(b);return ca==cb?a leftSegments(segments.begin(),segments.begin()+middle); + std::vector rightSegments(segments.begin()+middle,segments.end()); + const int left=BuildPassageNearestNode(passageIndex,std::move(leftSegments)); + const int right=BuildPassageNearestNode(passageIndex,std::move(rightSegments)); + PassageNearestNodes[passageIndex][nodeIndex].left=left; + PassageNearestNodes[passageIndex][nodeIndex].right=right; + } + return nodeIndex; + } + + int BuildPassageProofNode(std::size_t first,std::size_t end) + { + FPassageProofNode node;node.first=first;node.end=end; + node.minimum.fill(std::numeric_limits::infinity()); + node.maximum.fill(-std::numeric_limits::infinity()); + for(std::size_t index=first;index(PassageProofNodes.size()); + PassageProofNodes.push_back(node); + if(end-first>8) + { + std::size_t splitAxis=0; + for(std::size_t axis=1;axis<3;++axis) + if(node.maximum[axis]-node.minimum[axis] + >node.maximum[splitAxis]-node.minimum[splitAxis])splitAxis=axis; + const std::size_t middle=first+(end-first)/2; + std::nth_element(PassageProofBounds.begin()+first,PassageProofBounds.begin()+middle, + PassageProofBounds.begin()+end,[splitAxis](const auto& a,const auto& b) + {return a.minimum[splitAxis]+a.maximum[splitAxis] + ::max()); + node.maximum.fill(std::numeric_limits::min()); + for(std::size_t offset=first;offset(PassageFeatureNodes.size()); + PassageFeatureNodes.push_back(node); + if(end-first>8) + { + std::size_t splitAxis=0; + for(std::size_t axis=1;axis<3;++axis) + if(static_cast(node.maximum[axis])-node.minimum[axis] + >static_cast(node.maximum[splitAxis])-node.minimum[splitAxis]) + splitAxis=axis; + const std::size_t middle=first+(end-first)/2; + std::nth_element(PassageFeatureOrder.begin()+first,PassageFeatureOrder.begin()+middle, + PassageFeatureOrder.begin()+end,[this,splitAxis](std::uint32_t a,std::uint32_t b) + { + const auto& left=PassageFeatureBounds[a]; + const auto& right=PassageFeatureBounds[b]; + const double leftCenter=static_cast(left.minimumSubvoxels[splitAxis])*0.5 + +static_cast(left.maximumSubvoxels[splitAxis])*0.5; + const double rightCenter=static_cast(right.minimumSubvoxels[splitAxis])*0.5 + +static_cast(right.maximumSubvoxels[splitAxis])*0.5; + return leftCenter==rightCenter?a& minimum, + const std::array& maximum, + std::vector& result) const + { + if(nodeIndex<0)return; + const auto& node=PassageFeatureNodes[static_cast(nodeIndex)]; + for(std::size_t axis=0;axis<3;++axis) + if(node.maximum[axis]maximum[axis])return; + if(node.left<0) + { + for(std::size_t offset=node.first;offset=minimum[axis]; + if(overlaps)result.push_back(featureIndex); + } + return; + } + QueryPassageFeatureNode(node.left,minimum,maximum,result); + QueryPassageFeatureNode(node.right,minimum,maximum,result); + } + + std::vector BuildPassageFeatureTileList( + const std::array& minimum, + const std::array& maximum) const + { + std::vector result; + if(PassageFeatureBounds.empty())return result; + bool failOpen=PassageFeatureBoundsFailOpen; + for(std::size_t axis=0;axis<3;++axis)failOpen=failOpen||minimum[axis]>maximum[axis]; + if(failOpen) + { + result.resize(PassageFeatureBounds.size()); + for(std::size_t i=0;i(i); + return result; + } + if(!PassageFeatureNodes.empty())QueryPassageFeatureNode(0,minimum,maximum,result); + return result; + } + + void BuildPassageSpatialIndices() + { + PassageTileFeatures.clear();PassageFeatureBounds.clear(); + PassageFeatureOrder.clear();PassageFeatureNodes.clear(); + PassageFeatureBoundsFailOpen=false; + PassageNearestNodes.clear(); + PassageProofBounds.clear();PassageProofNodes.clear(); + PassageNearestNodes.resize(ResolvedLayout?ResolvedLayout->passages.size():0); + PassageColumnSegmentFlags.clear();PassageShortSpanSegmentFlags.clear(); + PassageHasShortSpanFlags.clear(); + if(!ResolvedLayout||ResolvedLayout->passages.empty())return; + constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + const double voxelsPerMetre=Owner&&Owner->CoreVoxelSizeMicrons>0 + ?1000000.0/Owner->CoreVoxelSizeMicrons:4.0; + const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre; + const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre; + PassageColumnSegmentFlags.resize(ResolvedLayout->passages.size()); + PassageShortSpanSegmentFlags.resize(ResolvedLayout->passages.size()); + for(std::size_t passageIndex=0;passageIndexpassages.size();++passageIndex) + { + const auto& passage=ResolvedLayout->passages[passageIndex]; + const std::size_t segmentCount=passage.controlPoints.size()>1 + ?passage.controlPoints.size()-1:0; + PassageColumnSegmentFlags[passageIndex].assign(segmentCount,0); + PassageShortSpanSegmentFlags[passageIndex].assign(segmentCount,0); + if(segmentCount>0) + { + std::vector segmentIds(segmentCount); + for(std::size_t i=0;i(std::max(a.radiusSubvoxels,b.radiusSubvoxels))/Q; + const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels); + const double footDepth=std::max(voxelsPerMetre,bridgeFootDepthVoxels); + FPassageProofBound proof; + proof.minimum={std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-horizontalExtent, + std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-horizontalExtent, + std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius-footDepth}; + proof.maximum={std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent, + std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent, + std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius}; + PassageProofBounds.push_back(proof); + const std::array featureMin={ + static_cast(std::floor(proof.minimum[0]*Q)), + static_cast(std::floor(proof.minimum[1]*Q)), + static_cast(std::floor(proof.minimum[2]*Q))}; + const std::array featureMax={ + static_cast(std::ceil(proof.maximum[0]*Q)), + static_cast(std::ceil(proof.maximum[1]*Q)), + static_cast(std::ceil(proof.maximum[2]*Q))}; + PassageFeatureBounds.push_back({static_cast(PassageTileFeatures.size()), + featureMin,featureMax}); + PassageTileFeatures.push_back({passageIndex,segment,false}); + } + const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&passage](const auto& value) + {return value.strateIndex==passage.upperStrateIndex;}); + if(upperStratum!=ResolvedLayout->strates.end()) + { + std::array route{}; + std::size_t routeCount=0; + for(const auto& anchor:upperStratum->anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + { + if(routeCount=2) + { + const double mouthReach=13.0*voxelsPerMetre*Q; + double minimumZ=std::numeric_limits::infinity(); + double maximumZ=-std::numeric_limits::infinity(); + for(std::size_t index=0;index(route[index]->values[2])); + maximumZ=std::max(maximumZ,static_cast(route[index]->values[2])); + } + const double mouthX=passage.upperMouthSubvoxels[0]; + const double mouthY=passage.upperMouthSubvoxels[1]; + FPassageProofBound supportProof; + supportProof.minimum={(mouthX-mouthReach)/Q,(mouthY-mouthReach)/Q, + (minimumZ-3.0*voxelsPerMetre*Q)/Q}; + supportProof.maximum={(mouthX+mouthReach)/Q,(mouthY+mouthReach)/Q, + (maximumZ+3.0*voxelsPerMetre*Q)/Q}; + PassageProofBounds.push_back(supportProof); + const std::array featureMin={ + static_cast(std::floor(supportProof.minimum[0]*Q)), + static_cast(std::floor(supportProof.minimum[1]*Q)), + static_cast(std::floor(supportProof.minimum[2]*Q))}; + const std::array featureMax={ + static_cast(std::ceil(supportProof.maximum[0]*Q)), + static_cast(std::ceil(supportProof.maximum[1]*Q)), + static_cast(std::ceil(supportProof.maximum[2]*Q))}; + PassageFeatureBounds.push_back({static_cast(PassageTileFeatures.size()), + featureMin,featureMax}); + PassageTileFeatures.push_back({passageIndex,0,false,true}); + } + } + } + for(const auto& crossing:PassageVoidCrossings) + { + if(crossing.passageIndex(crossing.maximumRadiusSubvoxels)/Q; + proof.minimum={crossing.centerXSubvoxels/Q-radius,crossing.centerYSubvoxels/Q-radius, + crossing.bottomZSubvoxels/Q}; + proof.maximum={crossing.centerXSubvoxels/Q+radius,crossing.centerYSubvoxels/Q+radius, + crossing.topZSubvoxels/Q}; + PassageProofBounds.push_back(proof); + const std::array featureMin={ + static_cast(std::floor(proof.minimum[0]*Q)), + static_cast(std::floor(proof.minimum[1]*Q)), + static_cast(std::floor(proof.minimum[2]*Q))}; + const std::array featureMax={ + static_cast(std::ceil(proof.maximum[0]*Q)), + static_cast(std::ceil(proof.maximum[1]*Q)), + static_cast(std::ceil(proof.maximum[2]*Q))}; + PassageFeatureBounds.push_back({static_cast(PassageTileFeatures.size()), + featureMin,featureMax}); + PassageTileFeatures.push_back({crossing.passageIndex,0,true}); + } + PassageHasShortSpanFlags.assign(PassageShortSpanSegmentFlags.size(),0); + for(std::size_t passageIndex=0;passageIndex( + std::any_of(PassageShortSpanSegmentFlags[passageIndex].begin(), + PassageShortSpanSegmentFlags[passageIndex].end(), + [](std::uint8_t flag){return flag!=0;})); + for(std::size_t i=0;i(i)); + for(const auto& feature:PassageFeatureBounds) + for(std::size_t axis=0;axis<3;++axis) + PassageFeatureBoundsFailOpen=PassageFeatureBoundsFailOpen + ||feature.minimumSubvoxels[axis]>feature.maximumSubvoxels[axis]; + if(!PassageFeatureOrder.empty())BuildPassageFeatureNode(0,PassageFeatureOrder.size()); + if(!PassageProofBounds.empty())BuildPassageProofNode(0,PassageProofBounds.size()); + } + + double PassageNearestNodeLowerBound(const FPassageNearestNode& node, + double qx,double qy,double qz) const + { + const double dx=qxnode.maximum[0]?qx-node.maximum[0]:0.0; + const double dy=qynode.maximum[1]?qy-node.maximum[1]:0.0; + const double dz=qznode.maximum[2]?qz-node.maximum[2]:0.0; + return DeterministicSqrt(dx*dx+dy*dy+dz*dz)-node.maximumRadius-1.0e-6; + } + + template + void VisitNearestPassageSpatialNode(std::size_t passageIndex,int nodeIndex, + std::size_t first,std::size_t end,double qx,double qy,double qz,int priority, + double& bestDistance,FVisit& visit) const + { + if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return; + const auto& nodes=PassageNearestNodes[passageIndex]; + const auto& node=nodes[static_cast(nodeIndex)]; + const auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first); + if(candidate==node.segments.end()||*candidate>=end)return; + const double lowerBound=PassageNearestNodeLowerBound(node,qx,qy,qz); + if(lowerBound>bestDistance)return; + if(node.left<0) + { + for(auto it=candidate;it!=node.segments.end()&&*it(node.left)],qx,qy,qz); + const double rightBound=PassageNearestNodeLowerBound( + nodes[static_cast(node.right)],qx,qy,qz); + const int nearChild=leftBound<=rightBound?node.left:node.right; + const int farChild=leftBound<=rightBound?node.right:node.left; + VisitNearestPassageSpatialNode(passageIndex,nearChild,first,end,qx,qy,qz, + priority,bestDistance,visit); + VisitNearestPassageSpatialNode(passageIndex,farChild,first,end,qx,qy,qz, + priority,bestDistance,visit); + } + + template + void VisitNearestPassageSegmentRange(std::size_t passageIndex,std::ptrdiff_t first, + std::ptrdiff_t last,double qx,double qy,double qz,int priority, + double& bestDistance,FVisit&& visit) const + { + if(first>last||passageIndex>=PassageNearestNodes.size() + ||PassageNearestNodes[passageIndex].empty())return; + auto callback=std::forward(visit); + VisitNearestPassageSpatialNode(passageIndex,0,static_cast(first), + static_cast(last+1),qx,qy,qz,priority,bestDistance,callback); + } + + bool PassagePotentialWithinNode(std::size_t passageIndex,int nodeIndex, + std::size_t first,std::size_t end,double qx,double qy,double qz, + double signedDistanceLimit) const + { + if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return false; + const auto& nodes=PassageNearestNodes[passageIndex]; + const auto& node=nodes[static_cast(nodeIndex)]; + const auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first); + if(candidate==node.segments.end()||*candidate>=end)return false; + if(PassageNearestNodeLowerBound(node,qx,qy,qz)>signedDistanceLimit)return false; + if(node.left<0)return true; + const double leftBound=PassageNearestNodeLowerBound( + nodes[static_cast(node.left)],qx,qy,qz); + const double rightBound=PassageNearestNodeLowerBound( + nodes[static_cast(node.right)],qx,qy,qz); + const int nearChild=leftBound<=rightBound?node.left:node.right; + const int farChild=leftBound<=rightBound?node.right:node.left; + return PassagePotentialWithinNode(passageIndex,nearChild,first,end,qx,qy,qz, + signedDistanceLimit) + ||PassagePotentialWithinNode(passageIndex,farChild,first,end,qx,qy,qz, + signedDistanceLimit); + } + + bool PassageMayAffectDensity(std::size_t passageIndex,std::ptrdiff_t carveBegin, + std::ptrdiff_t carveEnd,std::ptrdiff_t lowerMouthIndex, + std::ptrdiff_t mouthSearchBegin,std::ptrdiff_t mouthSearchEnd,std::ptrdiff_t lastIndex, + double qx,double qy,double qz,double signedDistanceLimit) const + { + const auto possible=[&](std::ptrdiff_t first,std::ptrdiff_t last) + { + return first<=last&&passageIndex(first), + static_cast(last+1),qx,qy,qz,signedDistanceLimit); + }; + return possible(carveBegin,carveEnd) + ||possible(0,std::min(8,lowerMouthIndex)-1) + ||possible(mouthSearchBegin,mouthSearchEnd-1) + ||possible(lowerMouthIndex,lastIndex-1); + } + + 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 + { + if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return false; + const auto& nodes=PassageNearestNodes[passageIndex]; + const auto& node=nodes[static_cast(nodeIndex)]; + auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first); + if(candidate==node.segments.end()||*candidate>=end)return false; + const double xyReach=0.39*qPerMetre; + if(qx+xyReachnode.maximum[0] + ||qy+xyReachnode.maximum[1])return false; + const double minimumCenterZ=node.minimum[2]-node.maximumRadius+0.20*qPerMetre; + const double maximumCenterZ=node.maximum[2]+2.06*qPerMetre; + if(qzmaximumCenterZ)return false; + const auto& passage=ResolvedLayout->passages[passageIndex]; + if(node.left<0) + { + for(auto it=candidate;it!=node.segments.end()&&*it(b.positionSubvoxels[0])-ax; + const double dy=static_cast(b.positionSubvoxels[1])-ay; + const double dz=static_cast(b.positionSubvoxels[2])-az; + const double lengthSq=dx*dx+dy*dy+dz*dz; + if(lengthSq<=0.0)continue; + const double horizontalLengthSq=dx*dx+dy*dy; + const double t=horizontalLengthSq>0.0 + ?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):0.0; + const double centerX=ax+t*dx,centerY=ay+t*dy; + const double centerZ=az+t*dz-(static_cast(a.radiusSubvoxels) + +t*(static_cast(b.radiusSubvoxels)-a.radiusSubvoxels))+1.13*qPerMetre; + const double horizontalX=qx-centerX,horizontalY=qy-centerY; + 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; + } + return false; + } + return PassagePlayerCapsuleBlocksSupportNode(passageIndex,node.left,first,end, + qx,qy,qz,qPerMetre) + ||PassagePlayerCapsuleBlocksSupportNode(passageIndex,node.right,first,end, + qx,qy,qz,qPerMetre); + } + + bool PassagePlayerCapsuleBlocksSupport(std::size_t passageIndex, + std::ptrdiff_t carveBegin,std::ptrdiff_t carveEnd,std::ptrdiff_t lowerMouthIndex, + std::ptrdiff_t mouthSearchBegin,std::ptrdiff_t mouthSearchEnd,std::ptrdiff_t lastIndex, + double qx,double qy,double qz,double qPerMetre) const + { + const auto blocked=[&](std::ptrdiff_t first,std::ptrdiff_t last) + { + return first<=last&&PassagePlayerCapsuleBlocksSupportNode(passageIndex,0, + static_cast(first),static_cast(last+1), + qx,qy,qz,qPerMetre); + }; + return blocked(carveBegin,carveEnd) + ||blocked(0,std::min(8,lowerMouthIndex)-1) + ||blocked(mouthSearchBegin,mouthSearchEnd-1) + ||blocked(lowerMouthIndex,lastIndex-1); + } + + bool PassageProofBoundsOverlapNode(int nodeIndex,const double minimum[3], + const double maximum[3]) const + { + if(nodeIndex<0)return false; + const auto& node=PassageProofNodes[static_cast(nodeIndex)]; + for(std::size_t axis=0;axis<3;++axis) + if(node.maximum[axis]maximum[axis])return false; + if(node.left<0) + { + for(std::size_t index=node.first;index=minimum[axis] + &&PassageProofBounds[index].minimum[axis]<=maximum[axis]; + if(overlap)return true; + } + return false; + } + return PassageProofBoundsOverlapNode(node.left,minimum,maximum) + ||PassageProofBoundsOverlapNode(node.right,minimum,maximum); + } + bool IsPassageColumnSegment(std::size_t PassageIndex,std::size_t SegmentIndex) const { - return std::any_of(PassageVoidCrossings.begin(),PassageVoidCrossings.end(), - [PassageIndex,SegmentIndex](const auto& crossing) - {return crossing.needsColumn&&crossing.passageIndex==PassageIndex - &&SegmentIndex>=crossing.firstSegment&&SegmentIndex<=crossing.lastSegment;}); + return PassageIndex=crossing.firstSegment&&SegmentIndex<=crossing.lastSegment;}); + return PassageIndexpassages.empty() - ||!Owner->PassageVoidCrossingCache)return; + if(!Owner||!ResolvedLayout||ResolvedLayout->passages.empty())return; + if(!Owner->PassageVoidCrossingCache) + {BuildPassageSpatialIndices();return;} auto& cache=*Owner->PassageVoidCrossingCache; std::lock_guard lock(cache.mutex); const std::string cacheKey=ResolvedLayout->stableIdentity+"/passage-void-columns-v1"; const auto cached=cache.byLayout.find(cacheKey); if(cached!=cache.byLayout.end()) - {PassageVoidCrossings=cached->second;return;} + {PassageVoidCrossings=cached->second;BuildPassageSpatialIndices();return;} constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double qPerMetre=Q*1000000.0/std::max(1, @@ -2109,6 +2829,7 @@ private: passage.stableId.c_str()); } cache.byLayout.emplace(cacheKey,PassageVoidCrossings); + BuildPassageSpatialIndices(); } static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z) @@ -6373,4 +7094,15 @@ private: uint64 GpuVisualDetailNodeCount=0,CpuVisualDetailNodeCount=0; FBuildStats Stats; bool bAnalyticOnly=false; + inline static thread_local const FScopedPassageTileQueries* ActivePassageTileQueries=nullptr; + std::vector PassageFeatureBounds; + std::vector PassageTileFeatures; + std::vector PassageFeatureOrder; + std::vector PassageFeatureNodes; + bool PassageFeatureBoundsFailOpen=false; + std::vector> PassageNearestNodes; + std::vector PassageProofBounds; + std::vector PassageProofNodes; + std::vector> PassageColumnSegmentFlags,PassageShortSpanSegmentFlags; + std::vector PassageHasShortSpanFlags; }; diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 391c55c..a0a00c1 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -2137,6 +2137,12 @@ namespace VoxelForgeCore out.diagnostics.featureListMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-FeatureListStart).count(); const ScopedWorldAssemblyFeatureQueriesV1 featureScope(featureQueries); + // GenerateMesh samples only origin..origin+extent plus the one-step normal halo; + // queryMinimum/queryMaximum above are exactly that inclusive domain. + // GenerateMesh n'échantillonne que origin..origin+extent et la marge de normale d'un pas ; + // queryMinimum/queryMaximum ci-dessus couvrent exactement ce domaine inclusif. + const FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope( + tileShape,queryMinimum,queryMaximum); const bool bDiagnosticColumnSampling=options.diagnosticGlobalOctaveDrop!=0 ||options.diagnosticColumnInterpolationStride>1; const std::uint32_t PreviousOctaveDrop=bDiagnosticColumnSampling diff --git a/Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md new file mode 100644 index 0000000..c5c382e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md @@ -0,0 +1,125 @@ +# Linux L28 — tile-bounded columns and passages + +Branch: wip/l25-helix, based on L27. No commit. No Unreal files changed. + +## Cost and culling + +The plain interleaved baseline puts the L27 overhead in tile streaming/meshing, not shape +compilation: mean whole-view time was 2.058 s versus L24's 1.527 s for SurfaceWorld (+34.8%), +and 19.961 s versus 16.699 s for FloatingIslands (+19.5%). Shape time stayed near 0.46 s +(SurfaceWorld) and 0.97 s (FloatingIslands). The initial extra work was passage composition +inside the map-wide density sampling loop. + +GenerateMesh now enters a passage-query scope using the exact inclusive tile sample bounds, +including the normal halo. A conservative feature AABB BVH supplies candidate route segments, +columns and mouth support. Long route nearest-distance searches use a per-passage segment BVH. +L9 proof boxes use a separate passage-bound BVH; disjoint roots reject far boxes without scanning +segments. If a native-only negative-control shape has no proof index, its box check falls back to +a conservative linear test. + +Instrumented full-view counts (diagnostic build; 2,370 desired tiles): + +| Recipe | Composition samples | Column guard: calls / tiles | Exact column SDF: calls / tiles | Route band samples / tiles | Route SDF unions / tiles | Route samples skipped / tiles | Feature candidates: tiles / total | +|---|---:|---:|---:|---:|---:|---:|---:| +| SurfaceWorld | 12,039,941 | 1,885,887 / 692 | 3,017 / 4 | 1,380,907 / 870 | 164,337 / 543 | 7,470,572 / 1,792 | 8 / 1,222 | +| FloatingIslands | 58,019,623 | 7,619,575 / 689 | 7,213 / 6 | 1,902,086 / 708 | 62,507 / 89 | 54,848,675 / 2,212 | 14 / 1,331 | + +The exact column SDF is spatially culled. The cheaper radial guard remains in tiles sharing the +column's Z slab because positive SDF values can lower positive base density; a broader tile skip +would need an exact bound on that field. This is part of the remaining SurfaceWorld cost. +Per composition sample, the profile counted 0.125 carve-segment tests, 0.036 support-segment tests +and 0.0136 route SDF unions in SurfaceWorld; FloatingIslands counted 0.0125, 0.0028 and 0.00108. +Column guards ran on 15.7% / 13.1% of samples, while the complex column SDF ran on 0.025% / 0.012%. + +## Interleaved whole-view cost + +Five W12 quartets ran in order L24, L28a, fresh L27-source build, L28b. Hashing was off. +Values below are means of the five samples; raw samples are in +Saved/Linux/linux28-r28-finalfix-cost5-{recipe}-{l24,l27,r28a,r28b}-p1..p5.log. +L24 used Saved/Linux/l24-source/bin/linux/VoxelForgeCoreHeadless-linux24-clang19; +L27 was rebuilt from the current L27 source for this comparison. + +| Recipe | L24 | L27 | L28a | L28b | L28a / L28b vs L24 | +|---|---:|---:|---:|---:|---:| +| SurfaceWorld | 1.527 s | 2.058 s | 1.592 s | 1.605 s | +4.30% / +5.12% | +| FloatingIslands | 16.699 s | 19.961 s | 16.651 s | 16.787 s | -0.29% / +0.53% | + +FloatingIslands returned to L24 cost. SurfaceWorld misses the ~3% target by 1.3–2.1 percentage +points. Its remaining excess is about 69–80 ms in streaming wall time and 13–15 ms in shape +time; the density-aware radial guard and per-tile query setup/traversal remain. The guard cannot +be removed bit-exactly without a proven upper bound on the incoming positive density. + +## Bit parity and audits + +Final Clang and GCC binaries match L27 for both recipes at W1, W8 and W12: all 12 hash records per +run match (full detail, whole view, LOD0–8, exact query). This is 12 compiler/recipe/worker +combinations with zero mismatches. Logs: +Saved/Linux/linux28-r28-hash-finalfix-{clang,gcc}-{surface-world,floating-islands}-w{1,8,12}.log. + +Representative hashes, equal to L27: + +| Recipe | Full detail: density / mesh / payload / topology | Whole view: density / mesh / payload / topology | Exact query: density / detail / collision | +|---|---|---|---| +| SurfaceWorld | ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae | 117547e5e18c6847 / db5fa276315ff2d4 / b4e9d01d70413fea / 03caf881fa047f1c | c949c4df2ce6f9d9 / 114c501e2d830d94 / e6011d3a248d4849 | +| FloatingIslands | ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec | 70ab5bc87dd174d2 / 6f357f9085a496f0 / 08ce7d4e036da65a / eb5eef04a5cb85d9 | 7b7c50ac24ccadcc / efcba20c0c70c415 / e604823a249029bf | + +The 24 m crossing matches fresh L27-source builds on both compilers and recipes: +SurfaceWorld, 314 tiles: density 3561ea23485e359f, mesh e8331620fb0fb908, +payload 765f7dad67c852aa, topology 3ccc09ac81c05d71. +FloatingIslands: density e3d0ea833bca4215, mesh 6b3d0f148ed83d93, +payload d6ed85f16acd895e, topology 9745683036522461. + +Across 4 styles × 2 recipes, capsule walkability, open air, naturalness, no-long-bridge, +strict-validator controls, route bindings and passage audits pass on final Clang/GCC binaries. +Final passage logs are +Saved/Linux/linux28-r28-finalpassage-{clang,gcc}-{surface-world,floating-islands}-style{0..3}.log. + +The final route/proof/readiness matrix also passes in all 16 compiler/style/recipe runs: +L13 route parity verifies 8/8 page pairs and 14 higher-LOD exact pairs with zero mismatches; +the readiness guard catches its negative control; L9 reports zero proof violations. +SurfaceWorld has 0 unknown blocks; FloatingIslands has 21,504 fallback/unknown blocks for each +style. The diagnostic full-view profile queried 26,946 FI proof boxes with only 40 path-bound and +1 column-bound tests, so far boxes reject at the BVH root. These unknowns are not evidence of a +fat passage bound. A fresh rerun of the saved L27 binary with the default owner input also reports +21,504; older L27 logs showing 17,408 did not reproduce. Final matrix logs are +Saved/Linux/linux28-r28-finalmatrix-{clang,gcc}-{surface-world,floating-islands}-style{0..3}.log. + +Two inherited SurfaceWorld FloorProfileBlend checks remain over the 2-voxel threshold in styles +1 and 3: 2.544 and 3.645 voxels. The same failures and values are in L27 logs for both compilers; +other styles pass. + +## Invariants, edits and run history + +Changed Core files: Standalone/Core/src/ShapeWorldInternal.h and +Standalone/Core/src/VoxelForgeCore.cpp. Navigation/design docs and this report were updated. +No Unreal edits or Unreal compile. L3 FP-contract preamble/self-test, L4 FLazy, L7 shared cache, +L9/L12 proof rules, L11 readiness, L13 route binding, L18/L19 shared ceiling, L21/L22 layout and +bands, L23–L27 passage behavior, ARCHITECTURE §8.10 and CLAUDE.md async rules remain intact. + +Failed or retried runs: + +- First parity/readiness command omitted --stream-benchmark and --stream-pipeline; rerun with both. +- Initial readiness negative-control crashed on an empty proof BVH in a temporary native-only + shape. Added the conservative no-index fallback; final readiness checks pass. +- The first final W1 hash command omitted the explicit owner data path and failed to open data + under Saved/Linux. Preserved as linux28-r28-hash-finalfix-clang-surface-world-w1-attempt1-failed.log; + rerun with Standalone/data/owner_world.json and all final W1/W8 hashes passed. +- Initial moving-crossing comparison used stale L27 logs. Rebuilt L27 from source and reran; all + four compiler/recipe hashes then matched. +- Older saved L27 proof logs reported 17,408 FloatingIslands unknown blocks. Reran the saved L27 + binaries and compared with final L28: both report 21,504, zero violations. +- An exploratory composed-bound FloatingIslands W12 hash differed; the density-threshold variant + restored equality and is the version measured here. An interrupted/empty Clang R15 build was + rerun successfully. + +No run exceeded 30 minutes or remained stuck for 15 minutes. Builds were Clang 19 and GCC 14, +W12 CPU affinity 0–11, with the 6 GiB address-space cap. No commit, push or stash was performed. + +## Orchestrator review (Claude) + +- Re-run by me (W12, no hashing, interleaved): SW L24 1.455/1.485/1.484 s vs L28 1.502/1.539/1.555 s (median +3.7%). + Accepted as the cost of the passage/column feature. FI is back to L24 cost. +- Bit-identical to L27 (12/12 configs + crossing). With this, `wip/l25-helix` (L25–L28) is merged into + `assembler/world`. +- Note for later: the pre-existing SW FloorProfileBlend check over threshold in styles 1/3 (2.544/3.645 voxels) + is unchanged since L27.