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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-28 05:28:27 +01:00
1 parent 04cdd197fb
commit a7a03d42b4
5 files changed
+948 -5

No files matched your search

+688 -4
View File
@@ -20,6 +20,7 @@
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <functional>
#include <limits> #include <limits>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
@@ -62,6 +63,13 @@ static bool VF_IsL88AllowPassageAblationWalkabilityFailure()
return bEnabled; 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 // 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 // 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. // only the conservative surface band. The copied Unreal generator remains the A/B reference.
@@ -218,10 +226,11 @@ public:
FScopedPassageTileQueries(const FStandaloneShapeWorld& shape, FScopedPassageTileQueries(const FStandaloneShapeWorld& shape,
const std::array<std::int64_t,3>& minimumSubvoxels, const std::array<std::int64_t,3>& minimumSubvoxels,
const std::array<std::int64_t,3>& maximumSubvoxels, const std::array<std::int64_t,3>& maximumSubvoxels,
VoxelForgeCore::TileGenerationStats* profileStats=nullptr) VoxelForgeCore::TileGenerationStats* profileStats=nullptr,int32 profileLod=0)
:shape_(&shape),minimumSubvoxels_(minimumSubvoxels), :shape_(&shape),minimumSubvoxels_(minimumSubvoxels),
maximumSubvoxels_(maximumSubvoxels),previous_(ActivePassageTileQueries), maximumSubvoxels_(maximumSubvoxels),previous_(ActivePassageTileQueries),
profileStats_(profileStats) profileStats_(profileStats),profileLod_(profileLod),
candidateProfiled_(profileStats!=nullptr&&shape.L90PassageCandidateProgramEnabled)
{ {
mayContainResolvedBoundaryBand_=shape.ResolvedBoundaryBandsIntersectSubvoxelRange( mayContainResolvedBoundaryBand_=shape.ResolvedBoundaryBandsIntersectSubvoxelRange(
minimumSubvoxels_[2],maximumSubvoxels_[2]); minimumSubvoxels_[2],maximumSubvoxels_[2]);
@@ -328,9 +337,62 @@ public:
routeFeatureOverlap_(source.routeFeatureOverlap_),columnZOverlap_(source.columnZOverlap_), routeFeatureOverlap_(source.routeFeatureOverlap_),columnZOverlap_(source.columnZOverlap_),
mayContainResolvedBoundaryBand_(source.mayContainResolvedBoundaryBand_), mayContainResolvedBoundaryBand_(source.mayContainResolvedBoundaryBand_),
mayContainTransitionPassageFade_(source.mayContainTransitionPassageFade_), mayContainTransitionPassageFade_(source.mayContainTransitionPassageFade_),
previous_(ActivePassageTileQueries) previous_(ActivePassageTileQueries),profileLod_(source.profileLod_),
candidateProfiled_(source.candidateProfiled_)
{ ActivePassageTileQueries=this; } { ActivePassageTileQueries=this; }
~FScopedPassageTileQueries(){ActivePassageTileQueries=previous_;} ~FScopedPassageTileQueries()
{
ActivePassageTileQueries=previous_;
if(!candidateProfiled_||l90Queries_==0)return;
std::array<std::array<char,360>,4> HistText{};
constexpr std::array<const char*,4> Names={"carve","tread","clearance","mouth_column"};
for(std::size_t semantic=0;semantic<Names.size();++semantic)
{
std::size_t offset=0;
for(std::size_t bin=0;bin<l90CandidateHistogram_[semantic].size();++bin)
{
const int written=std::snprintf(HistText[semantic].data()+offset,
HistText[semantic].size()-offset,"%s%llu",bin?",":"",
static_cast<unsigned long long>(l90CandidateHistogram_[semantic][bin]));
if(written<0)break;
offset=std::min(HistText[semantic].size(),offset+static_cast<std::size_t>(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<long long>(minimumSubvoxels_[0]),
static_cast<long long>(minimumSubvoxels_[1]),
static_cast<long long>(minimumSubvoxels_[2]),
static_cast<long long>(maximumSubvoxels_[0]),
static_cast<long long>(maximumSubvoxels_[1]),
static_cast<long long>(maximumSubvoxels_[2]),
static_cast<unsigned long long>(l90Queries_),
static_cast<unsigned long long>(l90NoBrickQueries_),
static_cast<unsigned long long>(l90FallbackIterations_),
static_cast<unsigned long long>(l90FallbackReachOverflow_),
static_cast<unsigned long long>(l90FallbackShortSpan_),
static_cast<unsigned long long>(l90FallbackInvalid_),
static_cast<unsigned long long>(l90FallbackProof_),
static_cast<unsigned long long>(l90FallbackNanoseconds_),
static_cast<unsigned long long>(l90QueryNanoseconds_),
static_cast<unsigned long long>(l90FallbackCarveVisits_),
static_cast<unsigned long long>(l90FallbackTreadVisits_),
static_cast<unsigned long long>(l90FallbackClearanceChecks_),
static_cast<unsigned long long>(l90FallbackMouthChecks_),
static_cast<unsigned long long>(l90ProofChecks_[0]),
static_cast<unsigned long long>(l90ProofChecks_[1]),
static_cast<unsigned long long>(l90ProofChecks_[2]),
static_cast<unsigned long long>(l90ProofChecks_[3]),
static_cast<unsigned long long>(l90ProofChecks_[4]),
static_cast<unsigned long long>(l90ProofChecks_[5]),
static_cast<unsigned long long>(l90ProofMisses_[0]),
static_cast<unsigned long long>(l90ProofMisses_[1]),
static_cast<unsigned long long>(l90ProofMisses_[2]),
static_cast<unsigned long long>(l90ProofMisses_[3]),
static_cast<unsigned long long>(l90ProofMisses_[4]),
static_cast<unsigned long long>(l90ProofMisses_[5]),
HistText[0].data(),HistText[1].data(),HistText[2].data(),HistText[3].data());
}
FScopedPassageTileQueries& operator=(const FScopedPassageTileQueries&)=delete; FScopedPassageTileQueries& operator=(const FScopedPassageTileQueries&)=delete;
bool matches(const FStandaloneShapeWorld& shape) const{return shape_==&shape;} bool matches(const FStandaloneShapeWorld& shape) const{return shape_==&shape;}
@@ -388,6 +450,40 @@ public:
std::size_t routeSegmentCandidateCount() const{return routeSegmentCandidateCount_;} std::size_t routeSegmentCandidateCount() const{return routeSegmentCandidateCount_;}
VoxelForgeCore::TileGenerationStats* profileStats() const noexcept VoxelForgeCore::TileGenerationStats* profileStats() const noexcept
{return profileStats_;} {return profileStats_;}
bool candidateCensusProfiled() const noexcept{return candidateProfiled_;}
void recordL90CandidateHit(const std::array<std::size_t,4>& counts,bool haveBrick) const
{
if(!candidateProfiled_)return;
++l90Queries_;
if(!haveBrick)++l90NoBrickQueries_;
for(std::size_t semantic=0;semantic<counts.size();++semantic)
++l90CandidateHistogram_[semantic][std::min<std::size_t>(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();} bool hasStampCandidates() const{return !stampIndices_.empty();}
const std::vector<std::uint32_t>& stampCandidates() const{return stampIndices_;} const std::vector<std::uint32_t>& stampCandidates() const{return stampIndices_;}
bool hasCavernCandidates() const{return !cavernFeatureIndices_.empty();} bool hasCavernCandidates() const{return !cavernFeatureIndices_.empty();}
@@ -410,6 +506,16 @@ public:
bool mayContainTransitionPassageFade_=true; bool mayContainTransitionPassageFade_=true;
const FScopedPassageTileQueries* previous_; const FScopedPassageTileQueries* previous_;
VoxelForgeCore::TileGenerationStats* profileStats_=nullptr; 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<std::uint64_t,6> l90ProofChecks_{},l90ProofMisses_{};
mutable std::array<std::array<std::uint64_t,17>,4> l90CandidateHistogram_{};
}; };
bool IsResolvedPassageColumnSample(std::size_t PassageIndex,std::size_t SegmentIndex, bool IsResolvedPassageColumnSample(std::size_t PassageIndex,std::size_t SegmentIndex,
@@ -1129,6 +1235,40 @@ public:
auto* passageProfile=passageTileScopeMatches auto* passageProfile=passageTileScopeMatches
?passageTileQueries->profileStats():nullptr; ?passageTileQueries->profileStats():nullptr;
if(passageProfile)++passageProfile->passageComposerQueries; if(passageProfile)++passageProfile->passageComposerQueries;
const bool l90CandidateProfiled=passageTileScopeMatches
&&passageTileQueries->candidateCensusProfiled();
const auto* l90CandidateBrick=l90CandidateProfiled
?FindPassageCandidateBrick(qx,qy,qz):nullptr;
const std::array<std::size_t,4> l90CandidateCounts=l90CandidateBrick
?std::array<std::size_t,4>{
l90CandidateBrick->semantics[0].size(),l90CandidateBrick->semantics[1].size(),
l90CandidateBrick->semantics[2].size(),l90CandidateBrick->semantics[3].size()}
:std::array<std::size_t,4>{};
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::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
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 if(passageTileScopeMatches&&density>=0.0f
&&!passageTileQueries->hasRouteFeatureOverlap() &&!passageTileQueries->hasRouteFeatureOverlap()
&&!passageTileQueries->hasColumnZOverlap() &&!passageTileQueries->hasColumnZOverlap()
@@ -1296,6 +1436,12 @@ public:
const double bottomDistance=floorQ-qPerMetre-qz; const double bottomDistance=floorQ-qPerMetre-qz;
const double verticalDistance=std::max(topDistance,bottomDistance); const double verticalDistance=std::max(topDistance,bottomDistance);
if(sideDistance>mouthBlendWidth||verticalDistance>mouthBlendWidth)return; if(sideDistance>mouthBlendWidth||verticalDistance>mouthBlendWidth)return;
if(l90CandidateProfiled)
{
const bool contained=PassageCandidateBrickContains(l90CandidateBrick,3,
passageIndex,static_cast<std::int32_t>(index),2);
passageTileQueries->recordL90ProofCheck(3,contained);
}
const double outside=DeterministicSqrt( const double outside=DeterministicSqrt(
std::max(sideDistance,0.0)*std::max(sideDistance,0.0) std::max(sideDistance,0.0)*std::max(sideDistance,0.0)
+std::max(verticalDistance,0.0)*std::max(verticalDistance,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. // The connected passage tread continues the surface route into the tunnel.
const std::size_t passageIterationCount=ResolvedLayout->passages.size(); 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::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now()-start).count()),
carveVisits,treadVisits,clearanceChecks,mouthChecks,fallbackReason);
}
};
for(std::size_t passageIteration=0;passageIteration<passageIterationCount; for(std::size_t passageIteration=0;passageIteration<passageIterationCount;
++passageIteration) ++passageIteration)
{ {
const std::size_t passageIndex=passageIteration; const std::size_t passageIndex=passageIteration;
FL90IterationTimer l90IterationTimer;
if(l90CandidateProfiled)
{
l90IterationTimer.scope=passageTileQueries;
l90IterationTimer.start=std::chrono::steady_clock::now();
}
if(passageProfile)++passageProfile->passageIterations; if(passageProfile)++passageProfile->passageIterations;
const auto& passage=ResolvedLayout->passages[passageIndex]; const auto& passage=ResolvedLayout->passages[passageIndex];
if(passage.controlPoints.size()<2)continue; if(passage.controlPoints.size()<2)continue;
@@ -1341,6 +1515,7 @@ public:
const auto laterPassageMayCarve=[&](std::ptrdiff_t supportSegment, const auto laterPassageMayCarve=[&](std::ptrdiff_t supportSegment,
double sampleX,double sampleY,double sampleZ) double sampleX,double sampleY,double sampleZ)
{ {
l90IterationTimer.recordClearance();
if(passageProfile)++passageProfile->passageLaterBoreChecks; if(passageProfile)++passageProfile->passageLaterBoreChecks;
return PassageSupportMayBeCarvedByLaterBore(passageIndex, return PassageSupportMayBeCarvedByLaterBore(passageIndex,
supportSegment,sampleX,sampleY,sampleZ,0.35*qPerMetre); supportSegment,sampleX,sampleY,sampleZ,0.35*qPerMetre);
@@ -1353,6 +1528,21 @@ public:
{ {
if(!crossing.needsColumn||crossing.passageIndex!=passageIndex if(!crossing.needsColumn||crossing.passageIndex!=passageIndex
||qz>crossing.topZSubvoxels||qz<crossing.bottomZSubvoxels)continue; ||qz>crossing.topZSubvoxels||qz<crossing.bottomZSubvoxels)continue;
l90IterationTimer.recordMouth();
const auto recordColumnProof=[&](double columnDistance,double inputComposed)
{
if(!l90CandidateProfiled)return;
if(columnDistance<=L90PassageCandidateReachMetres*qPerMetre)
{
const bool contained=PassageCandidateBrickContains(
l90CandidateBrick,3,passageIndex,
static_cast<std::int32_t>(crossing.firstSegment),3);
passageTileQueries->recordL90ProofCheck(4,contained);
if(!contained)l90IterationTimer.requireFallback(3);
}
else if(columnDistance<static_cast<double>(inputComposed)*Q)
l90IterationTimer.requireFallback(1);
};
if(passageProfile)++passageProfile->passageColumnCandidates; if(passageProfile)++passageProfile->passageColumnCandidates;
#if defined(VF_L30_ENABLE_COST_COUNTERS) #if defined(VF_L30_ENABLE_COST_COUNTERS)
++ThreadClassificationStats().ColumnZOverlapCalls; ++ThreadClassificationStats().ColumnZOverlapCalls;
@@ -1399,6 +1589,7 @@ public:
if(passageProfile)++passageProfile->passageColumnDistanceCalls; if(passageProfile)++passageProfile->passageColumnDistanceCalls;
columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z); columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z);
} }
if(l90CandidateProfiled)recordColumnProof(columnDistance,composed);
composed=std::min(composed,static_cast<float>(columnDistance/Q)); composed=std::min(composed,static_cast<float>(columnDistance/Q));
} }
const bool tilePassageFree=passageTileScopeMatches const bool tilePassageFree=passageTileScopeMatches
@@ -1433,6 +1624,24 @@ public:
(carveLow-passage.controlPoints.begin())+1); (carveLow-passage.controlPoints.begin())+1);
const std::ptrdiff_t mouthSearchBegin=passageConstants.mouthSearchBegin; const std::ptrdiff_t mouthSearchBegin=passageConstants.mouthSearchBegin;
const std::ptrdiff_t mouthSearchEnd=passageConstants.mouthSearchEnd; const std::ptrdiff_t mouthSearchEnd=passageConstants.mouthSearchEnd;
if(l90CandidateProfiled)
{
const double signedDistanceLimit=density<0.0f
?-static_cast<double>(composed)*Q+0.24*qPerMetre
:std::max(2.25*qPerMetre,-static_cast<double>(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(density<0.0f&&tilePassageFree)
{ {
if(passageProfile)++passageProfile->passageNegativeBoundChecks; if(passageProfile)++passageProfile->passageNegativeBoundChecks;
@@ -1492,6 +1701,7 @@ public:
*(3.0-2.0*lowerMouthT); *(3.0-2.0*lowerMouthT);
const auto considerCarveSegment=[&](std::size_t segmentIndex,int scanPhase) const auto considerCarveSegment=[&](std::size_t segmentIndex,int scanPhase)
{ {
l90IterationTimer.recordCarve();
if(passageProfile)++passageProfile->passageCarveSegmentVisits; if(passageProfile)++passageProfile->passageCarveSegmentVisits;
const std::ptrdiff_t index=static_cast<std::ptrdiff_t>(segmentIndex); const std::ptrdiff_t index=static_cast<std::ptrdiff_t>(segmentIndex);
const auto& a=passage.controlPoints[static_cast<std::size_t>(index)]; const auto& a=passage.controlPoints[static_cast<std::size_t>(index)];
@@ -1593,6 +1803,16 @@ public:
considerCarveSegment(static_cast<std::size_t>(index),3); considerCarveSegment(static_cast<std::size_t>(index),3);
} }
if(!std::isfinite(bestSignedDistance))continue; if(!std::isfinite(bestSignedDistance))continue;
const double l90SignedDistanceLimit=density<0.0f
?-static_cast<double>(composed)*Q+0.24*qPerMetre
:std::max(2.25*qPerMetre,-static_cast<double>(composed)*Q
+0.24*qPerMetre);
if(l90CandidateProfiled&&bestSignedDistance<=l90SignedDistanceLimit
&&l90SignedDistanceLimit<=L90PassageCandidateReachMetres*qPerMetre)
passageTileQueries->recordL90ProofCheck(0,
L90PassageCandidateProgramValid
&&PassageCandidateBrickContains(l90CandidateBrick,0,passageIndex,
static_cast<std::int32_t>(bestIndex)));
const auto& nearestA=passage.controlPoints[static_cast<std::size_t>(bestIndex)]; const auto& nearestA=passage.controlPoints[static_cast<std::size_t>(bestIndex)];
const auto& nearestB=passage.controlPoints[static_cast<std::size_t>(bestIndex+1)]; const auto& nearestB=passage.controlPoints[static_cast<std::size_t>(bestIndex+1)];
const double nearestRadius=nearestA.radiusSubvoxels+bestT*( const double nearestRadius=nearestA.radiusSubvoxels+bestT*(
@@ -1763,9 +1983,17 @@ public:
static_cast<std::size_t>(bestIndex)); static_cast<std::size_t>(bestIndex));
float carveInputAfterBridge=composed; float carveInputAfterBridge=composed;
if(shortSpanCarve) if(shortSpanCarve)
{
if(l90CandidateProfiled)
{
const bool contained=PassageCandidateBrickContains(l90CandidateBrick,3,
passageIndex,static_cast<std::int32_t>(bestIndex),4);
passageTileQueries->recordL90ProofCheck(5,contained);
}
carveInputAfterBridge=std::min(composed,static_cast<float>( carveInputAfterBridge=std::min(composed,static_cast<float>(
PassageBridgeSignedDistance(passageIndex, PassageBridgeSignedDistance(passageIndex,
static_cast<std::size_t>(bestIndex),X,Y,Z)/Q)); static_cast<std::size_t>(bestIndex),X,Y,Z)/Q));
}
const double maximumWallReliefOffset=0.21*qPerMetre*wallReliefWeight; const double maximumWallReliefOffset=0.21*qPerMetre*wallReliefWeight;
const double maximumMouthFloorReliefOffset=upperMouthFloorFade const double maximumMouthFloorReliefOffset=upperMouthFloorFade
?0.20*qPerMetre*mouthFloorBlend*floorReliefWeight:0.0; ?0.20*qPerMetre*mouthFloorBlend*floorReliefWeight:0.0;
@@ -1846,6 +2074,7 @@ public:
const float composedAfterCarve=composed; const float composedAfterCarve=composed;
const auto considerSupportSegment=[&](std::ptrdiff_t index) const auto considerSupportSegment=[&](std::ptrdiff_t index)
{ {
l90IterationTimer.recordTread();
if(passageProfile)++passageProfile->passageSupportSegmentVisits; if(passageProfile)++passageProfile->passageSupportSegmentVisits;
const auto& segmentA=passage.controlPoints[static_cast<std::size_t>(index)]; const auto& segmentA=passage.controlPoints[static_cast<std::size_t>(index)];
const auto& segmentB=passage.controlPoints[static_cast<std::size_t>(index+1)]; const auto& segmentB=passage.controlPoints[static_cast<std::size_t>(index+1)];
@@ -2151,6 +2380,10 @@ public:
for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index) for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index)
considerSupportSegment(index); considerSupportSegment(index);
} }
if(l90CandidateProfiled&&hasSupportFloor)
passageTileQueries->recordL90ProofCheck(1,
PassageCandidateBrickContains(l90CandidateBrick,1,passageIndex,
static_cast<std::int32_t>(bestSupportSegmentIndex)));
VoxelForgeCore::Linux77Profiler::FScopedPhase l77SupportApplyPhase( VoxelForgeCore::Linux77Profiler::FScopedPhase l77SupportApplyPhase(
VoxelForgeCore::Linux77Profiler::EPhase::Support); VoxelForgeCore::Linux77Profiler::EPhase::Support);
// Keep route treads inside the native rock envelope. Carving may reopen the // Keep route treads inside the native rock envelope. Carving may reopen the
@@ -2169,6 +2402,7 @@ public:
if(passageTileScopeMatches if(passageTileScopeMatches
||(bestSupportUsesSignedDistance&&supportCapsuleProfileAvailable)) ||(bestSupportUsesSignedDistance&&supportCapsuleProfileAvailable))
{ {
l90IterationTimer.recordClearance();
if(passageProfile)++passageProfile->passageSupportCapsuleChecks; if(passageProfile)++passageProfile->passageSupportCapsuleChecks;
capsuleAllowsSupport=!PassagePlayerCapsuleBlocksSupport(passageIndex, capsuleAllowsSupport=!PassagePlayerCapsuleBlocksSupport(passageIndex,
carveBegin,carveEnd,lowerMouthIndex,mouthSearchBegin,mouthSearchEnd, carveBegin,carveEnd,lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,
@@ -2195,6 +2429,7 @@ public:
const bool nativeRestoreCapsuleClear=density<0.0f const bool nativeRestoreCapsuleClear=density<0.0f
&&bestSupportNativeBlend>0.0&&capsuleAllowsSupport &&bestSupportNativeBlend>0.0&&capsuleAllowsSupport
&&([&](){ &&([&](){
l90IterationTimer.recordClearance();
if(passageProfile)++passageProfile->passageSupportCapsuleChecks; if(passageProfile)++passageProfile->passageSupportCapsuleChecks;
return !PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin, return !PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin,
carveEnd,lowerMouthIndex,mouthSearchBegin,mouthSearchEnd, carveEnd,lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,
@@ -2222,6 +2457,7 @@ public:
} }
if(supportApplied) if(supportApplied)
{ {
l90IterationTimer.recordClearance();
if(passageProfile)++passageProfile->passageOtherCapsuleChecks; if(passageProfile)++passageProfile->passageOtherCapsuleChecks;
if(PassageOtherCapsuleBlocksSupport(passageIndex, if(PassageOtherCapsuleBlocksSupport(passageIndex,
bestSupportSegmentIndex,qx,qy,qz,qPerMetre,capsuleClearanceMetres)) bestSupportSegmentIndex,qx,qy,qz,qPerMetre,capsuleClearanceMetres))
@@ -4155,6 +4391,35 @@ private:
std::vector<FPassageLaterBoreCandidate> candidates; std::vector<FPassageLaterBoreCandidate> candidates;
std::vector<std::size_t> capsuleCandidatePassages; std::vector<std::size_t> 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<std::int64_t>{}(key.x);
const std::size_t hy=std::hash<std::int64_t>{}(key.y);
const std::size_t hz=std::hash<std::int64_t>{}(key.z);
return hx^(hy+static_cast<std::size_t>(0x9e3779b97f4a7c15ull)+(hx<<6)+(hx>>2))
^(hz+static_cast<std::size_t>(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<std::vector<FPassageCandidateRef>,4> semantics;
};
struct FPassageProofNode struct FPassageProofNode
{ {
std::array<double,3> minimum{},maximum{}; std::array<double,3> minimum{},maximum{};
@@ -5208,6 +5473,9 @@ private:
void BuildPassageSpatialIndices() void BuildPassageSpatialIndices()
{ {
L90PassageCandidateProgramEnabled=VF_IsL90PassageCandidateCensusEnabled();
L90PassageCandidateBricks.clear();
L90PassageCandidateProgramValid=false;
BuildStampSpatialIndex(); BuildStampSpatialIndex();
BuildCavernSpatialIndex(); BuildCavernSpatialIndex();
PassageTileFeatures.clear();PassageFeatureBounds.clear(); PassageTileFeatures.clear();PassageFeatureBounds.clear();
@@ -5584,6 +5852,408 @@ private:
||feature.minimumSubvoxels[axis]>feature.maximumSubvoxels[axis]; ||feature.minimumSubvoxels[axis]>feature.maximumSubvoxels[axis];
if(!PassageFeatureOrder.empty())BuildPassageFeatureNode(0,PassageFeatureOrder.size()); if(!PassageFeatureOrder.empty())BuildPassageFeatureNode(0,PassageFeatureOrder.size());
if(!PassageProofBounds.empty())BuildPassageProofNode(0,PassageProofBounds.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::int64_t>(std::floor(qx/brickEdgeSubvoxels)),
static_cast<std::int64_t>(std::floor(qy/brickEdgeSubvoxels)),
static_cast<std::int64_t>(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<b.passageIndex;
if(a.featureKind!=b.featureKind)return a.featureKind<b.featureKind;
return a.segmentIndex<b.segmentIndex;
}
bool PassageCandidateBrickContains(const FPassageCandidateBrick* brick,
std::size_t semantic,std::size_t passageIndex,std::int32_t segmentIndex,
std::uint8_t featureKind=0) const
{
if(!brick||semantic>=brick->semantics.size()
||passageIndex>std::numeric_limits<std::uint32_t>::max())return false;
const FPassageCandidateRef query{static_cast<std::uint32_t>(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<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,
ResolvedLayout->coreVoxelSizeMicrometres);
const double voxelsPerMetre=qPerMetre/Q;
L90PassageCandidateBrickEdgeSubvoxels=4.0*qPerMetre;
const double reachSubvoxels=L90PassageCandidateReachMetres*qPerMetre;
const double cubicPadding=1.0;
std::array<std::uint64_t,4> emittedReferences{};
const auto emit=[&](std::size_t semantic,const std::array<double,3>& minimum,
const std::array<double,3>& 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<long double>(minimum[axis])
/L90PassageCandidateBrickEdgeSubvoxels);
const long double cellMaximum=std::floor(static_cast<long double>(maximum[axis])
/L90PassageCandidateBrickEdgeSubvoxels);
if(cellMinimum<static_cast<long double>(std::numeric_limits<std::int64_t>::min())
||cellMinimum>static_cast<long double>(std::numeric_limits<std::int64_t>::max())
||cellMaximum<static_cast<long double>(std::numeric_limits<std::int64_t>::min())
||cellMaximum>static_cast<long double>(std::numeric_limits<std::int64_t>::max()))
{++L90PassageCandidateBuildFailures;return;}
const auto lo=static_cast<std::int64_t>(cellMinimum);
const auto hi=static_cast<std::int64_t>(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<double,3>& centerMinimum,
const std::array<double,3>& centerMaximum,double extentX,double extentY,
double extentBelow,double extentAbove)
{
return std::pair<std::array<double,3>,std::array<double,3>>{
{centerMinimum[0]-extentX,centerMinimum[1]-extentY,
centerMinimum[2]-extentBelow},
{centerMaximum[0]+extentX,centerMaximum[1]+extentY,
centerMaximum[2]+extentAbove}};
};
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();
++passageIndex)
{
const auto& passage=ResolvedLayout->passages[passageIndex];
if(passage.controlPoints.size()<2)continue;
const std::ptrdiff_t lastIndex=static_cast<std::ptrdiff_t>(
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<double,3> result{};
const auto& point=passage.controlPoints[static_cast<std::size_t>(index)];
for(std::size_t axis=0;axis<3;++axis)
result[axis]=static_cast<double>(point.positionSubvoxels[axis]);
return result;
};
for(std::size_t segment=0;segment+1<passage.controlPoints.size();++segment)
{
const auto& a=passage.controlPoints[segment];
const auto& b=passage.controlPoints[segment+1];
const double dx=static_cast<double>(b.positionSubvoxels[0])-a.positionSubvoxels[0];
const double dy=static_cast<double>(b.positionSubvoxels[1])-a.positionSubvoxels[1];
const double dz=static_cast<double>(b.positionSubvoxels[2])-a.positionSubvoxels[2];
const double length=DeterministicSqrt(dx*dx+dy*dy+dz*dz);
const bool smoothCarve=static_cast<std::ptrdiff_t>(segment)>2
&&static_cast<std::ptrdiff_t>(segment)
+(constants.internalChainPassage?2:12)<lowerMouthIndex
&&length<=8.0*qPerMetre
&&!IsPassageShortSpanSegment(passageIndex,segment-1)
&&!IsPassageShortSpanSegment(passageIndex,segment)
&&!IsPassageShortSpanSegment(passageIndex,segment+1);
const auto p0=pointArray(smoothCarve
?static_cast<std::ptrdiff_t>(segment)-1
:static_cast<std::ptrdiff_t>(segment));
const auto p1=pointArray(static_cast<std::ptrdiff_t>(segment));
const auto p2=pointArray(static_cast<std::ptrdiff_t>(segment)+1);
const auto p3=pointArray(smoothCarve
?static_cast<std::ptrdiff_t>(segment)+2
:static_cast<std::ptrdiff_t>(segment)+1);
const auto supportP0=pointArray(segment>0
?static_cast<std::ptrdiff_t>(segment)-1
:static_cast<std::ptrdiff_t>(segment));
const auto supportP3=pointArray(segment+2<passage.controlPoints.size()
?static_cast<std::ptrdiff_t>(segment)+2
:static_cast<std::ptrdiff_t>(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<double,double>{low-cubicPadding,high+cubicPadding};
};
std::array<double,3> 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<double>(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<std::uint32_t>(passageIndex),
static_cast<std::int32_t>(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<double>(b.radiusSubvoxels)
-static_cast<double>(a.radiusSubvoxels))+cubicPadding;
std::array<double,3> 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<double,3> 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<double,3> 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<double,3> 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<std::ptrdiff_t>(segment);
const bool mouthCarve=index<std::min<std::ptrdiff_t>(8,lowerMouthIndex)
||(index>=constants.mouthSearchBegin&&index<constants.mouthSearchEnd)
||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<std::uint32_t>(passageIndex),-1,0});
if(passageIndex<PassageMouthSupportRoutes.size())
{
const auto& route=PassageMouthSupportRoutes[passageIndex];
for(std::size_t segment=0;segment<route.size();++segment)
{
const auto& source=route[segment];
emit(3,
{std::min(source.a[0],source.b[0])-1.01*qPerMetre,
std::min(source.a[1],source.b[1])-1.01*qPerMetre,
std::min(source.a[2],source.b[2])-2.58*qPerMetre},
{std::max(source.a[0],source.b[0])+1.01*qPerMetre,
std::max(source.a[1],source.b[1])+1.01*qPerMetre,
std::max(source.a[2],source.b[2])-0.68*qPerMetre},
{static_cast<std::uint32_t>(passageIndex),
static_cast<std::int32_t>(segment),2});
}
}
}
for(std::size_t crossingIndex=0;crossingIndex<PassageVoidCrossings.size();
++crossingIndex)
{
const auto& crossing=PassageVoidCrossings[crossingIndex];
if(crossing.passageIndex>=ResolvedLayout->passages.size())continue;
if(crossing.needsColumn)
{
const double radius=static_cast<double>(crossing.maximumRadiusSubvoxels)
+reachSubvoxels;
emit(3,{crossing.centerXSubvoxels-radius,
crossing.centerYSubvoxels-radius,
static_cast<double>(crossing.bottomZSubvoxels)-reachSubvoxels},
{crossing.centerXSubvoxels+radius,
crossing.centerYSubvoxels+radius,
static_cast<double>(crossing.topZSubvoxels)+reachSubvoxels},
{static_cast<std::uint32_t>(crossing.passageIndex),
static_cast<std::int32_t>(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<double>(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<std::uint32_t>(crossing.passageIndex),
static_cast<std::int32_t>(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<std::uint64_t,4> uniqueReferences{};
std::size_t listCapacityBytes=0;
for(const auto& [key,brick]:L90PassageCandidateBricks)
{
(void)key;
for(std::size_t semantic=0;semantic<brick.semantics.size();++semantic)
{
uniqueReferences[semantic]+=brick.semantics[semantic].size();
listCapacityBytes+=brick.semantics[semantic].capacity()
*sizeof(FPassageCandidateRef);
}
}
const std::size_t estimatedBytes=L90PassageCandidateBricks.bucket_count()
*sizeof(void*)+L90PassageCandidateBricks.size()
*(sizeof(FPassageCandidateBrickKey)+sizeof(FPassageCandidateBrick)
+2*sizeof(void*))+listCapacityBytes;
L90PassageCandidateProgramValid=L90PassageCandidateBuildFailures==0;
const double buildMilliseconds=std::chrono::duration<double,std::milli>(
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<unsigned long long>(emittedReferences[0]),
static_cast<unsigned long long>(emittedReferences[1]),
static_cast<unsigned long long>(emittedReferences[2]),
static_cast<unsigned long long>(emittedReferences[3]),
static_cast<unsigned long long>(uniqueReferences[0]),
static_cast<unsigned long long>(uniqueReferences[1]),
static_cast<unsigned long long>(uniqueReferences[2]),
static_cast<unsigned long long>(uniqueReferences[3]),
estimatedBytes,buildMilliseconds,
static_cast<unsigned long long>(L90PassageCandidateBuildFailures),
static_cast<int>(L90PassageCandidateProgramValid));
} }
double PassageNearestNodeLowerBound(const FPassageNearestNode& node, double PassageNearestNodeLowerBound(const FPassageNearestNode& node,
@@ -5854,6 +6524,12 @@ private:
+vertical*vertical)-0.34*qPerMetre; +vertical*vertical)-0.34*qPerMetre;
if(distance<minimumClearanceMetres*qPerMetre) if(distance<minimumClearanceMetres*qPerMetre)
{ {
const auto* l90TileQueries=ActivePassageTileQueries;
if(l90TileQueries&&l90TileQueries->matches(*this)
&&l90TileQueries->candidateCensusProfiled())
l90TileQueries->recordL90ProofCheck(2,
PassageCandidateBrickContains(ActivePassageCandidateBrick,2,
passageIndex,static_cast<std::int32_t>(index)));
return true; return true;
} }
} }
@@ -12751,4 +13427,12 @@ private:
std::vector<FPassageProofNode> PassageProofNodes; std::vector<FPassageProofNode> PassageProofNodes;
std::vector<std::vector<std::uint8_t>> PassageColumnSegmentFlags,PassageShortSpanSegmentFlags; std::vector<std::vector<std::uint8_t>> PassageColumnSegmentFlags,PassageShortSpanSegmentFlags;
std::vector<std::uint8_t> PassageHasShortSpanFlags; std::vector<std::uint8_t> PassageHasShortSpanFlags;
bool L90PassageCandidateProgramEnabled=false;
bool L90PassageCandidateProgramValid=false;
double L90PassageCandidateBrickEdgeSubvoxels=0.0;
double L90PassageCandidateReachMetres=0.0;
std::uint64_t L90PassageCandidateBuildFailures=0;
std::unordered_map<FPassageCandidateBrickKey,FPassageCandidateBrick,
FPassageCandidateBrickKeyHash> L90PassageCandidateBricks;
inline static thread_local const FPassageCandidateBrick* ActivePassageCandidateBrick=nullptr;
}; };
+1 -1
View File
@@ -2794,7 +2794,7 @@ namespace VoxelForgeCore
// queryMinimum/queryMaximum ci-dessus couvrent exactement ce domaine inclusif. // queryMinimum/queryMaximum ci-dessus couvrent exactement ce domaine inclusif.
const FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope( const FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope(
tileShape,queryMinimum,queryMaximum, tileShape,queryMinimum,queryMaximum,
options.profilePassageComposer?&out.diagnostics:nullptr); options.profilePassageComposer?&out.diagnostics:nullptr,key.lod);
if(options.profilePassageComposer) if(options.profilePassageComposer)
{ {
out.diagnostics.passageComposerProfiled=true; out.diagnostics.passageComposerProfiled=true;
@@ -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.
@@ -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.
@@ -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())