From a7a03d42b440a1483b7712f4e203af5adff3b60a Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Wed, 28 Oct 2026 05:28:27 +0100 Subject: [PATCH] diag: passage candidate compiler census (L90, opt-in); falsifier failed at 10 m reach 54,324 sparse 4 m bricks, uncapped carve/tread/clearance/mouth lists, built in 26-28 ms, ~12 MiB. Weighted over 30.7M near-LOD sample hits: active carve p90 13, mouth p90 13; reach fallback keeps 18.3% of passage time (FI 12%, TN 19%) - all because the density-dependent reach (-d*Q + 0.24 m) exceeds 10 m for deeply negative source samples. Not wired into production. R41 notes (Unreal LOD0-2 gap: ~0.7 s pre-request in BeginPlay, LOD2 tail, apply boost reset; L80's 4 missing tiles = re-requested empty collision-support tiles) attached. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- Standalone/Core/src/ShapeWorldInternal.h | 692 +++++++++++++++++- Standalone/Core/src/VoxelForgeCore.cpp | 2 +- .../Linux90/R41-unreal-gap-notes.md | 31 + .../WorldAssemblerM3/Linux90/REPORT.md | 33 + .../Linux90/RunL90CandidateCensus.py | 195 +++++ 5 files changed, 948 insertions(+), 5 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux90/R41-unreal-gap-notes.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux90/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux90/RunL90CandidateCensus.py diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index af40c01..7531550 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +63,13 @@ static bool VF_IsL88AllowPassageAblationWalkabilityFailure() return bEnabled; } +static bool VF_IsL90PassageCandidateCensusEnabled() +{ + static const bool bEnabled=VF_IsL88DiagnosticSwitchEnabled( + "VOXELFORGE_L90_PASSAGE_CANDIDATE_CENSUS"); + return bEnabled; +} + // Standalone-only world compiler. It compiles the runtime room/tunnel/passages into // axis-line intervals, composes rock/cave signs into packed Z-column words, then displaces // only the conservative surface band. The copied Unreal generator remains the A/B reference. @@ -218,10 +226,11 @@ public: FScopedPassageTileQueries(const FStandaloneShapeWorld& shape, const std::array& minimumSubvoxels, const std::array& maximumSubvoxels, - VoxelForgeCore::TileGenerationStats* profileStats=nullptr) + VoxelForgeCore::TileGenerationStats* profileStats=nullptr,int32 profileLod=0) :shape_(&shape),minimumSubvoxels_(minimumSubvoxels), maximumSubvoxels_(maximumSubvoxels),previous_(ActivePassageTileQueries), - profileStats_(profileStats) + profileStats_(profileStats),profileLod_(profileLod), + candidateProfiled_(profileStats!=nullptr&&shape.L90PassageCandidateProgramEnabled) { mayContainResolvedBoundaryBand_=shape.ResolvedBoundaryBandsIntersectSubvoxelRange( minimumSubvoxels_[2],maximumSubvoxels_[2]); @@ -328,9 +337,62 @@ public: routeFeatureOverlap_(source.routeFeatureOverlap_),columnZOverlap_(source.columnZOverlap_), mayContainResolvedBoundaryBand_(source.mayContainResolvedBoundaryBand_), mayContainTransitionPassageFade_(source.mayContainTransitionPassageFade_), - previous_(ActivePassageTileQueries) + previous_(ActivePassageTileQueries),profileLod_(source.profileLod_), + candidateProfiled_(source.candidateProfiled_) { ActivePassageTileQueries=this; } - ~FScopedPassageTileQueries(){ActivePassageTileQueries=previous_;} + ~FScopedPassageTileQueries() + { + ActivePassageTileQueries=previous_; + if(!candidateProfiled_||l90Queries_==0)return; + std::array,4> HistText{}; + constexpr std::array Names={"carve","tread","clearance","mouth_column"}; + for(std::size_t semantic=0;semantic(l90CandidateHistogram_[semantic][bin])); + if(written<0)break; + offset=std::min(HistText[semantic].size(),offset+static_cast(written)); + if(offset>=HistText[semantic].size())break; + } + } + std::printf("[L90PassageCandidateTile] lod=%d min=(%lld,%lld,%lld) max=(%lld,%lld,%lld) queries=%llu no_brick=%llu fallback_iterations=%llu fallback_reach=%llu fallback_shortspan=%llu fallback_invalid=%llu fallback_proof=%llu fallback_ns=%llu query_ns=%llu fallback_carve=%llu fallback_tread=%llu fallback_clearance=%llu fallback_mouth=%llu proof_checks=(%llu,%llu,%llu,%llu,%llu,%llu) proof_misses=(%llu,%llu,%llu,%llu,%llu,%llu) carve_hist=%s tread_hist=%s clearance_hist=%s mouth_column_hist=%s\n", + profileLod_,static_cast(minimumSubvoxels_[0]), + static_cast(minimumSubvoxels_[1]), + static_cast(minimumSubvoxels_[2]), + static_cast(maximumSubvoxels_[0]), + static_cast(maximumSubvoxels_[1]), + static_cast(maximumSubvoxels_[2]), + static_cast(l90Queries_), + static_cast(l90NoBrickQueries_), + static_cast(l90FallbackIterations_), + static_cast(l90FallbackReachOverflow_), + static_cast(l90FallbackShortSpan_), + static_cast(l90FallbackInvalid_), + static_cast(l90FallbackProof_), + static_cast(l90FallbackNanoseconds_), + static_cast(l90QueryNanoseconds_), + static_cast(l90FallbackCarveVisits_), + static_cast(l90FallbackTreadVisits_), + static_cast(l90FallbackClearanceChecks_), + static_cast(l90FallbackMouthChecks_), + static_cast(l90ProofChecks_[0]), + static_cast(l90ProofChecks_[1]), + static_cast(l90ProofChecks_[2]), + static_cast(l90ProofChecks_[3]), + static_cast(l90ProofChecks_[4]), + static_cast(l90ProofChecks_[5]), + static_cast(l90ProofMisses_[0]), + static_cast(l90ProofMisses_[1]), + static_cast(l90ProofMisses_[2]), + static_cast(l90ProofMisses_[3]), + static_cast(l90ProofMisses_[4]), + static_cast(l90ProofMisses_[5]), + HistText[0].data(),HistText[1].data(),HistText[2].data(),HistText[3].data()); + } FScopedPassageTileQueries& operator=(const FScopedPassageTileQueries&)=delete; bool matches(const FStandaloneShapeWorld& shape) const{return shape_==&shape;} @@ -388,6 +450,40 @@ public: std::size_t routeSegmentCandidateCount() const{return routeSegmentCandidateCount_;} VoxelForgeCore::TileGenerationStats* profileStats() const noexcept {return profileStats_;} + bool candidateCensusProfiled() const noexcept{return candidateProfiled_;} + void recordL90CandidateHit(const std::array& counts,bool haveBrick) const + { + if(!candidateProfiled_)return; + ++l90Queries_; + if(!haveBrick)++l90NoBrickQueries_; + for(std::size_t semantic=0;semantic(counts[semantic],16)]; + } + void recordL90Fallback(std::uint64_t elapsedNanoseconds, + std::uint64_t carveVisits,std::uint64_t treadVisits, + std::uint64_t clearanceChecks,std::uint64_t mouthChecks, + std::uint8_t reason) const + { + if(!candidateProfiled_)return; + ++l90FallbackIterations_; + l90FallbackNanoseconds_+=elapsedNanoseconds; + l90FallbackCarveVisits_+=carveVisits; + l90FallbackTreadVisits_+=treadVisits; + l90FallbackClearanceChecks_+=clearanceChecks; + l90FallbackMouthChecks_+=mouthChecks; + if(reason==1)++l90FallbackReachOverflow_; + else if(reason==2)++l90FallbackShortSpan_; + else if(reason==3)++l90FallbackProof_; + else ++l90FallbackInvalid_; + } + void recordL90QueryNanoseconds(std::uint64_t elapsedNanoseconds) const + {if(candidateProfiled_)l90QueryNanoseconds_+=elapsedNanoseconds;} + void recordL90ProofCheck(std::size_t semantic,bool contained) const + { + if(!candidateProfiled_||semantic>=l90ProofChecks_.size())return; + ++l90ProofChecks_[semantic]; + if(!contained)++l90ProofMisses_[semantic]; + } bool hasStampCandidates() const{return !stampIndices_.empty();} const std::vector& stampCandidates() const{return stampIndices_;} bool hasCavernCandidates() const{return !cavernFeatureIndices_.empty();} @@ -410,6 +506,16 @@ public: bool mayContainTransitionPassageFade_=true; const FScopedPassageTileQueries* previous_; VoxelForgeCore::TileGenerationStats* profileStats_=nullptr; + int32 profileLod_=0; + bool candidateProfiled_=false; + mutable std::uint64_t l90Queries_=0,l90NoBrickQueries_=0; + mutable std::uint64_t l90FallbackIterations_=0,l90FallbackNanoseconds_=0, + l90QueryNanoseconds_=0,l90FallbackCarveVisits_=0,l90FallbackTreadVisits_=0, + l90FallbackClearanceChecks_=0,l90FallbackMouthChecks_=0, + l90FallbackReachOverflow_=0,l90FallbackShortSpan_=0,l90FallbackInvalid_=0, + l90FallbackProof_=0; + mutable std::array l90ProofChecks_{},l90ProofMisses_{}; + mutable std::array,4> l90CandidateHistogram_{}; }; bool IsResolvedPassageColumnSample(std::size_t PassageIndex,std::size_t SegmentIndex, @@ -1129,6 +1235,40 @@ public: auto* passageProfile=passageTileScopeMatches ?passageTileQueries->profileStats():nullptr; if(passageProfile)++passageProfile->passageComposerQueries; + const bool l90CandidateProfiled=passageTileScopeMatches + &&passageTileQueries->candidateCensusProfiled(); + const auto* l90CandidateBrick=l90CandidateProfiled + ?FindPassageCandidateBrick(qx,qy,qz):nullptr; + const std::array l90CandidateCounts=l90CandidateBrick + ?std::array{ + l90CandidateBrick->semantics[0].size(),l90CandidateBrick->semantics[1].size(), + l90CandidateBrick->semantics[2].size(),l90CandidateBrick->semantics[3].size()} + :std::array{}; + if(l90CandidateProfiled) + passageTileQueries->recordL90CandidateHit(l90CandidateCounts, + l90CandidateBrick!=nullptr); + struct FL90QueryTimer + { + const FScopedPassageTileQueries* scope=nullptr; + std::chrono::steady_clock::time_point start{}; + ~FL90QueryTimer() + { + if(scope)scope->recordL90QueryNanoseconds(static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now()-start).count())); + } + } l90QueryTimer{l90CandidateProfiled?passageTileQueries:nullptr, + l90CandidateProfiled?std::chrono::steady_clock::now() + :std::chrono::steady_clock::time_point{}}; + struct FL90CandidateBrickScope + { + const FPassageCandidateBrick* previous=nullptr; + explicit FL90CandidateBrickScope(const FPassageCandidateBrick* current) + :previous(FStandaloneShapeWorld::ActivePassageCandidateBrick) + {FStandaloneShapeWorld::ActivePassageCandidateBrick=current;} + ~FL90CandidateBrickScope() + {FStandaloneShapeWorld::ActivePassageCandidateBrick=previous;} + } l90CandidateBrickScope(l90CandidateBrick); if(passageTileScopeMatches&&density>=0.0f &&!passageTileQueries->hasRouteFeatureOverlap() &&!passageTileQueries->hasColumnZOverlap() @@ -1296,6 +1436,12 @@ public: const double bottomDistance=floorQ-qPerMetre-qz; const double verticalDistance=std::max(topDistance,bottomDistance); if(sideDistance>mouthBlendWidth||verticalDistance>mouthBlendWidth)return; + if(l90CandidateProfiled) + { + const bool contained=PassageCandidateBrickContains(l90CandidateBrick,3, + passageIndex,static_cast(index),2); + passageTileQueries->recordL90ProofCheck(3,contained); + } const double outside=DeterministicSqrt( std::max(sideDistance,0.0)*std::max(sideDistance,0.0) +std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0)); @@ -1326,10 +1472,38 @@ public: }; // The connected passage tread continues the surface route into the tunnel. const std::size_t passageIterationCount=ResolvedLayout->passages.size(); + struct FL90IterationTimer + { + const FScopedPassageTileQueries* scope=nullptr; + std::chrono::steady_clock::time_point start{}; + bool fallback=false; + std::uint8_t fallbackReason=0; + std::uint64_t carveVisits=0,treadVisits=0,clearanceChecks=0,mouthChecks=0; + void requireFallback(std::uint8_t reason) + {fallback=true;fallbackReason=reason;} + void recordCarve(){++carveVisits;} + void recordTread(){++treadVisits;} + void recordClearance(){++clearanceChecks;} + void recordMouth(){++mouthChecks;} + ~FL90IterationTimer() + { + if(scope&&fallback) + scope->recordL90Fallback(static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now()-start).count()), + carveVisits,treadVisits,clearanceChecks,mouthChecks,fallbackReason); + } + }; for(std::size_t passageIteration=0;passageIterationpassageIterations; const auto& passage=ResolvedLayout->passages[passageIndex]; if(passage.controlPoints.size()<2)continue; @@ -1341,6 +1515,7 @@ public: const auto laterPassageMayCarve=[&](std::ptrdiff_t supportSegment, double sampleX,double sampleY,double sampleZ) { + l90IterationTimer.recordClearance(); if(passageProfile)++passageProfile->passageLaterBoreChecks; return PassageSupportMayBeCarvedByLaterBore(passageIndex, supportSegment,sampleX,sampleY,sampleZ,0.35*qPerMetre); @@ -1353,6 +1528,21 @@ public: { if(!crossing.needsColumn||crossing.passageIndex!=passageIndex ||qz>crossing.topZSubvoxels||qz(crossing.firstSegment),3); + passageTileQueries->recordL90ProofCheck(4,contained); + if(!contained)l90IterationTimer.requireFallback(3); + } + else if(columnDistance(inputComposed)*Q) + l90IterationTimer.requireFallback(1); + }; if(passageProfile)++passageProfile->passageColumnCandidates; #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().ColumnZOverlapCalls; @@ -1399,6 +1589,7 @@ public: if(passageProfile)++passageProfile->passageColumnDistanceCalls; columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z); } + if(l90CandidateProfiled)recordColumnProof(columnDistance,composed); composed=std::min(composed,static_cast(columnDistance/Q)); } const bool tilePassageFree=passageTileScopeMatches @@ -1433,6 +1624,24 @@ public: (carveLow-passage.controlPoints.begin())+1); const std::ptrdiff_t mouthSearchBegin=passageConstants.mouthSearchBegin; const std::ptrdiff_t mouthSearchEnd=passageConstants.mouthSearchEnd; + if(l90CandidateProfiled) + { + const double signedDistanceLimit=density<0.0f + ?-static_cast(composed)*Q+0.24*qPerMetre + :std::max(2.25*qPerMetre,-static_cast(composed)*Q + +0.24*qPerMetre); + const bool sourceProofAvailable=L90PassageCandidateProgramValid + &&std::isfinite(signedDistanceLimit) + &&signedDistanceLimit<=L90PassageCandidateReachMetres*qPerMetre + &&!(density>=0.0f&&PassageHasShortSpanSegments(passageIndex)); + if(!sourceProofAvailable) + { + const std::uint8_t reason=!L90PassageCandidateProgramValid + ||!std::isfinite(signedDistanceLimit)?0 + :signedDistanceLimit>L90PassageCandidateReachMetres*qPerMetre?1:2; + l90IterationTimer.requireFallback(reason); + } + } if(density<0.0f&&tilePassageFree) { if(passageProfile)++passageProfile->passageNegativeBoundChecks; @@ -1492,6 +1701,7 @@ public: *(3.0-2.0*lowerMouthT); const auto considerCarveSegment=[&](std::size_t segmentIndex,int scanPhase) { + l90IterationTimer.recordCarve(); if(passageProfile)++passageProfile->passageCarveSegmentVisits; const std::ptrdiff_t index=static_cast(segmentIndex); const auto& a=passage.controlPoints[static_cast(index)]; @@ -1593,6 +1803,16 @@ public: considerCarveSegment(static_cast(index),3); } if(!std::isfinite(bestSignedDistance))continue; + const double l90SignedDistanceLimit=density<0.0f + ?-static_cast(composed)*Q+0.24*qPerMetre + :std::max(2.25*qPerMetre,-static_cast(composed)*Q + +0.24*qPerMetre); + if(l90CandidateProfiled&&bestSignedDistance<=l90SignedDistanceLimit + &&l90SignedDistanceLimit<=L90PassageCandidateReachMetres*qPerMetre) + passageTileQueries->recordL90ProofCheck(0, + L90PassageCandidateProgramValid + &&PassageCandidateBrickContains(l90CandidateBrick,0,passageIndex, + static_cast(bestIndex))); const auto& nearestA=passage.controlPoints[static_cast(bestIndex)]; const auto& nearestB=passage.controlPoints[static_cast(bestIndex+1)]; const double nearestRadius=nearestA.radiusSubvoxels+bestT*( @@ -1763,9 +1983,17 @@ public: static_cast(bestIndex)); float carveInputAfterBridge=composed; if(shortSpanCarve) + { + if(l90CandidateProfiled) + { + const bool contained=PassageCandidateBrickContains(l90CandidateBrick,3, + passageIndex,static_cast(bestIndex),4); + passageTileQueries->recordL90ProofCheck(5,contained); + } carveInputAfterBridge=std::min(composed,static_cast( PassageBridgeSignedDistance(passageIndex, static_cast(bestIndex),X,Y,Z)/Q)); + } const double maximumWallReliefOffset=0.21*qPerMetre*wallReliefWeight; const double maximumMouthFloorReliefOffset=upperMouthFloorFade ?0.20*qPerMetre*mouthFloorBlend*floorReliefWeight:0.0; @@ -1846,6 +2074,7 @@ public: const float composedAfterCarve=composed; const auto considerSupportSegment=[&](std::ptrdiff_t index) { + l90IterationTimer.recordTread(); if(passageProfile)++passageProfile->passageSupportSegmentVisits; const auto& segmentA=passage.controlPoints[static_cast(index)]; const auto& segmentB=passage.controlPoints[static_cast(index+1)]; @@ -2151,6 +2380,10 @@ public: for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index) considerSupportSegment(index); } + if(l90CandidateProfiled&&hasSupportFloor) + passageTileQueries->recordL90ProofCheck(1, + PassageCandidateBrickContains(l90CandidateBrick,1,passageIndex, + static_cast(bestSupportSegmentIndex))); VoxelForgeCore::Linux77Profiler::FScopedPhase l77SupportApplyPhase( VoxelForgeCore::Linux77Profiler::EPhase::Support); // Keep route treads inside the native rock envelope. Carving may reopen the @@ -2169,6 +2402,7 @@ public: if(passageTileScopeMatches ||(bestSupportUsesSignedDistance&&supportCapsuleProfileAvailable)) { + l90IterationTimer.recordClearance(); if(passageProfile)++passageProfile->passageSupportCapsuleChecks; capsuleAllowsSupport=!PassagePlayerCapsuleBlocksSupport(passageIndex, carveBegin,carveEnd,lowerMouthIndex,mouthSearchBegin,mouthSearchEnd, @@ -2195,6 +2429,7 @@ public: const bool nativeRestoreCapsuleClear=density<0.0f &&bestSupportNativeBlend>0.0&&capsuleAllowsSupport &&([&](){ + l90IterationTimer.recordClearance(); if(passageProfile)++passageProfile->passageSupportCapsuleChecks; return !PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin, carveEnd,lowerMouthIndex,mouthSearchBegin,mouthSearchEnd, @@ -2222,6 +2457,7 @@ public: } if(supportApplied) { + l90IterationTimer.recordClearance(); if(passageProfile)++passageProfile->passageOtherCapsuleChecks; if(PassageOtherCapsuleBlocksSupport(passageIndex, bestSupportSegmentIndex,qx,qy,qz,qPerMetre,capsuleClearanceMetres)) @@ -4155,6 +4391,35 @@ private: std::vector candidates; std::vector capsuleCandidatePassages; }; + struct FPassageCandidateBrickKey + { + std::int64_t x=0,y=0,z=0; + bool operator==(const FPassageCandidateBrickKey&) const=default; + }; + struct FPassageCandidateBrickKeyHash + { + std::size_t operator()(const FPassageCandidateBrickKey& key) const noexcept + { + const std::size_t hx=std::hash{}(key.x); + const std::size_t hy=std::hash{}(key.y); + const std::size_t hz=std::hash{}(key.z); + return hx^(hy+static_cast(0x9e3779b97f4a7c15ull)+(hx<<6)+(hx>>2)) + ^(hz+static_cast(0x9e3779b97f4a7c15ull)+(hy<<6)+(hy>>2)); + } + }; + struct FPassageCandidateRef + { + std::uint32_t passageIndex=0; + std::int32_t segmentIndex=-1; + std::uint8_t featureKind=0; + bool operator==(const FPassageCandidateRef&) const=default; + }; + struct FPassageCandidateBrick + { + // carve, tread, clearance, mouth/column. Lists are complete for the + // declared geometry and reach envelopes; vectors are never capped. + std::array,4> semantics; + }; struct FPassageProofNode { std::array minimum{},maximum{}; @@ -5208,6 +5473,9 @@ private: void BuildPassageSpatialIndices() { + L90PassageCandidateProgramEnabled=VF_IsL90PassageCandidateCensusEnabled(); + L90PassageCandidateBricks.clear(); + L90PassageCandidateProgramValid=false; BuildStampSpatialIndex(); BuildCavernSpatialIndex(); PassageTileFeatures.clear();PassageFeatureBounds.clear(); @@ -5584,6 +5852,408 @@ private: ||feature.minimumSubvoxels[axis]>feature.maximumSubvoxels[axis]; if(!PassageFeatureOrder.empty())BuildPassageFeatureNode(0,PassageFeatureOrder.size()); if(!PassageProofBounds.empty())BuildPassageProofNode(0,PassageProofBounds.size()); + if(L90PassageCandidateProgramEnabled)BuildPassageCandidateProgram(); + } + + static FPassageCandidateBrickKey PassageCandidateBrickKeyFor(double qx,double qy, + double qz,double brickEdgeSubvoxels) + { + return {static_cast(std::floor(qx/brickEdgeSubvoxels)), + static_cast(std::floor(qy/brickEdgeSubvoxels)), + static_cast(std::floor(qz/brickEdgeSubvoxels))}; + } + + const FPassageCandidateBrick* FindPassageCandidateBrick(double qx,double qy, + double qz) const + { + if(!L90PassageCandidateProgramValid)return nullptr; + const auto key=PassageCandidateBrickKeyFor(qx,qy,qz, + L90PassageCandidateBrickEdgeSubvoxels); + const auto found=L90PassageCandidateBricks.find(key); + return found==L90PassageCandidateBricks.end()?nullptr:&found->second; + } + + static bool PassageCandidateRefLess(const FPassageCandidateRef& a, + const FPassageCandidateRef& b) + { + if(a.passageIndex!=b.passageIndex)return a.passageIndex=brick->semantics.size() + ||passageIndex>std::numeric_limits::max())return false; + const FPassageCandidateRef query{static_cast(passageIndex), + segmentIndex,featureKind}; + const auto& values=brick->semantics[semantic]; + return std::binary_search(values.begin(),values.end(),query,PassageCandidateRefLess); + } + + void BuildPassageCandidateProgram() + { + using FClock=std::chrono::steady_clock; + const auto buildStart=FClock::now(); + L90PassageCandidateBricks.clear(); + L90PassageCandidateProgramValid=false; + L90PassageCandidateBuildFailures=0; + L90PassageCandidateReachMetres=10.0; + constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + const double qPerMetre=Q*1000000.0/std::max(1, + ResolvedLayout->coreVoxelSizeMicrometres); + const double voxelsPerMetre=qPerMetre/Q; + L90PassageCandidateBrickEdgeSubvoxels=4.0*qPerMetre; + const double reachSubvoxels=L90PassageCandidateReachMetres*qPerMetre; + const double cubicPadding=1.0; + std::array emittedReferences{}; + const auto emit=[&](std::size_t semantic,const std::array& minimum, + const std::array& maximum,const FPassageCandidateRef& reference) + { + if(semantic>=4||!std::isfinite(L90PassageCandidateBrickEdgeSubvoxels) + ||L90PassageCandidateBrickEdgeSubvoxels<=0.0) + {++L90PassageCandidateBuildFailures;return;} + FPassageCandidateBrickKey first{},last{}; + for(std::size_t axis=0;axis<3;++axis) + { + if(!std::isfinite(minimum[axis])||!std::isfinite(maximum[axis]) + ||minimum[axis]>maximum[axis]) + {++L90PassageCandidateBuildFailures;return;} + const long double cellMinimum=std::floor(static_cast(minimum[axis]) + /L90PassageCandidateBrickEdgeSubvoxels); + const long double cellMaximum=std::floor(static_cast(maximum[axis]) + /L90PassageCandidateBrickEdgeSubvoxels); + if(cellMinimum(std::numeric_limits::min()) + ||cellMinimum>static_cast(std::numeric_limits::max()) + ||cellMaximum(std::numeric_limits::min()) + ||cellMaximum>static_cast(std::numeric_limits::max())) + {++L90PassageCandidateBuildFailures;return;} + const auto lo=static_cast(cellMinimum); + const auto hi=static_cast(cellMaximum); + if(axis==0){first.x=lo;last.x=hi;} + else if(axis==1){first.y=lo;last.y=hi;} + else {first.z=lo;last.z=hi;} + } + for(std::int64_t x=first.x;;++x) + { + for(std::int64_t y=first.y;;++y) + { + for(std::int64_t z=first.z;;++z) + { + auto& brick=L90PassageCandidateBricks[{x,y,z}]; + brick.semantics[semantic].push_back(reference); + ++emittedReferences[semantic]; + if(z==last.z)break; + } + if(y==last.y)break; + } + if(x==last.x)break; + } + }; + const auto pointBox=[&](const std::array& centerMinimum, + const std::array& centerMaximum,double extentX,double extentY, + double extentBelow,double extentAbove) + { + return std::pair,std::array>{ + {centerMinimum[0]-extentX,centerMinimum[1]-extentY, + centerMinimum[2]-extentBelow}, + {centerMaximum[0]+extentX,centerMaximum[1]+extentY, + centerMaximum[2]+extentAbove}}; + }; + + for(std::size_t passageIndex=0;passageIndexpassages.size(); + ++passageIndex) + { + const auto& passage=ResolvedLayout->passages[passageIndex]; + if(passage.controlPoints.size()<2)continue; + const std::ptrdiff_t lastIndex=static_cast( + passage.controlPoints.size())-1; + const auto& constants=PassageComposerConstants[passageIndex]; + const std::ptrdiff_t lowerMouthIndex=constants.lowerMouthIndex; + const auto pointArray=[&](std::ptrdiff_t index) + { + std::array result{}; + const auto& point=passage.controlPoints[static_cast(index)]; + for(std::size_t axis=0;axis<3;++axis) + result[axis]=static_cast(point.positionSubvoxels[axis]); + return result; + }; + for(std::size_t segment=0;segment+1(b.positionSubvoxels[0])-a.positionSubvoxels[0]; + const double dy=static_cast(b.positionSubvoxels[1])-a.positionSubvoxels[1]; + const double dz=static_cast(b.positionSubvoxels[2])-a.positionSubvoxels[2]; + const double length=DeterministicSqrt(dx*dx+dy*dy+dz*dz); + const bool smoothCarve=static_cast(segment)>2 + &&static_cast(segment) + +(constants.internalChainPassage?2:12)(segment)-1 + :static_cast(segment)); + const auto p1=pointArray(static_cast(segment)); + const auto p2=pointArray(static_cast(segment)+1); + const auto p3=pointArray(smoothCarve + ?static_cast(segment)+2 + :static_cast(segment)+1); + const auto supportP0=pointArray(segment>0 + ?static_cast(segment)-1 + :static_cast(segment)); + const auto supportP3=pointArray(segment+2(segment)+2 + :static_cast(segment)+1); + const auto catmullRange=[&](std::size_t axis,double t0,double t1) + { + const double c0=p1[axis]; + const double c1=0.5*(-supportP0[axis]+p2[axis]); + const double c2=0.5*(2.0*supportP0[axis]-5.0*p1[axis] + +4.0*p2[axis]-supportP3[axis]); + const double c3=0.5*(-supportP0[axis]+3.0*p1[axis] + -3.0*p2[axis]+supportP3[axis]); + const auto evaluate=[&](double t) + {return ((c3*t+c2)*t+c1)*t+c0;}; + double low=std::min(evaluate(t0),evaluate(t1)); + double high=std::max(evaluate(t0),evaluate(t1)); + const double qa=3.0*c3,qb=2.0*c2,qc=c1; + const auto includeRoot=[&](double t) + { + if(t<=t0||t>=t1||!std::isfinite(t))return; + const double value=evaluate(t); + low=std::min(low,value);high=std::max(high,value); + }; + if(std::abs(qa)<=1.0e-12) + {if(std::abs(qb)>1.0e-12)includeRoot(-qc/qb);} + else + { + const double discriminant=qb*qb-4.0*qa*qc; + if(discriminant>=0.0) + { + const double root=std::sqrt(discriminant); + includeRoot((-qb-root)/(2.0*qa)); + includeRoot((-qb+root)/(2.0*qa)); + } + } + return std::pair{low-cubicPadding,high+cubicPadding}; + }; + std::array curveMinimum{},curveMaximum{}; + for(std::size_t axis=0;axis<3;++axis) + { + const double c0=p1[axis]; + const double c1=0.5*(-p0[axis]+p2[axis]); + const double c2=0.5*(2.0*p0[axis]-5.0*p1[axis] + +4.0*p2[axis]-p3[axis]); + const double c3=0.5*(-p0[axis]+3.0*p1[axis] + -3.0*p2[axis]+p3[axis]); + const auto evaluate=[&](double t) + {return smoothCarve?((c3*t+c2)*t+c1)*t+c0:p1[axis]+t*(p2[axis]-p1[axis]);}; + double low=std::min(evaluate(0.0),evaluate(1.0)); + double high=std::max(evaluate(0.0),evaluate(1.0)); + const double qa=3.0*c3,qb=2.0*c2,qc=c1; + const auto includeRoot=[&](double t) + { + if(t<=0.0||t>=1.0||!std::isfinite(t))return; + const double value=evaluate(t); + low=std::min(low,value);high=std::max(high,value); + }; + if(smoothCarve) + { + if(std::abs(qa)<=1.0e-12) + {if(std::abs(qb)>1.0e-12)includeRoot(-qc/qb);} + else + { + const double discriminant=qb*qb-4.0*qa*qc; + if(discriminant>=0.0) + { + const double root=std::sqrt(discriminant); + includeRoot((-qb-root)/(2.0*qa)); + includeRoot((-qb+root)/(2.0*qa)); + } + } + } + curveMinimum[axis]=low-cubicPadding; + curveMaximum[axis]=high+cubicPadding; + } + const double radius=static_cast(std::max(a.radiusSubvoxels, + b.radiusSubvoxels)); + const double carveExtent=radius+reachSubvoxels + +(0.21+0.20)*qPerMetre+cubicPadding; + const auto carveBox=pointBox(curveMinimum,curveMaximum, + carveExtent,carveExtent,carveExtent,carveExtent); + const FPassageCandidateRef routeReference{ + static_cast(passageIndex), + static_cast(segment),0}; + emit(0,carveBox.first,carveBox.second,routeReference); + + // Treads extend by up to two metres through route knots, then project + // across a 1.55 m half-width with relief. Include Catmull support-height + // overshoot, extrapolated radius, and the full source-dependent floor band. + const auto supportZCurve=catmullRange(2,-0.5,1.5); + const double supportLinearMinimum=std::min(p1[2],p2[2])-0.5*std::abs(dz); + const double supportLinearMaximum=std::max(p1[2],p2[2])+0.5*std::abs(dz); + const double supportRadiusMaximum=radius + +0.5*std::abs(static_cast(b.radiusSubvoxels) + -static_cast(a.radiusSubvoxels))+cubicPadding; + std::array treadMinimum={ + std::min(p1[0],p2[0])-4.25*qPerMetre, + std::min(p1[1],p2[1])-4.25*qPerMetre, + std::min(supportLinearMinimum,supportZCurve.first) + -supportRadiusMaximum-2.5*qPerMetre}; + std::array treadMaximum={ + std::max(p1[0],p2[0])+4.25*qPerMetre, + std::max(p1[1],p2[1])+4.25*qPerMetre, + std::max(supportLinearMaximum,supportZCurve.second)+2.5*qPerMetre}; + emit(1,treadMinimum,treadMaximum,routeReference); + + // Player capsules use horizontal projection, a 0.34 m radius, and a + // 0.54 m axial half-length above the route floor. + std::array clearanceMinimum={ + std::min(p1[0],p2[0])-0.40*qPerMetre, + std::min(p1[1],p2[1])-0.40*qPerMetre, + std::min(p1[2],p2[2])-radius+0.20*qPerMetre}; + std::array clearanceMaximum={ + std::max(p1[0],p2[0])+0.40*qPerMetre, + std::max(p1[1],p2[1])+0.40*qPerMetre, + std::max(p1[2],p2[2])+2.06*qPerMetre}; + emit(2,clearanceMinimum,clearanceMaximum,routeReference); + + const std::ptrdiff_t index=static_cast(segment); + const bool mouthCarve=index(8,lowerMouthIndex) + ||(index>=constants.mouthSearchBegin&&index=lowerMouthIndex; + if(mouthCarve) + emit(3,carveBox.first,carveBox.second,{routeReference.passageIndex, + routeReference.segmentIndex,1}); + } + + // The upper-mouth support exception has a larger spatial envelope than the + // tunnel body and remains an explicit compiled operation. + const auto& upperMouth=passage.upperMouthSubvoxels; + const double upperReach=15.0*qPerMetre,upperZReach=8.0*qPerMetre; + emit(3,{upperMouth[0]-upperReach,upperMouth[1]-upperReach, + upperMouth[2]-upperZReach}, + {upperMouth[0]+upperReach,upperMouth[1]+upperReach, + upperMouth[2]+upperZReach}, + {static_cast(passageIndex),-1,0}); + + if(passageIndex(passageIndex), + static_cast(segment),2}); + } + } + } + + for(std::size_t crossingIndex=0;crossingIndex=ResolvedLayout->passages.size())continue; + if(crossing.needsColumn) + { + const double radius=static_cast(crossing.maximumRadiusSubvoxels) + +reachSubvoxels; + emit(3,{crossing.centerXSubvoxels-radius, + crossing.centerYSubvoxels-radius, + static_cast(crossing.bottomZSubvoxels)-reachSubvoxels}, + {crossing.centerXSubvoxels+radius, + crossing.centerYSubvoxels+radius, + static_cast(crossing.topZSubvoxels)+reachSubvoxels}, + {static_cast(crossing.passageIndex), + static_cast(crossing.firstSegment),3}); + } + else + { + const auto& passage=ResolvedLayout->passages[crossing.passageIndex]; + bool surfaceWorldUpper=false; + const std::int32_t upperStrate=passage.upperStrateIndex; + const auto upper=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[upperStrate](const auto& value) + {return value.strateIndex==upperStrate;}); + surfaceWorldUpper=upper!=ResolvedLayout->strates.end() + &&upper->passageCapabilities.hasSurfaceLanding; + const std::size_t shortSpanPadding=surfaceWorldUpper?2:0; + const std::size_t first=std::min(crossing.firstSegment>shortSpanPadding + ?crossing.firstSegment-shortSpanPadding:0, + passage.controlPoints.size()-1); + const std::size_t last=std::min(crossing.lastSegment+shortSpanPadding, + passage.controlPoints.size()-2); + for(std::size_t segment=first;segment<=last;++segment) + { + const auto& a=passage.controlPoints[segment]; + const auto& b=passage.controlPoints[segment+1]; + const double radius=static_cast(std::max(a.radiusSubvoxels, + b.radiusSubvoxels))+3.8*qPerMetre; + emit(3,{std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])-radius, + std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])-radius, + std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])-5.0*qPerMetre}, + {std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])+radius, + std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])+radius, + std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])+1.0*qPerMetre}, + {static_cast(crossing.passageIndex), + static_cast(segment),4}); + } + } + } + + for(auto& [key,brick]:L90PassageCandidateBricks) + { + (void)key; + for(auto& list:brick.semantics) + { + std::sort(list.begin(),list.end(),PassageCandidateRefLess); + list.erase(std::unique(list.begin(),list.end()),list.end()); + } + } + std::array uniqueReferences{}; + std::size_t listCapacityBytes=0; + for(const auto& [key,brick]:L90PassageCandidateBricks) + { + (void)key; + for(std::size_t semantic=0;semantic( + FClock::now()-buildStart).count(); + std::printf("[L90PassageCandidateBuild] brick_m=4 reach_m=%.1f bricks=%zu emitted=(%llu,%llu,%llu,%llu) unique=(%llu,%llu,%llu,%llu) bytes_estimate=%zu build_ms=%.3f failures=%llu valid=%d\n", + L90PassageCandidateReachMetres,L90PassageCandidateBricks.size(), + static_cast(emittedReferences[0]), + static_cast(emittedReferences[1]), + static_cast(emittedReferences[2]), + static_cast(emittedReferences[3]), + static_cast(uniqueReferences[0]), + static_cast(uniqueReferences[1]), + static_cast(uniqueReferences[2]), + static_cast(uniqueReferences[3]), + estimatedBytes,buildMilliseconds, + static_cast(L90PassageCandidateBuildFailures), + static_cast(L90PassageCandidateProgramValid)); } double PassageNearestNodeLowerBound(const FPassageNearestNode& node, @@ -5854,6 +6524,12 @@ private: +vertical*vertical)-0.34*qPerMetre; if(distancematches(*this) + &&l90TileQueries->candidateCensusProfiled()) + l90TileQueries->recordL90ProofCheck(2, + PassageCandidateBrickContains(ActivePassageCandidateBrick,2, + passageIndex,static_cast(index))); return true; } } @@ -12751,4 +13427,12 @@ private: std::vector PassageProofNodes; std::vector> PassageColumnSegmentFlags,PassageShortSpanSegmentFlags; std::vector PassageHasShortSpanFlags; + bool L90PassageCandidateProgramEnabled=false; + bool L90PassageCandidateProgramValid=false; + double L90PassageCandidateBrickEdgeSubvoxels=0.0; + double L90PassageCandidateReachMetres=0.0; + std::uint64_t L90PassageCandidateBuildFailures=0; + std::unordered_map L90PassageCandidateBricks; + inline static thread_local const FPassageCandidateBrick* ActivePassageCandidateBrick=nullptr; }; diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index e345edc..4dbe513 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -2794,7 +2794,7 @@ namespace VoxelForgeCore // queryMinimum/queryMaximum ci-dessus couvrent exactement ce domaine inclusif. const FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope( tileShape,queryMinimum,queryMaximum, - options.profilePassageComposer?&out.diagnostics:nullptr); + options.profilePassageComposer?&out.diagnostics:nullptr,key.lod); if(options.profilePassageComposer) { out.diagnostics.passageComposerProfiled=true; diff --git a/Standalone/Research/WorldAssemblerM3/Linux90/R41-unreal-gap-notes.md b/Standalone/Research/WorldAssemblerM3/Linux90/R41-unreal-gap-notes.md new file mode 100644 index 0000000..4382a9f --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux90/R41-unreal-gap-notes.md @@ -0,0 +1,31 @@ +## Ranked causes for L85 + +1. **The first tile request starts about 0.7 s later than headless.** [TRACE] The Unreal clock starts at `BeginPlay` (`0.000 s`), which ends at `0.598 s`. Desired-set construction takes only 3.7 ms at `0.789–0.793 s`; the first tile request is at `0.793 s`. The scheduler still reports `core_not_ready` at `0.772 s`. [CODE] `BeginPlay` synchronously exports and parses the Core snapshot, then launches shape compilation on a `BackgroundNormal` task; `UpdateChunksAroundPosition` returns while Core is not ready. ([trace]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l85/unreal-fi/fi-arrival-l85.trace.jsonl:18), [trace]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l85/unreal-fi/fi-arrival-l85.trace.jsonl:48), [trace]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l85/unreal-fi/fi-arrival-l85.trace.jsonl:52), [VoxelWorld.cpp]( /home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7646), [VoxelWorld.cpp]( /home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7782), [VoxelWorld.cpp]( /home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelWorld.cpp:11782).) + + [CODE] Strata/passage work is measured at roughly 76 ms. Snapshot serialization and `FromJson` run synchronously on the `BeginPlay` caller; shape compilation runs asynchronously and overlaps the rest of `BeginPlay`. The trace does not split the remaining `BeginPlay` time. Package or asset loading before `BeginPlay` is outside this trace; the recorded settings asset is transient. [INFERENCE] Headless starts its desired request around 60–80 ms from cold start, while Unreal’s first tile request is at 793 ms. That accounts for roughly 0.7 s of the gap as later pre-request work or a different clock boundary, but does not identify which part is avoidable. ([Linux87 report]( /home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux87/REPORT.md:9).) + +2. **The remaining difference is in Unreal’s post-request chain, especially the staged LOD2 tail.** [TRACE] Unreal’s LOD0 ring is resident at `2.391 s`; at `3.134 s`, LOD0 and LOD1 are loaded but 240 LOD2 tiles remain missing. All 1,216 LOD0–2 tiles are resident at `3.650 s`. That is 2.86 s from first request to full residency, versus about 2.2 s for the headless run. ([trace]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l85/unreal-fi/fi-arrival-l85.trace.jsonl:1455), [trace]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l85/unreal-fi/fi-arrival-l85.trace.jsonl:2438), [trace]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l85/unreal-fi/fi-arrival-l85.trace.jsonl:3135), [Linux85 report]( /home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux85/REPORT.md:21).) + + [CODE] Admission is frame-paced: the actor calls `UpdateChunksAroundPosition` once per tick. The trace shows a task cap of 8, ready queue depth 8, and total admission cap 16. At `3.134 s` all 16 slots are occupied and all 8 workers are running. So the cap is active, but this sample does not show idle workers waiting for admission. [INFERENCE] Frame pacing and LOD2 staging plausibly explain some of the post-request excess; the trace does not isolate their exact shares. ([VoxelWorld.cpp]( /home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7074), [VoxelWorld.cpp]( /home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:13449), [Linux87 report]( /home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux87/REPORT.md:26).) + +3. **The game-thread apply cap may add some tail latency, but it is not the main measured cost.** [TRACE] The startup boost ends at `3.157 s` because the desired epoch changed; that frame reports the normal limit of four mesh applies and hits the count limit. [CODE] Any desired-epoch change ends the boost. [INFERENCE] Keeping the boost active through the initial epoch churn is a small candidate to test, but the trace does not show that it accounts for a large fraction of 1.4 s. ([trace]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l85/unreal-fi/fi-arrival-l85.trace.jsonl:2458), [trace]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l85/unreal-fi/fi-arrival-l85.trace.jsonl:2466), [VoxelWorld.cpp]( /home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:9505).) + +4. **Worker count, publication, and collision cooking are weak explanations for this gap.** [TRACE] `streaming_background_worker_cap=1` yields 8 effective tile slots, matching the 8 TaskGraph background threads; the setting is a clamp toggle, not a one-worker limit. Headless FI readiness changes by only about 36 ms between W8 and W12 in L87 (124 ms in the L85 matrix), far below this gap. ([trace]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l85/unreal-fi/fi-arrival-l85.trace.jsonl:5), [VoxelWorld.cpp]( /home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:11592), [Linux87 report]( /home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux87/REPORT.md:38).) + + [TRACE] Unreal’s per-tile summaries show small apply times for LOD0–2; they cover the full trace, so they are indicative rather than an isolated startup breakdown. [CODE] The tile event records worker completion and game-thread application; application does not mean collision-ready. L85 measures residency through `LoadedTiles`, and its later milestone shows collision readiness is still a separate, incomplete count. RMC cooking therefore cannot explain the 3.650 s residency timestamp. ([trace summary]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l85/unreal-fi/fi-arrival-l85.trace.jsonl:12896), [VoxelWorld.cpp]( /home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:9908), [trace]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l85/unreal-fi/fi-arrival-l85.trace.jsonl:5843).) + +## L80’s four missing tiles + +[TRACE] The four are LOD0 `(47,-2,-99)`, `(47,-2,-100)`, `(47,-2,-101)`, and `(47,-2,-102)`. At `45.375 s`, desired LOD0 is 516, resident is 512, pending is zero, and all 516 are visually covered. The trace repeatedly records empty results for those keys and then requests them again. ([trace]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l80/movement/unreal/fast-L80.trace.jsonl:21823), [trace]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l80/movement/unreal/fast-L80.trace.jsonl:21828), [trace sample]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l80/movement/unreal/fast-L80.trace.jsonl:21843).) + +[INFERENCE] They most likely form the pawn collision gate’s four-tile downward support prefetch: the code requests the support tile and three below it. Empty results are marked both loaded and `CollisionNotRequired`; a later desired-set rebuild removes no-collision gate keys, while unload clears both that marker and loaded bookkeeping. The next gate check can then add the same keys again. This fits the repeated four-tile region compiles at the same center, though the trace does not record per-key collision-only membership or unload reasons. ([VoxelWorld.cpp]( /home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:8277), [VoxelWorld.cpp]( /home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:10653), [VoxelWorld.cpp]( /home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:14330), [trace]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l80/movement/unreal/fast-L80.trace.jsonl:21841).) + +This is **not a stranded worker task**: the empty tile work completes, and the commandlet ends with pending, cooks, and active tasks all zero. There is also a **readiness-check mismatch**: the commandlet requires every desired key to be in `LoadedTiles`, while its collision-set check accepts `CollisionNotRequiredTiles`. The short resident count is real at that instant, but the coverage count says it is not a visual hole. ([ExploreCommandlet.cpp]( /home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp:18132), [ExploreCommandlet.cpp]( /home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp:18142), [L80 log]( /home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l80/movement/unreal/fast-L80.log:3542).) + +## Cheapest confirmation and smallest fixes + +- **For the L85 timing:** add trace timestamps around snapshot serialization, `FromJson`, compile launch, compile worker start, and `bReady` publication. The existing trace already records first request, tile worker times, apply, and residency. One matched run would show whether the unexplained pre-request interval is snapshot work or shape compilation. +- **Smallest latency change to A/B:** keep the startup apply boost through initial desired-epoch changes, ending it on initial readiness or timeout. The trace shows the current early reset and a count-limited apply frame. It may shorten the LOD2 tail; it will not explain or remove the pre-request delay. +- **Smallest L80 logic fix:** retain known-empty collision classification across unloads for the current modification epoch, invalidating it when tile data changes. That should stop re-requesting those support keys; confirm their collision-only role with per-key logging before changing the readiness predicate. + +No files were changed; no builds, tests, or project binaries were run. \ No newline at end of file diff --git a/Standalone/Research/WorldAssemblerM3/Linux90/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux90/REPORT.md new file mode 100644 index 0000000..43144c6 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux90/REPORT.md @@ -0,0 +1,33 @@ +# Linux 90 — passage candidate census + +Read Astra pass 3 before implementation. The owner-world census compiled 54,324 sparse 4 m bricks with a 10 m reach and four independent, uncapped candidate lists. The density proof used the incoming-sample bound `-d·Q + 0.24 m`; samples whose bound exceeded 10 m explicitly kept the existing passage work. The build emitted 217,072 carve, 24,359 tread, 5,416 clearance, and 57,594 mouth/column references, with zero compiler failures. Build time was 26.3–27.9 ms; estimated memory was 12,667,000 bytes (about 12.1 MiB). + +List-size distributions are weighted across 30,681,829 near-LOD sample hits. Percentiles include zero-candidate samples; the active percentiles include only nonempty lists. The `16+` column is a reporting bucket; the stored lists have no cap. + +| Semantic | Nonempty hits | All-hit p90/p95 | Active p90/p95 | 16+ of hits | +|---|---:|---:|---:|---:| +| Carve | 52.89% | 11 / 13 | 13 / 14 | 1.65% | +| Tread | 33.53% | 3 / 4 | 4 / 4 | 0% | +| Clearance | 15.60% | 1 / 2 | 2 / 3 | 0% | +| Mouth/column | 41.35% | 11 / 12 | 13 / 14 | 0.88% | + +Measured passage time still retained by explicit reach fallback: + +| Stratum | Fallback share | +|---|---:| +| S0 | 24.94% | +| FI | 12.19% | +| TN | 19.03% | +| S3 | 42.57% | +| Weighted | 18.28% | + +Every final proof counter was zero for omitted carve, tread, clearance, route, column, or bridge candidates. All fallback was due to the 10 m reach limit. The 3,897,959 fallback iterations still visited 19.51M carve segments, 9.61M tread segments, and 3.77M clearance capsules (plus 14,163 mouth checks). Both stop conditions fail: carve and mouth/column active p90s are 13 candidates, and fallback exceeds 10% in every stratum. **Stop here; no junction evaluator prototype was built.** + +## Verification and runs + +- Fast tier: **PASS**, 269 checks and 5 diagnostics in 296.2 s. Pinned hash and determinism checks passed. +- Linux `VFHostEditor` after timing: **PASS**, 27/27 actions in 69.13 s with `-NoUBA -MaxParallelActions=4`; no address-space limit. Three existing RealtimeMesh deprecation warnings were emitted. +- The first census collection was discarded after review found its column proof recorder read the distance before assignment. The recorder was fixed and the complete four-stratum census rerun; final column proof misses are zero. +- No run exceeded 30 minutes. No prototype replay, W8/W12 prototype timing, capture, or version bump applies after the census stop. The compiled lists are opt-in census data and are not wired into production evaluation. + +Source stamp: `c797603e5272850855e448a6177ae32dccdeb20974484a725324ebbc0903106d`. Logs and census data are under `Saved/Linux/l90/`. No commit, push, or stash was made. diff --git a/Standalone/Research/WorldAssemblerM3/Linux90/RunL90CandidateCensus.py b/Standalone/Research/WorldAssemblerM3/Linux90/RunL90CandidateCensus.py new file mode 100644 index 0000000..7cf196e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux90/RunL90CandidateCensus.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +"""Compile and census spatial passage candidates on the four owner strata.""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l90/candidate-census" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARY = ROOT / "Saved/Linux/l90/bin/VoxelForgeCoreHeadless-l90-clang19" +STRATA = { + "S0": {"id": 0, "camera": (8.0, 16.0, -372.2626953125), "near": 1408, "whole": 2850}, + "FI": {"id": 1, "camera": (25.8369140625, 3.19921875, -737.0), "near": 1216, "whole": 2249}, + "TN": {"id": 2, "camera": (19.166015625, -14.2001953125, -753.3740234375), "near": 1344, "whole": 2233}, + "S3": {"id": 3, "camera": (-20.0625, -5.068359375, -900.353515625), "near": 1344, "whole": 2329}, +} +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+) cold_ready_s=([0-9.]+)$", re.M) +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=(\S+)$", re.M) +BUILD = re.compile(r"^\[L90PassageCandidateBuild\] (.*)$", re.M) +CENSUS = re.compile(r"^\[L90PassageCandidateTile\] (.*)$", re.M) +HIST_NAMES = ("carve_hist", "tread_hist", "clearance_hist", "mouth_column_hist") + + +def command(stratum: str) -> list[str]: + spec = STRATA[stratum] + camera = ",".join(f"{value:.12f}" for value in spec["camera"]) + ",0,0" + return [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "env", "VOXELFORGE_L90_PASSAGE_CANDIDATE_CENSUS=1", + "VOXELFORGE_L81_PASSAGE_BOUND_PADDING_VOXELS=0", "timeout", "1740s", + str(BINARY), str(DATA), "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--workers=8", "--clip-radius=3", "--max-clip-level=8", + "--max-level=8", f"--camera={camera}", "--profile-lod-work", + "--profile-passage-composer", "--stream-scheduler-policy=3", + ] + + +def parse_hist(value: str) -> list[int]: + items = [int(item) for item in value.split(",")] + if len(items) != 17: + raise ValueError(f"expected 17 histogram bins, got {len(items)}") + return items + + +def parse_fields(line: str) -> dict[str, str]: + return dict(FIELDS.findall(line)) + + +def parse_tuple(value: str, expected: int) -> list[int]: + items = [int(item) for item in value.strip("()").split(",")] + if len(items) != expected: + raise ValueError(f"expected {expected} values, got {value}") + return items + + +def run(stratum: str) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"{stratum.lower()}-w8-profile.log" + started = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command(stratum), cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1750) + output = log.read_text(encoding="utf-8", errors="replace") + near, stamp = NEAR.search(output), STAMP.search(output) + if result.returncode or near is None or stamp is None: + raise RuntimeError(f"run failed or required output is missing: {log}; exit={result.returncode}") + expected = STRATA[stratum]["near"] + if (int(near.group(1)), int(near.group(2))) != (expected, expected): + raise RuntimeError(f"incomplete near set in {log}: {near.group(1)}/{near.group(2)}") + builds = [parse_fields(match.group(1)) for match in BUILD.finditer(output)] + if not builds: + raise RuntimeError(f"candidate compiler build row missing: {log}") + + hist = [[0] * 17 for _ in range(4)] + census_rows = 0 + near_queries = no_brick = fallback_iterations = fallback_ns = query_ns = 0 + fallback_reach = fallback_shortspan = fallback_invalid = fallback_proof = 0 + fallback_carve = fallback_tread = fallback_clearance = fallback_mouth = 0 + proof_checks = [0] * 6 + proof_misses = [0] * 6 + for match in CENSUS.finditer(output): + fields = parse_fields(match.group(1)) + if int(fields["lod"]) > 2: + continue + census_rows += 1 + near_queries += int(fields["queries"]) + no_brick += int(fields["no_brick"]) + fallback_iterations += int(fields["fallback_iterations"]) + fallback_reach += int(fields["fallback_reach"]) + fallback_shortspan += int(fields["fallback_shortspan"]) + fallback_invalid += int(fields["fallback_invalid"]) + fallback_proof += int(fields["fallback_proof"]) + fallback_ns += int(fields["fallback_ns"]) + query_ns += int(fields["query_ns"]) + fallback_carve += int(fields["fallback_carve"]) + fallback_tread += int(fields["fallback_tread"]) + fallback_clearance += int(fields["fallback_clearance"]) + fallback_mouth += int(fields["fallback_mouth"]) + proof_checks = [a + b for a, b in zip(proof_checks, parse_tuple(fields["proof_checks"], 6))] + proof_misses = [a + b for a, b in zip(proof_misses, parse_tuple(fields["proof_misses"], 6))] + for semantic, name in enumerate(HIST_NAMES): + values = parse_hist(fields[name]) + hist[semantic] = [a + b for a, b in zip(hist[semantic], values)] + if census_rows == 0 or near_queries == 0: + raise RuntimeError(f"near-LOD candidate census rows missing: {log}") + pipeline = PIPELINE.search(output) + if pipeline is None: + raise RuntimeError(f"stream pipeline summary missing: {log}") + pipeline_fields = parse_fields(pipeline.group(1)) + row = { + "stratum": stratum, "workers": 8, "exit_code": result.returncode, + "wall_s": round(time.monotonic() - started, 3), + "lod02_ready_s": float(near.group(3)), + "whole_view_ready_s": float(pipeline_fields["whole_view_ready"].removesuffix("s")), + "source_stamp": stamp.group(1), "near_tiles": expected, + "census_scopes_lod02": census_rows, "near_query_hits": near_queries, + "no_brick_hits": no_brick, "fallback_iterations": fallback_iterations, + "fallback_reasons": {"reach_overflow": fallback_reach, "short_span": fallback_shortspan, + "invalid_compiler": fallback_invalid, "proof_miss": fallback_proof}, + "fallback_ns": fallback_ns, "query_ns": query_ns, + "fallback_share": fallback_ns / query_ns if query_ns else None, + "fallback_work": {"carve_visits": fallback_carve, "tread_visits": fallback_tread, + "clearance_checks": fallback_clearance, "mouth_checks": fallback_mouth}, + "proof_checks": dict(zip(("carve", "tread", "clearance", "mouth_route", "columns", "bridges"), proof_checks)), + "proof_misses": dict(zip(("carve", "tread", "clearance", "mouth_route", "columns", "bridges"), proof_misses)), + "histograms": {name.removesuffix("_hist"): values + for name, values in zip(HIST_NAMES, hist)}, + "build_rows": builds, "log": str(log.relative_to(ROOT)), + } + print("[L90CandidateCensusRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def aggregate(rows: list[dict]) -> dict: + total_hist = [[0] * 17 for _ in range(4)] + for row in rows: + for semantic, name in enumerate(HIST_NAMES): + values = row["histograms"][name.removesuffix("_hist")] + total_hist[semantic] = [a + b for a, b in zip(total_hist[semantic], values)] + weighted = {} + for semantic, name in enumerate(HIST_NAMES): + values = total_hist[semantic] + total = sum(values) + def percentile(p: float, first_bin: int = 0) -> int: + population = sum(values[first_bin:]) + target = max(1, int((p * population) + 0.999999999)) + seen = 0 + for index in range(first_bin, len(values)): + count = values[index] + seen += count + if seen >= target: + return index + return 16 + nonempty = sum(values[1:]) + weighted[name.removesuffix("_hist")] = { + "queries": total, + "histogram_0_to_15_plus": values, + "mean_lower_bound": (sum(i * n for i, n in enumerate(values)) / total + if total else 0.0), + "p50_lower_bound": percentile(0.50) if total else 0, + "p90_lower_bound": percentile(0.90) if total else 0, + "p95_lower_bound": percentile(0.95) if total else 0, + "share_16_plus": values[16] / total if total else 0.0, + "nonempty_share": nonempty / total if total else 0.0, + "nonempty_mean_lower_bound": ( + sum(i * n for i, n in enumerate(values)) / nonempty if nonempty else 0.0), + "nonempty_p90_lower_bound": percentile(0.90, 1) if nonempty else 0, + "nonempty_p95_lower_bound": percentile(0.95, 1) if nonempty else 0, + } + return {"strata": rows, "weighted_semantics": weighted} + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--strata", nargs="+", choices=tuple(STRATA), default=tuple(STRATA)) + args = parser.parse_args() + if not BINARY.is_file() or not DATA.is_file(): + raise SystemExit(f"missing input: {BINARY} or {DATA}") + rows = [run(stratum) for stratum in args.strata] + summary = aggregate(rows) + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print("[L90CandidateCensusSummary] " + json.dumps(summary["weighted_semantics"], sort_keys=True), flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())