diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 2357b62..8255e31 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -70,6 +70,20 @@ static bool VF_IsL90PassageCandidateCensusEnabled() return bEnabled; } +static bool VF_IsL98PassageReuseCountEnabled() +{ + static const bool bEnabled=VF_IsL88DiagnosticSwitchEnabled( + "VOXELFORGE_L98_PASSAGE_REUSE_COUNTS"); + return bEnabled; +} + +static bool VF_IsL98PassagePrepareTraceEnabled() +{ + static const bool bEnabled=VF_IsL88DiagnosticSwitchEnabled( + "VOXELFORGE_L98_PASSAGE_PREP_TRACE"); + return bEnabled; +} + static double VF_L91PassageSourceFloorVoxels() { static const double Value=[] @@ -251,6 +265,21 @@ public: std::ptrdiff_t first=-1,end=-1; std::vector activeNodes; }; + enum class EPreparedSegmentUse : std::uint8_t + { Carve,Support,Capsule,Mouth }; + struct FReuseCounters + { + std::atomic projectionColumns{0}; + std::atomic projectionComputes{0}; + std::atomic projectionHits{0}; + std::atomic floorProfileReads{0}; + std::array,4> preparedSegmentUses{}; + }; + struct FProjectionCacheEntry + { + std::uint64_t generation=0; + double numerator=0.0; + }; FScopedPassageTileQueries(const FStandaloneShapeWorld& shape, const std::array& minimumSubvoxels, const std::array& maximumSubvoxels, @@ -358,6 +387,14 @@ public: std::sort(values.begin(),values.end(),[](const auto& a,const auto& b) {return a.segmentIndex(); ActivePassageTileQueries=this; } // Duplicate the immutable per-tile masks onto a density-grid helper worker. Mutable @@ -379,11 +416,32 @@ public: candidateProfiled_(source.candidateProfiled_), passageSourceFloorVoxels_(source.passageSourceFloorVoxels_), passageCarveReachMetres_(source.passageCarveReachMetres_), - clampCounters_(source.clampCounters_),ownsClampCounters_(false) + clampCounters_(source.clampCounters_),ownsClampCounters_(false), + projectionCacheSize_(source.projectionCacheSize_), + reuseCounters_(source.reuseCounters_),ownsReuseCounters_(false) { ActivePassageTileQueries=this; } ~FScopedPassageTileQueries() { ActivePassageTileQueries=previous_; + if(ownsReuseCounters_&&reuseCounters_) + std::printf("[L98PassageReuseTile] lod=%d projection_columns=%llu projection_computes=%llu projection_hits=%llu floor_profile_reads=%llu carve_prepared=%llu support_prepared=%llu capsule_prepared=%llu mouth_prepared=%llu\n", + profileLod_, + static_cast(reuseCounters_->projectionColumns.load( + std::memory_order_relaxed)), + static_cast(reuseCounters_->projectionComputes.load( + std::memory_order_relaxed)), + static_cast(reuseCounters_->projectionHits.load( + std::memory_order_relaxed)), + static_cast(reuseCounters_->floorProfileReads.load( + std::memory_order_relaxed)), + static_cast(reuseCounters_->preparedSegmentUses[0].load( + std::memory_order_relaxed)), + static_cast(reuseCounters_->preparedSegmentUses[1].load( + std::memory_order_relaxed)), + static_cast(reuseCounters_->preparedSegmentUses[2].load( + std::memory_order_relaxed)), + static_cast(reuseCounters_->preparedSegmentUses[3].load( + std::memory_order_relaxed))); if(ownsClampCounters_&&clampCounters_&&passageSourceFloorVoxels_>0.0) std::printf("[L91PassageSourceClampTile] lod=%d floor_vox=%.3f source_clamped=%llu source_delta_sum=%.9g deepest_source_vox=%.9g\n", profileLod_,passageSourceFloorVoxels_, @@ -442,6 +500,54 @@ public: FScopedPassageTileQueries& operator=(const FScopedPassageTileQueries&)=delete; bool matches(const FStandaloneShapeWorld& shape) const{return shape_==&shape;} + bool beginProjectionColumn() const + { + const bool previous=projectionColumnActive_; + projectionColumnActive_=true; + ++projectionGeneration_; + if(projectionGeneration_==0) + { + for(auto& entry:projectionCache_)entry.generation=0; + projectionGeneration_=1; + } + if(reuseCounters_) + reuseCounters_->projectionColumns.fetch_add(1,std::memory_order_relaxed); + return previous; + } + void endProjectionColumn(bool previous) const + {projectionColumnActive_=previous;} + double projectionNumerator(std::size_t flatIndex,double qx,double qy, + double ax,double ay,double dx,double dy) const + { + const auto compute=[&](){return (qx-ax)*dx+(qy-ay)*dy;}; + if(!projectionColumnActive_||!shape_)return compute(); + if(projectionCache_.size()!=projectionCacheSize_) + projectionCache_.resize(projectionCacheSize_); + if(flatIndex>=projectionCache_.size())return compute(); + FProjectionCacheEntry& entry=projectionCache_[flatIndex]; + if(entry.generation==projectionGeneration_) + { + if(reuseCounters_) + reuseCounters_->projectionHits.fetch_add(1,std::memory_order_relaxed); + return entry.numerator; + } + entry.numerator=compute(); + entry.generation=projectionGeneration_; + if(reuseCounters_) + reuseCounters_->projectionComputes.fetch_add(1,std::memory_order_relaxed); + return entry.numerator; + } + void recordPreparedSegmentUse(EPreparedSegmentUse use) const + { + if(reuseCounters_) + reuseCounters_->preparedSegmentUses[static_cast(use)] + .fetch_add(1,std::memory_order_relaxed); + } + void recordPreparedFloorProfileRead() const + { + if(reuseCounters_) + reuseCounters_->floorProfileReads.fetch_add(1,std::memory_order_relaxed); + } static const FScopedPassageTileQueries* CurrentFor(const FStandaloneShapeWorld& shape) { const FScopedPassageTileQueries* current=ActivePassageTileQueries; @@ -579,6 +685,12 @@ public: double passageCarveReachMetres_=0.0; mutable std::shared_ptr clampCounters_; bool ownsClampCounters_=true; + std::size_t projectionCacheSize_=0; + mutable std::vector projectionCache_; + mutable std::uint64_t projectionGeneration_=0; + mutable bool projectionColumnActive_=false; + std::shared_ptr reuseCounters_; + bool ownsReuseCounters_=true; mutable std::uint64_t l90Queries_=0,l90NoBrickQueries_=0; mutable std::uint64_t l90FallbackIterations_=0,l90FallbackNanoseconds_=0, l90QueryNanoseconds_=0,l90FallbackCarveVisits_=0,l90FallbackTreadVisits_=0, @@ -589,6 +701,23 @@ public: mutable std::array,4> l90CandidateHistogram_{}; }; + class FScopedPassageColumnXYReuse + { + public: + explicit FScopedPassageColumnXYReuse(const FStandaloneShapeWorld& shape) + :queries_(FScopedPassageTileQueries::CurrentFor(shape)) + { + if(queries_)previousActive_=queries_->beginProjectionColumn(); + } + ~FScopedPassageColumnXYReuse() + {if(queries_)queries_->endProjectionColumn(previousActive_);} + FScopedPassageColumnXYReuse(const FScopedPassageColumnXYReuse&)=delete; + FScopedPassageColumnXYReuse& operator=(const FScopedPassageColumnXYReuse&)=delete; + private: + const FScopedPassageTileQueries* queries_=nullptr; + bool previousActive_=false; + }; + bool IsResolvedPassageColumnSample(std::size_t PassageIndex,std::size_t SegmentIndex, const VoxelForgeCore::Vec3d& Position) const { @@ -1410,21 +1539,25 @@ public: ||qzrecordPreparedFloorProfileRead(); + return FloorProfile>Value; + }; + const auto CarveHigh=std::lower_bound(FloorProfileQ.begin(), + FloorProfileQ.begin()+LowerMouthIndex+1, qz+Constants.carveSearchMargin, - [&](const auto& Point,double Value){return FloorProfile(Point)>Value;}); - const auto CarveLow=std::lower_bound(Passage.controlPoints.begin(), - Passage.controlPoints.begin()+LowerMouthIndex+1, + CompareFloorProfile); + const auto CarveLow=std::lower_bound(FloorProfileQ.begin(), + FloorProfileQ.begin()+LowerMouthIndex+1, qz-Constants.carveSearchMargin, - [&](const auto& Point,double Value){return FloorProfile(Point)>Value;}); + CompareFloorProfile); const std::ptrdiff_t CarveBegin=std::max(0, - (CarveHigh-Passage.controlPoints.begin())-1); + (CarveHigh-FloorProfileQ.begin())-1); const std::ptrdiff_t CarveEnd=std::min(LowerMouthIndex-1, - (CarveLow-Passage.controlPoints.begin())+1); + (CarveLow-FloorProfileQ.begin())+1); const double rawSignedDistanceLimit=-static_cast(density)*Q +0.24*qPerMetre; const double SignedDistanceLimit=passageCarveReachEnabled @@ -1514,16 +1647,20 @@ public: if(index>=route.size())return; if(passageProfile)++passageProfile->passageMouthRouteSegmentChecks; const auto& segment=route[index]; - const double dx=static_cast(segment.b[0]-segment.a[0]); - const double dy=static_cast(segment.b[1]-segment.a[1]); - const double lengthSq=dx*dx+dy*dy; - const double t=lengthSq>0.0?std::clamp(((qx-segment.a[0])*dx - +(qy-segment.a[1])*dy)/lengthSq,0.0,1.0):0.0; - const double centerX=segment.a[0]+t*dx,centerY=segment.a[1]+t*dy; - const double centerZ=segment.a[2] - +t*(static_cast(segment.b[2])-segment.a[2]); + const auto* l98TileQueries=ActivePassageTileQueries; + if(l98TileQueries&&l98TileQueries->matches(*this)) + l98TileQueries->recordPreparedSegmentUse( + FScopedPassageTileQueries::EPreparedSegmentUse::Mouth); + const double dx=segment.preparedDelta[0],dy=segment.preparedDelta[1]; + const double lengthSq=segment.horizontalLengthSq; + const double t=lengthSq>0.0?std::clamp( + PassageMouthSupportXYProjectionNumerator(passageIndex,index,qx,qy) + /lengthSq,0.0,1.0):0.0; + const double centerX=segment.preparedA[0]+t*dx, + centerY=segment.preparedA[1]+t*dy; + const double centerZ=segment.preparedA[2]+t*segment.preparedDelta[2]; const double routeRadius=segment.hasRadius - ?segment.a[3]+t*(static_cast(segment.b[3])-segment.a[3]):0.0; + ?segment.preparedA[3]+t*segment.preparedDelta[3]:0.0; const double halfWidth=std::clamp(routeRadius, 0.42*qPerMetre,0.56*qPerMetre); const double horizontalX=qx-centerX,horizontalY=qy-centerY; @@ -1703,25 +1840,31 @@ public: { continue; } - const auto floorProfile=[&](const auto& point) - {return point.positionSubvoxels[2]-point.radiusSubvoxels+playerCenterAboveFloorQ;}; const std::ptrdiff_t lastIndex=passageConstants.lastIndex; const std::ptrdiff_t lowerMouthIndex=passageConstants.lowerMouthIndex; - const auto mainEnd=passage.controlPoints.begin()+lowerMouthIndex+1; + const auto& floorProfileQ=passageConstants.controlFloorProfileQ; // The tube radius is measured from its centerline, while floorProfile is // one player-center offset away. Search the full vertical radius of the SDF; // a narrow point window misses earlier turns of a low-pitch helix. const double carveSearchMargin=passageConstants.carveSearchMargin; - const auto carveHigh=std::lower_bound(passage.controlPoints.begin(),mainEnd, + const auto compareFloorProfile=[&](double floorProfile,double value) + { + if(passageTileScopeMatches) + passageTileQueries->recordPreparedFloorProfileRead(); + return floorProfile>value; + }; + const auto carveHigh=std::lower_bound(floorProfileQ.begin(), + floorProfileQ.begin()+lowerMouthIndex+1, qz+carveSearchMargin, - [&](const auto& point,double value){return floorProfile(point)>value;}); - const auto carveLow=std::lower_bound(passage.controlPoints.begin(),mainEnd, + compareFloorProfile); + const auto carveLow=std::lower_bound(floorProfileQ.begin(), + floorProfileQ.begin()+lowerMouthIndex+1, qz-carveSearchMargin, - [&](const auto& point,double value){return floorProfile(point)>value;}); + compareFloorProfile); const std::ptrdiff_t carveBegin=std::max(0, - (carveHigh-passage.controlPoints.begin())-1); + (carveHigh-floorProfileQ.begin())-1); const std::ptrdiff_t carveEnd=std::min(lowerMouthIndex-1, - (carveLow-passage.controlPoints.begin())+1); + (carveLow-floorProfileQ.begin())+1); const std::ptrdiff_t mouthSearchBegin=passageConstants.mouthSearchBegin; const std::ptrdiff_t mouthSearchEnd=passageConstants.mouthSearchEnd; if(l90CandidateProfiled) @@ -1801,27 +1944,27 @@ public: { l90IterationTimer.recordCarve(); if(passageProfile)++passageProfile->passageCarveSegmentVisits; + const FPassageSegmentData& segment=GetPassageSegmentData( + passageIndex,segmentIndex); + if(passageTileScopeMatches) + passageTileQueries->recordPreparedSegmentUse( + FScopedPassageTileQueries::EPreparedSegmentUse::Carve); 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]); - const double ay=static_cast(a.positionSubvoxels[1]); - const double az=static_cast(a.positionSubvoxels[2]); - const double dx=static_cast(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; + const double ax=segment.a[0],ay=segment.a[1],az=segment.a[2]; + const double dx=segment.delta[0],dy=segment.delta[1],dz=segment.delta[2]; + const double lengthSq=segment.lengthSq; if(lengthSq<=0.0)return; - const double t=std::clamp(((qx-ax)*dx+(qy-ay)*dy+(qz-az)*dz)/lengthSq,0.0,1.0); + const double projectionXY=PassageXYProjectionNumerator( + passageIndex,segmentIndex,qx,qy); + const double t=std::clamp((projectionXY+(qz-az)*dz)/lengthSq,0.0,1.0); if(!passageTileScopeMatches) { - const double horizontalLengthSq=dx*dx+dy*dy; + const double horizontalLengthSq=segment.horizontalLengthSq; const double playerT=horizontalLengthSq>0.0 - ?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):t; + ?std::clamp(projectionXY/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 playerRadius=segment.radiusA+playerT*segment.radiusDelta; const double playerCenterZ=az+playerT*dz-playerRadius+playerCenterAboveFloorQ; const double playerVertical=std::max(0.0, std::abs(qz-playerCenterZ)-0.54*qPerMetre); @@ -1832,23 +1975,21 @@ public: double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz; const bool smoothCarveJoin=index>2 &&index+(internalChainPassage?2:12)passageSmoothCarveJoins; - const auto& p0=passage.controlPoints[static_cast(index-1)]; - const auto& p3=passage.controlPoints[static_cast(index+2)]; constexpr double SmoothJoinWeight=1.0; const double t2=t*t,t3=t2*t; for(std::size_t axis=0;axis<3;++axis) { - const double v0=static_cast(p0.positionSubvoxels[axis]); - const double v1=static_cast(a.positionSubvoxels[axis]); - const double v2=static_cast(b.positionSubvoxels[axis]); - const double v3=static_cast(p3.positionSubvoxels[axis]); + const double v0=segment.catmullBefore[axis]; + const double v1=segment.a[axis]; + const double v2=segment.b[axis]; + const double v3=segment.catmullAfter[axis]; const double curve=0.5*((2.0*v1)+(-v0+v2)*t +(2.0*v0-5.0*v1+4.0*v2-v3)*t2 +(-v0+3.0*v1-3.0*v2+v3)*t3); @@ -1860,8 +2001,7 @@ public: } } 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 radius=segment.radiusA+t*segment.radiusDelta; const double signedDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius; if(signedDistance(bestIndex))); - const auto& nearestA=passage.controlPoints[static_cast(bestIndex)]; - const auto& nearestB=passage.controlPoints[static_cast(bestIndex+1)]; - const double nearestRadius=nearestA.radiusSubvoxels+bestT*( - static_cast(nearestB.radiusSubvoxels)-nearestA.radiusSubvoxels); + const FPassageSegmentData& nearestSegment=PassageSegmentData[passageIndex][ + static_cast(bestIndex)]; + const double nearestRadius=nearestSegment.radiusA + +bestT*nearestSegment.radiusDelta; const double distanceToCenter=std::max(1.0e-9,bestSignedDistance+nearestRadius); const double verticalWallAlignment=distanceToCenter>1.0e-9 - ?std::clamp(-(qz-(nearestA.positionSubvoxels[2]+bestT*( - static_cast(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2]))) + ?std::clamp(-(qz-(nearestSegment.a[2]+bestT*nearestSegment.delta[2])) /distanceToCenter,0.0,1.0):0.0; const std::uint32_t passageReliefSeed=static_cast(Owner?Owner->Seed:0); float cachedWallRelief=0.0f,cachedMouthSurfaceRelief=0.0f, @@ -1959,15 +2098,11 @@ public: }; const auto routeSegmentLengthMetres=[&](std::ptrdiff_t routeIndex) { - const auto& before=passage.controlPoints[static_cast(routeIndex)]; - const auto& after=passage.controlPoints[static_cast(routeIndex+1)]; - const double dx=static_cast(after.positionSubvoxels[0]) - -before.positionSubvoxels[0]; - const double dy=static_cast(after.positionSubvoxels[1]) - -before.positionSubvoxels[1]; - const double dz=static_cast(after.positionSubvoxels[2]) - -before.positionSubvoxels[2]; - return DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre; + if(passageTileScopeMatches) + passageTileQueries->recordPreparedSegmentUse( + FScopedPassageTileQueries::EPreparedSegmentUse::Mouth); + return PassageSegmentData[passageIndex][ + static_cast(routeIndex)].lengthMetres; }; double distanceFromOpenMouth=std::numeric_limits::infinity(); if(bestIndex<=2) @@ -2007,19 +2142,22 @@ public: routeIndex(3,lowerMouthIndex);++routeIndex) { if(passageProfile)++passageProfile->passageMouthRouteSegmentChecks; - const auto& before=passage.controlPoints[static_cast(routeIndex)]; - const auto& after=passage.controlPoints[static_cast(routeIndex+1)]; - const double ax=static_cast(before.positionSubvoxels[0]); - const double ay=static_cast(before.positionSubvoxels[1]); - const double az=static_cast(before.positionSubvoxels[2]); - const double dx=static_cast(after.positionSubvoxels[0])-ax; - const double dy=static_cast(after.positionSubvoxels[1])-ay; - const double dz=static_cast(after.positionSubvoxels[2])-az; - const double horizontalLengthSq=dx*dx+dy*dy; + const FPassageSegmentData& routeSegment=PassageSegmentData[ + passageIndex][static_cast(routeIndex)]; + if(passageTileScopeMatches) + passageTileQueries->recordPreparedSegmentUse( + FScopedPassageTileQueries::EPreparedSegmentUse::Mouth); + const double ax=routeSegment.a[0],ay=routeSegment.a[1], + az=routeSegment.a[2]; + const double dx=routeSegment.delta[0],dy=routeSegment.delta[1], + dz=routeSegment.delta[2]; + const double horizontalLengthSq=routeSegment.horizontalLengthSq; const double t=horizontalLengthSq>0.0?std::clamp( - ((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):0.0; + PassageXYProjectionNumerator(passageIndex, + static_cast(routeIndex),qx,qy) + /horizontalLengthSq,0.0,1.0):0.0; double projectedX=ax+t*dx,projectedY=ay+t*dy; - const double horizontalLength=DeterministicSqrt(horizontalLengthSq); + const double horizontalLength=routeSegment.horizontalLength; if(horizontalLength>1.0e-6) { const double pathRelief=std::clamp(static_cast( @@ -2039,8 +2177,7 @@ public: { nearestEntryPlanDistance=planDistance; const double centerZ=az+t*dz; - const double radius=before.radiusSubvoxels+t*( - static_cast(after.radiusSubvoxels)-before.radiusSubvoxels); + const double radius=routeSegment.radiusA+t*routeSegment.radiusDelta; mouthEntryFloorQ=centerZ-radius+0.20*qPerMetre; const double centerDistance=DeterministicSqrt(planDx*planDx +planDy*planDy+(qz-centerZ)*(qz-centerZ)); @@ -2182,13 +2319,15 @@ public: { l90IterationTimer.recordTread(); if(passageProfile)++passageProfile->passageSupportSegmentVisits; + const FPassageSegmentData& segment=GetPassageSegmentData( + passageIndex,static_cast(index)); + if(passageTileScopeMatches) + passageTileQueries->recordPreparedSegmentUse( + FScopedPassageTileQueries::EPreparedSegmentUse::Support); 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 double dx=segment.delta[0],dy=segment.delta[1],dz=segment.delta[2]; + const double radiusA=segment.radiusA; + const double radiusB=segment.radiusB; const bool entryMouthShoulder=index<=2; // Keep the tread within the probe band and below the capsule's // lowest sampled point. @@ -2200,7 +2339,7 @@ public: double floorOffset=bridgeDeck ?PassageBridgeDeckOffsetMetres(passageIndex,static_cast(index)) :std::lerp(internalChainPassage?0.20:0.15,0.20,lowerMouthBlend); - const double horizontalLengthSq=dx*dx+dy*dy; + const double horizontalLengthSq=segment.horizontalLengthSq; const bool overlapSupportJoin=index+12(index)) &&(index==0||(!IsPassageShortSpanSegment(passageIndex, @@ -2208,25 +2347,21 @@ public: 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; + ?PassageXYProjectionNumerator(passageIndex, + static_cast(index),qx,qy)/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; + ?std::min(0.5,2.0*qPerMetre/segment.horizontalLength):0.0; const double supportT=horizontalLengthSq>0.0 ?std::clamp(projection,-overlapFraction,1.0+overlapFraction):bestT; - double centerX=segmentA.positionSubvoxels[0]+supportT*dx; - 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); + double centerX=segment.a[0]+supportT*dx; + double centerY=segment.a[1]+supportT*dy; + double centerZ=segment.a[2]+supportT*dz; + double radius=radiusA+supportT*segment.radiusDelta; if(smoothSupportJoin) { if(passageProfile)++passageProfile->passageSmoothSupportJoins; - const auto& p0=passage.controlPoints[static_cast(index-1)]; - const auto& p3=passage.controlPoints[static_cast(index+2)]; const double t2=supportT*supportT,t3=t2*supportT; const auto interpolate=[&](double v0,double v1,double v2,double v3) { @@ -2235,18 +2370,18 @@ public: +(-v0+3.0*v1-3.0*v2+v3)*t3); }; const double SmoothSupportHeightWeight=internalChainPassage?0.0:0.25; - centerZ+=(interpolate(p0.positionSubvoxels[2],segmentA.positionSubvoxels[2], - segmentB.positionSubvoxels[2],p3.positionSubvoxels[2])-centerZ) + centerZ+=(interpolate(segment.catmullBefore[2],segment.a[2], + segment.b[2],segment.catmullAfter[2])-centerZ) *SmoothSupportHeightWeight; - radius+=(interpolate(p0.radiusSubvoxels,segmentA.radiusSubvoxels, - segmentB.radiusSubvoxels,p3.radiusSubvoxels)-radius) + radius+=(interpolate(segment.catmullRadiusBefore,radiusA, + radiusB,segment.catmullRadiusAfter)-radius) *SmoothSupportHeightWeight; } double treadRelief=0.0; if(entryMouthShoulder ||(index>=lowerMouthIndex-2&&index<=lowerMouthIndex+2)) { - const double horizontalLength=DeterministicSqrt(horizontalLengthSq); + const double horizontalLength=segment.horizontalLength; if(horizontalLength>1.0e-6) { const double pathRelief=std::clamp(static_cast( @@ -2272,8 +2407,7 @@ public: // not turn that tread into a ceiling or mid-air shelf. // 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 double supportSegmentLength=segment.lengthMetres; const double sparseRouteThreshold=VoxelForgeCore::WorldAssemblyEvaluatorVersion>=188 &&floatingIslandPassage&&passage.style==0?1.0:1.5; const bool sparseRouteSupport=supportSegmentLength>sparseRouteThreshold; @@ -2311,17 +2445,11 @@ public: double prefixMetres=0.0; for(std::ptrdiff_t routeIndex=0;routeIndex(routeIndex)]; - const auto& after=passage.controlPoints[ - static_cast(routeIndex+1)]; - const double dx=static_cast(after.positionSubvoxels[0]) - -before.positionSubvoxels[0]; - const double dy=static_cast(after.positionSubvoxels[1]) - -before.positionSubvoxels[1]; - const double dz=static_cast(after.positionSubvoxels[2]) - -before.positionSubvoxels[2]; - prefixMetres+=DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre; + if(passageTileScopeMatches) + passageTileQueries->recordPreparedSegmentUse( + FScopedPassageTileQueries::EPreparedSegmentUse::Support); + prefixMetres+=PassageSegmentData[passageIndex][ + static_cast(routeIndex)].lengthMetres; } distanceFromMouthMetres=std::max(0.0, prefixMetres+supportT*supportSegmentLength); @@ -2332,17 +2460,11 @@ public: for(std::ptrdiff_t routeIndex=index+1; routeIndex(routeIndex)]; - const auto& after=passage.controlPoints[ - static_cast(routeIndex+1)]; - const double dx=static_cast(after.positionSubvoxels[0]) - -before.positionSubvoxels[0]; - const double dy=static_cast(after.positionSubvoxels[1]) - -before.positionSubvoxels[1]; - const double dz=static_cast(after.positionSubvoxels[2]) - -before.positionSubvoxels[2]; - suffixMetres+=DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre; + if(passageTileScopeMatches) + passageTileQueries->recordPreparedSegmentUse( + FScopedPassageTileQueries::EPreparedSegmentUse::Support); + suffixMetres+=PassageSegmentData[passageIndex][ + static_cast(routeIndex)].lengthMetres; } distanceFromMouthMetres=std::max(0.0, suffixMetres+(1.0-supportT)*supportSegmentLength); @@ -2465,11 +2587,11 @@ public: // whose floor is just below this density query, not the closest wall. const double supportProfileOffset=surfaceToTunnelSupport?1.13:0.93; const double supportProfileTarget=qz+supportProfileOffset*qPerMetre; - const auto supportProfile=std::lower_bound(passage.controlPoints.begin(),mainEnd, - supportProfileTarget,[&](const auto& point,double value) - {return floorProfile(point)>value;}); + const auto supportProfile=std::lower_bound(floorProfileQ.begin(), + floorProfileQ.begin()+lowerMouthIndex+1,supportProfileTarget, + compareFloorProfile); const std::ptrdiff_t profileIndex=std::clamp( - supportProfile-passage.controlPoints.begin(),0,lowerMouthIndex); + supportProfile-floorProfileQ.begin(),0,lowerMouthIndex); // At floor height the nearest 3D centerline can belong to the next // coil below. The descending floor profile identifies the coil that // owns this tread, even when it is more than eight control points away. @@ -4470,12 +4592,77 @@ private: double firstFloorQ=0.0,lastFloorQ=0.0,carveSearchMargin=0.0; std::ptrdiff_t lowerMouthIndex=1,lastIndex=1; std::ptrdiff_t mouthSearchBegin=0,mouthSearchEnd=0; + std::vector controlFloorProfileQ; }; + struct FPassageSegmentData + { + std::array a{},b{},delta{},catmullBefore{},catmullAfter{}; + double radiusA=0.0,radiusB=0.0,radiusDelta=0.0; + double catmullRadiusBefore=0.0,catmullRadiusAfter=0.0; + double lengthSq=0.0,horizontalLengthSq=0.0; + double lengthSubvoxels=0.0,horizontalLength=0.0,lengthMetres=0.0; + }; + struct FPreparedPassageCapsuleSegment + { + std::array a{},delta{}; + double horizontalLengthSq=0.0; + }; + const FPassageSegmentData& GetPassageSegmentData(std::size_t passageIndex, + std::size_t segmentIndex) const + {return PassageSegmentData[passageIndex][segmentIndex];} + + double PassageXYProjectionNumerator(std::size_t passageIndex, + std::size_t segmentIndex,double qx,double qy) const + { + const FPassageSegmentData& segment=GetPassageSegmentData( + passageIndex,segmentIndex); + const std::size_t flatIndex=PassageSegmentOffsets[passageIndex]+segmentIndex; + const auto* tileQueries=ActivePassageTileQueries; + if(tileQueries&&tileQueries->matches(*this)) + return tileQueries->projectionNumerator(flatIndex,qx,qy, + segment.a[0],segment.a[1],segment.delta[0],segment.delta[1]); + return (qx-segment.a[0])*segment.delta[0] + +(qy-segment.a[1])*segment.delta[1]; + } + + double PassageCapsuleRouteXYProjectionNumerator(std::size_t stratumIndex, + std::size_t segmentIndex,double qx,double qy) const + { + const FPreparedPassageCapsuleSegment& segment= + PassageStratumCapsuleSegments[stratumIndex][segmentIndex]; + const std::size_t flatIndex=PassageSegmentOffsets.back() + +PassageMouthSupportSegmentOffsets.back() + +PassageStratumCapsuleSegmentOffsets[stratumIndex]+segmentIndex; + const auto* tileQueries=ActivePassageTileQueries; + if(tileQueries&&tileQueries->matches(*this)) + return tileQueries->projectionNumerator(flatIndex,qx,qy, + segment.a[0],segment.a[1],segment.delta[0],segment.delta[1]); + return (qx-segment.a[0])*segment.delta[0] + +(qy-segment.a[1])*segment.delta[1]; + } + struct FPassageMouthSupportSegment { std::array a{},b{}; + std::array preparedA{},preparedDelta{}; + double horizontalLengthSq=0.0; bool hasRadius=false; }; + double PassageMouthSupportXYProjectionNumerator(std::size_t passageIndex, + std::size_t routeIndex,double qx,double qy) const + { + const FPassageMouthSupportSegment& segment= + PassageMouthSupportRoutes[passageIndex][routeIndex]; + const std::size_t flatIndex=PassageSegmentOffsets.back() + +PassageMouthSupportSegmentOffsets[passageIndex]+routeIndex; + const auto* tileQueries=ActivePassageTileQueries; + if(tileQueries&&tileQueries->matches(*this)) + return tileQueries->projectionNumerator(flatIndex,qx,qy, + segment.preparedA[0],segment.preparedA[1], + segment.preparedDelta[0],segment.preparedDelta[1]); + return (qx-segment.preparedA[0])*segment.preparedDelta[0] + +(qy-segment.preparedA[1])*segment.preparedDelta[1]; + } struct FPassageNearestNode { std::array minimum{},maximum{}; @@ -5520,18 +5707,75 @@ private: void BuildPassageComposerConstants() { + const bool tracePreparation=VF_IsL98PassagePrepareTraceEnabled(); + const auto preparationStart=tracePreparation + ?std::chrono::steady_clock::now():std::chrono::steady_clock::time_point{}; PassageComposerConstants.clear(); - if(!ResolvedLayout)return; + PassageSegmentData.clear(); + PassageSegmentOffsets.clear(); + PassageStratumCapsuleSegments.clear(); + PassageStratumCapsuleSegmentOffsets.clear(); + if(!ResolvedLayout) + { + if(tracePreparation) + std::printf("[L98PassagePrepare] passages=0 segments=0 bytes=0 prepare_ms=%.3f\n", + std::chrono::duration( + std::chrono::steady_clock::now()-preparationStart).count()); + return; + } PassageComposerConstants.resize(ResolvedLayout->passages.size()); + PassageSegmentData.resize(ResolvedLayout->passages.size()); + PassageSegmentOffsets.resize(ResolvedLayout->passages.size()+1,0); + PassageStratumCapsuleSegments.resize(ResolvedLayout->strates.size()); + PassageStratumCapsuleSegmentOffsets.resize(ResolvedLayout->strates.size()+1,0); constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double qPerMetre=Q*1000000.0/ std::max(1,ResolvedLayout->coreVoxelSizeMicrometres); const double playerCenterAboveFloorQ=1.13*qPerMetre; + std::size_t preparedSegments=0; + std::size_t preparedFloorSamples=0; for(std::size_t passageIndex=0;passageIndexpassages.size(); ++passageIndex) { + PassageSegmentOffsets[passageIndex]=preparedSegments; const auto& passage=ResolvedLayout->passages[passageIndex]; - if(passage.controlPoints.size()<2)continue; + if(passage.controlPoints.size()<2) + { + PassageSegmentOffsets[passageIndex+1]=preparedSegments; + continue; + } + auto& segmentData=PassageSegmentData[passageIndex]; + segmentData.resize(passage.controlPoints.size()-1); + for(std::size_t segmentIndex=0;segmentIndex(a.positionSubvoxels[axis]); + data.b[axis]=static_cast(b.positionSubvoxels[axis]); + data.delta[axis]=data.b[axis]-data.a[axis]; + data.catmullBefore[axis]=static_cast(p0.positionSubvoxels[axis]); + data.catmullAfter[axis]=static_cast(p3.positionSubvoxels[axis]); + } + data.radiusA=static_cast(a.radiusSubvoxels); + data.radiusB=static_cast(b.radiusSubvoxels); + data.radiusDelta=static_cast(b.radiusSubvoxels)-a.radiusSubvoxels; + data.catmullRadiusBefore=static_cast(p0.radiusSubvoxels); + data.catmullRadiusAfter=static_cast(p3.radiusSubvoxels); + const double dx=data.delta[0],dy=data.delta[1],dz=data.delta[2]; + data.lengthSq=dx*dx+dy*dy+dz*dz; + data.horizontalLengthSq=dx*dx+dy*dy; + data.lengthSubvoxels=DeterministicSqrt(data.lengthSq); + data.horizontalLength=DeterministicSqrt(data.horizontalLengthSq); + data.lengthMetres=data.lengthSubvoxels/qPerMetre; + ++preparedSegments; + } + PassageSegmentOffsets[passageIndex+1]=preparedSegments; const auto supportStratum=[&](std::int32_t strateIndex) ->const VoxelForgeCore::ResolvedLayoutStratumV2* { @@ -5563,10 +5807,16 @@ private: &&supportUpper->passageCapabilities.hasFloatingIslandLanding; constants.surfaceToTunnelSupport=supportUpper&&supportLower &&supportUpper->lowerInterfaceCapabilities.surfaceToTunnelLandingSupport; - constants.firstFloorQ=passage.controlPoints.front().positionSubvoxels[2] - -passage.controlPoints.front().radiusSubvoxels+playerCenterAboveFloorQ; - constants.lastFloorQ=passage.controlPoints.back().positionSubvoxels[2] - -passage.controlPoints.back().radiusSubvoxels+playerCenterAboveFloorQ; + constants.controlFloorProfileQ.resize(passage.controlPoints.size()); + for(std::size_t pointIndex=0;pointIndex(passage.controlPoints.size())-1; constants.lowerMouthIndex=std::clamp( static_cast(passage.lowerMouthControlIndex),1, @@ -5578,6 +5828,43 @@ private: constants.carveSearchMargin=std::max(passage.mouthRadiusSubvoxels, passage.midRadiusSubvoxels)+0.25*qPerMetre; } + std::size_t preparedCapsuleSegments=0; + for(std::size_t stratumIndex=0;stratumIndexstrates.size(); + ++stratumIndex) + { + PassageStratumCapsuleSegmentOffsets[stratumIndex]=preparedCapsuleSegments; + const auto& stratum=ResolvedLayout->strates[stratumIndex]; + std::vector routePoints; + for(const auto& anchor:stratum.anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + routePoints.push_back(&anchor); + auto& prepared=PassageStratumCapsuleSegments[stratumIndex]; + if(routePoints.size()>1)prepared.resize(routePoints.size()-1); + for(std::size_t segmentIndex=0;segmentIndexvalues[axis]; + segment.delta[axis]=routePoints[segmentIndex+1]->values[axis] + -segment.a[axis]; + } + const double dx=segment.delta[0],dy=segment.delta[1]; + segment.horizontalLengthSq=dx*dx+dy*dy; + ++preparedCapsuleSegments; + } + PassageStratumCapsuleSegmentOffsets[stratumIndex+1]=preparedCapsuleSegments; + } + if(tracePreparation) + std::printf("[L98PassagePrepare] passages=%zu segments=%zu floor_samples=%zu capsule_route_segments=%zu bytes=%zu prepare_ms=%.3f\n", + PassageSegmentData.size(),preparedSegments, + preparedFloorSamples, + preparedCapsuleSegments, + preparedSegments*sizeof(FPassageSegmentData) + +preparedFloorSamples*sizeof(double) + +preparedCapsuleSegments*sizeof(FPreparedPassageCapsuleSegment), + std::chrono::duration( + std::chrono::steady_clock::now()-preparationStart).count()); } void BuildPassageSpatialIndices() @@ -5595,6 +5882,7 @@ private: PassageNearestNodes.resize(ResolvedLayout?ResolvedLayout->passages.size():0); PassageMouthSupportRoutes.clear(); PassageMouthSupportRoutes.resize(ResolvedLayout?ResolvedLayout->passages.size():0); + PassageMouthSupportSegmentOffsets.assign(PassageMouthSupportRoutes.size()+1,0); PassageLaterBoreSources.clear(); PassageColumnSegmentFlags.clear();PassageShortSpanSegmentFlags.clear(); PassageHasShortSpanFlags.clear(); @@ -5832,6 +6120,36 @@ private: } } } + const bool traceMouthPreparation=VF_IsL98PassagePrepareTraceEnabled(); + const auto mouthPreparationStart=traceMouthPreparation + ?std::chrono::steady_clock::now():std::chrono::steady_clock::time_point{}; + std::size_t mouthSupportSegmentCount=0; + for(std::size_t passageIndex=0;passageIndex(segment.a[axis]); + segment.preparedDelta[axis]=axis<2 + ?static_cast(segment.b[axis]-segment.a[axis]) + :static_cast(segment.b[axis])-segment.a[axis]; + } + const double dx=segment.preparedDelta[0]; + const double dy=segment.preparedDelta[1]; + segment.horizontalLengthSq=dx*dx+dy*dy; + ++mouthSupportSegmentCount; + } + PassageMouthSupportSegmentOffsets[passageIndex+1]=mouthSupportSegmentCount; + } + if(traceMouthPreparation) + std::printf("[L98PassageMouthPrepare] segments=%zu bytes=%zu prepare_ms=%.3f\n", + mouthSupportSegmentCount, + mouthSupportSegmentCount*sizeof(FPassageMouthSupportSegment), + std::chrono::duration( + std::chrono::steady_clock::now()-mouthPreparationStart).count()); // Precompute the later-bore leaves that can intersect each tread segment. // Passage support samples normally stay within this source envelope, so the // hot query can test a short list of physical bounds instead of walking a @@ -6627,19 +6945,23 @@ private: 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; + const FPassageSegmentData& segment=GetPassageSegmentData( + passageIndex,index); + const auto* l98TileQueries=ActivePassageTileQueries; + if(l98TileQueries&&l98TileQueries->matches(*this)) + l98TileQueries->recordPreparedSegmentUse( + FScopedPassageTileQueries::EPreparedSegmentUse::Capsule); + const double ax=segment.a[0],ay=segment.a[1],az=segment.a[2]; + const double dx=segment.delta[0],dy=segment.delta[1],dz=segment.delta[2]; + const double lengthSq=segment.lengthSq; if(lengthSq<=0.0)continue; - const double horizontalLengthSq=dx*dx+dy*dy; + const double horizontalLengthSq=segment.horizontalLengthSq; const double t=horizontalLengthSq>0.0 - ?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):0.0; + ?std::clamp(PassageXYProjectionNumerator(passageIndex,index,qx,qy) + /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 centerZ=az+t*dz-(segment.radiusA + +t*segment.radiusDelta)+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 @@ -6741,27 +7063,27 @@ private: constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double qPerMetre=Q*1000000.0/std::max(1, ResolvedLayout->coreVoxelSizeMicrometres); - const auto& a=passage.controlPoints[SegmentIndex]; - const auto& b=passage.controlPoints[SegmentIndex+1]; const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[&passage](const auto& value) {return value.strateIndex==passage.upperStrateIndex;}); const bool surfaceWorldUpper=upperStratum!=ResolvedLayout->strates.end() &&upperStratum->passageCapabilities.hasSurfaceLanding; - const double ax=static_cast(a.positionSubvoxels[0]); - const double ay=static_cast(a.positionSubvoxels[1]); - const double dx=static_cast(b.positionSubvoxels[0])-ax; - const double dy=static_cast(b.positionSubvoxels[1])-ay; - const double horizontalLengthSq=dx*dx+dy*dy; + const FPassageSegmentData& segment=GetPassageSegmentData(PassageIndex,SegmentIndex); + const auto* l98TileQueries=ActivePassageTileQueries; + if(l98TileQueries&&l98TileQueries->matches(*this)) + l98TileQueries->recordPreparedSegmentUse( + FScopedPassageTileQueries::EPreparedSegmentUse::Support); + const double ax=segment.a[0],ay=segment.a[1]; + const double dx=segment.delta[0],dy=segment.delta[1]; + const double horizontalLengthSq=segment.horizontalLengthSq; const double qx=static_cast(X)*Q,qy=static_cast(Y)*Q; const double qz=static_cast(Z)*Q; const double t=horizontalLengthSq>0.0 - ?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):0.0; + ?std::clamp(PassageXYProjectionNumerator(PassageIndex,SegmentIndex,qx,qy) + /horizontalLengthSq,0.0,1.0):0.0; const double centerX=ax+t*dx,centerY=ay+t*dy; - const double centerZ=a.positionSubvoxels[2]+t*( - static_cast(b.positionSubvoxels[2])-a.positionSubvoxels[2]); - const double radius=a.radiusSubvoxels+t*( - static_cast(b.radiusSubvoxels)-a.radiusSubvoxels); + const double centerZ=segment.a[2]+t*segment.delta[2]; + const double radius=segment.radiusA+t*segment.radiusDelta; // Embed the deck 13 cm above the bore bottom: within the 25 cm support // probe, while leaving 16 cm between it and the capsule's lowest sample. const double floorQ=centerZ-radius @@ -6819,27 +7141,31 @@ private: const double capsuleRadius=0.37*qPerMetre; const double capsuleHalfHeight=0.86*qPerMetre; const double capsuleRadiusSquared=capsuleRadius*capsuleRadius; - const VoxelForgeCore::ResolvedLayoutAnchorV2* previous=nullptr; - for(const auto& anchor:lower->anchors) + const std::size_t lowerStratumIndex=static_cast( + lower-ResolvedLayout->strates.begin()); + const auto& routeSegments=PassageStratumCapsuleSegments[ + lowerStratumIndex]; + for(std::size_t segmentIndex=0;segmentIndexvalues[0],ay=previous->values[1]; - const double dx=static_cast(anchor.values[0])-ax; - const double dy=static_cast(anchor.values[1])-ay; - const double lengthSquared=dx*dx+dy*dy; - const double t=lengthSquared>0.0?std::clamp( - ((qx-ax)*dx+(qy-ay)*dy)/lengthSquared,0.0,1.0):0.0; - const double routeX=ax+t*dx,routeY=ay+t*dy; - const double routeZ=previous->values[2]+t*( - static_cast(anchor.values[2])-previous->values[2]); - const double routeDx=qx-routeX,routeDy=qy-routeY; - if(routeDx*routeDx+routeDy*routeDy<=capsuleRadiusSquared - &&std::abs(qz-routeZ)<=capsuleHalfHeight) - return maximumRadius+qPerMetre; - } - previous=&anchor; + const FPreparedPassageCapsuleSegment& segment= + routeSegments[segmentIndex]; + const auto* l98TileQueries=ActivePassageTileQueries; + if(l98TileQueries&&l98TileQueries->matches(*this)) + l98TileQueries->recordPreparedSegmentUse( + FScopedPassageTileQueries::EPreparedSegmentUse::Capsule); + const double ax=segment.a[0],ay=segment.a[1]; + const double dx=segment.delta[0],dy=segment.delta[1]; + const double lengthSquared=segment.horizontalLengthSq; + const double t=lengthSquared>0.0?std::clamp( + PassageCapsuleRouteXYProjectionNumerator(lowerStratumIndex, + segmentIndex,qx,qy)/lengthSquared,0.0,1.0):0.0; + const double routeX=ax+t*dx,routeY=ay+t*dy; + const double routeZ=segment.a[2]+t*segment.delta[2]; + const double routeDx=qx-routeX,routeDy=qy-routeY; + if(routeDx*routeDx+routeDy*routeDy<=capsuleRadiusSquared + &&std::abs(qz-routeZ)<=capsuleHalfHeight) + return maximumRadius+qPerMetre; } } } @@ -12778,6 +13104,7 @@ public: return; } if(Count<=0||OutDensity==nullptr)return; + FScopedPassageColumnXYReuse passageColumnXYReuse(*this); FScopedRoute l77DispatchRoute(ERoute::Dispatch); FScopedPhase l77DispatchPhase(EPhase::Dispatch); FScopedDensityQueryProfile queryProfile(EDensityQueryKind::Lattice, @@ -13543,7 +13870,13 @@ private: bool PassageFeatureBoundsFailOpen=false; std::vector> PassageNearestNodes; std::vector> PassageMouthSupportRoutes; + std::vector PassageMouthSupportSegmentOffsets; std::vector PassageComposerConstants; + std::vector> PassageSegmentData; + std::vector PassageSegmentOffsets; + std::vector> + PassageStratumCapsuleSegments; + std::vector PassageStratumCapsuleSegmentOffsets; std::vector> PassageLaterBoreSources; std::vector PassageProofBounds; std::vector PassageProofNodes; diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index abfc4b7..e811f19 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace vfcore::detail { @@ -2799,6 +2800,94 @@ namespace VoxelForgeCore options.profilePassageComposer?&out.diagnostics:nullptr,key.lod, options.diagnosticPassageSourceFloorVoxels, options.diagnosticPassageCarveReachMetres); + static std::atomic l98ProjectionProbeStarted{false}; + const char* l98ProjectionProbeSwitch=std::getenv( + "VOXELFORGE_L98_PASSAGE_REUSE_PROBE"); + bool expectedProbeStart=false; + if(l98ProjectionProbeSwitch&&l98ProjectionProbeSwitch[0]=='1' + &&l98ProjectionProbeSwitch[1]=='\0'&&!tileShape.IsNativeOnly() + &&impl_->resolvedLayout + &&!impl_->resolvedLayout->passages.empty() + &&l98ProjectionProbeStarted.compare_exchange_strong(expectedProbeStart,true, + std::memory_order_relaxed)) + { + constexpr std::int64_t ProbePadding=8 + *WorldAssemblySubvoxelsPerVoxel; + std::array probeMinimum={ + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()}; + std::array probeMaximum={ + std::numeric_limits::min(), + std::numeric_limits::min(), + std::numeric_limits::min()}; + const auto includeProbePoint=[&](std::size_t axis,double coordinate, + double radius) + { + if(!std::isfinite(coordinate)||!std::isfinite(radius))return; + probeMinimum[axis]=std::min(probeMinimum[axis], + static_cast(std::floor(coordinate-radius))-ProbePadding); + probeMaximum[axis]=std::max(probeMaximum[axis], + static_cast(std::ceil(coordinate+radius))+ProbePadding); + }; + for(const auto& passage:impl_->resolvedLayout->passages) + for(const auto& point:passage.controlPoints) + for(std::size_t axis=0;axis<3;++axis) + includeProbePoint(axis,static_cast( + point.positionSubvoxels[axis]), + static_cast(point.radiusSubvoxels)); + for(const auto& stratum:impl_->resolvedLayout->strates) + for(const auto& anchor:stratum.anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + for(std::size_t axis=0;axis<3;++axis) + includeProbePoint(axis,anchor.values[axis],0.0); + std::uint64_t probeRuns=0,probeSamples=0,probeMismatches=0; + if(probeMinimum[0]<=probeMaximum[0] + &&probeMinimum[1]<=probeMaximum[1] + &&probeMinimum[2]<=probeMaximum[2]) + { + const FStandaloneShapeWorld::FScopedPassageTileQueries probeScope( + tileShape,probeMinimum,probeMaximum,nullptr,key.lod); + constexpr int32 ProbeCount=8; + float runDensity[ProbeCount]{}; + for(const auto& passage:impl_->resolvedLayout->passages) + for(std::size_t segmentIndex=0; + segmentIndex+1(std::llround( + subvoxels/WorldAssemblySubvoxelsPerVoxel));}; + const std::int32_t x=toVoxel(0.5*( + static_cast(a.positionSubvoxels[0]) + +static_cast(b.positionSubvoxels[0]))); + const std::int32_t y=toVoxel(0.5*( + static_cast(a.positionSubvoxels[1]) + +static_cast(b.positionSubvoxels[1]))); + const std::int32_t zMid=toVoxel(0.5*( + static_cast(a.positionSubvoxels[2]) + +static_cast(b.positionSubvoxels[2]))); + const std::int32_t zStart=zMid-ProbeCount/2; + tileShape.DensityAtLatticeZRun(x,y,zStart,1,ProbeCount,runDensity); + ++probeRuns; + probeSamples+=ProbeCount; + for(std::int32_t lane=0;lane(scalarDensity) + !=std::bit_cast(runDensity[lane])) + ++probeMismatches; + } + } + } + std::printf("[L98PassageReuseProbe] z_runs=%llu samples=%llu scalar_mismatches=%llu status=%s\n", + static_cast(probeRuns), + static_cast(probeSamples), + static_cast(probeMismatches), + probeRuns>0&&probeMismatches==0?"PASS":"FAIL"); + } if(options.profilePassageComposer) { out.diagnostics.passageComposerProfiled=true; diff --git a/Standalone/Research/WorldAssemblerM3/Linux98/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux98/REPORT.md new file mode 100644 index 0000000..aabbd83 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux98/REPORT.md @@ -0,0 +1,61 @@ +# L98 — exact passage building blocks + +## Changes and work counts + +Passage setup now prepares per-segment chord endpoints/deltas, squared lengths, radii, Catmull–Rom inputs, and floor-profile samples once per world. Capsule-route and mouth-support callbacks also use prepared segment endpoints and deltas. The scalar expression grouping, divisions, and square roots remain in the same order. Relief interpolation sharing was not added. + +Measured preparation per world: 310 passage segments, 313 floor samples, and 32 capsule-route segments; the trace reports 66,296 bytes. The 32 mouth-route segments report another 4,608 bytes. Preparation measured 0.025–0.040 ms in the whole-view hash runs (mouth preparation rounded to 0.000 ms). + +The streamed-view counters below are prepared-data reads/uses, not baseline arithmetic counts. They directly confirm the prepared paths were exercised. Avoided recomputations are inferred from the replaced per-sample expressions; the old arithmetic was not separately instrumented. + +| World | Floor-profile reads | Carve uses | Support/tread uses | Capsule uses | Mouth-support uses | +|---|---:|---:|---:|---:|---:| +| FI | 129,382,181 | 8,255,393 | 5,131,690 | 2,693,464 | 14,773,980 | +| TN | 396,540,009 | 50,346,149 | 19,176,668 | 11,657,176 | 63,632,356 | +| SW | 318,412,931 | 50,093,885 | 17,065,041 | 10,323,467 | 60,102,531 | +| VS | 99,874,533 | 5,141,277 | 3,142,836 | 662,072 | 8,984,390 | + +The XY cache keeps the scalar numerator grouping `(qx-ax)*dx + (qy-ay)*dy`; clamp, winner, reach, and composition remain per lane. A direct probe exercised 310 z-runs × 8 samples in each world: 2,558 numerator computations and 31,085 cache hits, with zero density mismatches against scalar evaluation. The measured hit fraction was 92.4% of numerator requests. The ordinary streamed view does not call the z-run API, so these XY reuse counts come from the opt-in direct probe, not natural view traffic. + +## Exactness + +Compared rebuilt committed-HEAD baseline to L98 Clang, then L98 Clang to L98 GCC. Density tile hashes aggregate the density sample bit patterns. All comparisons had zero tile-hash mismatches at every LOD, zero LOD0–2 density-tile mismatches, and identical whole-view density, mesh, payload, and topology hashes. + +| World | Whole-view tiles | LOD0–2 density mismatches | Whole-view density | Mesh | Payload | Topology | +|---|---:|---:|---|---|---|---| +| FI | 2,850 | 0 | `40b76d0072a2adcd` | `8559df0850d4d7f1` | `7700965ff31ac702` | `0e7f2a16b4cc71a9` | +| TN | 2,249 | 0 | `99ca213b21ce86e4` | `2977525405b79fff` | `3d7d2efb581710bd` | `af1916c949224871` | +| SW | 2,233 | 0 | `8d23c26f5c092be1` | `15c3aad2e29d9ea0` | `3d48f68d511c21df` | `5b1584676884edd8` | +| VS | 2,329 | 0 | `876b44981d106be3` | `aaef57cb08bd6845` | `b19617df9f4acbb1` | `e8eee991260adff4` | + +The direct XY probe also reported 2,480 sample comparisons and zero mismatches for each world. Hash runs and probe logs are under `Saved/Linux/l98/hashes/` and `Saved/Linux/l98/reuse-counts/`. + +## L77 passage-share census + +Mean near-LOD0–2 passage share, three profile runs per world and binary: + +| World | HEAD before | L98 after | Change | +|---|---:|---:|---:| +| FI | 57.82% | 56.74% | −1.08 pp | +| TN | 55.94% | 55.70% | −0.24 pp | + +This is a census comparison, not evidence of a timing gain. Logs and raw run values are in `Saved/Linux/l98/census/`. + +## Cold readiness + +Median seconds from three interleaved HEAD/L98 pairs per cell. Each binary/stratum/worker-count cell had a warmup. Cold timing includes world/layout setup and passage preparation. The LOD0–2 and whole-view tile counts are included below. + +| World | LOD0–2 / whole tiles | W8 HEAD → L98 (LOD0–2 / whole s) | W12 HEAD → L98 (LOD0–2 / whole s) | +|---|---:|---:|---:| +| FI | 1,408 / 2,850 | 1.070 / 2.341 → 1.099 / 2.369 | 1.029 / 2.147 → 1.005 / 2.123 | +| TN | 1,216 / 2,249 | 2.672 / 4.034 → 2.680 / 4.059 | 2.211 / 3.357 → 2.207 / 3.357 | +| SW | 1,344 / 2,233 | 2.713 / 3.929 → 2.496 / 3.623 | 2.255 / 3.313 → 2.176 / 3.183 | +| VS | 1,344 / 2,329 | 1.292 / 2.315 → 1.264 / 2.257 | 1.094 / 2.015 → 1.107 / 2.041 | + +All 48 measured readiness runs completed. Results are close and mixed across worlds; they do not show a broad readiness speedup. Full results and logs are in `Saved/Linux/l98/readiness/`. + +## Verification and failed attempts + +- Fast tier: PASS (269 pass checks, 5 diagnostic checks), 80 runs, 303.5 s. The first invocation stopped before checks because its output directory had no built headless executable; building it and rerunning passed. Summary: `Saved/Linux/l98/verify-fast/fast-summary.json`. +- Linux UE build: `VFHostEditor Linux Development`, `-NoUBA -MaxParallelActions=4`, succeeded in 36.60 s after timing. The first attempt stopped before compilation when UBT could not rotate its default log in the read-only user config directory. Retry used an explicit log at `Saved/Linux/l98/unreal-build-ubt.log`; no `--as` limit was used. +- No measured run exceeded the 30-minute limit. All experiment processes used `taskset -c 0-11`; headless runs also used the 6 GiB address-space limit. diff --git a/Standalone/Research/WorldAssemblerM3/Linux98/RunL98Census.py b/Standalone/Research/WorldAssemblerM3/Linux98/RunL98Census.py new file mode 100644 index 0000000..5fa3855 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux98/RunL98Census.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Three interleaved L77 passage-share censuses for pre-L98 and L98 Core.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l98/census" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "pre": ROOT / "Saved/Linux/l98/profile-pre-bin/VoxelForgeCoreHeadless-l98-pre-profile", + "post": ROOT / "Saved/Linux/l98/profile-post-bin/VoxelForgeCoreHeadless-l98-post-profile", +} +CAMERAS = { + "FI": (25.8369140625, 3.19921875, -737.0), + "TN": (19.166015625, -14.2001953125, -753.3740234375), +} +SAMPLE = re.compile(r"^\[L77Sample\] pc=(0x\w+) lod=(\d+) .*?phase=(\S+) .*?weight=(\d+)$", re.M) +SAMPLER = re.compile(r"^\[L77Sampler\] action=stop .*?samples=(\d+).*?status=PASS$", re.M) + + +def run(world: str, label: str, rep: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"{label}-{world.lower()}-r{rep}.log" + camera = ",".join(f"{value:.12f}" for value in CAMERAS[world]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "env", "timeout", "1740s", str(BINARIES[label]), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--linux77-profile", + ] + start = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + output = log.read_text(encoding="utf-8", errors="replace") + samples = [(int(lod), phase, int(weight)) + for _pc, lod, phase, weight in SAMPLE.findall(output)] + sampler = SAMPLER.search(output) + if result.returncode or not samples or sampler is None: + raise RuntimeError(f"census failed: exit={result.returncode} {log}") + near = [row for row in samples if row[0] <= 2] + passage = sum(weight for _, phase, weight in near if phase == "passages") + total = sum(weight for _, _, weight in near) + row = { + "world": world, "binary": label, "replicate": rep, + "exit_code": result.returncode, "wall_s": round(time.monotonic() - start, 3), + "sampler_samples": int(sampler.group(1)), "near_lod_samples": len(near), + "near_lod_weighted_ticks": total, + "near_lod_passage_weighted_ticks": passage, + "near_lod_passage_share_pct": 100.0 * passage / total, + "log": str(log.relative_to(ROOT)), + } + print("[L98CensusRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + for binary in (*BINARIES.values(), DATA): + if not binary.is_file(): + raise SystemExit(f"missing input: {binary}") + rows = [] + orders = (("pre", "post"), ("post", "pre"), ("pre", "post")) + for rep, order in enumerate(orders, 1): + for label in order: + for world in CAMERAS: + rows.append(run(world, label, rep)) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n", + encoding="utf-8") + summary = {} + for world in CAMERAS: + summary[world] = {} + for label in BINARIES: + values = [row["near_lod_passage_share_pct"] for row in rows + if row["world"] == world and row["binary"] == label] + summary[world][label] = { + "runs_pct": values, "mean_pct": sum(values) / len(values), + } + (OUT / "summary.json").write_text(json.dumps( + {"runs": rows, "near_lod_passage_share": summary}, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux98/RunL98Hashes.py b/Standalone/Research/WorldAssemblerM3/Linux98/RunL98Hashes.py new file mode 100644 index 0000000..c39380b --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux98/RunL98Hashes.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Compare L97 baseline and L98 Clang/GCC whole-view tile hashes at LOD0-2.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l98/hashes" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "HEAD": ROOT / "Saved/Linux/l98/pre-bin/VoxelForgeCoreHeadless-l98-pre-clang", + "L98-Clang": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19", + "L98-GCC": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-gcc", +} +CAMERAS = { + 0: (8.0, 16.0, -372.2626953125), + 1: (25.8369140625, 3.19921875, -737.0), + 2: (19.166015625, -14.2001953125, -753.3740234375), + 3: (-20.0625, -5.068359375, -900.353515625), +} +TILE_HASH = re.compile( + r"^\[PipelineTileHash\] lod=(-?\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) " + r"density=(0x[0-9a-f]+) mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) " + r"topology=(0x[0-9a-f]+)$", re.M) +WHOLE_HASH = re.compile( + r"^\[PipelineWholeViewHash\] tiles=(\d+) density=(0x[0-9a-f]+) " + r"mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) topology=(0x[0-9a-f]+)$", re.M) +FIELDS = ("density", "mesh", "payload", "topology") + + +def run(strate: int, label: str) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"{label.lower()}-s{strate}.log" + camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "env", "VOXELFORGE_L98_PASSAGE_PREP_TRACE=1", "timeout", "1740s", + str(BINARIES[label]), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=8", f"--camera={camera}", + "--stream-hash-tiles", + ] + start = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + output = log.read_text(encoding="utf-8", errors="replace") + hashes = { + (int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))): + m.group(5, 6, 7, 8) for m in TILE_HASH.finditer(output) + } + whole = WHOLE_HASH.search(output) + if result.returncode or whole is None or len(hashes) != int(whole.group(1)): + raise RuntimeError(f"hash run failed or incomplete: exit={result.returncode} {log}") + prep = re.findall(r"^\[L98PassagePrepare\] (.*)$", output, re.M) + mouth_prep = re.findall(r"^\[L98PassageMouthPrepare\] (.*)$", output, re.M) + row = { + "binary": label, "strate": strate, "exit_code": result.returncode, + "wall_s": round(time.monotonic() - start, 3), "tile_hash_count": len(hashes), + "tile_hashes_by_lod": {str(lod): sum(1 for key in hashes if key[0] == lod) + for lod in sorted({key[0] for key in hashes})}, + "whole_hash": whole.group(1, 2, 3, 4, 5), "hashes": hashes, + "prepare_traces": prep, "mouth_prepare_traces": mouth_prep, + "log": str(log.relative_to(ROOT)), + } + print("[L98HashRun] " + json.dumps( + {k: v for k, v in row.items() if k != "hashes"}, + sort_keys=True), flush=True) + return row + + +def compare(strate: int, left_label: str, right_label: str, + left: dict, right: dict) -> dict: + mismatch_keys = sorted(key for key in set(left["hashes"]) | set(right["hashes"]) + if left["hashes"].get(key) != right["hashes"].get(key)) + component_mismatches = {} + for component_index, component in enumerate(FIELDS): + component_mismatches[component] = sum( + left["hashes"].get(key, (None,) * 4)[component_index] + != right["hashes"].get(key, (None,) * 4)[component_index] + for key in set(left["hashes"]) | set(right["hashes"])) + near_density_mismatches = sum( + key[0] <= 2 and left["hashes"].get(key, (None,) * 4)[0] + != right["hashes"].get(key, (None,) * 4)[0] + for key in set(left["hashes"]) | set(right["hashes"])) + item = { + "strate": strate, "left": left_label, "right": right_label, + "tiles_left": len(left["hashes"]), "tiles_right": len(right["hashes"]), + "whole_hash_identical": left["whole_hash"] == right["whole_hash"], + "tile_hashes_identical": not mismatch_keys, + "mismatch_tile_count": len(mismatch_keys), + "component_mismatch_tile_counts": component_mismatches, + "lod0_2_density_mismatch_tile_count": near_density_mismatches, + "first_mismatches": mismatch_keys[:8], + "left_whole_hash": left["whole_hash"], "right_whole_hash": right["whole_hash"], + } + print("[L98HashComparison] " + json.dumps(item, sort_keys=True), flush=True) + return item + + +def main() -> int: + for binary in (*BINARIES.values(), DATA): + if not binary.is_file(): + raise SystemExit(f"missing input: {binary}") + runs, comparisons = [], [] + for strate in CAMERAS: + by_binary = {label: run(strate, label) for label in BINARIES} + comparisons.append(compare(strate, "HEAD", "L98-Clang", + by_binary["HEAD"], by_binary["L98-Clang"])) + comparisons.append(compare(strate, "L98-Clang", "L98-GCC", + by_binary["L98-Clang"], by_binary["L98-GCC"])) + runs.extend({key: value for key, value in row.items() if key != "hashes"} + for row in by_binary.values()) + summary = {"comparisons": comparisons, "runs": runs} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + return 0 if all(item["tile_hashes_identical"] and item["whole_hash_identical"] + for item in comparisons) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux98/RunL98Readiness.py b/Standalone/Research/WorldAssemblerM3/Linux98/RunL98Readiness.py new file mode 100644 index 0000000..60a077e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux98/RunL98Readiness.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""L80 cold-readiness protocol: three interleaved L97/L98 pairs at W8 and W12.""" +from __future__ import annotations + +import json +import re +import statistics +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l98/readiness" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "HEAD": ROOT / "Saved/Linux/l98/pre-bin/VoxelForgeCoreHeadless-l98-pre-clang", + "L98": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19", +} +CAMERAS = { + 0: (8.0, 16.0, -372.2626953125), + 1: (25.8369140625, 3.19921875, -737.0), + 2: (19.166015625, -14.2001953125, -753.3740234375), + 3: (-20.0625, -5.068359375, -900.353515625), +} +PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M) +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +NEAR = re.compile(r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) " + r"cold_ready_s=([0-9.]+)$", re.M) + + +def run(strate: int, workers: int, label: str, phase: str, rep: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"w{workers}-s{strate}-{label.lower()}-{phase}{rep}.log" + camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "env", "timeout", "1740s", str(BINARIES[label]), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8", + "--max-level=8", f"--camera={camera}", "--profile-lod-work", + "--stream-scheduler-policy=3", + ] + start = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + output = log.read_text(encoding="utf-8", errors="replace") + pipeline_match, near_match = PIPELINE.search(output), NEAR.search(output) + if result.returncode or pipeline_match is None or near_match is None: + raise RuntimeError(f"readiness run failed: exit={result.returncode} {log}") + pipeline = dict(FIELDS.findall(pipeline_match.group(1))) + row = { + "strate": strate, "workers": workers, "binary": label, "phase": phase, + "replicate": rep, "exit_code": result.returncode, + "wall_s": round(time.monotonic() - start, 3), + "lod02_tiles": f"{near_match.group(1)}/{near_match.group(2)}", + "lod02_ready_s": float(near_match.group(3)), + "whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")), + "whole_tiles": pipeline["whole_tiles"].split("/")[0], + "log": str(log.relative_to(ROOT)), + } + print("[L98ReadinessRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + for binary in (*BINARIES.values(), DATA): + if not binary.is_file(): + raise SystemExit(f"missing input: {binary}") + rows = [] + orders = (("HEAD", "L98"), ("L98", "HEAD"), ("HEAD", "L98")) + for workers in (8, 12): + for strate in CAMERAS: + for label in BINARIES: + run(strate, workers, label, "warmup", 0) + for strate in CAMERAS: + for rep, order in enumerate(orders, 1): + for label in order: + rows.append(run(strate, workers, label, "measured", rep)) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n", + encoding="utf-8") + summary = [] + for workers in (8, 12): + for strate in CAMERAS: + group = [row for row in rows + if row["workers"] == workers and row["strate"] == strate] + item = {"workers": workers, "strate": strate, "measured_pairs": 3} + for label in BINARIES: + subset = [row for row in group if row["binary"] == label] + for metric in ("lod02_ready_s", "whole_view_ready_s"): + values = [row[metric] for row in subset] + item[f"{label}_{metric}_runs"] = values + item[f"{label}_{metric}_median"] = statistics.median(values) + item[f"{label}_process_wall_s_runs"] = [row["wall_s"] for row in subset] + summary.append(item) + (OUT / "summary.json").write_text(json.dumps( + {"runs": rows, "summary": summary}, indent=2) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux98/RunL98ReuseCounts.py b/Standalone/Research/WorldAssemblerM3/Linux98/RunL98ReuseCounts.py new file mode 100644 index 0000000..499ae30 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux98/RunL98ReuseCounts.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Count prepared-segment use and XY projection reuse in LOD0-2 world views.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l98/reuse-counts" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARY = ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19" +CAMERAS = { + 0: (8.0, 16.0, -372.2626953125), + 1: (25.8369140625, 3.19921875, -737.0), + 2: (19.166015625, -14.2001953125, -753.3740234375), + 3: (-20.0625, -5.068359375, -900.353515625), +} +REUSE = re.compile( + r"^\[L98PassageReuseTile\] lod=(-?\d+) projection_columns=(\d+) " + r"projection_computes=(\d+) projection_hits=(\d+) floor_profile_reads=(\d+) " + r"carve_prepared=(\d+) support_prepared=(\d+) capsule_prepared=(\d+) " + r"mouth_prepared=(\d+)$", re.M) +PREP = re.compile(r"^\[L98PassagePrepare\] (.*)$", re.M) +MOUTH_PREP = re.compile(r"^\[L98PassageMouthPrepare\] (.*)$", re.M) +PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M) +PROBE = re.compile(r"^\[L98PassageReuseProbe\] z_runs=(\d+) samples=(\d+) " + r"scalar_mismatches=(\d+) status=(\S+)$", re.M) + + +def run(strate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"s{strate}.log" + camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "env", "VOXELFORGE_L98_PASSAGE_REUSE_COUNTS=1", + "VOXELFORGE_L98_PASSAGE_PREP_TRACE=1", + "VOXELFORGE_L98_PASSAGE_REUSE_PROBE=1", "timeout", "1740s", str(BINARY), + str(DATA), "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--workers=8", "--clip-radius=3", "--max-clip-level=8", + "--max-level=8", f"--camera={camera}", + ] + start = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + output = log.read_text(encoding="utf-8", errors="replace") + tile_rows = [tuple(map(int, match.groups())) for match in REUSE.finditer(output)] + prep = PREP.findall(output) + mouth_prep = MOUTH_PREP.findall(output) + pipeline = PIPELINE.search(output) + probe = PROBE.search(output) + if (result.returncode or not tile_rows or not prep or pipeline is None or probe is None + or probe.group(4) != "PASS"): + raise RuntimeError(f"reuse-count run failed or incomplete: exit={result.returncode} {log}") + totals = [sum(row[index] for row in tile_rows) for index in range(1, 9)] + lod_totals = {} + for lod in sorted({row[0] for row in tile_rows}): + lod_rows = [row for row in tile_rows if row[0] == lod] + lod_totals[str(lod)] = { + "tile_scope_rows": len(lod_rows), + "projection_columns": sum(row[1] for row in lod_rows), + "projection_computes": sum(row[2] for row in lod_rows), + "projection_hits": sum(row[3] for row in lod_rows), + "floor_profile_reads": sum(row[4] for row in lod_rows), + "carve_prepared_uses": sum(row[5] for row in lod_rows), + "support_prepared_uses": sum(row[6] for row in lod_rows), + "capsule_prepared_uses": sum(row[7] for row in lod_rows), + "mouth_prepared_uses": sum(row[8] for row in lod_rows), + } + row = { + "strate": strate, "exit_code": result.returncode, + "wall_s": round(time.monotonic() - start, 3), "tile_scope_rows": len(tile_rows), + "lod_scope_rows": {str(lod): sum(1 for row in tile_rows if row[0] == lod) + for lod in sorted({row[0] for row in tile_rows})}, + "lod_totals": lod_totals, + "projection_columns": totals[0], "projection_computes": totals[1], + "projection_hits": totals[2], "floor_profile_reads": totals[3], + "carve_prepared_uses": totals[4], "support_prepared_uses": totals[5], + "capsule_prepared_uses": totals[6], "mouth_prepared_uses": totals[7], + "projection_probe": { + "z_runs": int(probe.group(1)), "samples": int(probe.group(2)), + "scalar_mismatches": int(probe.group(3)), "status": probe.group(4), + }, + "prepare_traces": prep, "mouth_prepare_traces": mouth_prep, + "stream_pipeline": pipeline.group(1), "log": str(log.relative_to(ROOT)), + } + print("[L98ReuseCountRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + if not BINARY.is_file() or not DATA.is_file(): + raise SystemExit(f"missing input: {BINARY} or {DATA}") + rows = [run(strate) for strate in CAMERAS] + (OUT / "summary.json").write_text(json.dumps({"runs": rows}, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())