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 <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;
};
+1 -1
View File
@@ -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;