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:
1 parent
04cdd197fb
commit
a7a03d42b4
5 files changed
+948
-5
No files matched your search
@@ -20,6 +20,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -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<std::int64_t,3>& minimumSubvoxels,
|
||||
const std::array<std::int64_t,3>& 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<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;
|
||||
|
||||
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<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();}
|
||||
const std::vector<std::uint32_t>& 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<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,
|
||||
@@ -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<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
|
||||
&&!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<std::int32_t>(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::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;
|
||||
++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;
|
||||
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.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 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<float>(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<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(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<std::ptrdiff_t>(segmentIndex);
|
||||
const auto& a=passage.controlPoints[static_cast<std::size_t>(index)];
|
||||
@@ -1593,6 +1803,16 @@ public:
|
||||
considerCarveSegment(static_cast<std::size_t>(index),3);
|
||||
}
|
||||
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& nearestB=passage.controlPoints[static_cast<std::size_t>(bestIndex+1)];
|
||||
const double nearestRadius=nearestA.radiusSubvoxels+bestT*(
|
||||
@@ -1763,9 +1983,17 @@ public:
|
||||
static_cast<std::size_t>(bestIndex));
|
||||
float carveInputAfterBridge=composed;
|
||||
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>(
|
||||
PassageBridgeSignedDistance(passageIndex,
|
||||
static_cast<std::size_t>(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<std::size_t>(index)];
|
||||
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)
|
||||
considerSupportSegment(index);
|
||||
}
|
||||
if(l90CandidateProfiled&&hasSupportFloor)
|
||||
passageTileQueries->recordL90ProofCheck(1,
|
||||
PassageCandidateBrickContains(l90CandidateBrick,1,passageIndex,
|
||||
static_cast<std::int32_t>(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<FPassageLaterBoreCandidate> candidates;
|
||||
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
|
||||
{
|
||||
std::array<double,3> 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::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,
|
||||
@@ -5854,6 +6524,12 @@ private:
|
||||
+vertical*vertical)-0.34*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;
|
||||
}
|
||||
}
|
||||
@@ -12751,4 +13427,12 @@ private:
|
||||
std::vector<FPassageProofNode> PassageProofNodes;
|
||||
std::vector<std::vector<std::uint8_t>> PassageColumnSegmentFlags,PassageShortSpanSegmentFlags;
|
||||
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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
Reference in new issue
Block a user