diff --git a/Standalone/Core/include/VoxelForgeCore/SharedColumnCache.h b/Standalone/Core/include/VoxelForgeCore/SharedColumnCache.h index 13d25b0..09c0e75 100644 --- a/Standalone/Core/include/VoxelForgeCore/SharedColumnCache.h +++ b/Standalone/Core/include/VoxelForgeCore/SharedColumnCache.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -12,6 +13,15 @@ namespace VoxelForgeCoreShared { + struct FColumnCacheMetrics + { + std::uint64_t hits = 0; + std::uint64_t misses = 0; + std::uint64_t computes = 0; + std::uint64_t conflictReplacements = 0; + std::uint64_t lockWaitNanoseconds = 0; + }; + // Exact input identity for a pure XY column value. LOD is deliberately not part of this key: // the same recipe/plan column has the same bits at every sampling step. struct FColumnCacheKey @@ -69,13 +79,21 @@ namespace VoxelForgeCoreShared struct FSpinGuard { - explicit FSpinGuard(std::atomic_flag& value) : Flag(value) + explicit FSpinGuard(std::atomic_flag& value, FColumnCacheMetrics* metrics) + : Flag(value), Metrics(metrics) { + const auto started = Metrics ? std::chrono::steady_clock::now() + : std::chrono::steady_clock::time_point{}; while (Flag.test_and_set(std::memory_order_acquire)) std::this_thread::yield(); + if (Metrics) + Metrics->lockWaitNanoseconds += static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now() - started).count()); } ~FSpinGuard() { Flag.clear(std::memory_order_release); } std::atomic_flag& Flag; + FColumnCacheMetrics* Metrics; }; public: @@ -94,7 +112,7 @@ namespace VoxelForgeCoreShared template TValue getOrCompute(const FColumnCacheKey& key, TFactory&& factory, - bool& computed) + bool& computed, FColumnCacheMetrics* metrics = nullptr) { computed = false; const FKeyWords keyWords = key.words(); @@ -102,13 +120,23 @@ namespace VoxelForgeCoreShared const std::size_t setIndex = static_cast(tag) & (SetCount - 1); FSet& set = Sets[setIndex]; TValue value{}; - if (TryReadSet(set, keyWords, tag, value)) return value; + if (TryReadSet(set, keyWords, tag, value)) + { + if (metrics) ++metrics->hits; + return value; + } + if (metrics) ++metrics->misses; - FSpinGuard lock(set.Writer); - if (TryReadSet(set, keyWords, tag, value)) return value; + FSpinGuard lock(set.Writer, metrics); + if (TryReadSet(set, keyWords, tag, value)) + { + if (metrics) ++metrics->hits; + return value; + } value = std::forward(factory)(); computed = true; + if (metrics) ++metrics->computes; FValueWords valueWords{}; std::memcpy(valueWords.data(), &value, sizeof(TValue)); @@ -118,6 +146,7 @@ namespace VoxelForgeCoreShared { replacement = way; break; } if (replacement == Ways) { + if (metrics) ++metrics->conflictReplacements; replacement = set.NextWay; set.NextWay = static_cast((set.NextWay + 1) % Ways); } diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 8b2cc5b..093050c 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -139,6 +139,14 @@ namespace VoxelForgeCore std::uint64_t columnCacheConflictMisses = 0; std::uint64_t sharedColumnCacheHits = 0; std::uint64_t columnEvaluations = 0; + std::uint64_t islandLocalCacheHits = 0; + std::uint64_t islandLocalCacheMisses = 0; + std::uint64_t islandLocalCacheConflictReplacements = 0; + std::uint64_t islandSharedCacheHits = 0; + std::uint64_t islandSharedCacheMisses = 0; + std::uint64_t islandSharedCacheComputes = 0; + std::uint64_t islandSharedCacheConflictReplacements = 0; + std::uint64_t islandSharedCacheLockWaitNanoseconds = 0; std::uint64_t uniformBlockProofsChecked = 0; std::uint64_t uniformBlockProofSamplesChecked = 0; std::uint64_t uniformBlockProofFallbackBlocks = 0; @@ -160,8 +168,22 @@ namespace VoxelForgeCore double marchingCubesSkirtMilliseconds = 0.0; double marchingCubesOtherMilliseconds = 0.0; std::uint64_t marchingCubesGradientFallbackDensityCalls = 0; + std::uint64_t marchingCubesGradientCalls = 0; std::uint64_t marchingCubesEdgeMidpointDensityCalls = 0; + std::uint64_t marchingCubesEdgeMidpointDensityMemoHits = 0; std::uint64_t marchingCubesGradientMemoHits = 0; + std::uint64_t marchingCubesX8DensityCalls = 0; + std::uint64_t marchingCubesX8ActiveLanes = 0; + std::uint64_t marchingCubesCandidateCells = 0; + std::uint64_t marchingCubesZBandRejectedCells = 0; + std::uint64_t marchingCubesCellClassificationCalls = 0; + std::uint64_t marchingCubesSurfaceCellCalls = 0; + std::uint64_t marchingCubesProofSkippedSolidCells = 0; + std::uint64_t marchingCubesProofSkippedAirCells = 0; + std::uint64_t marchingCubesProofSkippedDensitySamples = 0; + std::uint64_t marchingCubesSkirtEdgeCandidates = 0; + std::uint64_t marchingCubesSkirtBoundaryCandidates = 0; + std::uint64_t marchingCubesSkirtDensityProbes = 0; double groundRefinementExclusiveMilliseconds = 0.0; double capRefinementExclusiveMilliseconds = 0.0; double meshAssemblyMilliseconds = 0.0; @@ -1475,6 +1497,7 @@ namespace VoxelForgeCore bool roundTunnels = false; bool enhancedTunnelWalls = true; bool profileDetailBand = false; + bool parallelSignEdgeRepair = false; // World-wide walkability is a validation pass, not an input to the compiled field. // Keep it enabled for benchmarks/audits; the interactive viewer can defer it. bool validatePassageRoutes = true; @@ -1677,6 +1700,12 @@ 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: 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; + bool exactMidpointDensityMemo = false; + bool activeLaneBatching = false; + std::uint32_t proofBlockCells = 8; // Diagnostic only; fills and checks every lattice sample in blocks certified uniform. // Diagnostic seulement; evalue et verifie tous les echantillons des blocs uniformes. bool auditUniformBlockProofs = false; diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h index 62fbc29..4551f09 100644 --- a/Standalone/Core/src/FloatingIslandsInternal.h +++ b/Standalone/Core/src/FloatingIslandsInternal.h @@ -471,6 +471,26 @@ namespace floating_island_detail double DeltaX=0.0,DeltaY=0.0,Radial=0.0,Angle=0.0,LobePhase=0.0,TopSurface=0.0; }; + struct FIslandColumnCacheInstrumentation + { + std::uint64_t localHits=0,localMisses=0,localConflictReplacements=0; + VoxelForgeCoreShared::FColumnCacheMetrics shared; + }; + + inline FIslandColumnCacheInstrumentation*& ActiveIslandColumnCacheInstrumentation() noexcept + { + static thread_local FIslandColumnCacheInstrumentation* active=nullptr; + return active; + } + + inline FIslandColumnCacheInstrumentation* SetIslandColumnCacheInstrumentation( + FIslandColumnCacheInstrumentation* value) noexcept + { + FIslandColumnCacheInstrumentation* previous=ActiveIslandColumnCacheInstrumentation(); + ActiveIslandColumnCacheInstrumentation()=value; + return previous; + } + using FSharedIslandColumnCache = VoxelForgeCoreShared::TSharedColumnValueCache; @@ -501,6 +521,7 @@ namespace floating_island_detail IndexKey^=IndexKey>>33; const std::size_t Base=static_cast(IndexKey&(SetCount-1))*Ways; FIslandColumnCacheEntry* Replacement=&cache[Base+((IndexKey>>11)&(Ways-1))]; + bool bFoundInvalid=false; for(std::size_t Way=0;WaylocalHits; + return Entry; + } + if(!Entry.bValid&&!bFoundInvalid) + {Replacement=&Entry;bFoundInvalid=true;} + } + if(auto* stats=ActiveIslandColumnCacheInstrumentation()) + { + ++stats->localMisses; + if(!bFoundInvalid)++stats->localConflictReplacements; } const auto build=[&]() @@ -562,7 +593,9 @@ namespace floating_island_detail static_cast(reinterpret_cast(&plan)), plan.contentHash,SampleX,SampleY,island.stableId}; bool computed=false; - Entry=shared->getOrCompute(key,build,computed); + auto* stats=ActiveIslandColumnCacheInstrumentation(); + Entry=shared->getOrCompute(key,build,computed, + stats?&stats->shared:nullptr); } else Entry=build(); *Replacement=Entry; diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index d1ba1f6..4df0109 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -2246,7 +2246,8 @@ public: const std::vector& InGpuVisualDetailTiles={}, const std::vector& InCpuVisualDetailTiles={}, const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly=nullptr, - const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout=nullptr) + const VoxelForgeCore::ResolvedLayoutSnapshotV3* InResolvedLayout=nullptr, + bool bInParallelSignEdgeRepair=false) { using Clock=std::chrono::steady_clock; const auto TotalStart=Clock::now(); @@ -2257,6 +2258,7 @@ public: BindResolvedStratumFields(); bAnalyticOnly=false; bProfileDetailBand=bInProfileDetailBand; + bParallelSignEdgeRepair=bInParallelSignEdgeRepair; for(int32 A=0;A<3;++A) { Min[A]=(&MinVoxels.X)[A]-2; @@ -9084,6 +9086,8 @@ private: uint64 CompleteSignEdgeDistances() { + if(bParallelSignEdgeRepair&&Scheduler) + return CompleteSignEdgeDistancesParallel(); uint64 AddedTotal=0,SignCorrections=0; const size_t Columns=static_cast(Size[0])*Size[1]; for(int32 Pass=0;Pass<4;++Pass) @@ -9183,6 +9187,145 @@ private: return AddedTotal; } + uint64 CompleteSignEdgeDistancesParallel() + { + uint64 AddedTotal=0,SignCorrections=0; + const size_t Columns=static_cast(Size[0])*Size[1]; + for(int32 Pass=0;Pass<4;++Pass) + { + std::vector MissingNodes; + const auto AddMissing=[&](int32 X,int32 Y,int32 Z) + { + if(XMax[0]||YMax[1]||ZMax[2])return; + const size_t Column=static_cast(Y-Min[1])*Size[0] + +static_cast(X-Min[0]); + const size_t Zi=static_cast(Z-Min[2]); + const size_t Word=Column*WordsPerZ+(Zi>>6); + const uint64 Mask=uint64{1}<<(Zi&63u); + if((CandidateWords[Word]&Mask)==0) + MissingNodes.push_back((Zi*static_cast(Size[1]) + +static_cast(Y-Min[1]))*Size[0] + +static_cast(X-Min[0])); + }; + + // Discover against a stable pass snapshot. Density work is then independent per + // lattice node; packed candidates and occupancy corrections publish in node order. + for(size_t Line=0;Line(Line%Size[0]); + const int32 Y=Min[1]+static_cast(Line/Size[0]); + const size_t Base=Line*WordsPerZ; + for(size_t W=0;W>63:0; + const uint64 PreviousCandidates=W?CandidateWords[Word-1]>>63:0; + uint64 Valid=(Size[2]-W*64)>=64?~uint64{0} + :((uint64{1}<<(Size[2]-W*64))-1); + if(W==0)Valid&=~uint64{1}; + const uint64 ShiftedRock=(Rock<<1)|PreviousRock; + const uint64 ShiftedCandidates=(Candidates<<1)|PreviousCandidates; + uint64 Missing=((Rock^ShiftedRock)&Valid) + &~(Candidates&ShiftedCandidates); + while(Missing) + { + const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1; + const int32 Z=Min[2]+static_cast(W*64+Bit); + AddMissing(X,Y,Z);AddMissing(X,Y,Z-1); + } + if(X(W*64+Bit)); + AddMissing(X+1,Y,Min[2]+static_cast(W*64+Bit)); + } + } + if(Y(Size[0])*WordsPerZ; + Missing=(Rock^RockWords[Other])&Valid + &~(Candidates&CandidateWords[Other]); + while(Missing) + { + const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1; + AddMissing(X,Y,Min[2]+static_cast(W*64+Bit)); + AddMissing(X,Y+1,Min[2]+static_cast(W*64+Bit)); + } + } + } + } + std::sort(MissingNodes.begin(),MissingNodes.end()); + MissingNodes.erase(std::unique(MissingNodes.begin(),MissingNodes.end()), + MissingNodes.end()); + std::vector Densities(MissingNodes.size()); + struct FParallelContext + { + FStandaloneShapeWorld* Shape; + const std::vector* Nodes; + std::vector* Values; + int32 Minimum[3]; + int32 Extent[3]; + } Context{this,&MissingNodes,&Densities,{Min[0],Min[1],Min[2]}, + {Size[0],Size[1],Size[2]}}; + const auto EvaluateNode=[](void* Raw,size_t Index,size_t) + { + auto& C=*static_cast(Raw); + const size_t Node=(*C.Nodes)[Index]; + const int32 X=static_cast(Node%static_cast(C.Extent[0])); + const size_t YZ=Node/static_cast(C.Extent[0]); + const int32 Y=static_cast(YZ%static_cast(C.Extent[1])); + const int32 Z=static_cast(YZ/static_cast(C.Extent[1])); + (*C.Values)[Index]=C.Shape->AnalyticDensityAt( + static_cast(C.Minimum[0]+X),static_cast(C.Minimum[1]+Y), + static_cast(C.Minimum[2]+Z)); + }; + Scheduler->parallel_for(MissingNodes.size(),64, + static_cast(std::max(1,WorkerCount)),&Context,EvaluateNode,nullptr,0); + + uint64 AddedThisPass=0; + bool bSignChanged=false; + for(size_t Index=0;Index(Node%static_cast(Size[0])); + const size_t YZ=Node/static_cast(Size[0]); + const int32 Y=Min[1]+static_cast(YZ%static_cast(Size[1])); + const int32 Z=Min[2]+static_cast(YZ/static_cast(Size[1])); + const size_t Column=static_cast(Y-Min[1])*Size[0] + +static_cast(X-Min[0]); + const size_t Zi=static_cast(Z-Min[2]); + const size_t Word=Column*WordsPerZ+(Zi>>6); + const uint64 Mask=uint64{1}<<(Zi&63u); + if(CandidateWords[Word]&Mask)continue; + const float Density=Densities[Index]; + StoreSurfaceDistance(Node,Density); + CandidateWords[Word]|=Mask; + ++AddedThisPass; + const bool bExactRock=Density<0.0f; + const bool bPackedRock=(RockWords[Word]&Mask)!=0; + if(bExactRock!=bPackedRock) + { + if(bExactRock)RockWords[Word]|=Mask; + else RockWords[Word]&=~Mask; + ++SignCorrections; + bSignChanged=true; + } + } + AddedTotal+=AddedThisPass; + if(!bSignChanged)break; + } + std::printf("[GridStairDistanceRepair] exact_endpoint_proxies=%llu occupancy_corrections=%llu mode=parallel_deterministic\n", + static_cast(AddedTotal), + static_cast(SignCorrections)); + return AddedTotal; + } + void StoreSurfaceDistance(size_t Node,float Density) { int32 Q=static_cast(std::lround(Density*256.0f)); @@ -10191,6 +10334,7 @@ private: const VoxelForgeCore::JobScheduler* Scheduler=nullptr; bool RoundTunnelsEnabled=false; bool bProfileDetailBand=false; + bool bParallelSignEdgeRepair=false; int32 Min[3]{},Max[3]{},ClipMin[3]{},ClipMax[3]{},Size[3]{}; FIntVector EditBoundsMin=FIntVector(MAX_int32,MAX_int32,MAX_int32); FIntVector EditBoundsMax=FIntVector(MIN_int32,MIN_int32,MIN_int32); diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 4c6c6ac..887d5b0 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -1069,6 +1069,18 @@ namespace VoxelForgeCore } }; + struct FScopedIslandColumnCacheInstrumentation + { + floating_island_detail::FIslandColumnCacheInstrumentation* Previous=nullptr; + explicit FScopedIslandColumnCacheInstrumentation( + floating_island_detail::FIslandColumnCacheInstrumentation* Current,bool Enabled) + :Previous(Enabled?floating_island_detail::SetIslandColumnCacheInstrumentation(Current):nullptr), + bEnabled(Enabled){} + ~FScopedIslandColumnCacheInstrumentation() + {if(bEnabled)floating_island_detail::SetIslandColumnCacheInstrumentation(Previous);} + bool bEnabled=false; + }; + World::World(ResolvedWorldSnapshot snapshot):impl_(std::make_unique()) { if(snapshot.data_) @@ -1394,7 +1406,8 @@ namespace VoxelForgeCore static_cast(std::clamp(request.workerLimit,1,16)),ToNative(request.detail), request.includeFarWorld,error,&scheduler,request.roundTunnels,request.profileDetailBand, request.validatePassageRoutes,request.gpuVisualDetailTiles, - request.cpuVisualDetailTiles,impl_->worldAssembly.get(),impl_->resolvedLayout.get()); + request.cpuVisualDetailTiles,impl_->worldAssembly.get(),impl_->resolvedLayout.get(), + request.parallelSignEdgeRepair); if(ok) { impl_->shape=std::move(shape); @@ -1999,6 +2012,9 @@ namespace VoxelForgeCore FScopedSharedColumnCaches sharedColumnCaches(options.useSharedColumnCache ?impl_->sharedHeightfieldColumns.get():nullptr, options.useSharedColumnCache?impl_->sharedIslandColumns.get():nullptr); + floating_island_detail::FIslandColumnCacheInstrumentation islandCacheInstrumentation; + FScopedIslandColumnCacheInstrumentation islandCacheScope( + &islandCacheInstrumentation,options.instrumentTileDrain); const auto& world=*impl_->owner; FStandaloneShapeWorld nativeOnlyShape; FIntVector nativeOnlyMinimum,nativeOnlyMaximum; @@ -2028,6 +2044,10 @@ namespace VoxelForgeCore mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors; mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups; mesher.bStandaloneEnableGradientMemoization=options.memoizeMcGradients; + mesher.bStandaloneEnableExactMidpointDensityMemo=options.exactMidpointDensityMemo; + mesher.bStandaloneEnableActiveLaneBatching=options.activeLaneBatching; + mesher.bStandaloneProofBlockCells=static_cast(std::clamp( + options.proofBlockCells,1u,static_cast(std::max(1,CHUNK_SIZE)))); mesher.bGenerateSkirts=world.bGenerateSkirts; mesher.SkirtCells=static_cast(world.SkirtCells) *static_cast(world.OwnerVoxelSizeMicrons) @@ -2138,6 +2158,21 @@ namespace VoxelForgeCore int64 sampleCount=0;std::vector densityValues; FVoxelMarchingCubesStageTimings McStageTimings; TileMesh out;out.key=key; + const auto CopyIslandCacheMetrics=[&]() + { + if(!options.instrumentTileDrain)return; + out.diagnostics.islandLocalCacheHits=islandCacheInstrumentation.localHits; + out.diagnostics.islandLocalCacheMisses=islandCacheInstrumentation.localMisses; + out.diagnostics.islandLocalCacheConflictReplacements= + islandCacheInstrumentation.localConflictReplacements; + out.diagnostics.islandSharedCacheHits=islandCacheInstrumentation.shared.hits; + out.diagnostics.islandSharedCacheMisses=islandCacheInstrumentation.shared.misses; + out.diagnostics.islandSharedCacheComputes=islandCacheInstrumentation.shared.computes; + out.diagnostics.islandSharedCacheConflictReplacements= + islandCacheInstrumentation.shared.conflictReplacements; + out.diagnostics.islandSharedCacheLockWaitNanoseconds= + islandCacheInstrumentation.shared.lockWaitNanoseconds; + }; out.diagnostics.featureRefinedResolution=bFeatureRefinedResolution; if(options.profileMesherStages) { @@ -2291,6 +2326,7 @@ namespace VoxelForgeCore out.diagnostics.columnCoordinateCaptureComplete=Columns.coordinateCaptureComplete; out.diagnostics.evaluatedColumnCoordinates=Columns.evaluatedCoordinates; } + CopyIslandCacheMetrics(); out.densitySampleCount=0; return out; } @@ -2558,9 +2594,29 @@ namespace VoxelForgeCore out.diagnostics.marchingCubesOtherMilliseconds=McStageTimings.OtherMilliseconds; out.diagnostics.marchingCubesGradientFallbackDensityCalls= McStageTimings.GradientFallbackDensityCalls; + out.diagnostics.marchingCubesGradientCalls=McStageTimings.GradientCalls; out.diagnostics.marchingCubesEdgeMidpointDensityCalls= McStageTimings.EdgeMidpointDensityCalls; + out.diagnostics.marchingCubesEdgeMidpointDensityMemoHits= + McStageTimings.EdgeMidpointDensityMemoHits; out.diagnostics.marchingCubesGradientMemoHits=McStageTimings.GradientMemoHits; + out.diagnostics.marchingCubesX8DensityCalls=McStageTimings.X8DensityCalls; + out.diagnostics.marchingCubesX8ActiveLanes=McStageTimings.X8ActiveLanes; + out.diagnostics.marchingCubesCandidateCells=McStageTimings.CandidateCells; + out.diagnostics.marchingCubesZBandRejectedCells=McStageTimings.ZBandRejectedCells; + out.diagnostics.marchingCubesCellClassificationCalls= + McStageTimings.CellClassificationCalls; + out.diagnostics.marchingCubesSurfaceCellCalls=McStageTimings.SurfaceCellCalls; + out.diagnostics.marchingCubesProofSkippedSolidCells= + McStageTimings.ProofSkippedSolidCells; + out.diagnostics.marchingCubesProofSkippedAirCells= + McStageTimings.ProofSkippedAirCells; + out.diagnostics.marchingCubesProofSkippedDensitySamples= + McStageTimings.ProofSkippedDensitySamples; + out.diagnostics.marchingCubesSkirtEdgeCandidates=McStageTimings.SkirtEdgeCandidates; + out.diagnostics.marchingCubesSkirtBoundaryCandidates= + McStageTimings.SkirtBoundaryCandidates; + out.diagnostics.marchingCubesSkirtDensityProbes=McStageTimings.SkirtDensityProbes; } const double MeshingMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-MeshingStart).count(); @@ -2822,6 +2878,7 @@ namespace VoxelForgeCore } out.diagnostics.meshAssemblyMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-MeshAssemblyStart).count(); + CopyIslandCacheMetrics(); return out; } diff --git a/Standalone/Research/WorldAssemblerM3/Linux55/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux55/REPORT.md new file mode 100644 index 0000000..87f396e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux55/REPORT.md @@ -0,0 +1,101 @@ +# L55 Linux report + +## Result + +Added opt-in headless tile-drain instrumentation and measured the requested kernel A/Bs. The only enabled speed cut is deterministic parallel FI sign-edge repair. It preserves the serial output hashes. Helper cap remains 8; proof size, active-lane batching, and SW midpoint memo remain opt-in/off. + +## Instrumentation + +`--instrument-tile-drain` emits clearly labelled `[CpuTileInstrumentation]`, `[CpuTileTimeline]`, and `[CpuTileStages]` records. Timeline events include queued, dispatch, and finish timestamps per tile, tile/LOD key, worker, queue depth, active workers, rank, and service/wait time. Buffered events are dumped after the run. Stage records cover classification and field sampling, MC grid/cell/gradient/interpolation/dedup, connector and ground/cap work, skirts, and assembly. + +Counters include proof-skipped air/solid cells and avoided samples, separate Z-band rejects, X8 calls/active lanes, `GradAt` calls/memo hits, midpoint attempts/hits, skirt edge candidates/boundary edges/probes/triangles, and FI local/shared cache hits, misses, computes, conflicts, and lock wait. With instrumentation off, paired source-vs-HEAD medians stayed within 2%; the largest observed difference was +1.98% (caves, within run noise). + +## Static all-LOD drain + +Clean static traces are 2,370 tiles per recipe, with no hash or camera-movement work. Worker seconds are summed across workers, not wall time. + +| LOD | SW worker-s | FI worker-s | +|---:|---:|---:| +| 0 | 2.238 | 29.710 | +| 1 | 1.234 | 2.825 | +| 2 | 1.147 | 2.159 | +| 3 | 0.818 | 1.830 | +| 4 | 1.426 | 2.115 | +| 5 | 0.553 | 0.852 | +| 6 | 1.940 | 2.779 | +| 7 | 1.103 | 2.057 | +| 8 | 2.857 | 5.850 | +| **Total** | **13.316** | **50.178** | + +SW’s LOD1–8 work is 11.078 worker-s, led by LOD8 (2.857), LOD6 (1.940), and LOD4 (1.426). This is why an LOD0-only cut cannot get SW to 1 s. Across all LODs, grid work is 5.503 worker-s for SW and 36.414 for FI; skirts are 1.810 and 2.787 worker-s. FI LOD0 alone is 27.610 s grid plus 1.163 s skirts. + +Queue p95 wait/service by priority tier (ms): + +| Recipe | Near 27 | Coarse LOD2+ | Distant LOD0/1 detail | +|---|---:|---:|---:| +| SW | 11.377 / 9.283 | 806.408 / 21.861 | 1101.381 / 8.270 | +| FI | 129.055 / 76.012 | 1576.290 / 26.440 | 4143.439 / 98.045 | + +The last 20 completions are LOD1 for both; the detailed tile keys and worker IDs are in `Saved/Linux/l55/timeline-static/queue-summaries.txt`. Workers were idle while work remained queued for 9.957 ms total in SW and 45.993 ms in FI (maximum observed intervals 0.930 and 13.613 ms). The long tails are work, not a large scheduler-idle gap. + +Representative LOD0 counters: + +| Counter | SW | FI | +|---|---:|---:| +| Candidate / classified cells | 16,777,216 / 16,777,216 | 14,483,456 / 13,591,040 | +| Proof skips (air / solid) | 0 / 0 | 892,416 / 0 | +| X8 calls / active lanes | 3,449,600 / 21,952,000 | 862,400 / 5,488,000 | +| `GradAt` calls / memo hits | 2,061,424 / 1,502,790 | 1,128,688 / 816,926 | +| Midpoint attempts / memo hits | 244,777 / 0 | 39,108 / 0 | +| Skirt edges / boundary / probes / triangles | 1,546,335 / 44,429 / 492,056 / 179,600 | 846,714 / 30,492 / 480,053 / 198,464 | + +Z-band rejects are separate from proof skips: 368,896 cells across LOD3–5 for each recipe. FI LOD0 cache counts: local 34.576M hits, 27.683M misses, 27.597M conflicts; shared 23.951M hits, 3.767M misses, 3.733M computes, 3.696M conflicts, and 150.689 ms lock wait. All-LOD per-tile spans/counters are in `Saved/Linux/l55/timeline-static/sw.log` and `fi.log`. + +## Measured cuts and A/Bs + +- **FI sign-edge repair:** parallel compute plus sorted serial publication reduced the five-pair median sign-edge substage from 535 ms to 47 ms (−487 ms). In the isolated A/B, shape was 0.979→0.481 s, first tile 1.336→0.815 s, near ring 1.496→0.998 s, whole view 5.898→5.462 s (−7.39%). +- **Proof block size:** FI block4 vs block8 changed whole view 6.140→6.380 s (+3.91%); block16 vs block8 was 6.166→6.322 s (+2.53%). Reject both as defaults. Their mesh/payload/topology hashes matched, but density hashes changed, so they remain experimental. +- **Active-lane batching:** FI whole view 6.206→6.173 s (−0.53%, within noise); exact hashes, no default. +- **Exact midpoint memo:** SW LOD0 memo hit 178,257 of 244,777 attempts and reduced interpolation worker time 381→156 ms, but whole view only 1.878→1.870 s (−8 ms, −0.43%); shape/first/near each regressed slightly. No default. +- **Shape helper cap 8→12:** regressed FI whole view 5.732→5.989 s (+4.48%) and SW 1.833→1.839 s (+0.33%). Restored cap 8. + +## Five interleaved before/after pairs + +Five pairs per recipe, baseline HEAD vs final source; table values are medians at 24 m camera steps, in seconds. Movement p50/p95 are milliseconds and are shown at both 8 m and 24 m steps. + +| Recipe | Shape | First tile | Near ring | Whole view | +|---|---:|---:|---:|---:| +| SW | .459→.458 | .538→.533 | .558→.556 | 1.783→1.783 | +| FI | .951→.498 | 1.284→.854 | 1.458→1.039 | 6.090→5.653 | +| Caves | .064→.065 | .139→.134 | .204→.199 | .817→.817 | +| Periodic Hills profile | .471→.462 | .508→.501 | .532→.518 | 1.418→1.420 | + +| Recipe | 8 m moving p50/p95 | 24 m moving p50/p95 | +|---|---:|---:| +| SW | 24.402/47.833→25.125/49.942 | 63.315/114.997→63.278/114.984 | +| FI | 319.059/501.971→316.029/500.346 | 589.648/977.264→594.046/984.720 | +| Caves | 1.314/1.315→1.351/1.352 | 1.382/1.386→1.399/1.401 | +| Periodic Hills profile | 25.766/51.661→26.506/51.856 | 56.703/109.455→57.792/110.586 | + +## Budget to target + +The clean instrumented runs measured SW at 0.404 s shape + 1.186 s tile drain (1.590 s total), and FI after sign-edge repair at 0.445 s shape + 4.454 s tile drain (4.899 s total). These are diagnostic runs; the uninstrumented paired medians above are the score. + +- **SW to 1 s:** tile drain must fall to about 0.596 s. With ideal W12 balance, that permits at most 7.152 worker-s versus 13.316 measured, a cut of 6.164 worker-s (46%). Removing all LOD0 work saves at most 0.186 s of ideal drain. Removing all LOD6–8 work saves at most 0.492 s; both are ceilings and would violate the quality bar if done indiscriminately. Exact cuts must span LOD0 and the far/mid LOD kernels. +- **FI to 2 s:** tile drain must fall to about 1.555 s, or at most 18.660 worker-s at ideal W12 balance versus 50.178 measured (31.518 worker-s, 63%). LOD0 alone costs 2.476 ideal drain seconds; even deleting all LOD1–8 work would leave about 2.921 s including shape. LOD0 must also lose at least 11.05 worker-s (37%) in that impossible best case, so the practical plan needs large LOD0 grid savings plus reductions across other LODs. + +## Proofs, tiers, hashes, and run accounting + +- Fast, medium, and full `verify.py` tiers passed in 153.3 s, 902.8 s, and 1091.2 s. Medium covered LOD0–8, seeded SW/FI/profile at W1/W8/W12, four cave cameras at W1/W8/W12, and captures. Full passed GCC parity, random camera sweep, 30 determinism cells, and five cost pairs each for SW/FI. No visible regression was reported by capture checks. +- FI proof audit: block4 checked 225,438 blocks / 77,325,234 samples; block16 checked 3,324 / 22,799,316; zero violations. Default block8 and brute-force interval checks also passed. The parallel repair hash matrix had 9/9 Clang/GCC W1/W8/W12 comparisons exact, zero differing tiles (2,370 per run). +- No selected output change, so no recipe version bump. FI Clang/W12 parallel whole-view hashes: density `0x55f117f15cd44103`, mesh `0xd987bfc3a5888d04`, payload `0xf04ce3c0b238a8c4`, topology `0xa4601550e27a18a4`. +- Source stamp: `8ee96d6cd3fbb1cd9c3f84d52698f19671c53326b5f87172f8a34c56b5a1a5e9`. Binary SHA256: Clang headless `7bd64ef2930f69079cd8b2f2f3c5d8c21e1cb6e4f81e5b0a3071abf1aff85d11`; GCC headless `1a3326224d6dab92e7e3260a8270d9f6668365a5156b96eae7d7cc40e38b3ba0`; Clang capture `5c51ea92321280ee35ba689d486747c530c28cc20955d4b5ffc4ce5980cf7229`. +- Trial builds: **7/15**. Failed/non-result attempts: initial fast preflight stopped before build because the candidate/TMPDIR was absent; first cave timeline fixture/layout was invalid and was rerun with the canonical caves fixture; first FI block4 invocation failed before the binary started because its output directory was absent. One Clang build succeeded with the generic artifact name and was superseded by the correctly named Clang build. A final summary inspection tried `jq`, which is unavailable; the summaries were read with wrapped Python instead. One earlier read-only JSON inspection used bare `python3` outside the required wrapper; it did no build or benchmark and is recorded as a limit exception. All benchmark/build/verification processes otherwise used the required CPU, address-space, job, time, and TMPDIR limits. No installs, commit, push, or stash. + +## Orchestrator review (Claude) + +Accepted. The stamp matches the final Clang and GCC builds (`8ee96d6c…`); trial count 7 of 15. The instrumentation +is the lasting value: SW is 13.3 worker-s and FI 50.2 worker-s, and their long tails are work, not scheduler idle. +The parallel FI sign-edge repair is exact and moves FI's first tile from 1.28 to 0.85 s. **Lead for next:** the FI +LOD0 column cache shows 27.6 M local conflicts against 34.6 M hits, plus 3.7 M shared conflicts. That's heavy +thrashing on the 27.6 worker-s LOD0 grid path. diff --git a/Standalone/Research/WorldAssemblerM3/Research24/REPORT.md b/Standalone/Research/WorldAssemblerM3/Research24/REPORT.md new file mode 100644 index 0000000..5df00d3 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Research24/REPORT.md @@ -0,0 +1,53 @@ +# R24 — Slab + Sparse-Motif Generator + +## Design + +Reuse the L53 `Heightfield2D` base: `D0 = min(z − h(x,y), c(x,y) − z)`, with `h` and `c` supplied by `flatField`, `periodicHills` or `mirroredRelief`. The L53 profiles already have exact interval checks. Add the court as a radial floor modifier, `h = hL53 + Σ Δj·s((r−rj)/wj)`, where `s(t)=3t²−2t³` is clamped to `[0,1]`. [L53]() [R20]() + +Resolve fixed per-strate motif counts in stable-ID order. Derive each candidate sequence from `(seed, strate, ID)`, check route and mouth keepouts, resolved bands, and pairwise spacing, then fail if no safe position exists. The existing resolver already sorts IDs, uses bounded deterministic attempts and rejects unsafe placement; add the missing pairwise spacing rule. Use 80–240 m monolith spacing for the lone-object feel. [Stamp resolver]() [R43]() + +Use the BVH to prove a tile misses all feature bounds. For candidate hits, evaluate each feature interval and compose in stable-ID order; classify all-air or all-solid only when the **final** interval excludes zero. Include bevel, smoothing and irregularity support in bounds. Extend `isTileShapeFree` for every rock feature; L44 showed that a missing feature envelope can make worker-count results diverge. [R20 interval guidance]() [L44]() + +Keep profile floor/cap surfaces distinct from resolved ceiling and bedrock bands. Use profile intervals to seat features; keep their support inside the resolved content bounds. + +## Motifs, bounds and walkability + +All rock fields use negative-inside SDFs and union into the base with `D=min(D0,Sfeatures)`. Bounds include any extra support from smoothing or irregularity. + +- **Monolith:** Use a vertical capped-cylinder SDF. Let `qr=ρ−R`, `qz=|z−zc|−H/2`, and `S=√(max(qr,0)²+max(qz,0)²)+min(max(qr,qz),0)`. Use `R=0.8–2 m`, `H=12–32 m`; its AABB is `[xc±R]×[yc±R]×[zc±H/2]`. For query tiles, compute exact radial min/max distance to the center and exact vertical distance bounds, then evaluate the monotone SDF at interval endpoints. Set equal X/Y extents if reusing `fillPillar` for a circular column. Keep it scenic: route clearance is at least `R+0.34 m` plus margin. [R20]() [Pillar SDF]() + +- **Ramp fin:** Use `q=|Rᵀ(p−p0)|−a` and `S=length(max(q,0))+min(max(qx,qy,qz),0)`. For world bounds, use half-extents `e=|R|a`; transform the query box to conservative local coordinate intervals for SDF bounds. Suggested dimensions are 8–20 m face width, 1–2 m thickness and 12–32 m rise at 75–90°. The optional ~1 m crest is decorative. Faces and crest are non-walkable; keep a separate bypass at or below 15°. [R20]() + +- **Terrace and pool court:** This is a heightfield modifier, not an SDF stamp. Bound `r` exactly over each tile’s XY box and propagate its range through the smoothsteps; support ends at the outer ring radius. Use 2–5 rings, 0.15–0.4 m rise per ring, 3–6 m transition width, and 4–12 m court radius. Walkability depends on the composed floor grade and capsule support. On flat ground, a 0.4 m rise over 3 m is 11.3°; on the 7.9° periodic profile it can exceed 15°, so widen the transition or route around it. [R20]() [L53]() + +- **Dais/platform:** Use an explicit rounded-box SDF: `q=|Rᵀ(p−p0)|−a+rb`, `S=length(max(q,0))+min(max(q),0)−rb`. Bounds are `p0±|R|a`. Keep platforms decorative until their top joins the route and passes the final-field support check. Current `fillDais` changes from rounded box in vocabulary v1 to ellipsoid in v2, so give platforms an explicit versioned shape kind. The L43 dais half-extents `(3.2, 2.8, 0.9) m` are a visual reference, not a walkability result. [Dais SDF]() [L43]() + +## Water + +Keep water as a separate positive-inside occupancy channel `W`. For the pool, use `Wpool=min(z−hpool(r), zw−z, Rp−r)`. For each fixed stream segment, use `Wstream=min(z−bed(s), zw(s)−z, width(s)/2−dxy)` with end caps; union segments and pool with `max`. Join the stream level to the pool surface and store its direction for the static material. Render a local pool top and stream surface; do not simulate flow. + +The existing water fill is vertical and the renderer creates a player-following, non-colliding horizon plane. Local water needs body records, its own bounds/BVH and tile scheduler so an all-air rock tile can still emit water. Keep the bypass dry. [R20]() [Water renderer]() [Current fill]() + +## Sharp geometry and cost + +At 25 cm sampling, Marching Cubes can bevel a 90° edge by up to about 17.7 cm depending on phase. Encode intentional foot, cap and platform fillets in the geometry: R16 recommends at least 0.5 m at LOD0 and about 0.75 m preferred. Treat `smoothWidth` as a field-space width and measure the resulting geometric radius. Keep physical bevels fixed across LOD; an 8-voxel LOD8 step is 2 m, so a 1.6 m diameter monolith or 1 m crest may disappear without feature-aware LOD retention. [R20]() [R16]() [CGAL 6.3 Isosurfacing manual](https://cgal.github.io/9465/doc/Isosurfacing_3/index.html) + +The existing sharp path is limited to tagged planar boxes at interior LOD0; tile borders and coarse LOD fall back to Marching Cubes, and the full 2:1 transition gate still fails. Gate fins on phase-shifted remeshes, tile borders, LOD transitions and matching collision. R21 also notes that L49’s one-void proposal is unvalidated in this checkout. [M2]() [R21]() + +Expect profile-class cost plus local feature-hit work: L53 profiles measured about 1.33 s cold W12; L54 caves about 0.73 s. L43’s small stamp delta covers no-hit tiles only, so it does not measure motif evaluation. Use ≤1.75 s cold W12 as a provisional target and establish paired on-feature costs. [L53 timings]() [L54 timings]() [L43 cost scope]() + +## Authoring and slices + +Add an optional `CoreGeometry` block to the existing per-strate `UVoxelStrateDefinition` asset. Per strate, author the base profile, fixed motif counts, stable IDs, metre size ranges, candidate regions, spacing/keepouts, quantized pose, attachment policy, bevel and palette IDs. Add pool/stream records for radius, depth, surface, path, width, bed and water palette. Convert metre fields once to integer micrometres at the adapter; include resolved geometry and water records in `stableIdentity`. [R17]() [R8]() [Epic Data Assets](https://dev.epicgames.com/documentation/unreal-engine/data-assets-in-unreal-engine) [UE units](https://dev.epicgames.com/documentation/en-us/unreal-engine/units-of-measurement-in-unreal-engine) + +For each slice, extend `verify.py` with a custom fixture, brute-force interval and BVH checks, false all-air/all-solid controls, a whole-surface audit over feature-bound ROIs, final-field capsule reachability and W1/W8/W12 hash checks. The L48 whole-surface audit is not yet wired into the tiers. [Verify tiers]() [R21]() + +1. **Fast:** `flatField`, one monolith and one decorative platform. Audit monolith foot/cap and platform edges; check route clearance and deterministic placement. +2. **Medium:** add periodic/mirrored profiles and 1–4 fins. Audit fin foot, tip, crest and borders; add actual phase remesh, LOD0–8, 2:1 transition and collision gates. +3. **Full:** add the terrace court, pool and stream. Audit ring/shoreline/stream-bank surfaces, water intervals and wet tiles over rock-all-air regions; include dry-route reachability, Clang/GCC and the 30-cell determinism sweep. + +Capture every slice with a wide eye-level view that keeps the cap and motif in frame, plus a high oblique overview that shows spacing and route. L53’s quality gates passed while its views missed both the hills and cap. [L53 capture review]() + +**Owner question:** Approve an authored ~0.5 m fin-edge radius for the first prototype, or hold fins until the feature-aware cross-LOD path passes? + +Read-only review only; no files changed and no scripts, tests, builds or binaries were run. \ No newline at end of file diff --git a/Standalone/Source/StandaloneCoreScheduler.h b/Standalone/Source/StandaloneCoreScheduler.h index 6eec364..3c89584 100644 --- a/Standalone/Source/StandaloneCoreScheduler.h +++ b/Standalone/Source/StandaloneCoreScheduler.h @@ -14,13 +14,16 @@ class FStandaloneCoreScheduler final : public VoxelForgeCore::JobScheduler { public: + explicit FStandaloneCoreScheduler(std::size_t helperLimit=12) + :HelperLimit(std::clamp(helperLimit,1,12)){} + double parallel_for(std::size_t count,std::size_t batchSize,std::size_t workerLimit, void* context,Task task,double* workerActiveSeconds, std::size_t workerActiveCapacity) const override { if(count==0)return 0.0; const std::size_t workers=std::clamp(workerLimit,1, - std::min(8,std::max(1,count))); + std::min(HelperLimit,std::max(1,count))); const std::size_t grain=std::max(1,batchSize); std::atomic next{0}; std::vector active(workers,0.0); @@ -50,4 +53,7 @@ public: } return total; } + +private: + std::size_t HelperLimit=8; }; diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 8418cb8..3910ed6 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -46,6 +46,13 @@ namespace bool ScreenSpaceGate=false; bool AuditUniformBlockProofs=false; bool ProfileLodWork=false; + bool InstrumentTileDrain=false; + bool ExactMidpointDensityMemo=false; + bool ActiveLaneBatching=false; + bool ParallelSignEdgeRepair=false; + bool HasSignEdgeRepairOverride=false; + int ProofBlockCells=8; + int ShapeHelperLimit=8; bool AuditBasePageEquivalence=false; bool AuditRouteParity=false; bool RegionCrossing=false; @@ -1143,6 +1150,11 @@ namespace " --screen-space-gate (measure display error and historical negative controls)\n" " --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" + " --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" + " --shape-helper-limit=8|12 (default 8; shape build A/B)\n" " --audit-base-page-equivalence (hash one tile through both shape routes)\n" " --audit-route-parity (compare page, native-only, exact-query and mesh routes)\n" " --audit-tile-shape-readiness (run a deliberately mis-marked tile against the native-only guard)\n" @@ -1190,6 +1202,25 @@ namespace else if(arg=="--screen-space-gate")options.ScreenSpaceGate=true; 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=="--exact-midpoint-density-memo")options.ExactMidpointDensityMemo=true; + else if(arg=="--active-lane-batching")options.ActiveLaneBatching=true; + else if(arg=="--parallel-sign-edge-repair") + {options.ParallelSignEdgeRepair=true;options.HasSignEdgeRepairOverride=true;} + else if(arg=="--serial-sign-edge-repair") + {options.ParallelSignEdgeRepair=false;options.HasSignEdgeRepairOverride=true;} + else if(arg.starts_with("--proof-block-cells=")) + { + if(!readInt("--proof-block-cells=",options.ProofBlockCells) + ||options.ProofBlockCells<1||options.ProofBlockCells>32) + {std::fprintf(stderr,"--proof-block-cells must be in 1..32\n");return false;} + } + else if(arg.starts_with("--shape-helper-limit=")) + { + if(!readInt("--shape-helper-limit=",options.ShapeHelperLimit) + ||(options.ShapeHelperLimit!=8&&options.ShapeHelperLimit!=12)) + {std::fprintf(stderr,"--shape-helper-limit must be 8 or 12\n");return false;} + } else if(arg=="--audit-base-page-equivalence")options.AuditBasePageEquivalence=true; else if(arg=="--audit-route-parity")options.AuditRouteParity=true; else if(arg=="--region-crossing")options.RegionCrossing=true; @@ -1751,7 +1782,7 @@ int main(int argc,char** argv) if(!snapshot) {std::fprintf(stderr,"Shape interval build failed: %s\n",error.c_str());return 2;} - FStandaloneCoreScheduler scheduler; + FStandaloneCoreScheduler scheduler(static_cast(options.ShapeHelperLimit)); const FVector cameraStepVoxels=options.HasCameraStep ?FVector(options.CameraStepMeters.X*1000000.0f/std::max(1,ownerWorld->CoreVoxelSizeMicrons), options.CameraStepMeters.Y*1000000.0f/std::max(1,ownerWorld->CoreVoxelSizeMicrons), @@ -1768,5 +1799,8 @@ int main(int argc,char** argv) options.AuditRouteParity,options.AuditResolvedLayoutBoundaries, options.AuditResolvedLayoutPassages,options.AuditStrateTransitions, &resolvedLayout,options.StreamHashTiles,options.AuditLandmarkReachability, - options.AuditOreVeinYield,options.AuditNativeCaves); + options.AuditOreVeinYield,options.AuditNativeCaves,options.InstrumentTileDrain, + options.ExactMidpointDensityMemo,options.ActiveLaneBatching, + options.ProofBlockCells,options.HasSignEdgeRepairOverride + ?options.ParallelSignEdgeRepair:options.FloatingIslandsRecipe); } diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index ced111d..5d092bb 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -1422,7 +1422,10 @@ namespace StandaloneStreamBenchmark bool bAuditStrateTransitions=false, const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout=nullptr, bool bPrintTileHashes=false,bool bAuditLandmarkReachability=false, - bool bAuditOreVeinYield=false,bool bAuditNativeCaves=false) + bool bAuditOreVeinYield=false,bool bAuditNativeCaves=false, + bool bInstrumentTileDrain=false,bool bExactMidpointDensityMemo=false, + bool bActiveLaneBatching=false,int32 ProofBlockCells=8, + bool bParallelSignEdgeRepair=false) { using Clock=std::chrono::steady_clock; FStandaloneOwnerWorld& World=*OwnerWorld; @@ -1575,6 +1578,7 @@ namespace StandaloneStreamBenchmark ShapeRequest.clipMinVoxel={ClipMin.X,ClipMin.Y,ClipMin.Z}; ShapeRequest.clipMaxVoxel={ClipMax.X,ClipMax.Y,ClipMax.Z}; ShapeRequest.workerLimit=static_cast(std::clamp(WorkerCount,1,16)); + ShapeRequest.parallelSignEdgeRepair=bParallelSignEdgeRepair; ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld; ShapeRequest.roundTunnels=bRoundTunnels; ShapeRequest.enhancedTunnelWalls=bEnhancedTunnelWalls; @@ -1582,7 +1586,20 @@ namespace StandaloneStreamBenchmark bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap, FarHorizonLOD8InteriorStepVoxels,bUseSharedColumnCache); Streamer.SetUniformBlockProofAuditEnabled(bAuditUniformBlockProofs); - if(bScreenSpaceGate||bReportLodWorkProfile)Streamer.SetMesherStageProfilingEnabled(true); + Streamer.SetTileDrainInstrumentationEnabled(bInstrumentTileDrain); + Streamer.SetExactMidpointDensityMemoEnabled(bExactMidpointDensityMemo); + Streamer.SetActiveLaneBatchingEnabled(bActiveLaneBatching); + Streamer.SetProofBlockCells(ProofBlockCells); + if(bScreenSpaceGate||bReportLodWorkProfile||bInstrumentTileDrain) + Streamer.SetMesherStageProfilingEnabled(true); + if(bInstrumentTileDrain) + { + Streamer.EnableColdTimelineTrace(ColdStart,nullptr); + std::printf("[CpuTileInstrumentation] enabled=true labeled=true flush=after_run proof_block_cells=%d active_lane_batching=%s midpoint_density_memo=%s parallel_sign_edge_repair=%s\n", + ProofBlockCells,bActiveLaneBatching?"on":"off", + bExactMidpointDensityMemo?"on":"off", + bParallelSignEdgeRepair?"on":"off"); + } const auto StreamStart=Clock::now(); const FStandaloneStreamingUpdate EarlyUpdate=Streamer.StartWithDesired(ShapeFreeDesired); if(EarlyUpdate.DesiredTiles!=static_cast(ShapeFreeDesired.Tiles.size())) @@ -4830,6 +4847,7 @@ namespace StandaloneStreamBenchmark } } Streamer.Stop(); + Streamer.DumpColdTimelineTrace(); bool bScreenSpaceGatePassing=true; if(bScreenSpaceGate) { diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h index 3ea89e7..9be5625 100644 --- a/Standalone/Source/StreamingWorld.h +++ b/Standalone/Source/StreamingWorld.h @@ -41,6 +41,7 @@ struct FStandaloneDesiredTileSet struct FStandaloneCpuTile { FVoxelTileKey Key; + int32 WorkerId=-1; uint64 RequestedCrossing=0; std::vector Vertices; std::vector Indices; @@ -358,6 +359,25 @@ struct FStandaloneTileJob std::chrono::steady_clock::time_point StartedAt{}; }; +struct FStandaloneCpuTileTimelineEvent +{ + enum class EType : uint8 { Queued, Dispatch, Finish }; + EType Type=EType::Queued; + FVoxelTileKey Key; + int32 WorkerId=-1; + size_t QueueDepth=0; + size_t ActiveWorkers=0; + size_t PriorityRank=0; + std::chrono::steady_clock::time_point At{}; + std::chrono::steady_clock::time_point QueuedAt{}; + std::chrono::steady_clock::time_point StartedAt{}; + std::chrono::steady_clock::time_point FinishedAt{}; + double WorkerMilliseconds=-1.0; + uint64 DensitySamples=0; + uint32 SkirtTriangles=0; + VoxelForgeCore::TileGenerationStats Diagnostics{}; +}; + struct FStandaloneTileJobHeapOrder { bool operator()(const FStandaloneTileJob& A,const FStandaloneTileJob& B)const @@ -453,9 +473,45 @@ public: { TimelineOrigin=Origin; TimelineFrameOrdinal=FrameOrdinal; + TimelineQueueEvents.clear(); + TimelineWorkerEvents.clear(); + TimelineWorkerEvents.resize(static_cast(WorkerCount)); bColdTimelineTrace=true; } + void DumpColdTimelineTrace() const + { + if(!bColdTimelineTrace)return; + std::vector Events=TimelineQueueEvents; + size_t Total=Events.size(); + for(const auto& WorkerEvents:TimelineWorkerEvents)Total+=WorkerEvents.size(); + Events.reserve(Total); + for(const auto& WorkerEvents:TimelineWorkerEvents) + Events.insert(Events.end(),WorkerEvents.begin(),WorkerEvents.end()); + std::stable_sort(Events.begin(),Events.end(),[](const auto& A,const auto& B) + {return A.At(TimelineFrame()), + Event.WorkerId,Event.QueueDepth,Event.ActiveWorkers,Event.PriorityRank, + TimelineMilliseconds(Event.QueuedAt),TimelineMilliseconds(Event.StartedAt), + TimelineMilliseconds(Event.FinishedAt), + Event.StartedAt.time_since_epoch().count()!=0 + ?std::chrono::duration(Event.At-Event.StartedAt).count():-1.0, + Event.WorkerMilliseconds); + if(Event.Type==FStandaloneCpuTileTimelineEvent::EType::Finish) + PrintTileStageEvent(Event); + } + std::fflush(stdout); + } + FStandaloneStreamingUpdate Start(const FVector& CameraPositionVoxels, const std::vector& ResidentKeys={}, uint64 CrossingIndex=0) @@ -465,7 +521,7 @@ public: std::lock_guard Lock(Mutex); bStop=false; } - for(int32 I=0;I Lock(Mutex); bStop=false; } - for(int32 I=0;I(ProofBlockCells); Options.auditUniformBlockProofs=bAuditUniformBlockProofs&&!ForceGenerate; Options.diagnosticGlobalOctaveDrop=DiagnosticGlobalOctaveDrop; Options.diagnosticCellStride=std::max(1,DiagnosticCellStride); @@ -928,7 +996,7 @@ private: return Out; } - void WorkerLoop() + void WorkerLoop(int32 WorkerId) { constexpr uint32 PriorityTaskBurst=3; uint32 ConsecutivePriorityTasks=0; @@ -936,6 +1004,7 @@ private: { FStandaloneTileJob Job; std::function PriorityTask; + size_t DispatchQueueDepth=0,DispatchActiveWorkers=0; { std::unique_lock Lock(Mutex); Wake.wait(Lock,[&](){return bStop||!PriorityTasks.empty()||!Queue.empty();}); @@ -952,6 +1021,7 @@ private: std::pop_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{}); Job=Queue.back();Queue.pop_back();Job.StartedAt=FClock::now(); Running.insert(Job.Key);ConsecutivePriorityTasks=0; + DispatchQueueDepth=Queue.size();DispatchActiveWorkers=Running.size(); } } if(PriorityTask) @@ -959,14 +1029,21 @@ private: PriorityTask(); continue; } - if(bColdTimelineTrace)TraceTileEvent("started",Job.Key,Job.StartedAt,Job.QueuedAt,Job.StartedAt); + if(bColdTimelineTrace) + RecordTimelineEvent(FStandaloneCpuTileTimelineEvent::EType::Dispatch,Job.Key, + WorkerId,DispatchQueueDepth,DispatchActiveWorkers,Job.PriorityRank, + Job.StartedAt,Job.QueuedAt,Job.StartedAt); FStandaloneCpuTile Tile=Generate(Job); - if(bColdTimelineTrace)TraceTileEvent("finished",Tile.Key,Tile.CompletedAt, - Tile.QueuedAt,Tile.StartedAt,Tile.CompletedAt,Tile.WorkerSeconds*1000.0); + Tile.WorkerId=WorkerId; { std::lock_guard Lock(Mutex); if(bStop)return; Running.erase(Tile.Key); + if(bColdTimelineTrace) + RecordTimelineEvent(FStandaloneCpuTileTimelineEvent::EType::Finish,Tile.Key, + WorkerId,Queue.size(),Running.size(),Job.PriorityRank,Tile.CompletedAt, + Tile.QueuedAt,Tile.StartedAt,Tile.CompletedAt,Tile.WorkerSeconds*1000.0, + &Tile.CoreDiagnostics,Tile.DensitySamples,Tile.SkirtTriangleCount); Ready.push_back(std::move(Tile)); } } @@ -982,14 +1059,65 @@ private: { return TimelineFrameOrdinal?TimelineFrameOrdinal->load(std::memory_order_relaxed):0; } - void TraceTileEvent(const char* Event,const FVoxelTileKey& Key,FClock::time_point At, - FClock::time_point Queued={},FClock::time_point Started={},FClock::time_point Finished={}, - double WorkerMs=-1.0)const + static void PrintTileStageEvent(const FStandaloneCpuTileTimelineEvent& Event) { - std::printf("[CpuTileTimeline] event=%s key=(%d,%d,%d,lod%d) at_ms=%.3f frame=%llu queued_ms=%.3f started_ms=%.3f finished_ms=%.3f worker_ms=%.3f\n", - Event,Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,TimelineMilliseconds(At), - static_cast(TimelineFrame()),TimelineMilliseconds(Queued), - TimelineMilliseconds(Started),TimelineMilliseconds(Finished),WorkerMs); + const auto& D=Event.Diagnostics; + std::printf("[CpuTileStages] key=(%d,%d,%d,lod%d) worker_id=%d worker_ms=%.3f samples=%llu classify_ms=%.3f column_ms=%.3f column_exclusive_ms=%.3f mc_ms=%.3f mc_grid_ms=%.3f mc_cell_ms=%.3f mc_gradient_ms=%.3f mc_interpolation_ms=%.3f mc_dedup_ms=%.3f mc_skirt_ms=%.3f mc_other_ms=%.3f connector_ms=%.3f ground_ms=%.3f cap_ms=%.3f horizon_ms=%.3f mesh_assembly_ms=%.3f candidate_cells=%llu classified_cells=%llu surface_cells=%llu proof_solid_cells=%llu proof_air_cells=%llu proof_density_samples=%llu zband_reject_cells=%llu x8_calls=%llu x8_active_lanes=%llu grad_calls=%llu grad_memo_hits=%llu grad_fallback_calls=%llu midpoint_attempts=%llu midpoint_memo_hits=%llu skirt_edges=%llu skirt_boundary_edges=%llu skirt_density_probes=%llu skirt_triangles=%u fi_local_hit=%llu fi_local_miss=%llu fi_local_conflicts=%llu fi_shared_hit=%llu fi_shared_miss=%llu fi_shared_computes=%llu fi_shared_conflicts=%llu fi_lock_wait_us=%.3f\n", + Event.Key.Coord.X,Event.Key.Coord.Y,Event.Key.Coord.Z,Event.Key.Level, + Event.WorkerId,Event.WorkerMilliseconds, + static_cast(Event.DensitySamples),D.classificationMilliseconds, + D.columnComputeMilliseconds,D.columnEvaluationExclusiveMilliseconds, + D.marchingCubesExclusiveMilliseconds+D.marchingCubesSkirtMilliseconds, + D.marchingCubesDensityGridMilliseconds,D.marchingCubesCellClassificationMilliseconds, + D.marchingCubesGradientNormalsMilliseconds,D.marchingCubesEdgeInterpolationMilliseconds, + D.marchingCubesVertexDedupStreamMilliseconds,D.marchingCubesSkirtMilliseconds, + D.marchingCubesOtherMilliseconds,D.farConnectorShellMilliseconds, + D.groundRefinementExclusiveMilliseconds,D.capRefinementExclusiveMilliseconds, + D.farHorizonMilliseconds,D.meshAssemblyMilliseconds, + static_cast(D.marchingCubesCandidateCells), + static_cast(D.marchingCubesCellClassificationCalls), + static_cast(D.marchingCubesSurfaceCellCalls), + static_cast(D.marchingCubesProofSkippedSolidCells), + static_cast(D.marchingCubesProofSkippedAirCells), + static_cast(D.marchingCubesProofSkippedDensitySamples), + static_cast(D.marchingCubesZBandRejectedCells), + static_cast(D.marchingCubesX8DensityCalls), + static_cast(D.marchingCubesX8ActiveLanes), + static_cast(D.marchingCubesGradientCalls), + static_cast(D.marchingCubesGradientMemoHits), + static_cast(D.marchingCubesGradientFallbackDensityCalls), + static_cast(D.marchingCubesEdgeMidpointDensityCalls), + static_cast(D.marchingCubesEdgeMidpointDensityMemoHits), + static_cast(D.marchingCubesSkirtEdgeCandidates), + static_cast(D.marchingCubesSkirtBoundaryCandidates), + static_cast(D.marchingCubesSkirtDensityProbes), + Event.SkirtTriangles, + static_cast(D.islandLocalCacheHits), + static_cast(D.islandLocalCacheMisses), + static_cast(D.islandLocalCacheConflictReplacements), + static_cast(D.islandSharedCacheHits), + static_cast(D.islandSharedCacheMisses), + static_cast(D.islandSharedCacheComputes), + static_cast(D.islandSharedCacheConflictReplacements), + static_cast(D.islandSharedCacheLockWaitNanoseconds)/1000.0); + } + void RecordTimelineEvent(FStandaloneCpuTileTimelineEvent::EType Type, + const FVoxelTileKey& Key,int32 WorkerId,size_t QueueDepth,size_t ActiveWorkers, + size_t PriorityRank,FClock::time_point At,FClock::time_point Queued={}, + FClock::time_point Started={},FClock::time_point Finished={},double WorkerMs=-1.0, + const VoxelForgeCore::TileGenerationStats* Diagnostics=nullptr, + uint64 DensitySamples=0,uint32 SkirtTriangles=0) + { + FStandaloneCpuTileTimelineEvent Event; + Event.Type=Type;Event.Key=Key;Event.WorkerId=WorkerId; + Event.QueueDepth=QueueDepth;Event.ActiveWorkers=ActiveWorkers; + Event.PriorityRank=PriorityRank;Event.At=At;Event.QueuedAt=Queued; + Event.StartedAt=Started;Event.FinishedAt=Finished;Event.WorkerMilliseconds=WorkerMs; + Event.DensitySamples=DensitySamples;Event.SkirtTriangles=SkirtTriangles; + if(Diagnostics)Event.Diagnostics=*Diagnostics; + if(WorkerId>=0&&static_cast(WorkerId)(WorkerId)].push_back(Event); + else TimelineQueueEvents.push_back(Event); } FStandaloneOwnerWorld* World=nullptr; inline static std::atomic bCoarseFirstSchedulingEnabled{true}; @@ -1009,6 +1137,10 @@ private: bool bSharedColumnCache=true; int32 Lod8HorizonInteriorStepVoxels=0; bool bProfileMesherStages=false; + bool bInstrumentTileDrain=false; + bool bExactMidpointDensityMemo=false; + bool bActiveLaneBatching=false; + int32 ProofBlockCells=8; bool bAuditUniformBlockProofs=false; std::atomic CacheHitCount{0}; mutable std::mutex Mutex; @@ -1028,4 +1160,6 @@ private: bool bColdTimelineTrace=false; FClock::time_point TimelineOrigin{}; const std::atomic* TimelineFrameOrdinal=nullptr; + std::vector TimelineQueueEvents; + std::vector> TimelineWorkerEvents; }; diff --git a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index b28b134..4e4ff12 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -16,7 +16,9 @@ #include "HAL/IConsoleManager.h" #include "Misc/CommandLine.h" #include +#include #include +#include namespace { @@ -546,7 +548,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, // la borne (un contact purement tangent est exclu — sa traversée serait hors bande). CzLo = FMath::Max(CzLo, FloorDivI((int64)BandZMinVox - OriginVoxels.Z, Step)); CzHi = FMath::Min(CzHi, FloorDivI((int64)BandZMaxVox - OriginVoxels.Z, Step)); - if (CzHi < CzLo) { return MeshData; } // tuile entièrement hors bande → vide + if (CzHi < CzLo) + { + if(OutStageTimings) + { + const uint64 Candidate=static_cast(CellsPerAxis)*CellsPerAxis*CellsPerAxis; + OutStageTimings->CandidateCells=Candidate; + OutStageTimings->ZBandRejectedCells=Candidate; + } + return MeshData; + } // tuile entièrement hors bande → vide } const int32 GzLo = CzLo - 1; // coins CzLo..CzHi+1, gradients ±1 const int32 GzHi = CzHi + 2; // (== -1..GridDim sans bande) @@ -568,9 +579,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, && (bNativeSurfaceTileProof || OutBlockProofAudit != nullptr || bStandaloneShapeProof))); bool bCanSkipProofBlockGrid = Step <= 2 && bStandaloneShapeProof; + const int32 ProofBlockCells=std::clamp(bStandaloneProofBlockCells,1,CellsPerAxis); if (bUseProofBlocks) { - constexpr int32 BlockCells = 8; + const int32 BlockCells = ProofBlockCells; BlocksPerAxis = (CellsPerAxis + BlockCells - 1) / BlockCells; BlockSkip.Init(0, BlocksPerAxis * BlocksPerAxis * BlocksPerAxis); bool bHaveUniformBlock = false; @@ -917,7 +929,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, RequiredGridSamples.SetNumUninitialized(MDim*MDim*MDim); FMemory::Memzero(RequiredGridSamples.GetData(), static_cast(RequiredGridSamples.Num())*sizeof(uint8)); - constexpr int32 BlockCells=8; + const int32 BlockCells=ProofBlockCells; for(int32 BlockZ=0;BlockZ(CellsPerAxis)*CellsPerAxis; + const int32 ActiveZ=FMath::Max(0,CzHi-CzLo+1); + OutStageTimings->CandidateCells=PlaneCells*static_cast(CellsPerAxis); + OutStageTimings->ZBandRejectedCells=PlaneCells + *static_cast(CellsPerAxis-ActiveZ); + if(bSkipProofBlockGrid) + { + for(int32 Z=GzLo;Z<=GzHi;++Z) + for(int32 Y=-1;Y<=GridDim;++Y) + for(int32 X=-1;X<=GridDim;++X) + { + const int32 Index=((Z+1)*MDim+(Y+1))*MDim+(X+1); + if(RequiredGridSamples[Index]==0) + ++OutStageTimings->ProofSkippedDensitySamples; + } + } + } const bool bCapturePreEditGrid = OutPreEditDensityGrid != nullptr && !bUseSharedDensityGrid && Step == 1 && CellsPerAxis == CHUNK_SIZE @@ -1199,7 +1230,35 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, &&ActiveShapeWorld->IsAnalyticOnly() &&!bUseSharedDensityGrid&&!bCapturePreEditGrid&&!bSkipProofBlockGrid; const bool bUseShapeWorldXBatch=ActiveShapeWorld!=nullptr - &&!bUseSharedDensityGrid&&!bCapturePreEditGrid&&!bSkipProofBlockGrid; + &&!bUseSharedDensityGrid&&!bCapturePreEditGrid + &&(!bSkipProofBlockGrid||bStandaloneEnableActiveLaneBatching); + const auto SampleShapeWorldXBatch=[&](int32 GX0,int32 BatchCount,int32 gy,int32 gz) + { + float Densities[8]{}; + ActiveShapeWorld->DensityAtLatticeX8(OriginVoxels.X+GX0*Step,Step, + OriginVoxels.Y+gy*Step,OriginVoxels.Z+gz*Step,BatchCount,Densities); + if(OutStageTimings) + { + ++OutStageTimings->X8DensityCalls; + OutStageTimings->X8ActiveLanes+=static_cast(BatchCount); + } + for(int32 Lane=0;Lane& YGroup:YGroups) @@ -1277,6 +1336,26 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, { for(size_t Offset=0;Offset(BatchCount)(BatchCount)]==GX0+BatchCount) + { + const int32 NextGX=XGroup[Offset+static_cast(BatchCount)]; + const int32 NextIndex=((gz+1)*MDim+(gy+1))*MDim+(NextGX+1); + if(RequiredGridSamples[NextIndex]==0)break; + ++BatchCount; + } + if(BatchCount<2)SampleGridPoint(GX0,gy,gz); + else SampleShapeWorldXBatch(GX0,BatchCount,gy,gz); + Offset+=static_cast(BatchCount); + continue; + } const int32 BatchCount=static_cast(std::min( 8,XGroup.size()-Offset)); const int32 GX0=XGroup[Offset]; @@ -1291,30 +1370,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, continue; } - float Densities[8]{}; - ActiveShapeWorld->DensityAtLatticeX8( - OriginVoxels.X+GX0*Step,Step, - OriginVoxels.Y+gy*Step,OriginVoxels.Z+gz*Step, - BatchCount,Densities); - for(int32 Lane=0;Lane(BatchCount); } } @@ -1398,7 +1454,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, // cubes loop will consume. This is intentionally redundant with ClassifyTile: it is a // cheap soundness fence around the proof/cache hand-off. Unknown, zero, or opposite-sign // vertices clear the candidate, so an unproven block pays the ordinary cell path. - constexpr int32 BlockCells = 8; + const int32 BlockCells = ProofBlockCells; for (int32 BlockZ = 0; BlockZ < BlocksPerAxis; ++BlockZ) { const int32 BlockMinZ = BlockZ * BlockCells; @@ -1454,6 +1510,31 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, } } } + if(OutStageTimings&&bUseProofBlocks) + { + for(int32 BlockZ=0;BlockZ(XCount)*YCount*ZCount; + if(Reason==1)OutStageTimings->ProofSkippedSolidCells+=Skipped; + else if(Reason==2)OutStageTimings->ProofSkippedAirCells+=Skipped; + } + } + } // ── CAPTURE-DURING-MESHING ── // Si demandé et que la tuile est pleine résolution (CellsPerAxis==CHUNK_SIZE ⇒ Step==1< FVector { + if(OutStageTimings)++OutStageTimings->GradientCalls; const int32 CacheIndex=((gz+1)*MDim+(gy+1))*MDim+(gx+1); if(bUseGradientMemo&&GradientMemoValid[CacheIndex]!=0) { @@ -1577,6 +1659,25 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, uint64 AuditCellClassificationCycles=0,AuditGradientCycles=0; uint64 AuditInterpolationCycles=0,AuditDedupStreamCycles=0; uint64 AuditGradientSamples=0,AuditInterpolationSamples=0,AuditDedupStreamSamples=0; + struct FMidpointDensityKey + { + uint64 X=0,Y=0,Z=0; + bool operator==(const FMidpointDensityKey&)const=default; + }; + struct FMidpointDensityKeyHash + { + size_t operator()(const FMidpointDensityKey& Key)const noexcept + { + uint64 Hash=Key.X^std::rotl(Key.Y,21)^std::rotl(Key.Z,42); + Hash^=Hash>>30;Hash*=0xbf58476d1ce4e5b9ull; + Hash^=Hash>>27;Hash*=0x94d049bb133111ebull; + Hash^=Hash>>31; + return static_cast(Hash); + } + }; + std::unordered_map MidpointDensityMemo; + if(bStandaloneEnableExactMidpointDensityMemo&&ShapeWorldContext&&Step==1) + MidpointDensityMemo.reserve(1024); uint8 CaseIndexRow[CHUNK_SIZE]{}; uint8 SurfaceRow[CHUNK_SIZE]{}; uint8 SampleClassificationRow[CHUNK_SIZE]{}; @@ -1593,14 +1694,21 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, for (int32 GroupX = 0; GroupX < CellsPerAxis; GroupX += 8) { const int32 GroupCount = FMath::Min(8, CellsPerAxis - GroupX); - if (bUseProofBlocks - && BlockSkip[((cz / 8) * BlocksPerAxis + (cy / 8)) * BlocksPerAxis - + (GroupX / 8)] != 0) + bool bAnySkipped=false,bAllSkipped=bUseProofBlocks; + if(bUseProofBlocks) { - continue; + for(int32 Lane=0;LaneEdgeMidpointDensityCalls; - const float MidDensity=ShapeWorldContext->DensityAtUnclamped( - Midpoint.X/GeometryVoxelSize,Midpoint.Y/GeometryVoxelSize, - Midpoint.Z/GeometryVoxelSize); + const double MidX=Midpoint.X/GeometryVoxelSize; + const double MidY=Midpoint.Y/GeometryVoxelSize; + const double MidZ=Midpoint.Z/GeometryVoxelSize; + float MidDensity=0.0f; + if(bStandaloneEnableExactMidpointDensityMemo) + { + const FMidpointDensityKey Key{std::bit_cast(MidX), + std::bit_cast(MidY),std::bit_cast(MidZ)}; + const auto Found=MidpointDensityMemo.find(Key); + if(Found!=MidpointDensityMemo.end()) + { + MidDensity=Found->second; + if(OutStageTimings) + ++OutStageTimings->EdgeMidpointDensityMemoHits; + } + else + { + MidDensity=ShapeWorldContext->DensityAtUnclamped(MidX,MidY,MidZ); + MidpointDensityMemo.emplace(Key,MidDensity); + } + } + else MidDensity=ShapeWorldContext->DensityAtUnclamped(MidX,MidY,MidZ); if(FMath::Abs(MidDensity-IsoLevel)<1.0e-6f) T=0.5f; else if((MidDensity SurfaceVertexDensitySamples; + const auto CountSkirtDensityProbe=[&]() + {if(OutStageTimings)++OutStageTimings->SkirtDensityProbes;}; if(ShapeWorldContext) SurfaceVertexDensitySamples.assign(static_cast(MeshData.Vertices.Num()), std::numeric_limits::quiet_NaN()); @@ -1945,12 +2080,18 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, { if(!ShapeWorldContext)return 0.0f; if(OriginalIndex<0||static_cast(OriginalIndex)>=SurfaceVertexDensitySamples.size()) + { + CountSkirtDensityProbe(); return ShapeWorldContext->DensityAt(Position.X/GeometryVoxelSize, Position.Y/GeometryVoxelSize,Position.Z/GeometryVoxelSize); + } float& Density=SurfaceVertexDensitySamples[static_cast(OriginalIndex)]; if(std::isnan(Density)) + { + CountSkirtDensityProbe(); Density=ShapeWorldContext->DensityAt(Position.X/GeometryVoxelSize, Position.Y/GeometryVoxelSize,Position.Z/GeometryVoxelSize); + } return Density; }; @@ -1974,8 +2115,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, { FSkirtVertex Vertex{Position,Normal,Color,0.0f,OriginalIndex}; if(!bOnSurface&&ShapeWorldContext) + { + CountSkirtDensityProbe(); Vertex.Density=ShapeWorldContext->DensityAt(Position.X/GeometryVoxelSize, Position.Y/GeometryVoxelSize,Position.Z/GeometryVoxelSize); + } return Vertex; }; const auto InterpolateVertex=[&](const FSkirtVertex& A,const FSkirtVertex& B,float T) @@ -1990,10 +2134,13 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, static_cast(FMath::Clamp(FMath::RoundToInt(FMath::Lerp(static_cast(A.Color.G),static_cast(B.Color.G),T)),0,255)), static_cast(FMath::Clamp(FMath::RoundToInt(FMath::Lerp(static_cast(A.Color.B),static_cast(B.Color.B),T)),0,255)), static_cast(FMath::Clamp(FMath::RoundToInt(FMath::Lerp(static_cast(A.Color.A),static_cast(B.Color.A),T)),0,255))); - Out.Density=ShapeWorldContext - ?ShapeWorldContext->DensityAt(Out.Position.X/GeometryVoxelSize, - Out.Position.Y/GeometryVoxelSize,Out.Position.Z/GeometryVoxelSize) - :FMath::Lerp(A.Density,B.Density,T); + if(ShapeWorldContext) + { + CountSkirtDensityProbe(); + Out.Density=ShapeWorldContext->DensityAt(Out.Position.X/GeometryVoxelSize, + Out.Position.Y/GeometryVoxelSize,Out.Position.Z/GeometryVoxelSize); + } + else Out.Density=FMath::Lerp(A.Density,B.Density,T); Out.OriginalIndex=INDEX_NONE; return Out; }; @@ -2052,6 +2199,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, const FVector Position=P0.Position*static_cast(WA) +P1.Position*static_cast(WB) +P2.Position*static_cast(WC); + CountSkirtDensityProbe(); return ShapeWorldContext->DensityAt(Position.X/GeometryVoxelSize, Position.Y/GeometryVoxelSize,Position.Z/GeometryVoxelSize); }; @@ -2101,6 +2249,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, { const FVector Center=(P0.Position+P1.Position+P2.Position) /(3.0f*GeometryVoxelSize); + CountSkirtDensityProbe(); if(ShapeWorldContext->DensityAt(Center.X,Center.Y,Center.Z)>0.0f)continue; } Tris.Add(PolygonIndices[0]);Tris.Add(PolygonIndices[Fan]);Tris.Add(PolygonIndices[Fan+1]); @@ -2113,12 +2262,14 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, const int32 Tri[3] = { Tris[t], Tris[t + 1], Tris[t + 2] }; for (int32 e = 0; e < 3; ++e) { + if(OutStageTimings)++OutStageTimings->SkirtEdgeCandidates; const int32 iA = Tri[e], iB = Tri[(e + 1) % 3]; // COPIES par valeur — AddSkirtVert réalloue Vertices/Normals (invaliderait des refs). const FVector PA = MeshData.Vertices[iA]; const FVector PB = MeshData.Vertices[iB]; const uint8 FaceMask=BoundaryFaceMask(PA,PB); if (!FaceMask) continue; + if(OutStageTimings)++OutStageTimings->SkirtBoundaryCandidates; const FVector NA = MeshData.Normals[iA]; const FVector NB = MeshData.Normals[iB]; @@ -2183,6 +2334,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, if(OutStageTimings) { + OutStageTimings->CellClassificationCalls=CellClassificationCalls; + OutStageTimings->SurfaceCellCalls=SurfaceCellCalls; const double CycleToMs=FPlatformTime::GetSecondsPerCycle64()*1000.0; const auto ScaleSampledCycles=[CycleToMs](uint64 Cycles,uint64 Samples,uint64 Calls) { diff --git a/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h b/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h index 1450776..503caed 100644 --- a/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h +++ b/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h @@ -53,8 +53,22 @@ struct FVoxelMarchingCubesStageTimings double SkirtMilliseconds = 0.0; double OtherMilliseconds = 0.0; uint64 GradientFallbackDensityCalls = 0; + uint64 GradientCalls = 0; uint64 EdgeMidpointDensityCalls = 0; + uint64 EdgeMidpointDensityMemoHits = 0; uint64 GradientMemoHits = 0; + uint64 X8DensityCalls = 0; + uint64 X8ActiveLanes = 0; + uint64 CandidateCells = 0; + uint64 ZBandRejectedCells = 0; + uint64 CellClassificationCalls = 0; + uint64 SurfaceCellCalls = 0; + uint64 ProofSkippedSolidCells = 0; + uint64 ProofSkippedAirCells = 0; + uint64 ProofSkippedDensitySamples = 0; + uint64 SkirtEdgeCandidates = 0; + uint64 SkirtBoundaryCandidates = 0; + uint64 SkirtDensityProbes = 0; }; // Opt-in soundness audit for density blocks which the Core mesher would skip. @@ -192,6 +206,9 @@ public: bool bStandaloneSkipColors = false; bool bStandaloneSkipMaterialGroups = false; bool bStandaloneEnableGradientMemoization = true; + bool bStandaloneEnableExactMidpointDensityMemo = false; + bool bStandaloneEnableActiveLaneBatching = false; + int32 bStandaloneProofBlockCells = 8; // Seuil de l'isosurface dans le champ de densité. // Convention MC: densité < IsoLevel = solide, >= = air.