diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 2313ad8..79ec2ad 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -238,11 +238,11 @@ namespace GVoxelForgeStreamingResultBacklogCap, TEXT("Maximum queued results plus admitted tile tasks; 0 disables the safety cap. This is separate from the worker-slot cap.")); - int32 GVoxelForgeStreamingReadyQueueDepth = 8; + int32 GVoxelForgeStreamingReadyQueueDepth = 16; FAutoConsoleVariableRef CVarVoxelForgeStreamingReadyQueueDepth( TEXT("voxel.StreamingReadyQueueDepth"), GVoxelForgeStreamingReadyQueueDepth, - TEXT("Extra admitted tile tasks kept ready beyond the worker slots, up to one worker-set; 0 disables the refill queue.")); + TEXT("Extra admitted tile tasks kept ready beyond worker slots, up to four worker-sets/64 tasks; 0 disables the refill queue.")); int32 GVoxelForgeStreamingBackgroundWorkerCap = 1; FAutoConsoleVariableRef CVarVoxelForgeStreamingBackgroundWorkerCap( @@ -312,8 +312,8 @@ namespace FAutoConsoleVariableRef CVarVoxelForgeStreamingStartupApplyBoost( TEXT("voxel.StreamingStartupApplyBoost"), GVoxelForgeStreamingStartupApplyBoost, - TEXT("Bounded cold-view upload budget (default on): 1 allows a 14 ms / 12-mesh budget until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget.")); - constexpr int32 StreamingStartupMaxMeshApplies = 12; + TEXT("Bounded cold-view upload budget (default on): 1 allows a 14 ms / 24-mesh cap until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget.")); + constexpr int32 StreamingStartupMaxMeshApplies = 24; constexpr float StreamingStartupApplyBudgetMilliseconds = 14.0f; constexpr double StreamingStartupApplyMaxSeconds = 30.0; @@ -2207,7 +2207,7 @@ void AVoxelWorld::BeginPlay() CommandLineStreamingReadyQueueDepth)) { GVoxelForgeStreamingReadyQueueDepth = FMath::Clamp( - CommandLineStreamingReadyQueueDepth, 0, 32); + CommandLineStreamingReadyQueueDepth, 0, 64); } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingBackgroundWorkerCap="), CommandLineStreamingBackgroundWorkerCap)) @@ -2634,7 +2634,8 @@ void AVoxelWorld::BeginPlay() FTaskGraphInterface& TaskGraph = FTaskGraphInterface::Get(); LowLevelTasks::FScheduler& TaskScheduler = LowLevelTasks::FScheduler::Get(); const int32 QueueDepth = FMath::Clamp( - GVoxelForgeStreamingReadyQueueDepth, 0, GetMaxConcurrentStreamingTasks()); + GVoxelForgeStreamingReadyQueueDepth, 0, + FMath::Min(64, FMath::Max(1, GetMaxConcurrentStreamingTasks()) * 4)); VoxelForgeStartupTrace::RecordEvent(TEXT("worker_backend_config"), FString::Printf( TEXT("\"logical_cores\":%d,\"effective_tile_slots\":%d," "\"task_api\":\"UE::Tasks::Launch\",\"task_scheduler_workers\":%u," @@ -11873,11 +11874,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw } } const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); - // Keep at most one worker-set of ready work so an admission pass can feed idle slots - // without letting far work consume the near reserve. / Garder une file d'un ensemble - // de workers pour nourrir les slots sans consommer la reserve proche. + // Keep several bounded worker-sets ready so frame-paced admission can feed the + // background lane continuously without moving mesh application onto the game thread. const int32 ReadyQueueDepth = FMath::Clamp( - GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks); + GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4)); const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth; const int32 AuthoredNearTaskReserve = GetNearTaskReserve(MaxTasks); const FIntVector CenterChunk = GetTileCoordAtLocalCm(WorldToLocalCm(CenterPosition)); @@ -12787,7 +12787,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); // Core and worker lane clamped. / Core et voie worker plafonnes. const int32 ReadyQueueDepth = FMath::Clamp( - GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks); + GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4)); const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth; const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon; const int32 NearReserve = EffectiveNearTaskReserve >= 0 @@ -13130,7 +13130,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); const int32 ReadyQueueDepth = FMath::Clamp( - GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks); + GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4)); const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth; const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon; const int32 NearReserve = EffectiveNearTaskReserve >= 0 diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 4308313..efe4b08 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -131,6 +131,31 @@ namespace VoxelForgeCore std::uint64_t mixedBoxes = 0; std::uint64_t unknownBoxes = 0; std::uint64_t fieldCalls = 0; + // Opt-in composer diagnostics. These stay zero during ordinary generation. + bool passageComposerProfiled = false; + std::uint64_t passageTileFeatureCount = 0; + std::uint64_t passageTileRouteSegmentCandidates = 0; + std::uint64_t passageTileMouthSegmentCandidates = 0; + std::uint64_t passageComposerQueries = 0; + std::uint64_t passageIterations = 0; + std::uint64_t passageUpperMouthFilterChecks = 0; + std::uint64_t passageColumnCandidates = 0; + std::uint64_t passageColumnDistanceCalls = 0; + std::uint64_t passageNegativeBoundChecks = 0; + std::uint64_t passageNegativeBoundRejects = 0; + std::uint64_t passagePositiveBoundChecks = 0; + std::uint64_t passagePositiveBoundRejects = 0; + std::uint64_t passageCarveSegmentVisits = 0; + std::uint64_t passageSmoothCarveJoins = 0; + std::uint64_t passageSupportSegmentVisits = 0; + std::uint64_t passageSmoothSupportJoins = 0; + std::uint64_t passageSupportCapsuleChecks = 0; + std::uint64_t passageLaterBoreChecks = 0; + std::uint64_t passageOtherCapsuleChecks = 0; + std::uint64_t passageMouthSupportCalls = 0; + std::uint64_t passageMouthRouteSegmentChecks = 0; + std::uint64_t passageReliefFieldCalls = 0; + std::uint64_t passageSmoothstepEvaluations = 0; std::uint64_t floatFieldCalls = 0; std::uint64_t latticeFieldCalls = 0; std::uint64_t unclampedFieldCalls = 0; @@ -1745,6 +1770,8 @@ namespace VoxelForgeCore // Separate cold pass only; samples the MC inner stages without changing output. // Passe froide separee seulement; mesure les etapes internes MC sans changer la sortie. bool profileMesherStages = false; + // Research-only: count passage-composer work per generated tile. + bool profilePassageComposer = false; // Research-only: collect tile-drain kernel counters and floating-island cache metrics. // Inactive on ordinary generation so the hot path only pays the option check. bool instrumentTileDrain = false; diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index b2fc279..eaf3afd 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -172,11 +172,19 @@ public: class FScopedPassageTileQueries { public: + struct FCandidateSegment + { + std::size_t segmentIndex=0; + std::array minimumSubvoxels{},maximumSubvoxels{}; + }; + FScopedPassageTileQueries(const FStandaloneShapeWorld& shape, const std::array& minimumSubvoxels, - const std::array& maximumSubvoxels) + const std::array& maximumSubvoxels, + VoxelForgeCore::TileGenerationStats* profileStats=nullptr) :shape_(&shape),minimumSubvoxels_(minimumSubvoxels), - maximumSubvoxels_(maximumSubvoxels),previous_(ActivePassageTileQueries) + maximumSubvoxels_(maximumSubvoxels),previous_(ActivePassageTileQueries), + profileStats_(profileStats) { mayContainResolvedBoundaryBand_=shape.ResolvedBoundaryBandsIntersectSubvoxelRange( minimumSubvoxels_[2],maximumSubvoxels_[2]); @@ -190,6 +198,7 @@ public: passageRoutes_.assign(shape.ResolvedLayout?shape.ResolvedLayout->passages.size():0,0); passageColumns_=passageRoutes_; passageMouthSupports_=passageRoutes_; + passageMouthSupportSegments_.resize(passageRoutes_.size()); for(const auto& crossing:shape.PassageVoidCrossings) if(crossing.needsColumn &&crossing.bottomZSubvoxels<=maximumSubvoxels_[2] @@ -200,14 +209,27 @@ public: if(featureIndex>=shape.PassageTileFeatures.size())continue; const auto& feature=shape.PassageTileFeatures[featureIndex]; if(feature.passageIndex>=passageRoutes_.size())continue; + const auto& featureBounds=shape.PassageFeatureBounds[featureIndex]; + const FCandidateSegment candidate{feature.segmentIndex, + featureBounds.minimumSubvoxels,featureBounds.maximumSubvoxels}; if(feature.bColumn)passageColumns_[feature.passageIndex]=1; - else if(feature.bMouthSupport)passageMouthSupports_[feature.passageIndex]=1; + else if(feature.bMouthSupport) + { + passageMouthSupports_[feature.passageIndex]=1; + passageMouthSupportSegments_[feature.passageIndex].push_back(candidate); + } else { passageRoutes_[feature.passageIndex]=1; + ++routeSegmentCandidateCount_; routeFeatureOverlap_=true; } } + for(auto& values:passageMouthSupportSegments_) + { + std::sort(values.begin(),values.end(),[](const auto& a,const auto& b) + {return a.segmentIndex& mouthSupportSegments(std::size_t passageIndex) const + { + static const std::vector Empty; + return passageIndexResolvedBoundaryBandMayAffectAtZMicrometres(zMicrometres); } std::size_t featureCount() const{return featureIndices_.size();} + std::size_t routeSegmentCandidateCount() const{return routeSegmentCandidateCount_;} + VoxelForgeCore::TileGenerationStats* profileStats() const noexcept + {return profileStats_;} bool hasStampCandidates() const{return !stampIndices_.empty();} const std::vector& stampCandidates() const{return stampIndices_;} bool hasCavernCandidates() const{return !cavernFeatureIndices_.empty();} @@ -248,10 +279,13 @@ public: std::vector stampIndices_; std::vector cavernFeatureIndices_; std::vector passageRoutes_,passageColumns_,passageMouthSupports_; + std::vector> passageMouthSupportSegments_; + std::size_t routeSegmentCandidateCount_=0; bool routeFeatureOverlap_=false,columnZOverlap_=false; bool mayContainResolvedBoundaryBand_=true; bool mayContainTransitionPassageFade_=true; const FScopedPassageTileQueries* previous_; + VoxelForgeCore::TileGenerationStats* profileStats_=nullptr; }; bool IsResolvedPassageColumnSample(std::size_t PassageIndex,std::size_t SegmentIndex, @@ -953,9 +987,13 @@ public: const auto* passageTileQueries=ActivePassageTileQueries; const bool passageTileScopeMatches=passageTileQueries &&passageTileQueries->matches(*this); + auto* passageProfile=passageTileScopeMatches + ?passageTileQueries->profileStats():nullptr; + if(passageProfile)++passageProfile->passageComposerQueries; bool queryNearUpperMouthSupport=false; for(const auto& passage:ResolvedLayout->passages) { + if(passageProfile)++passageProfile->passageUpperMouthFilterChecks; const double dx=qx-static_cast(passage.upperMouthSubvoxels[0]); const double dy=qy-static_cast(passage.upperMouthSubvoxels[1]); const double dz=qz-static_cast(passage.upperMouthSubvoxels[2]); @@ -972,26 +1010,20 @@ public: const double playerCenterAboveFloorQ=1.13*qPerMetre; float composed=density; float deferredSupportDensity=std::numeric_limits::infinity(); - const auto composeRouteMouthSupport=[&](std::int32_t strateIndex, - const std::array& mouth) + const auto composeRouteMouthSupport=[&](std::size_t passageIndex) { // Surface-route treads may reinforce native solid terrain only. if(!(density<0.0f))return; + if(passageIndex>=ResolvedLayout->passages.size() + ||passageIndex>=PassageMouthSupportRoutes.size())return; + const auto& passage=ResolvedLayout->passages[passageIndex]; + const auto& mouth=passage.upperMouthSubvoxels; const double mouthDx=qx-static_cast(mouth[0]); const double mouthDy=qy-static_cast(mouth[1]); const double routeSupportReach=13.0*qPerMetre; if(mouthDx*mouthDx+mouthDy*mouthDy>routeSupportReach*routeSupportReach)return; - const auto stratum=std::find_if(ResolvedLayout->strates.begin(), - ResolvedLayout->strates.end(),[strateIndex](const auto& value) - {return value.strateIndex==strateIndex;}); - if(stratum==ResolvedLayout->strates.end())return; - std::vector route; - route.reserve(stratum->anchors.size()); - for(const auto& anchor:stratum->anchors) - if(anchor.kind=="route_point"&&anchor.values.size()>=3) - route.push_back(&anchor); - if(route.size()<2)return; - const std::size_t lastSegment=route.size()-1; + const auto& route=PassageMouthSupportRoutes[passageIndex]; + if(route.empty())return; constexpr double MouthSupportBlendMetres=0.45; // Route anchors encode the player center at half the authored capsule // height above the tread. Keep the support deck below that same foot @@ -999,18 +1031,21 @@ public: constexpr double RouteCenterAboveFootMetres=0.88; const double mouthBlendWidth=MouthSupportBlendMetres*qPerMetre; double mouthSupportDistance=std::numeric_limits::infinity(); - for(std::size_t index=0;index(b.values[0]-a.values[0]); - const double dy=static_cast(b.values[1]-a.values[1]); + if(index>=route.size())return; + if(passageProfile)++passageProfile->passageMouthRouteSegmentChecks; + const auto& segment=route[index]; + const double dx=static_cast(segment.b[0]-segment.a[0]); + const double dy=static_cast(segment.b[1]-segment.a[1]); const double lengthSq=dx*dx+dy*dy; - const double t=lengthSq>0.0?std::clamp(((qx-a.values[0])*dx - +(qy-a.values[1])*dy)/lengthSq,0.0,1.0):0.0; - const double centerX=a.values[0]+t*dx,centerY=a.values[1]+t*dy; - const double centerZ=a.values[2]+t*(static_cast(b.values[2])-a.values[2]); - const double routeRadius=a.values.size()>=4 - ?a.values[3]+t*(static_cast(b.values[3])-a.values[3]):0.0; + const double t=lengthSq>0.0?std::clamp(((qx-segment.a[0])*dx + +(qy-segment.a[1])*dy)/lengthSq,0.0,1.0):0.0; + const double centerX=segment.a[0]+t*dx,centerY=segment.a[1]+t*dy; + const double centerZ=segment.a[2] + +t*(static_cast(segment.b[2])-segment.a[2]); + const double routeRadius=segment.hasRadius + ?segment.a[3]+t*(static_cast(segment.b[3])-segment.a[3]):0.0; const double halfWidth=std::clamp(routeRadius, 0.42*qPerMetre,0.56*qPerMetre); const double horizontalX=qx-centerX,horizontalY=qy-centerY; @@ -1022,12 +1057,29 @@ public: const double topDistance=qz-floorQ; const double bottomDistance=floorQ-qPerMetre-qz; const double verticalDistance=std::max(topDistance,bottomDistance); - if(sideDistance>mouthBlendWidth||verticalDistance>mouthBlendWidth)continue; + if(sideDistance>mouthBlendWidth||verticalDistance>mouthBlendWidth)return; const double outside=DeterministicSqrt( std::max(sideDistance,0.0)*std::max(sideDistance,0.0) +std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0)); const double inside=std::min(std::max(sideDistance,verticalDistance),0.0); mouthSupportDistance=std::min(mouthSupportDistance,outside+inside); + }; + if(passageTileScopeMatches) + { + for(const auto& candidate:passageTileQueries->mouthSupportSegments(passageIndex)) + { + const bool containsPoint=qx>=candidate.minimumSubvoxels[0] + &&qx<=candidate.maximumSubvoxels[0] + &&qy>=candidate.minimumSubvoxels[1] + &&qy<=candidate.maximumSubvoxels[1] + &&qz>=candidate.minimumSubvoxels[2] + &&qz<=candidate.maximumSubvoxels[2]; + if(containsPoint)evaluateSegment(candidate.segmentIndex); + } + } + else + { + for(std::size_t index=0;indexpassages.size();++passageIndex) { + if(passageProfile)++passageProfile->passageIterations; const auto& passage=ResolvedLayout->passages[passageIndex]; if(passage.controlPoints.size()<2)continue; const auto supportStratum=[&](std::int32_t strateIndex) @@ -1071,6 +1124,7 @@ public: const auto laterPassageMayCarve=[&](std::ptrdiff_t supportSegment, double sampleX,double sampleY,double sampleZ) { + if(passageProfile)++passageProfile->passageLaterBoreChecks; return PassageSupportMayBeCarvedByLaterBore(passageIndex, supportSegment,sampleX,sampleY,sampleZ,0.35*qPerMetre); }; @@ -1086,6 +1140,7 @@ public: { if(!crossing.needsColumn||crossing.passageIndex!=passageIndex ||qz>crossing.topZSubvoxels||qzpassageColumnCandidates; #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().ColumnZOverlapCalls; #endif @@ -1120,6 +1175,7 @@ public: #else if(horizontalSquared>exteriorLimit*exteriorLimit)continue; #endif + if(passageProfile)++passageProfile->passageColumnDistanceCalls; columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z); } else @@ -1127,6 +1183,7 @@ public: #if defined(VF_L30_ENABLE_COST_COUNTERS) ++ThreadClassificationStats().ColumnSignedDistanceCalls; #endif + if(passageProfile)++passageProfile->passageColumnDistanceCalls; columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z); } composed=std::min(composed,static_cast(columnDistance/Q)); @@ -1166,12 +1223,16 @@ public: const std::ptrdiff_t mouthSearchBegin=std::max(0,lowerMouthIndex-8); const std::ptrdiff_t mouthSearchEnd=std::min(lastIndex, lowerMouthIndex+8); - if(density<0.0f&&tilePassageFree - &&!PassageMayAffectDensity(passageIndex,carveBegin,carveEnd, + if(density<0.0f&&tilePassageFree) + { + if(passageProfile)++passageProfile->passageNegativeBoundChecks; + if(!PassageMayAffectDensity(passageIndex,carveBegin,carveEnd, lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz, -static_cast(composed)*Q+0.24*qPerMetre)) - { - continue; + { + if(passageProfile)++passageProfile->passageNegativeBoundRejects; + continue; + } } // For a positive source sample, the tube can only change it within relief reach; // the supported column tread is within 2.25 m of its centerline. A short-span @@ -1181,10 +1242,18 @@ public: // n'a pas cette borne, donc sa requête complète est conservée. const bool canCullPositiveRoute=passageTileScopeMatches&&density>=0.0f &&!PassageHasShortSpanSegments(passageIndex); - if(canCullPositiveRoute&&!PassageMayAffectDensity(passageIndex,carveBegin,carveEnd, - lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz, - std::max(2.25*qPerMetre,-static_cast(composed)*Q - +0.24*qPerMetre)))continue; + if(canCullPositiveRoute) + { + if(passageProfile)++passageProfile->passagePositiveBoundChecks; + if(!PassageMayAffectDensity(passageIndex,carveBegin,carveEnd, + lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz, + std::max(2.25*qPerMetre,-static_cast(composed)*Q + +0.24*qPerMetre))) + { + if(passageProfile)++passageProfile->passagePositiveBoundRejects; + continue; + } + } double bestSignedDistance=std::numeric_limits::infinity(); double bestPlayerCapsuleSignedDistance=std::numeric_limits::infinity(); double bestT=0.0; @@ -1208,10 +1277,12 @@ public: lowerMouthDx*lowerMouthDx+lowerMouthDy*lowerMouthDy +lowerMouthDz*lowerMouthDz)/qPerMetre; const double lowerMouthT=std::clamp((20.0-lowerMouthDistance)/12.0,0.0,1.0); + if(passageProfile)++passageProfile->passageSmoothstepEvaluations; const double lowerMouthBlend=lowerMouthT*lowerMouthT *(3.0-2.0*lowerMouthT); const auto considerCarveSegment=[&](std::size_t segmentIndex,int scanPhase) { + if(passageProfile)++passageProfile->passageCarveSegmentVisits; const std::ptrdiff_t index=static_cast(segmentIndex); const auto& a=passage.controlPoints[static_cast(index)]; const auto& b=passage.controlPoints[static_cast(index+1)]; @@ -1249,6 +1320,7 @@ public: &&!IsPassageShortSpanSegment(passageIndex,segmentIndex+1); if(smoothCarveJoin) { + if(passageProfile)++passageProfile->passageSmoothCarveJoins; const auto& p0=passage.controlPoints[static_cast(index-1)]; const auto& p3=passage.controlPoints[static_cast(index+2)]; constexpr double SmoothJoinWeight=1.0; @@ -1326,12 +1398,11 @@ public: const float mouthSurfaceRelief=VF_FieldSurfaceRelief(static_cast(X)*1.5f, static_cast(Y)*1.5f,static_cast(Z)*1.5f, static_cast(Owner?Owner->Seed:0)^0x4D4F5554u); - const float mouthTerrainRelief=VF_FieldSurfaceRelief(static_cast(X)*1.5f, - static_cast(Y)*1.5f,static_cast(Z)*1.5f, - static_cast(Owner?Owner->Seed:0)^0x4D4F5554u); + const float mouthTerrainRelief=mouthSurfaceRelief; const float mouthTreadFieldRelief=VF_FieldSurfaceRelief(static_cast(X)*0.75f, static_cast(Y)*0.75f,static_cast(Z)*0.75f, static_cast(Owner?Owner->Seed:0)^0x54485244u); + if(passageProfile)passageProfile->passageReliefFieldCalls+=3; const auto routeSegmentLengthMetres=[&](std::ptrdiff_t routeIndex) { const auto& before=passage.controlPoints[static_cast(routeIndex)]; @@ -1381,6 +1452,7 @@ public: for(std::ptrdiff_t routeIndex=0; routeIndex(3,lowerMouthIndex);++routeIndex) { + if(passageProfile)++passageProfile->passageMouthRouteSegmentChecks; const auto& before=passage.controlPoints[static_cast(routeIndex)]; const auto& after=passage.controlPoints[static_cast(routeIndex+1)]; const double ax=static_cast(before.positionSubvoxels[0]); @@ -1402,6 +1474,7 @@ public: static_cast((az+t*dz)/qPerMetre*1.5), static_cast(Owner?Owner->Seed:0)^0x4D4F5554u)), -1.0,1.0); + if(passageProfile)++passageProfile->passageReliefFieldCalls; const double lateralOffset=pathRelief*0.18*qPerMetre; projectedX-=dy/horizontalLength*lateralOffset; projectedY+=dx/horizontalLength*lateralOffset; @@ -1437,10 +1510,12 @@ public: if(std::isfinite(distanceFromOpenMouth)) { const double fade=std::clamp(distanceFromOpenMouth/11.0,0.0,1.0); + if(passageProfile)++passageProfile->passageSmoothstepEvaluations; const double smooth=fade*fade*(3.0-2.0*fade); mouthFloorBlend=1.0-smooth; } const double floorReliefT=std::clamp((verticalWallAlignment-0.55)/0.45,0.0,1.0); + if(passageProfile)++passageProfile->passageSmoothstepEvaluations; const double floorReliefWeight=floorReliefT*floorReliefT *(3.0-2.0*floorReliefT); const double floorReliefOffset=std::clamp( @@ -1466,6 +1541,7 @@ public: constexpr double mouthCarveFadeMetres=8.0; mouthCarveFade=std::clamp(distanceFromOpenMouth/mouthCarveFadeMetres, 0.0,1.0); + if(passageProfile)++passageProfile->passageSmoothstepEvaluations; mouthCarveFade=mouthCarveFade*mouthCarveFade*(3.0-2.0*mouthCarveFade); } const double floorAlignment=std::max(verticalWallAlignment, @@ -1477,6 +1553,7 @@ public: const double belowFloor=(mouthEntryFloorQ-qz)/qPerMetre; const double supportRamp=std::clamp(belowFloor/0.15,0.0,1.0); const double supportFade=std::clamp((1.0-belowFloor)/0.35,0.0,1.0); + if(passageProfile)passageProfile->passageSmoothstepEvaluations+=2; const double rampSmooth=supportRamp*supportRamp*(3.0-2.0*supportRamp); const double fadeSmooth=supportFade*supportFade*(3.0-2.0*supportFade); floorBelowWeight=rampSmooth*fadeSmooth; @@ -1485,6 +1562,7 @@ public: // former bore-wide alignment feather left a straight platform-width // bridge across the FI mouth. double mouthWalkLaneT=std::clamp((1.05-mouthEntryPlanDistance)/0.60,0.0,1.0); + if(passageProfile)++passageProfile->passageSmoothstepEvaluations; mouthWalkLaneT=mouthWalkLaneT*mouthWalkLaneT*(3.0-2.0*mouthWalkLaneT); const double mouthWalkLaneWeight=upperMouthFloorFade?mouthWalkLaneT:0.0; const double floorCarveWeight=1.0-(1.0-mouthCarveFade) @@ -1496,6 +1574,7 @@ public: const float composedAfterCarve=composed; const auto considerSupportSegment=[&](std::ptrdiff_t index) { + if(passageProfile)++passageProfile->passageSupportSegmentVisits; const auto& segmentA=passage.controlPoints[static_cast(index)]; const auto& segmentB=passage.controlPoints[static_cast(index+1)]; const double dx=static_cast(segmentB.positionSubvoxels[0])-segmentA.positionSubvoxels[0]; @@ -1538,6 +1617,7 @@ public: static_cast(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels); if(smoothSupportJoin) { + if(passageProfile)++passageProfile->passageSmoothSupportJoins; const auto& p0=passage.controlPoints[static_cast(index-1)]; const auto& p3=passage.controlPoints[static_cast(index+2)]; const double t2=supportT*supportT,t3=t2*supportT; @@ -1568,6 +1648,7 @@ public: static_cast(centerZ/qPerMetre*1.5), static_cast(Owner?Owner->Seed:0)^0x4D4F5554u)), -1.0,1.0); + if(passageProfile)++passageProfile->passageReliefFieldCalls; treadRelief=pathRelief; const double lateralOffset=pathRelief*0.18*qPerMetre; centerX-=dy/horizontalLength*lateralOffset; @@ -1660,8 +1741,10 @@ public: suffixMetres+(1.0-supportT)*supportSegmentLength); } double mouthReliefT=std::clamp(distanceFromMouthMetres/8.0,0.0,1.0); + if(passageProfile)++passageProfile->passageSmoothstepEvaluations; mouthReliefT=mouthReliefT*mouthReliefT*(3.0-2.0*mouthReliefT); double mouthSupportT=std::clamp((distanceFromMouthMetres-0.25)/4.0,0.0,1.0); + if(passageProfile)++passageProfile->passageSmoothstepEvaluations; mouthSupportT=mouthSupportT*mouthSupportT*(3.0-2.0*mouthSupportT); const double mouthSupportWidth=std::lerp(0.82,1.00,mouthSupportT); const double supportHalfWidthMetres=mouthTread?mouthSupportWidth @@ -1687,6 +1770,7 @@ public: ?std::clamp((horizontalDistance -(supportHalfWidth-supportEdgeBlendMetres*qPerMetre)) /(supportEdgeBlendMetres*qPerMetre),0.0,1.0):0.0; + if(passageProfile)++passageProfile->passageSmoothstepEvaluations; const double supportEdgeSmooth=supportEdgeT*supportEdgeT *(3.0-2.0*supportEdgeT); const double mouthSupportThicknessVoxels=std::lerp( @@ -1700,6 +1784,7 @@ public: { const double mouthBlendT=std::clamp( distanceFromMouthMetres/0.75,0.0,1.0); + if(passageProfile)++passageProfile->passageSmoothstepEvaluations; mouthSupportBlend=0.75+0.25*mouthBlendT*mouthBlendT *(3.0-2.0*mouthBlendT); } @@ -1802,12 +1887,17 @@ public: const double capsuleQueryZ=bestSupportUsesSignedDistance?bestSupportFloorQ:qz; const bool supportCapsuleProfileAvailable=passageIndex=capsuleClearanceMetres*qPerMetre; + { + if(passageProfile)++passageProfile->passageSupportCapsuleChecks; + capsuleAllowsSupport=!PassagePlayerCapsuleBlocksSupport(passageIndex, + carveBegin,carveEnd,lowerMouthIndex,mouthSearchBegin,mouthSearchEnd, + lastIndex,qx,qy,capsuleQueryZ,qPerMetre,capsuleClearanceMetres); + } + else capsuleAllowsSupport= + bestPlayerCapsuleSignedDistance>=capsuleClearanceMetres*qPerMetre; bool supportApplied=false; bool supportNeedsDeferral=false; // Outside bridge and short-span segments, add the tread only where the @@ -1826,9 +1916,12 @@ public: // and noise. The capsule test keeps neighbouring coils open. const bool nativeRestoreCapsuleClear=density<0.0f &&bestSupportNativeBlend>0.0&&capsuleAllowsSupport - &&!PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin, - carveEnd,lowerMouthIndex,mouthSearchBegin,mouthSearchEnd, - lastIndex,qx,qy,qz,qPerMetre,capsuleClearanceMetres); + &&([&](){ + if(passageProfile)++passageProfile->passageSupportCapsuleChecks; + return !PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin, + carveEnd,lowerMouthIndex,mouthSearchBegin,mouthSearchEnd, + lastIndex,qx,qy,qz,qPerMetre,capsuleClearanceMetres); + })(); if(nativeRestoreCapsuleClear) { const double nativeSurfaceDetail=std::clamp( @@ -1851,6 +1944,7 @@ public: } if(supportApplied) { + if(passageProfile)++passageProfile->passageOtherCapsuleChecks; if(PassageOtherCapsuleBlocksSupport(passageIndex, bestSupportSegmentIndex,qx,qy,qz,qPerMetre,capsuleClearanceMetres)) { @@ -1875,8 +1969,10 @@ public: const bool tileHasSupport=passageTileScopeMatches; if(!tileHasSupport ||ActivePassageTileQueries->hasMouthSupport(passageIndex)) - composeRouteMouthSupport(passage.upperStrateIndex, - passage.upperMouthSubvoxels); + { + if(passageProfile)++passageProfile->passageMouthSupportCalls; + composeRouteMouthSupport(passageIndex); + } } composed=std::min(composed,deferredSupportDensity); return composed; @@ -3332,6 +3428,11 @@ private: std::size_t passageIndex=0,segmentIndex=0; bool bColumn=false,bMouthSupport=false; }; + struct FPassageMouthSupportSegment + { + std::array a{},b{}; + bool hasRadius=false; + }; struct FPassageNearestNode { std::array minimum{},maximum{}; @@ -4309,6 +4410,8 @@ private: PassageNearestNodes.clear(); PassageProofBounds.clear();PassageProofNodes.clear(); PassageNearestNodes.resize(ResolvedLayout?ResolvedLayout->passages.size():0); + PassageMouthSupportRoutes.clear(); + PassageMouthSupportRoutes.resize(ResolvedLayout?ResolvedLayout->passages.size():0); PassageLaterBoreSources.clear(); PassageColumnSegmentFlags.clear();PassageShortSpanSegmentFlags.clear(); PassageHasShortSpanFlags.clear(); @@ -4316,6 +4419,7 @@ private: constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); const double voxelsPerMetre=Owner&&Owner->CoreVoxelSizeMicrons>0 ?1000000.0/Owner->CoreVoxelSizeMicrons:4.0; + const double qPerMetre=Q*voxelsPerMetre; const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre; const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre; PassageColumnSegmentFlags.resize(ResolvedLayout->passages.size()); @@ -4367,9 +4471,12 @@ private: { std::array route{}; std::size_t routeCount=0; + std::vector allRoutePoints; + allRoutePoints.reserve(upperStratum->anchors.size()); for(const auto& anchor:upperStratum->anchors) if(anchor.kind=="route_point"&&anchor.values.size()>=3) { + allRoutePoints.push_back(&anchor); if(routeCount=2) { + auto& supportRoutes=PassageMouthSupportRoutes[passageIndex]; + supportRoutes.reserve(allRoutePoints.size()-1); + for(std::size_t index=0;index+1values[axis]; + segment.b[axis]=allRoutePoints[index+1]->values[axis]; + } + segment.hasRadius=allRoutePoints[index]->values.size()>=4 + &&allRoutePoints[index+1]->values.size()>=4; + if(segment.hasRadius) + { + segment.a[3]=allRoutePoints[index]->values[3]; + segment.b[3]=allRoutePoints[index+1]->values[3]; + } + supportRoutes.push_back(segment); + } const double mouthReach=13.0*voxelsPerMetre*Q; double minimumZ=std::numeric_limits::infinity(); double maximumZ=-std::numeric_limits::infinity(); @@ -4395,17 +4521,32 @@ private: supportProof.maximum={(mouthX+mouthReach)/Q,(mouthY+mouthReach)/Q, (maximumZ+3.0*voxelsPerMetre*Q)/Q}; PassageProofBounds.push_back(supportProof); - const std::array featureMin={ - static_cast(std::floor(supportProof.minimum[0]*Q)), - static_cast(std::floor(supportProof.minimum[1]*Q)), - static_cast(std::floor(supportProof.minimum[2]*Q))}; - const std::array featureMax={ - static_cast(std::ceil(supportProof.maximum[0]*Q)), - static_cast(std::ceil(supportProof.maximum[1]*Q)), - static_cast(std::ceil(supportProof.maximum[2]*Q))}; - PassageFeatureBounds.push_back({static_cast(PassageTileFeatures.size()), - featureMin,featureMax}); - PassageTileFeatures.push_back({passageIndex,0,false,true}); + for(std::size_t index=0;index featureMinimum={ + std::min(segment.a[0],segment.b[0])-static_cast( + std::ceil(HorizontalReachMetres*qPerMetre)), + std::min(segment.a[1],segment.b[1])-static_cast( + std::ceil(HorizontalReachMetres*qPerMetre)), + static_cast(std::floor( + std::min(segment.a[2],segment.b[2]) + -MouthSupportBottomOffsetMetres*qPerMetre))}; + const std::array featureMaximum={ + std::max(segment.a[0],segment.b[0])+static_cast( + std::ceil(HorizontalReachMetres*qPerMetre)), + std::max(segment.a[1],segment.b[1])+static_cast( + std::ceil(HorizontalReachMetres*qPerMetre)), + static_cast(std::ceil( + std::max(segment.a[2],segment.b[2]) + -MouthSupportTopOffsetMetres*qPerMetre))}; + PassageFeatureBounds.push_back({static_cast( + PassageTileFeatures.size()),featureMinimum,featureMaximum}); + PassageTileFeatures.push_back({passageIndex,index,false,true}); + } } } } @@ -4414,7 +4555,6 @@ private: // hot query can test a short list of physical bounds instead of walking a // full spatial tree for every density sample. PassageLaterBoreSources.resize(ResolvedLayout->passages.size()); - const double qPerMetre=Q*voxelsPerMetre; for(std::size_t sourcePassageIndex=0; sourcePassageIndexpassages.size();++sourcePassageIndex) { @@ -11170,6 +11310,7 @@ private: std::vector PassageFeatureNodes; bool PassageFeatureBoundsFailOpen=false; std::vector> PassageNearestNodes; + std::vector> PassageMouthSupportRoutes; std::vector> PassageLaterBoreSources; std::vector PassageProofBounds; std::vector PassageProofNodes; diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index d7f64e9..fb4c62a 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -2620,7 +2620,22 @@ namespace VoxelForgeCore // GenerateMesh n'échantillonne que origin..origin+extent et la marge de normale d'un pas ; // queryMinimum/queryMaximum ci-dessus couvrent exactement ce domaine inclusif. const FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope( - tileShape,queryMinimum,queryMaximum); + tileShape,queryMinimum,queryMaximum, + options.profilePassageComposer?&out.diagnostics:nullptr); + if(options.profilePassageComposer) + { + out.diagnostics.passageComposerProfiled=true; + out.diagnostics.passageTileFeatureCount=passageFeatureScope.featureCount(); + if(impl_->resolvedLayout) + { + out.diagnostics.passageTileRouteSegmentCandidates= + passageFeatureScope.routeSegmentCandidateCount(); + for(std::size_t passageIndex=0; + passageIndexresolvedLayout->passages.size();++passageIndex) + out.diagnostics.passageTileMouthSegmentCandidates+= + passageFeatureScope.mouthSupportSegments(passageIndex).size(); + } + } const bool bDiagnosticColumnSampling=options.diagnosticGlobalOctaveDrop!=0 ||options.diagnosticColumnInterpolationStride>1; const std::uint32_t PreviousOctaveDrop=bDiagnosticColumnSampling diff --git a/Standalone/Research/WorldAssemblerM3/Linux37/AuditMouthShapeL37.py b/Standalone/Research/WorldAssemblerM3/Linux37/AuditMouthShapeL37.py index 6c0f159..1113ab7 100644 --- a/Standalone/Research/WorldAssemblerM3/Linux37/AuditMouthShapeL37.py +++ b/Standalone/Research/WorldAssemblerM3/Linux37/AuditMouthShapeL37.py @@ -4,10 +4,11 @@ import argparse import importlib.util import math from collections import defaultdict, deque +import os from pathlib import Path ROOT = Path(__file__).resolve().parents[4] -BASELINES = ROOT / "Standalone/Tools/verify/baselines" +BASELINES = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")) / "baselines" PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py" SPEC = importlib.util.spec_from_file_location("ring_profile_l37", PROFILE_PATH) PROFILE = importlib.util.module_from_spec(SPEC) diff --git a/Standalone/Research/WorldAssemblerM3/Linux37/CapturePassageStandardSetL37.py b/Standalone/Research/WorldAssemblerM3/Linux37/CapturePassageStandardSetL37.py index c700a7a..08f7663 100644 --- a/Standalone/Research/WorldAssemblerM3/Linux37/CapturePassageStandardSetL37.py +++ b/Standalone/Research/WorldAssemblerM3/Linux37/CapturePassageStandardSetL37.py @@ -13,9 +13,9 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[4] -BASELINES = ROOT / "Standalone/Tools/verify/baselines" OUT = Path(os.environ.get("VOXELFORGE_L37_OUT", os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37"))) +BASELINES = OUT / "baselines" BIN = OUT / "bin" CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR", OUT / "captures")) diff --git a/Standalone/Research/WorldAssemblerM3/Linux37/RunMouthCreaseAuditL37.py b/Standalone/Research/WorldAssemblerM3/Linux37/RunMouthCreaseAuditL37.py index 42c1c8f..cbf29c6 100644 --- a/Standalone/Research/WorldAssemblerM3/Linux37/RunMouthCreaseAuditL37.py +++ b/Standalone/Research/WorldAssemblerM3/Linux37/RunMouthCreaseAuditL37.py @@ -8,10 +8,11 @@ import json import math import re import argparse +import os from pathlib import Path ROOT = Path(__file__).resolve().parents[4] -BASELINES = ROOT / "Standalone/Tools/verify/baselines" +BASELINES = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")) / "baselines" BASE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37/RunMouthFragmentAuditL37.py" SPEC = importlib.util.spec_from_file_location("mouth_fragment_l37", BASE_PATH) BASE = importlib.util.module_from_spec(SPEC) diff --git a/Standalone/Research/WorldAssemblerM3/Linux37/RunRingAuditsL37.py b/Standalone/Research/WorldAssemblerM3/Linux37/RunRingAuditsL37.py index ae42dad..8594716 100644 --- a/Standalone/Research/WorldAssemblerM3/Linux37/RunRingAuditsL37.py +++ b/Standalone/Research/WorldAssemblerM3/Linux37/RunRingAuditsL37.py @@ -10,8 +10,8 @@ import os from pathlib import Path ROOT = Path(__file__).resolve().parents[4] -BASELINES = ROOT / "Standalone/Tools/verify/baselines" OUT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")) +BASELINES = OUT / "baselines" BIN = Path(os.environ.get("VOXELFORGE_VERIFY_HEADLESS", OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang")) OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA", diff --git a/Standalone/Research/WorldAssemblerM3/Linux47/CaptureLandmarkL47.py b/Standalone/Research/WorldAssemblerM3/Linux47/CaptureLandmarkL47.py index 79d60c4..dea2a1c 100644 --- a/Standalone/Research/WorldAssemblerM3/Linux47/CaptureLandmarkL47.py +++ b/Standalone/Research/WorldAssemblerM3/Linux47/CaptureLandmarkL47.py @@ -14,7 +14,7 @@ import time ROOT = Path(__file__).resolve().parents[4] -BASELINES = ROOT / "Standalone/Tools/verify/baselines" +BASELINES = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l47")) / "baselines" VERIFY = ROOT / "Standalone/Tools/verify" LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37" sys.path.insert(0, str(VERIFY)) diff --git a/Standalone/Research/WorldAssemblerM3/Linux47/MeasureEntranceL47.py b/Standalone/Research/WorldAssemblerM3/Linux47/MeasureEntranceL47.py index 920ba72..5dc8e9d 100644 --- a/Standalone/Research/WorldAssemblerM3/Linux47/MeasureEntranceL47.py +++ b/Standalone/Research/WorldAssemblerM3/Linux47/MeasureEntranceL47.py @@ -6,12 +6,13 @@ import importlib.util import argparse import json import math +import os from pathlib import Path import sys ROOT = Path(__file__).resolve().parents[4] -BASELINES = ROOT / "Standalone/Tools/verify/baselines" +BASELINES = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l47")) / "baselines" PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py" spec = importlib.util.spec_from_file_location("ring_profile", PROFILE_PATH) if spec is None or spec.loader is None: diff --git a/Standalone/Research/WorldAssemblerM3/Linux72/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux72/REPORT.md new file mode 100644 index 0000000..30afbba --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux72/REPORT.md @@ -0,0 +1,61 @@ +# Linux 72 report + +## Unreal timeline + +Three L71 and three L72 traces used the same 2,850 tile keys. The readback value `streaming_background_worker_cap=1` selects the TaskGraph background lane; Unreal reported eight background workers and eight effective tile slots. The delay was queue admission and result draining, not RMC construction. Raising ready queue depth from 8 to 16 and the startup apply count cap from 12 to 24 moved readiness forward. The hard game-thread apply budget stays 14 ms, and the moving-player admission cap still prevents a ready backlog during fast travel. + +| Median milestone | L71 | L72 | +|---|---:|---:| +| Last tile requested | 4.577 s | 3.680 s | +| Last worker started | 4.583 s | 3.696 s | +| Last worker finished | 4.597 s | 3.712 s | +| Last tile applied | 4.754 s | 3.724 s | +| Initial set ready | 4.775 s | 3.741 s | +| Walkable gate ready | 1.068 s | 1.107 s | +| Queue p95 | 122.7 ms | 34.8 ms | +| Request-to-apply p95 | 139.1 ms | 45.3 ms | + +The worker-finish-to-last-apply tail fell from 157 ms to 11 ms. L72 per-frame apply work was p95 0.73 ms, max 6.26 ms, below the 14 ms cap; the cap was not hit. Commandlet frame interval p95 was 18.85 ms. Initial-set readiness still takes 3.74 s (about 199 p95 frame intervals), so the roughly 1 s target is not met. Against the 2.41 s headless stride-0 whole-view median, about 1.33 s remains in the Unreal path. RMC build p95 was 0.32 ms. + +Headless whole-view timing used three interleaved before/after pairs per stratum: + +| Stratum | Tiles | Before median | After median | +|---|---:|---:|---:| +| 0 | 2,850 | 2.538 s | 2.412 s | +| 1 | 2,249 | 6.382 s | 6.343 s | +| 2 | 2,233 | 5.718 s | 5.443 s | +| 3 | 2,329 | 2.661 s | 2.624 s | + +## Passage composer + +The L71 40× compares 18.8 ms for nonempty passage tiles with 0.5 ms across all LOD0 tiles; 441 of 512 LOD0 tiles were empty. Empty tiles can stop after classification, while a nonempty passage tile fills and meshes a density grid. It is not a matched per-sample comparison. These profiles do not isolate the passage-specific share of total tile time: passage controls have about twice the samples of the terrain control, and no matched nonempty terrain control was profiled. Native calls were 27,367 for terrain (17,680 samples) and 43,243/44,767 for passage tiles (36,439/37,975 samples). + +Within the composer, route-mouth checks were a large avoidable scan. The shared composer now filters mouth-support segments against per-tile bounds and reuses the identical mouth relief result. Passage carve/support visits and smoothstep counts stayed the same. The exact counters are below; they show reduced work, but do not identify the dominant timed composer term. + +| Tile (LOD0) | Samples | Native calls before/after | Mouth route checks before → after | Relief calls before → after | +|---|---:|---:|---:|---:| +| Terrain `(5,-2,-47)` | 17,680 | 27,367 | — | — | +| Passage `(1,0,-47)` | 36,439 | 43,243 | 612,242 → 123,090 | 384,444 → 343,414 | +| Passage `(0,1,-47)` | 37,975 | 44,767 | 1,042,612 → 131,334 | 407,910 → 364,554 | + +Field calls and world hashes are unchanged. The two passage samples reduced route checks by 80% and 87%. A single instrumented worker-time comparison was mixed (100.0→91.8 ms and 155.2→157.1 ms), so it does not establish a stable per-tile wall-time gain. L71 identified near-field density-grid sampling and meshing as the largest cost; L72 does not isolate which composer term takes the most time. + +## Integrity and housekeeping + +- Hashes matched before/after at W1, W8, and W12 with Clang and GCC: all 2,850 keys and density, mesh, payload, and topology hashes matched. +- Fast tier passed in 270.2 s (269 PASS, 5 DIAGNOSTIC); gates and thresholds are unchanged. +- Compressed all 11 verification meshes with Python `lzma`: 78.54 MB → 18.37 MB (23.4% of original). `verify.py` expands them into its output baselines folder before use. + +## Source changes + +- `Source/VoxelForge/Private/VoxelWorld.cpp`: queue depth and startup apply count tuning, retaining the 14 ms hard budget and moving-player admission behavior. +- `Standalone/Core/src/ShapeWorldInternal.h`: tile-local passage support segment filtering and reused mouth relief evaluation. +- `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/VoxelForgeCore.cpp`, `Standalone/Source/StreamingWorld.h`, `Standalone/Source/StreamBenchmarkPipeline.h`, `Standalone/Source/StreamBenchmarkHeadless.cpp`: opt-in per-tile composer counters. +- `Standalone/Tools/verify/verify.py` and the Linux37/Linux47 audit scripts: decompressed baseline path handling. + +## Failed and rejected runs + +- The first Unreal build referenced unavailable .NET 8.0.300; retry with installed 8.0.412 succeeded. +- An initial hash invocation omitted `--stream-hash-tiles` and emitted no hash rows; corrected six-config matrix passed. +- The first fast-tier attempt lacked built executables, then an audit helper still read the compressed baseline path. Both were corrected; the rerun passed. +- A no-feature passage cull changed density hashes in 433 tiles and was reverted. A point-bound trial rejected no negative LOD0 samples, and a carve-seed trial added one candidate visit per query; both were dropped. A depth-32 queue probe worsened LOD0 request-to-apply p95 (102.6 ms) and walkable readiness (1.204 s), so depth 16 was retained. diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 932c986..83235de 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -46,6 +46,7 @@ namespace bool ScreenSpaceGate=false; bool AuditUniformBlockProofs=false; bool ProfileLodWork=false; + bool ProfilePassageComposer=false; bool InstrumentTileDrain=false; bool ExactMidpointDensityMemo=false; bool ActiveLaneBatching=false; @@ -1439,6 +1440,7 @@ namespace " --audit-uniform-block-proofs (fill and check every interval-certified block)\n" " --profile-lod-work (report per-LOD density sample counts)\n" " --instrument-tile-drain (opt-in CpuTileTimeline and per-tile stages/counters)\n" + " --profile-passage-composer (emit per-tile passage work counters)\n" " --exact-midpoint-density-memo (exact-input midpoint query memo A/B)\n" " --active-lane-batching --proof-block-cells=N (proof-masked density fill A/B)\n" " --parallel-sign-edge-repair --serial-sign-edge-repair\n" @@ -1494,6 +1496,8 @@ namespace else if(arg=="--audit-uniform-block-proofs")options.AuditUniformBlockProofs=true; else if(arg=="--profile-lod-work")options.ProfileLodWork=true; else if(arg=="--instrument-tile-drain")options.InstrumentTileDrain=true; + else if(arg=="--profile-passage-composer") + {options.ProfilePassageComposer=true;options.InstrumentTileDrain=true;} else if(arg=="--exact-midpoint-density-memo")options.ExactMidpointDensityMemo=true; else if(arg=="--active-lane-batching")options.ActiveLaneBatching=true; else if(arg=="--parallel-sign-edge-repair") @@ -2214,5 +2218,5 @@ int main(int argc,char** argv) options.ProofBlockCells,options.HasSignEdgeRepairOverride ?options.ParallelSignEdgeRepair:options.FloatingIslandsRecipe, options.AuditNativeLattice,options.LatticeOrphanControl, - options.CaptureSkipWalkability); + options.CaptureSkipWalkability,options.ProfilePassageComposer); } diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 3fa2aec..8e2ed08 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -1872,7 +1872,8 @@ namespace StandaloneStreamBenchmark bool bExactMidpointDensityMemo=false, bool bActiveLaneBatching=false,int32 ProofBlockCells=8, bool bParallelSignEdgeRepair=false,bool bAuditNativeLattice=false, - bool bLatticeOrphanControl=false,bool bCaptureSkipWalkability=false) + bool bLatticeOrphanControl=false,bool bCaptureSkipWalkability=false, + bool bProfilePassageComposer=false) { using Clock=std::chrono::steady_clock; FStandaloneOwnerWorld& World=*OwnerWorld; @@ -2037,6 +2038,7 @@ namespace StandaloneStreamBenchmark FarHorizonLOD8InteriorStepVoxels,bUseSharedColumnCache); Streamer.SetUniformBlockProofAuditEnabled(bAuditUniformBlockProofs); Streamer.SetTileDrainInstrumentationEnabled(bInstrumentTileDrain); + Streamer.SetPassageComposerProfilingEnabled(bProfilePassageComposer); Streamer.SetExactMidpointDensityMemoEnabled(bExactMidpointDensityMemo); Streamer.SetActiveLaneBatchingEnabled(bActiveLaneBatching); Streamer.SetProofBlockCells(ProofBlockCells); diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h index 9be5625..82e7849 100644 --- a/Standalone/Source/StreamingWorld.h +++ b/Standalone/Source/StreamingWorld.h @@ -710,6 +710,8 @@ public: {bProfileMesherStages=Enabled;} void SetTileDrainInstrumentationEnabled(bool Enabled) {bInstrumentTileDrain=Enabled;} + void SetPassageComposerProfilingEnabled(bool Enabled) + {bProfilePassageComposer=Enabled;} void SetExactMidpointDensityMemoEnabled(bool Enabled) {bExactMidpointDensityMemo=Enabled;} void SetActiveLaneBatchingEnabled(bool Enabled) @@ -890,6 +892,7 @@ private: Options.useSharedColumnCache=bSharedColumnCache; Options.lod8HorizonInteriorStepVoxels=Lod8HorizonInteriorStepVoxels; Options.profileMesherStages=(bProfileMesherStages||bInstrumentTileDrain)&&!ForceGenerate; + Options.profilePassageComposer=bProfilePassageComposer; Options.instrumentTileDrain=bInstrumentTileDrain&&!ForceGenerate; Options.exactMidpointDensityMemo=bExactMidpointDensityMemo&&!ForceGenerate; Options.activeLaneBatching=bActiveLaneBatching&&!ForceGenerate; @@ -1100,6 +1103,34 @@ private: static_cast(D.islandSharedCacheComputes), static_cast(D.islandSharedCacheConflictReplacements), static_cast(D.islandSharedCacheLockWaitNanoseconds)/1000.0); + if(D.passageComposerProfiled) + std::printf("[PassageComposerTile] key=(%d,%d,%d,lod%d) worker_ms=%.3f samples=%llu native_field_calls=%llu features=%llu route_candidates=%llu mouth_candidates=%llu queries=%llu passage_iterations=%llu upper_mouth_checks=%llu column_candidates=%llu column_distance_calls=%llu negative_bounds=%llu negative_rejects=%llu positive_bounds=%llu positive_rejects=%llu carve_segment_visits=%llu smooth_carve_joins=%llu support_segment_visits=%llu smooth_support_joins=%llu support_capsule_checks=%llu later_bore_checks=%llu other_capsule_checks=%llu mouth_support_calls=%llu mouth_route_segment_checks=%llu relief_field_calls=%llu smoothstep_evaluations=%llu\n", + Event.Key.Coord.X,Event.Key.Coord.Y,Event.Key.Coord.Z,Event.Key.Level, + Event.WorkerMilliseconds,static_cast(Event.DensitySamples), + static_cast(D.fieldCalls), + static_cast(D.passageTileFeatureCount), + static_cast(D.passageTileRouteSegmentCandidates), + static_cast(D.passageTileMouthSegmentCandidates), + static_cast(D.passageComposerQueries), + static_cast(D.passageIterations), + static_cast(D.passageUpperMouthFilterChecks), + static_cast(D.passageColumnCandidates), + static_cast(D.passageColumnDistanceCalls), + static_cast(D.passageNegativeBoundChecks), + static_cast(D.passageNegativeBoundRejects), + static_cast(D.passagePositiveBoundChecks), + static_cast(D.passagePositiveBoundRejects), + static_cast(D.passageCarveSegmentVisits), + static_cast(D.passageSmoothCarveJoins), + static_cast(D.passageSupportSegmentVisits), + static_cast(D.passageSmoothSupportJoins), + static_cast(D.passageSupportCapsuleChecks), + static_cast(D.passageLaterBoreChecks), + static_cast(D.passageOtherCapsuleChecks), + static_cast(D.passageMouthSupportCalls), + static_cast(D.passageMouthRouteSegmentChecks), + static_cast(D.passageReliefFieldCalls), + static_cast(D.passageSmoothstepEvaluations)); } void RecordTimelineEvent(FStandaloneCpuTileTimelineEvent::EType Type, const FVoxelTileKey& Key,int32 WorkerId,size_t QueueDepth,size_t ActiveWorkers, @@ -1138,6 +1169,7 @@ private: int32 Lod8HorizonInteriorStepVoxels=0; bool bProfileMesherStages=false; bool bInstrumentTileDrain=false; + bool bProfilePassageComposer=false; bool bExactMidpointDensityMemo=false; bool bActiveLaneBatching=false; int32 ProofBlockCells=8; diff --git a/Standalone/Tools/verify/baselines/landmarks/l45-ramp-control.vfmesh.xz b/Standalone/Tools/verify/baselines/landmarks/l45-ramp-control.vfmesh.xz new file mode 100644 index 0000000..8995ef2 Binary files /dev/null and b/Standalone/Tools/verify/baselines/landmarks/l45-ramp-control.vfmesh.xz differ diff --git a/Standalone/Tools/verify/baselines/landmarks/l46-entrance-control.vfmesh.xz b/Standalone/Tools/verify/baselines/landmarks/l46-entrance-control.vfmesh.xz new file mode 100644 index 0000000..4a36a18 Binary files /dev/null and b/Standalone/Tools/verify/baselines/landmarks/l46-entrance-control.vfmesh.xz differ diff --git a/Standalone/Tools/verify/baselines/landmarks/l47-wall-foot-control.vfmesh.xz b/Standalone/Tools/verify/baselines/landmarks/l47-wall-foot-control.vfmesh.xz new file mode 100644 index 0000000..1dd7cb5 Binary files /dev/null and b/Standalone/Tools/verify/baselines/landmarks/l47-wall-foot-control.vfmesh.xz differ diff --git a/Standalone/Tools/verify/baselines/landmarks/plain-native-control.vfmesh.xz b/Standalone/Tools/verify/baselines/landmarks/plain-native-control.vfmesh.xz new file mode 100644 index 0000000..f866744 Binary files /dev/null and b/Standalone/Tools/verify/baselines/landmarks/plain-native-control.vfmesh.xz differ diff --git a/Standalone/Tools/verify/baselines/mouth/l35/owner-fi-ground-mouth.vfmesh.xz b/Standalone/Tools/verify/baselines/mouth/l35/owner-fi-ground-mouth.vfmesh.xz new file mode 100644 index 0000000..97156d0 Binary files /dev/null and b/Standalone/Tools/verify/baselines/mouth/l35/owner-fi-ground-mouth.vfmesh.xz differ diff --git a/Standalone/Tools/verify/baselines/mouth/l36/owner-fi-ground-mouth.vfmesh.xz b/Standalone/Tools/verify/baselines/mouth/l36/owner-fi-ground-mouth.vfmesh.xz new file mode 100644 index 0000000..302dbfe Binary files /dev/null and b/Standalone/Tools/verify/baselines/mouth/l36/owner-fi-ground-mouth.vfmesh.xz differ diff --git a/Standalone/Tools/verify/baselines/mouth/l37-no-tread/owner-fi-ground-mouth.vfmesh.xz b/Standalone/Tools/verify/baselines/mouth/l37-no-tread/owner-fi-ground-mouth.vfmesh.xz new file mode 100644 index 0000000..dfb3c1a Binary files /dev/null and b/Standalone/Tools/verify/baselines/mouth/l37-no-tread/owner-fi-ground-mouth.vfmesh.xz differ diff --git a/Standalone/Tools/verify/baselines/mouth/l37-no-tread/owner-sw-ground-mouth.vfmesh.xz b/Standalone/Tools/verify/baselines/mouth/l37-no-tread/owner-sw-ground-mouth.vfmesh.xz new file mode 100644 index 0000000..a4df0b0 Binary files /dev/null and b/Standalone/Tools/verify/baselines/mouth/l37-no-tread/owner-sw-ground-mouth.vfmesh.xz differ diff --git a/Standalone/Tools/verify/baselines/mouth/l37-no-tread/upper-sw-fixture-ground-mouth.vfmesh.xz b/Standalone/Tools/verify/baselines/mouth/l37-no-tread/upper-sw-fixture-ground-mouth.vfmesh.xz new file mode 100644 index 0000000..d53e876 Binary files /dev/null and b/Standalone/Tools/verify/baselines/mouth/l37-no-tread/upper-sw-fixture-ground-mouth.vfmesh.xz differ diff --git a/Standalone/Tools/verify/baselines/ring/l31-fi-floor-v19.vfmesh.xz b/Standalone/Tools/verify/baselines/ring/l31-fi-floor-v19.vfmesh.xz new file mode 100644 index 0000000..eccc8ff Binary files /dev/null and b/Standalone/Tools/verify/baselines/ring/l31-fi-floor-v19.vfmesh.xz differ diff --git a/Standalone/Tools/verify/baselines/ring/l32-fi-floor-l30-camera-negative.vfmesh.xz b/Standalone/Tools/verify/baselines/ring/l32-fi-floor-l30-camera-negative.vfmesh.xz new file mode 100644 index 0000000..1be68eb Binary files /dev/null and b/Standalone/Tools/verify/baselines/ring/l32-fi-floor-l30-camera-negative.vfmesh.xz differ diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py index 45448c9..6606a60 100644 --- a/Standalone/Tools/verify/verify.py +++ b/Standalone/Tools/verify/verify.py @@ -6,6 +6,7 @@ import argparse import atexit import hashlib import json +import lzma import math import os from pathlib import Path @@ -22,7 +23,8 @@ import zlib ROOT = Path(__file__).resolve().parents[3] VERIFY_DIR = ROOT / "Standalone/Tools/verify" -BASELINES = VERIFY_DIR / "baselines" +BASELINE_SOURCE = VERIFY_DIR / "baselines" +BASELINES = BASELINE_SOURCE LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37" LINUX47 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux47" LINUX53 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux53" @@ -54,11 +56,25 @@ ARGS = PARSER.parse_args() if ARGS.sanitize and ARGS.tier != "full": PARSER.error("--sanitize is available only with the full tier; use the sanitize tier on its own") OUT = ARGS.out.resolve() +BASELINES = OUT / "baselines" ROUND_NAME = OUT.name os.environ["VOXELFORGE_VERIFY_OUT"] = str(OUT) os.environ["PYTHONDONTWRITEBYTECODE"] = "1" sys.dont_write_bytecode = True + +def prepare_baselines() -> None: + """Copy pinned inputs into the output tree and expand compressed mesh fixtures.""" + shutil.copytree(BASELINE_SOURCE, BASELINES, dirs_exist_ok=True) + for compressed in sorted(BASELINES.rglob("*.vfmesh.xz")): + expanded = compressed.with_suffix("") + expanded.parent.mkdir(parents=True, exist_ok=True) + with lzma.open(compressed, "rb") as source, expanded.open("wb") as target: + shutil.copyfileobj(source, target, 1024 * 1024) + + +prepare_baselines() + sys.path.insert(0, str(VERIFY_DIR)) from owner_step import measure as measure_owner_step from runner import VerificationFailure, VerificationRunner, bounded_run