diff --git a/Standalone/Core/src/CoreNamespacePreamble.h b/Standalone/Core/src/CoreNamespacePreamble.h index 5362f75..df31e40 100644 --- a/Standalone/Core/src/CoreNamespacePreamble.h +++ b/Standalone/Core/src/CoreNamespacePreamble.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 8626d7e..68889cb 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -3566,6 +3566,7 @@ public: double PackedSignBuildSeconds = 0.0; double PassageSweepPrepSeconds = 0.0; double CandidateSweepPrepSeconds = 0.0; + double StartupPrepOverlapSeconds = 0.0; double VisualDetailPrepSeconds = 0.0; double DetailApplySeconds = 0.0; double SignEdgeDistanceSeconds = 0.0; @@ -3576,6 +3577,7 @@ public: double FarParentShapeSeconds = 0.0; double FarSharedPrimitiveSeconds = 0.0; double FarBinSeconds = 0.0; + double PassageValidationWorkerSeconds = 0.0; FParallelTiming FarParentGeometryLines; FParallelTiming RockIntervalLines; FParallelTiming CandidateAxisLines; @@ -3810,19 +3812,56 @@ public: WordsPerZ=(NZ+63u)/64u; RockWords.assign(static_cast(Size[0])*Size[1]*WordsPerZ,0); SurfaceDistanceQ=std::make_unique_for_overwrite(static_cast(NodeCount)); - Substage=Clock::now(); - BuildPackedRockIntervals(Stats.RockIntervalLines); - Stats.PackedSignBuildSeconds=std::chrono::duration(Clock::now()-Substage).count(); - // Native density queries for non-heightfield strates use the exact interval - // evaluator. Build those native intervals before scanning passage support; - // the scan itself still completes before any resolved column is composed. - Substage=Clock::now(); - BuildPassageVoidCrossingPlan(); - Stats.PassageSweepPrepSeconds=std::chrono::duration(Clock::now()-Substage).count(); - Substage=Clock::now(); - BuildCandidateBands(Stats.CandidateAxisLines,Stats.CandidateMergeLines, - Stats.CandidateMergeSeconds); - Stats.CandidateSweepPrepSeconds=std::chrono::duration(Clock::now()-Substage).count(); + const bool bOverlapStartupPrep=WorkerCount>=4&&Scheduler; + if(bOverlapStartupPrep) + { + // The packed Z intervals and X/Y candidate sweeps read the same immutable + // primitive/bin tables but write separate arrays. Split the helper budget + // between them so overlap does not double the configured worker count. + const int32 CandidateWorkers=WorkerCount/2; + const int32 IntervalWorkers=WorkerCount-CandidateWorkers; + const auto PrepStart=Clock::now(); + auto PassagePrepFuture=std::async(std::launch::async,[this] + { + const auto Start=Clock::now(); + BuildPassageVoidCrossingPlan(); + return std::chrono::duration(Clock::now()-Start).count(); + }); + auto CandidateAxesFuture=std::async(std::launch::async,[this,CandidateWorkers] + { + const auto Start=Clock::now(); + BuildCandidateAxisBands(Stats.CandidateAxisLines,CandidateWorkers); + return std::chrono::duration(Clock::now()-Start).count(); + }); + Substage=Clock::now(); + BuildPackedRockIntervals(Stats.RockIntervalLines,IntervalWorkers); + Stats.PackedSignBuildSeconds=std::chrono::duration(Clock::now()-Substage).count(); + const double CandidateAxesSeconds=CandidateAxesFuture.get(); + Substage=Clock::now(); + MergeCandidateBands(Stats.CandidateMergeLines,Stats.CandidateMergeSeconds); + const double CandidateMergeWallSeconds= + std::chrono::duration(Clock::now()-Substage).count(); + Stats.CandidateSweepPrepSeconds=CandidateAxesSeconds+CandidateMergeWallSeconds; + Stats.PassageSweepPrepSeconds=PassagePrepFuture.get(); + Stats.StartupPrepOverlapSeconds= + std::chrono::duration(Clock::now()-PrepStart).count(); + } + else + { + Substage=Clock::now(); + BuildPackedRockIntervals(Stats.RockIntervalLines); + Stats.PackedSignBuildSeconds=std::chrono::duration(Clock::now()-Substage).count(); + // Native density queries for non-heightfield strates use the exact interval + // evaluator. Build those native intervals before scanning passage support; + // the scan itself still completes before any resolved column is composed. + Substage=Clock::now(); + BuildPassageVoidCrossingPlan(); + Stats.PassageSweepPrepSeconds=std::chrono::duration(Clock::now()-Substage).count(); + Substage=Clock::now(); + BuildCandidateBands(Stats.CandidateAxisLines,Stats.CandidateMergeLines, + Stats.CandidateMergeSeconds); + Stats.CandidateSweepPrepSeconds=std::chrono::duration(Clock::now()-Substage).count(); + } Substage=Clock::now(); if(!BuildGpuVisualDetailNodeSets(InGpuVisualDetailTiles,InCpuVisualDetailTiles,Error)) return false; @@ -3877,9 +3916,10 @@ public: Stats.CandidateMergeLines.WallSeconds*1000.0,Stats.CandidateMergeLines.WorkerSeconds*1000.0, Stats.CandidateMergeSeconds*1000.0,Stats.DetailBandLines.WallSeconds*1000.0, Stats.DetailBandLines.WorkerSeconds*1000.0); - std::printf("[ShapeSubstage] detail_apply=%.3fms sign_edge_distances=%.3fms passage_validation=%.3fms shared_field_primitives=%.3fms far_parents=%.3fms\n", + std::printf("[ShapeSubstage] detail_apply=%.3fms sign_edge_distances=%.3fms passage_validation=%.3fms passage_validation_worker=%.3fms shared_field_primitives=%.3fms far_parents=%.3fms\n", Stats.DetailApplySeconds*1000.0,Stats.SignEdgeDistanceSeconds*1000.0, - Stats.PassageValidationSeconds*1000.0,Stats.SharedFieldSeconds*1000.0, + Stats.PassageValidationSeconds*1000.0,Stats.PassageValidationWorkerSeconds*1000.0, + Stats.SharedFieldSeconds*1000.0, Stats.FarShapeSeconds*1000.0); std::printf("[ShapeBreakdown] owner_shapes=%.3fms owner_shape_worker=%.3fms owner_bins=%.3fms packed_signs=%.3fms passage_prep=%.3fms candidate_sweeps=%.3fms visual_detail_prep=%.3fms detail_band=%.3fms sign_edge_repair=%.3fms route_validation=%.3fms far_parent_shapes=%.3fms far_shared_primitives=%.3fms far_bins=%.3fms\n", Stats.LocalPrimitiveSeconds*1000.0,Stats.OwnerShapeWorkerSeconds*1000.0, @@ -3889,6 +3929,10 @@ public: Stats.DetailApplySeconds*1000.0,Stats.SignEdgeDistanceSeconds*1000.0, Stats.PassageValidationSeconds*1000.0,Stats.FarParentShapeSeconds*1000.0, Stats.FarSharedPrimitiveSeconds*1000.0,Stats.FarBinSeconds*1000.0); + if(bOverlapStartupPrep) + std::printf("[ShapeStartupOverlap] packed_candidate_passage_wall=%.3fms packed_helpers=%d candidate_helpers=%d mode=parallel_exact_disjoint_outputs\n", + Stats.StartupPrepOverlapSeconds*1000.0,WorkerCount-WorkerCount/2, + WorkerCount/2); std::printf("[ShapeAllocation] words=%.2fMiB candidate_axis_words=%.2fMiB proxy=%.2fMiB primitives=%.2fMiB spatial_bins=%llu spatial_bin_refs=%llu spatial_bin_capacity=%llu inline_bins=%llu overflow_bins=%llu far_primitive_build=%.3fms far_bin_rebuild=%.3fms\n", static_cast(WordBytes)/1048576.0,static_cast(CandidateAxisBytes)/1048576.0, static_cast(Stats.SignNodes*sizeof(int16))/1048576.0, @@ -12423,7 +12467,7 @@ private: } } - void BuildPackedRockIntervals(FParallelTiming& Timing) + void BuildPackedRockIntervals(FParallelTiming& Timing,int32 ParallelWorkerLimit=0) { const int32 NX=Size[0],NY=Size[1],NZ=Size[2]; CandidateAxisWords[2].assign(static_cast(NX)*NY*WordsPerZ,0); @@ -12482,11 +12526,10 @@ private: for(const FInterval& R:Solid)FillBits(RockWords,Base,R.Lo,R.Hi,true); // Critical passage air is the final authored override, including at landing floors. for(const FInterval& R:ProtectedAir)FillBits(RockWords,Base,R.Lo,R.Hi,false); - },&Timing,nullptr,16); + },&Timing,nullptr,16,ParallelWorkerLimit); } - void BuildCandidateBands(FParallelTiming& AxisTiming,FParallelTiming& MergeTiming, - double& MergeSeconds) + void BuildCandidateAxisBands(FParallelTiming& AxisTiming,int32 ParallelWorkerLimit=0) { const int32 N[3]={Size[0],Size[1],Size[2]}; for(int32 Axis=0;Axis<3;++Axis) @@ -12513,8 +12556,12 @@ private: FillBits(CandidateAxisWords[Axis],Base,Lo-Min[Axis],Hi-Min[Axis],true); FillBits(CandidateAxisWords[Axis],Base,Lo2-Min[Axis],Hi2-Min[Axis],true); } - },&AxisTiming,nullptr,16); + },&AxisTiming,nullptr,16,ParallelWorkerLimit); } + } + + void MergeCandidateBands(FParallelTiming& MergeTiming,double& MergeSeconds) + { const auto MergeStart=std::chrono::steady_clock::now(); CandidateWords=std::move(CandidateAxisWords[2]); for(int32 Axis=0;Axis<2;++Axis) @@ -12563,6 +12610,13 @@ private: MergeSeconds=std::max(0.0,MergeElapsed-MergeTiming.WallSeconds); } + void BuildCandidateBands(FParallelTiming& AxisTiming,FParallelTiming& MergeTiming, + double& MergeSeconds) + { + BuildCandidateAxisBands(AxisTiming); + MergeCandidateBands(MergeTiming,MergeSeconds); + } + bool BuildGpuVisualDetailNodeSets( const std::vector& GpuTiles, const std::vector& CpuTiles, @@ -14134,59 +14188,114 @@ private: &&ResolvedLayout&&!ResolvedLayout->passages.empty()) { constexpr double SampleStep=0.5; - uint32 PassageIndex=0; - for(const auto& passage:ResolvedLayout->passages) + constexpr double SubvoxelsPerVoxel=static_cast( + VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + struct FRoute + { + std::string Id; + std::size_t PointCount=0; + uint64 Samples=0,Failures=0; + int32 FirstFailure[3]={0,0,0}; + }; + struct FSegment + { + std::size_t Route=0,Index=0,SampleCount=0; + double Length=0.0; + uint64 Failures=0; + }; + struct FSample { - const auto& points=passage.controlPoints; - if(points.size()<2){++PassageIndex;continue;} - uint64 routeSamples=0,routeFailures=0; - int32 firstFailure[3]={0,0,0}; - for(std::size_t segment=0;segment+1(a[0])/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; - const double ay=static_cast(a[1])/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; - const double az=static_cast(a[2])/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; - const double bx=static_cast(b[0])/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; - const double by=static_cast(b[1])/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; - const double bz=static_cast(b[2])/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; - const double length=std::sqrt((bx-ax)*(bx-ax)+(by-ay)*(by-ay)+(bz-az)*(bz-az)); - const int32 count=std::max(1,static_cast(std::ceil(length/SampleStep))); - uint64 segmentFailures=0; - for(int32 sample=0;sample<=count;++sample) + std::size_t Segment=0; + double X=0.0,Y=0.0,Z=0.0; + int32 VX=0,VY=0,VZ=0; + }; + std::vector Routes; + std::vector Segments; + std::vector Samples; + Routes.reserve(ResolvedLayout->passages.size()); + for(std::size_t PassageIndex=0; + PassageIndexpassages.size();++PassageIndex) + { + const auto& Passage=ResolvedLayout->passages[PassageIndex]; + const auto& Points=Passage.controlPoints; + FRoute Route;Route.Id=Passage.stableId;Route.PointCount=Points.size(); + const std::size_t RouteIndex=Routes.size(); + Routes.push_back(std::move(Route)); + if(Points.size()<2)continue; + for(std::size_t SegmentIndex=0;SegmentIndex+1(A[0])/SubvoxelsPerVoxel; + const double AY=static_cast(A[1])/SubvoxelsPerVoxel; + const double AZ=static_cast(A[2])/SubvoxelsPerVoxel; + const double BX=static_cast(B[0])/SubvoxelsPerVoxel; + const double BY=static_cast(B[1])/SubvoxelsPerVoxel; + const double BZ=static_cast(B[2])/SubvoxelsPerVoxel; + const double DX=BX-AX,DY=BY-AY,DZ=BZ-AZ; + FSegment Segment;Segment.Route=RouteIndex;Segment.Index=SegmentIndex; + Segment.Length=std::sqrt(DX*DX+DY*DY+DZ*DZ); + const int32 Count=std::max(1,static_cast( + std::ceil(Segment.Length/SampleStep))); + Segment.SampleCount=static_cast(Count)+1; + const std::size_t SegmentIndexInList=Segments.size(); + for(int32 SampleIndex=0;SampleIndex<=Count;++SampleIndex) { - const double t=static_cast(sample)/count; - const int32 vx=static_cast(std::lround(ax+(bx-ax)*t)); - const int32 vy=static_cast(std::lround(ay+(by-ay)*t)); - const int32 vz=static_cast(std::lround(az+(bz-az)*t)); - ++Stats.WalkSamples; - ++routeSamples; - const float density=DensityAt(static_cast(ax+(bx-ax)*t), - static_cast(ay+(by-ay)*t), - static_cast(az+(bz-az)*t)); - if(!std::isfinite(density)||density<0.0f) - { - ++Stats.WalkFailures; - ++routeFailures; - ++segmentFailures; - if(routeFailures==1) - {firstFailure[0]=vx;firstFailure[1]=vy;firstFailure[2]=vz;} - } + const double T=static_cast(SampleIndex)/Count; + FSample Sample;Sample.Segment=SegmentIndexInList; + Sample.X=AX+DX*T;Sample.Y=AY+DY*T;Sample.Z=AZ+DZ*T; + Sample.VX=static_cast(std::lround(Sample.X)); + Sample.VY=static_cast(std::lround(Sample.Y)); + Sample.VZ=static_cast(std::lround(Sample.Z)); + Samples.push_back(Sample); } - if(segmentFailures) - std::fprintf(stderr,"[WalkabilitySegment] source=resolved_core_layout passage=%s segment=%zu failures=%llu/%llu length=%.2f\n", - passage.stableId.c_str(),segment, - static_cast(segmentFailures), - static_cast(std::max(1,static_cast(std::ceil(length/SampleStep))+1)), - length); - } - if(routeFailures) + Segments.push_back(Segment); + } + } + std::vector Failures(Samples.size(),0); + FParallelTiming ValidationTiming; + ParallelLines(Samples.size(),[&](size_t SampleIndex) + { + const FSample& Sample=Samples[SampleIndex]; + const float Density=DensityAt(static_cast(Sample.X), + static_cast(Sample.Y),static_cast(Sample.Z)); + Failures[SampleIndex]=!std::isfinite(Density)||Density<0.0f; + },&ValidationTiming,nullptr,32); + Stats.PassageValidationWorkerSeconds=ValidationTiming.WorkerSeconds; + for(const FSegment& Segment:Segments) + Routes[Segment.Route].Samples+=Segment.SampleCount; + for(std::size_t SampleIndex=0;SampleIndex(Segment.Failures), + static_cast(Segment.SampleCount),Segment.Length); + } + uint32 PassageIndex=0; + for(const FRoute& Route:Routes) + { + if(Route.Failures) std::fprintf(stderr,"[WalkabilityRoute] source=resolved_core_layout passage=%u id=%s points=%zu failures=%llu/%llu first_solid=(%d,%d,%d)\n", - PassageIndex,passage.stableId.c_str(),points.size(), - static_cast(routeFailures), - static_cast(routeSamples),firstFailure[0], - firstFailure[1],firstFailure[2]); + PassageIndex,Route.Id.c_str(),Route.PointCount, + static_cast(Route.Failures), + static_cast(Route.Samples),Route.FirstFailure[0], + Route.FirstFailure[1],Route.FirstFailure[2]); ++PassageIndex; } if(Stats.WalkFailures) @@ -14199,10 +14308,8 @@ private: static_cast(Stats.WalkSamples)); return Stats.WalkFailures==0; } - // An imported Unreal-resolved layout is the canonical route source. An empty - // passage list is meaningful for blocked controls, so never fall back to the - // regenerated legacy owner route in that case. Recipe overrides without an - // exported Core plan retain their generated owner route audit. + // An imported Unreal-resolved layout is the canonical route source. An empty + // passage list is meaningful for blocked controls; never substitute regenerated routes. if(Owner&&Owner->bHasUnrealResolvedCorePlan&&ResolvedLayout)return true; constexpr double SampleStep=0.5; uint32 PassageIndex=0; @@ -14283,10 +14390,13 @@ private: } template void ParallelLines(size_t Count,Fn&& Work,FParallelTiming* Timing=nullptr, - std::array* WorkerActiveSeconds=nullptr,size_t WorkBatch=1) const + std::array* WorkerActiveSeconds=nullptr,size_t WorkBatch=1, + int32 ParallelWorkerLimit=0) const { const auto WallStart=std::chrono::steady_clock::now(); - if(WorkerCount<=1||Count<512||!Scheduler) + const int32 EffectiveWorkerLimit=ParallelWorkerLimit>0 + ?std::clamp(ParallelWorkerLimit,1,WorkerCount):WorkerCount; + if(EffectiveWorkerLimit<=1||Count<512||!Scheduler) { const auto WorkerStart=std::chrono::steady_clock::now(); for(size_t I=0;I ActiveSeconds{}; const double WorkerSeconds=Scheduler->parallel_for(Count,WorkBatch, - static_cast(WorkerCount),&Dispatch,Invoke,ActiveSeconds.data(),ActiveSeconds.size()); + static_cast(EffectiveWorkerLimit),&Dispatch,Invoke, + ActiveSeconds.data(),ActiveSeconds.size()); if(WorkerActiveSeconds)*WorkerActiveSeconds=ActiveSeconds; if(Timing) { diff --git a/Standalone/Source/StandaloneShapeWorld.h b/Standalone/Source/StandaloneShapeWorld.h index 7fa23d4..ae90b35 100644 --- a/Standalone/Source/StandaloneShapeWorld.h +++ b/Standalone/Source/StandaloneShapeWorld.h @@ -1,3 +1,5 @@ #pragma once +#include + #include "../Core/src/ShapeWorldInternal.h"