From 87b0e5d8cdacc42901cf3671b5dd918c318ef21a Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 19 Oct 2026 15:40:25 +0200 Subject: [PATCH] wip(assembler): round S13 -- tile cost counted per LOD (diagnostics only, no output change) Exclusive per-stage tile timers and probes. Findings: LOD0 is MC-exclusive (10.4 of 14 ms p95, 243 ns/sample); LOD4's 3x cost is column-cache misses (8,224 vs ~1,600-1,900 at LOD3/5), cap refine secondary; per-tile feature lists cost ~0.001 ms; cross-worker column repeats common (277k at LOD0). Global octave drop and stride-2 interpolation probes fail the 2 cm gate. Worker cap 1 kept (cap 0 worse 40 m/s tick tail). Report: Standalone/Research/WorldAssemblerM3/Round17. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- ARCHITECTURE.md | 22 + CODEMAP.md | 12 + .../Private/VoxelForgeExploreCommandlet.cpp | 380 ++++++++++++++++-- .../include/VoxelForgeCore/VoxelForgeCore.h | 7 +- Standalone/Core/src/VoxelForgeCore.cpp | 27 +- Standalone/Core/src/WorldAssemblerInternal.h | 17 +- .../WorldAssemblerM3/Round17/REPORT.md | 277 +++++++++++++ Standalone/Source/StandaloneViewer.cpp | 89 ++-- Standalone/Source/StreamingWorld.h | 8 +- 9 files changed, 763 insertions(+), 76 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Round17/REPORT.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b7dd3d6..8cf4038 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1756,6 +1756,28 @@ near and `BackgroundNormal` for horizon tasks. The task-pickup changes do not al triangulation, collision, or Core output, so no recipe version changes. See [`Standalone/Research/WorldAssemblerM3/Round16/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round16/REPORT.md). +### 8.34 Tile cost and column reuse audit — World Assembler S13 / M3 Round 17 + +The owner SurfaceWorld uses 32 marching-cubes cells only at LOD0–1; LOD2 and farther use 16 +coarse cells. Including the normal halo, those are 35³ and 19³ density samples, respectively. +Round 17 added exclusive timing for classification, column evaluations during MC/ground/cap, +non-column MC/refinement work, and mesh assembly. Percentiles are independent. The measured +owner route is a planar `Heightfield2D` surface: its recipe noise is evaluated while computing XY +columns, with no volumetric cave, tunnel, room, or route density term. The two-tile interpolation +probe reduced measured column-compute time by about 71–74% at stride two, but exceeded the 2 cm error gate at +LOD1 and every tested coarse LOD; stride four also exceeded it on LOD0. It had no near-zero fallback +and was not enabled in generation. + +LOD4 is the first far-cap refinement level: its cap grid is 16 voxels (4 m), while LOD5–7 use the +128-voxel boundary grid (32 m); ground refinement begins at LOD5. LOD4's cap pass costs about +4.87 ms of column evaluation plus 1.26 ms exclusive at the eight-tile p95, but its largest measured +cost is 29.36 ms of column evaluation during MC. LOD4 also has 8,224 column-cache misses per sampled +tile p95 versus 1,634 at LOD3 and 1,916 at LOD5. The cache remains thread-local. The all-LOD overlap +probe found substantial cross-worker duplicate XY evaluations, which is evidence for a future +shared-page A/B test, not proof that a shared cache will pay after synchronization and lifetime +costs. Round 17 changed no density, mesh, collision, or recipe output; recipe versions remain +unchanged. See [`Standalone/Research/WorldAssemblerM3/Round17/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round17/REPORT.md). + ## 9. Multiplayer model (listen-server first, dedicated-friendly) > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a diff --git a/CODEMAP.md b/CODEMAP.md index 9bb49d9..6acaec4 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -1083,6 +1083,18 @@ submit/start/end times and the OS worker thread ID. No tile generation, mesh, de Core recipe output changes; recipe versions remain unchanged. See ARCHITECTURE §8.33 and [`Standalone/Research/WorldAssemblerM3/Round16/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round16/REPORT.md). +### 3.19 Exclusive tile cost and all-LOD column overlap — World Assembler S13 / M3 Round 17 + +`VoxelForgeCore::TileGenerationStats` now exposes classification-exclusive time and the column +evaluation performed during marching cubes, ground refinement, and cap refinement. The BuildHost +shared-feature audit reports these p95 stages and per-density-sample costs for owner SurfaceWorld +tiles at LOD0, 1, 3, 4, 5, and 8. Standalone `--profile-column-overlap` opts into XY miss-coordinate +capture for all LODs and reports repeated evaluations across same-LOD tiles and workers; the older +`--profile-far-column-overlap` spelling remains an alias. `--profile-column-overlap` changes no +generation output. Measurement-only stride and octave probes are confined to the shared-feature +audit and are not runtime controls. The result and limits are recorded in +[`Standalone/Research/WorldAssemblerM3/Round17/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round17/REPORT.md). + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index 3a5aa9b..57e68d9 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -15940,13 +15940,282 @@ bool RunCoreSharedFeatureCostAudit(VoxelForgeCore::World& World, const std::vector& Keys,const TCHAR* Label, const VoxelForgeCore::WorldAssemblyManifest* Assembly,FString& OutError) { - for (const int32 LOD : {0,1,7,8}) + if(Assembly!=nullptr&&FCString::Stricmp(Label,TEXT("surface"))==0) + { + struct FProbeColumn + { + std::int64_t Ground=0; + std::int64_t Cap=0; + }; + const auto P95=[](TArray Values) + { Values.Sort(); return Values.IsEmpty() ? 0.0 : Values[FMath::Clamp( + FMath::CeilToInt(Values.Num()*0.95f)-1,0,Values.Num()-1)]; }; + const auto MaxValue=[](const TArray& Values) + { return Values.IsEmpty() ? 0.0 : *std::max_element(Values.GetData(),Values.GetData()+Values.Num()); }; + const std::int32_t TileVoxelCount=static_cast( + World.configuration().tileVoxelCount); + constexpr std::int64_t Subvoxels=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + constexpr double CoreVoxelSizeCm=25.0; + for(const std::int32_t LOD:{0,1,3,4,5,8}) + { + for(const std::int32_t Stride:{2,4}) + { + const std::int32_t Cells=LOD GroundErrorsCm,CapErrorsCm,BaselineColumnMs,CandidateColumnMs; + std::uint64_t BaselineColumns=0,CandidateColumns=0,FiniteFeatureTiles=0; + std::int32_t Tiles=0; + for(const auto& Key:Keys) + { + if(Key.lod!=LOD||Tiles>=2)continue; + const std::int64_t TileExtent=static_cast(TileVoxelCount)<(Key.coordinate.x)*TileExtent; + const std::int64_t OriginY=static_cast(Key.coordinate.y)*TileExtent; + const std::int64_t OriginZ=static_cast(Key.coordinate.z)*TileExtent; + const std::int64_t TileMinimumZ=OriginZ; + const std::int64_t TileMaximumZ=OriginZ+TileExtent; + const auto PlanIt=std::find_if(Assembly->plan.strates.begin(), + Assembly->plan.strates.end(),[TileMinimumZ,TileMaximumZ,TileVoxelCount](const auto& Plan) + { + const std::int64_t PlanMinimumZ=static_cast( + std::min(Plan.bottomChunkZ,Plan.topChunkZ))*TileVoxelCount; + const std::int64_t PlanMaximumZ=(static_cast( + std::max(Plan.bottomChunkZ,Plan.topChunkZ))+1)*TileVoxelCount; + return TileMinimumZ<=PlanMaximumZ&&TileMaximumZ>=PlanMinimumZ; + }); + if(PlanIt==Assembly->plan.strates.end())continue; + + const VoxelForgeCore::Heightfield2D Field(Assembly->recipe,*PlanIt); + if(!Field.isValid())continue; + const std::array Minimum={ + (OriginX-TileStep)*Subvoxels,(OriginY-TileStep)*Subvoxels, + (OriginZ-TileStep)*Subvoxels}; + const std::array Maximum={ + (OriginX+TileExtent+TileStep)*Subvoxels, + (OriginY+TileExtent+TileStep)*Subvoxels, + (OriginZ+TileExtent+TileStep)*Subvoxels}; + std::vector Queries; + Queries.push_back({Field.recipe(),Field.plan(),Field.buildFeatureTileList(Minimum,Maximum)}); + const VoxelForgeCore::ScopedWorldAssemblyFeatureQueriesV1 QueryScope{ + std::span(Queries)}; + const bool bHasFiniteFeature=std::any_of(PlanIt->features.begin(), + PlanIt->features.end(),[&](const auto& Feature) + { + return (Feature.type==2||Feature.type==4) + &&Feature.minimumSubvoxels[0]<=Maximum[0] + &&Feature.maximumSubvoxels[0]>=Minimum[0] + &&Feature.minimumSubvoxels[1]<=Maximum[1] + &&Feature.maximumSubvoxels[1]>=Minimum[1] + &&Feature.minimumSubvoxels[2]<=Maximum[2] + &&Feature.maximumSubvoxels[2]>=Minimum[2]; + }); + if(bHasFiniteFeature)++FiniteFeatureTiles; + + std::vector Exact(static_cast(Cells+1)*(Cells+1)); + VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true); + for(std::int32_t Y=0;Y<=Cells;++Y) + for(std::int32_t X=0;X<=Cells;++X) + { + const auto Sample=Field.point((OriginX+X*TileStep)*Subvoxels, + (OriginY+Y*TileStep)*Subvoxels,0); + Exact[static_cast(Y)*(Cells+1)+X]={ + Sample.groundHeightSubvoxels,Sample.capHeightSubvoxels}; + } + const auto& BaselineStats=VoxelForgeCore::Heightfield2D::threadColumnCacheStats(); + BaselineColumnMs.Add(BaselineStats.columnComputeMilliseconds); + BaselineColumns+=BaselineStats.columnMisses; + + std::vector Axis; + for(std::int32_t Index=0;Index<=Cells;Index+=Stride)Axis.push_back(Index); + if(Axis.empty()||Axis.back()!=Cells)Axis.push_back(Cells); + std::vector Coarse(static_cast(Axis.size())*Axis.size()); + VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true); + for(std::size_t Y=0;Y(Upper-Axis.begin()); + const std::size_t Low=High==0?0:High-1; + return std::pair{Low,High}; + }; + const auto Lerp=[](double A,double B,double T){return A+(B-A)*T;}; + for(std::int32_t Y=0;Y<=Cells;++Y) + for(std::int32_t X=0;X<=Cells;++X) + { + const auto [X0,X1]=Bracket(X); + const auto [Y0,Y1]=Bracket(Y); + const double TX=Axis[X1]==Axis[X0]?0.0 + :static_cast(X-Axis[X0])/(Axis[X1]-Axis[X0]); + const double TY=Axis[Y1]==Axis[Y0]?0.0 + :static_cast(Y-Axis[Y0])/(Axis[Y1]-Axis[Y0]); + const auto& C00=Coarse[Y0*Axis.size()+X0]; + const auto& C10=Coarse[Y0*Axis.size()+X1]; + const auto& C01=Coarse[Y1*Axis.size()+X0]; + const auto& C11=Coarse[Y1*Axis.size()+X1]; + const double Ground=Lerp(Lerp(static_cast(C00.Ground), + static_cast(C10.Ground),TX),Lerp(static_cast(C01.Ground), + static_cast(C11.Ground),TX),TY); + const double Cap=Lerp(Lerp(static_cast(C00.Cap), + static_cast(C10.Cap),TX),Lerp(static_cast(C01.Cap), + static_cast(C11.Cap),TX),TY); + const auto& Reference=Exact[static_cast(Y)*(Cells+1)+X]; + GroundErrorsCm.Add(std::abs(Ground-Reference.Ground)*CoreVoxelSizeCm/Subvoxels); + CapErrorsCm.Add(std::abs(Cap-Reference.Cap)*CoreVoxelSizeCm/Subvoxels); + } + ++Tiles; + } + std::uint64_t GroundOver2Cm=0,CapOver2Cm=0; + for(const double Error:GroundErrorsCm)if(Error>2.0)++GroundOver2Cm; + for(const double Error:CapErrorsCm)if(Error>2.0)++CapOver2Cm; + double BaselineAverageMs=0.0,CandidateAverageMs=0.0; + for(const double Value:BaselineColumnMs)BaselineAverageMs+=Value; + for(const double Value:CandidateColumnMs)CandidateAverageMs+=Value; + if(!BaselineColumnMs.IsEmpty())BaselineAverageMs/=BaselineColumnMs.Num(); + if(!CandidateColumnMs.IsEmpty())CandidateAverageMs/=CandidateColumnMs.Num(); + UE_LOG(LogTemp,Display, + TEXT("[WorldAssemblerInterpolationProbe] label=%s lod=%d tiles=%d stride_samples=%d exact_column_evals=%llu coarse_column_evals=%llu column_eval_saving_pct=%.2f baseline_column_avg_ms=%.5f coarse_column_avg_ms=%.5f ground_error_p95_cm=%.4f ground_error_max_cm=%.4f ground_over_2cm=%llu/%d cap_error_p95_cm=%.4f cap_error_max_cm=%.4f cap_over_2cm=%llu/%d finite_feature_tiles=%llu fallback=none"), + TEXT("surface"),LOD,Tiles,Stride, + static_cast(BaselineColumns), + static_cast(CandidateColumns), + BaselineAverageMs>0.0?100.0*(1.0-CandidateAverageMs/BaselineAverageMs):0.0, + BaselineAverageMs,CandidateAverageMs, + P95(GroundErrorsCm),MaxValue(GroundErrorsCm), + static_cast(GroundOver2Cm),GroundErrorsCm.Num(), + P95(CapErrorsCm),MaxValue(CapErrorsCm), + static_cast(CapOver2Cm),CapErrorsCm.Num(), + static_cast(FiniteFeatureTiles)); + } + } + for(const std::int32_t LOD:{0,1,3,4,5,8}) + for(const std::uint32_t Drop:{1u,2u}) + { + const std::int32_t Cells=LOD GroundErrorsCm,CapErrorsCm,BaselineColumnMs,CandidateColumnMs; + std::uint64_t BaselineColumns=0,CandidateColumns=0,FiniteFeatureTiles=0; + std::int32_t Tiles=0; + for(const auto& Key:Keys) + { + if(Key.lod!=LOD||Tiles>=2)continue; + const std::int64_t TileExtent=static_cast(TileVoxelCount)<(Key.coordinate.x)*TileExtent; + const std::int64_t OriginY=static_cast(Key.coordinate.y)*TileExtent; + const std::int64_t OriginZ=static_cast(Key.coordinate.z)*TileExtent; + const std::int64_t TileMinimumZ=OriginZ,TileMaximumZ=OriginZ+TileExtent; + const auto PlanIt=std::find_if(Assembly->plan.strates.begin(), + Assembly->plan.strates.end(),[TileMinimumZ,TileMaximumZ,TileVoxelCount](const auto& Plan) + { + const std::int64_t PlanMinimumZ=static_cast( + std::min(Plan.bottomChunkZ,Plan.topChunkZ))*TileVoxelCount; + const std::int64_t PlanMaximumZ=(static_cast( + std::max(Plan.bottomChunkZ,Plan.topChunkZ))+1)*TileVoxelCount; + return TileMinimumZ<=PlanMaximumZ&&TileMaximumZ>=PlanMinimumZ; + }); + if(PlanIt==Assembly->plan.strates.end())continue; + const VoxelForgeCore::Heightfield2D Field(Assembly->recipe,*PlanIt); + if(!Field.isValid())continue; + const std::array Minimum={ + (OriginX-TileStep)*Subvoxels,(OriginY-TileStep)*Subvoxels, + (OriginZ-TileStep)*Subvoxels}; + const std::array Maximum={ + (OriginX+TileExtent+TileStep)*Subvoxels, + (OriginY+TileExtent+TileStep)*Subvoxels, + (OriginZ+TileExtent+TileStep)*Subvoxels}; + std::vector Queries; + Queries.push_back({Field.recipe(),Field.plan(),Field.buildFeatureTileList(Minimum,Maximum)}); + const VoxelForgeCore::ScopedWorldAssemblyFeatureQueriesV1 QueryScope{ + std::span(Queries)}; + const bool bHasFiniteFeature=std::any_of(PlanIt->features.begin(), + PlanIt->features.end(),[&](const auto& Feature) + { + return (Feature.type==2||Feature.type==4) + &&Feature.minimumSubvoxels[0]<=Maximum[0] + &&Feature.maximumSubvoxels[0]>=Minimum[0] + &&Feature.minimumSubvoxels[1]<=Maximum[1] + &&Feature.maximumSubvoxels[1]>=Minimum[1] + &&Feature.minimumSubvoxels[2]<=Maximum[2] + &&Feature.maximumSubvoxels[2]>=Minimum[2]; + }); + if(bHasFiniteFeature)++FiniteFeatureTiles; + std::vector Exact(static_cast(Cells+1)*(Cells+1)); + VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true,0); + for(std::int32_t Y=0;Y<=Cells;++Y) + for(std::int32_t X=0;X<=Cells;++X) + { + const auto Sample=Field.point((OriginX+X*TileStep)*Subvoxels, + (OriginY+Y*TileStep)*Subvoxels,0); + Exact[static_cast(Y)*(Cells+1)+X]={ + Sample.groundHeightSubvoxels,Sample.capHeightSubvoxels}; + } + const auto& BaselineStats=VoxelForgeCore::Heightfield2D::threadColumnCacheStats(); + BaselineColumnMs.Add(BaselineStats.columnComputeMilliseconds); + BaselineColumns+=BaselineStats.columnMisses; + VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true,Drop); + for(std::int32_t Y=0;Y<=Cells;++Y) + for(std::int32_t X=0;X<=Cells;++X) + { + const auto Sample=Field.point((OriginX+X*TileStep)*Subvoxels, + (OriginY+Y*TileStep)*Subvoxels,0); + GroundErrorsCm.Add(std::abs(static_cast(Sample.groundHeightSubvoxels) + -Exact[static_cast(Y)*(Cells+1)+X].Ground) + *CoreVoxelSizeCm/Subvoxels); + CapErrorsCm.Add(std::abs(static_cast(Sample.capHeightSubvoxels) + -Exact[static_cast(Y)*(Cells+1)+X].Cap) + *CoreVoxelSizeCm/Subvoxels); + } + const auto& CandidateStats=VoxelForgeCore::Heightfield2D::threadColumnCacheStats(); + CandidateColumnMs.Add(CandidateStats.columnComputeMilliseconds); + CandidateColumns+=CandidateStats.columnMisses; + ++Tiles; + } + std::uint64_t GroundOver2Cm=0,CapOver2Cm=0; + for(const double Error:GroundErrorsCm)if(Error>2.0)++GroundOver2Cm; + for(const double Error:CapErrorsCm)if(Error>2.0)++CapOver2Cm; + double BaselineAverageMs=0.0,CandidateAverageMs=0.0; + for(const double Value:BaselineColumnMs)BaselineAverageMs+=Value; + for(const double Value:CandidateColumnMs)CandidateAverageMs+=Value; + if(!BaselineColumnMs.IsEmpty())BaselineAverageMs/=BaselineColumnMs.Num(); + if(!CandidateColumnMs.IsEmpty())CandidateAverageMs/=CandidateColumnMs.Num(); + UE_LOG(LogTemp,Display, + TEXT("[WorldAssemblerFrequencyProbe] label=surface lod=%d tiles=%d drop_highest_octaves=%d exact_column_evals=%llu reduced_column_evals=%llu column_eval_saving_pct=%.2f baseline_column_avg_ms=%.5f reduced_column_avg_ms=%.5f ground_error_p95_cm=%.4f ground_error_max_cm=%.4f ground_over_2cm=%llu/%d cap_error_p95_cm=%.4f cap_error_max_cm=%.4f cap_over_2cm=%llu/%d finite_feature_tiles=%llu"), + LOD,Tiles,static_cast(Drop), + static_cast(BaselineColumns), + static_cast(CandidateColumns), + BaselineAverageMs>0.0?100.0*(1.0-CandidateAverageMs/BaselineAverageMs):0.0, + BaselineAverageMs,CandidateAverageMs, + P95(GroundErrorsCm),MaxValue(GroundErrorsCm), + static_cast(GroundOver2Cm),GroundErrorsCm.Num(), + P95(CapErrorsCm),MaxValue(CapErrorsCm), + static_cast(CapOver2Cm),CapErrorsCm.Num(), + static_cast(FiniteFeatureTiles)); + } + VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true,0); + } + for (const int32 LOD : {0,1,3,4,5,8}) { TArray FullTimes,CandidateTimes,FullShellTimes,CandidateShellTimes,ListTimes; TArray UnknownBoxes,FieldCalls,ColumnMisses,StratumShellTimes,ConnectorShellTimes; - TArray ClassificationTimes,ColumnComputeTimes,MeshingTimes,HorizonTimes,RepairSkipped; + TArray DensitySamples,ClassificationTimes,ClassificationExclusiveTimes; + TArray ColumnComputeTimes,MeshingTimes,HorizonTimes,RepairSkipped; TArray ColumnEvalExclusiveTimes,MarchingExclusiveTimes,GroundRefineExclusiveTimes; TArray CapRefineExclusiveTimes,MeshAssemblyTimes; + TArray MarchingColumnTimes,GroundColumnTimes,CapColumnTimes; + TArray ClassificationNsPerSample,ColumnNsPerSample,MarchingNsPerSample; + TArray GroundNsPerSample,CapNsPerSample,AssemblyNsPerSample,ListNsPerSample; + TArray MarchingColumnNsPerSample,GroundColumnNsPerSample,CapColumnNsPerSample; TArray StratumFieldCalls,ConnectorFieldCalls,ShellTriangles,GroundTriangles,GroundRemoved,CapTriangles; for (const auto& Key : Keys) { @@ -15955,57 +16224,98 @@ bool RunCoreSharedFeatureCostAudit(VoxelForgeCore::World& World, Options.hashDensity=true; Options.useAssemblyFeatureQueries=false; std::string Error; - // Warm both routes before measuring: column/page caches must not favor the second route. - // Chauffer les deux parcours : les caches ne doivent pas favoriser le second. - if (!World.generateTile(Key,Options,Error)) return false; + // Check density and mesh parity with hashes, then time uncached runtime generation. + // Verifier la parite avec hashes, puis mesurer la generation sans cache de colonnes. + const auto FullParity=World.generateTile(Key,Options,Error); + if (!FullParity) return false; Options.useAssemblyFeatureQueries=true; - if (!World.generateTile(Key,Options,Error)) return false; + const auto CandidateParity=World.generateTile(Key,Options,Error); + if (!CandidateParity||FullParity->densityHash!=CandidateParity->densityHash + ||FullParity->meshHash!=CandidateParity->meshHash + ||FullParity->payloadHash!=CandidateParity->payloadHash + ||FullParity->topologyHash!=CandidateParity->topologyHash) + { OutError=TEXT("Shared feature full/candidate tile hashes differ."); return false; } + Options.hashDensity=false; Options.useAssemblyFeatureQueries=false; + VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true); const double FullStart=FPlatformTime::Seconds(); auto Full=World.generateTile(Key,Options,Error); FullTimes.Add((FPlatformTime::Seconds()-FullStart)*1000.0); Options.useAssemblyFeatureQueries=true; + VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true); const double CandidateStart=FPlatformTime::Seconds(); auto Candidate=World.generateTile(Key,Options,Error); CandidateTimes.Add((FPlatformTime::Seconds()-CandidateStart)*1000.0); - if (!Full||!Candidate||Full->densityHash!=Candidate->densityHash - ||Full->meshHash!=Candidate->meshHash||Full->payloadHash!=Candidate->payloadHash - ||Full->topologyHash!=Candidate->topologyHash) - { OutError=TEXT("Shared feature full/candidate tile hashes differ."); return false; } + if (!Full||!Candidate) + { OutError=TEXT("Shared feature full/candidate measured generation failed."); return false; } FullShellTimes.Add(Full->diagnostics.farShellMilliseconds); CandidateShellTimes.Add(Candidate->diagnostics.farShellMilliseconds); ListTimes.Add(Candidate->diagnostics.featureListMilliseconds); - UnknownBoxes.Add(static_cast(Full->diagnostics.unknownBoxes)); - FieldCalls.Add(static_cast(Full->diagnostics.fieldCalls)); - ColumnMisses.Add(static_cast(Full->diagnostics.columnCacheMisses)); - ClassificationTimes.Add(Full->diagnostics.classificationMilliseconds); - ColumnComputeTimes.Add(Full->diagnostics.columnComputeMilliseconds); - ColumnEvalExclusiveTimes.Add(Full->diagnostics.columnEvaluationExclusiveMilliseconds); - MarchingExclusiveTimes.Add(Full->diagnostics.marchingCubesExclusiveMilliseconds); - GroundRefineExclusiveTimes.Add(Full->diagnostics.groundRefinementExclusiveMilliseconds); - CapRefineExclusiveTimes.Add(Full->diagnostics.capRefinementExclusiveMilliseconds); - MeshAssemblyTimes.Add(Full->diagnostics.meshAssemblyMilliseconds); - MeshingTimes.Add(Full->diagnostics.meshingMilliseconds); - HorizonTimes.Add(Full->diagnostics.farHorizonMilliseconds); - RepairSkipped.Add(Full->diagnostics.farShellRepairSkipped?1.0:0.0); - StratumShellTimes.Add(Full->diagnostics.farStratumShellMilliseconds); - ConnectorShellTimes.Add(Full->diagnostics.farConnectorShellMilliseconds); - StratumFieldCalls.Add(static_cast(Full->diagnostics.farStratumFieldCalls)); - ConnectorFieldCalls.Add(static_cast(Full->diagnostics.farConnectorFieldCalls)); - ShellTriangles.Add(static_cast(Full->diagnostics.farShellTrianglesAdded)); - GroundTriangles.Add(static_cast(Full->diagnostics.horizonGroundTriangles)); - GroundRemoved.Add(static_cast(Full->diagnostics.horizonGroundTrianglesRemoved)); - CapTriangles.Add(static_cast(Full->diagnostics.horizonCapTriangles)); + const auto& Diagnostics=Candidate->diagnostics; + const double SampleCount=static_cast(Candidate->densitySampleCount); + DensitySamples.Add(static_cast(Candidate->densitySampleCount)); + UnknownBoxes.Add(static_cast(Diagnostics.unknownBoxes)); + FieldCalls.Add(static_cast(Diagnostics.fieldCalls)); + ColumnMisses.Add(static_cast(Diagnostics.columnCacheMisses)); + ClassificationTimes.Add(Diagnostics.classificationMilliseconds); + ClassificationExclusiveTimes.Add(Diagnostics.classificationExclusiveMilliseconds); + ColumnComputeTimes.Add(Diagnostics.columnComputeMilliseconds); + ColumnEvalExclusiveTimes.Add(Diagnostics.columnEvaluationExclusiveMilliseconds); + MarchingExclusiveTimes.Add(Diagnostics.marchingCubesExclusiveMilliseconds); + MarchingColumnTimes.Add(Diagnostics.marchingCubesColumnComputeMilliseconds); + GroundRefineExclusiveTimes.Add(Diagnostics.groundRefinementExclusiveMilliseconds); + GroundColumnTimes.Add(Diagnostics.groundRefinementColumnComputeMilliseconds); + CapRefineExclusiveTimes.Add(Diagnostics.capRefinementExclusiveMilliseconds); + CapColumnTimes.Add(Diagnostics.capRefinementColumnComputeMilliseconds); + MeshAssemblyTimes.Add(Diagnostics.meshAssemblyMilliseconds); + MeshingTimes.Add(Diagnostics.meshingMilliseconds); + HorizonTimes.Add(Diagnostics.farHorizonMilliseconds); + RepairSkipped.Add(Diagnostics.farShellRepairSkipped?1.0:0.0); + StratumShellTimes.Add(Diagnostics.farStratumShellMilliseconds); + ConnectorShellTimes.Add(Diagnostics.farConnectorShellMilliseconds); + StratumFieldCalls.Add(static_cast(Diagnostics.farStratumFieldCalls)); + ConnectorFieldCalls.Add(static_cast(Diagnostics.farConnectorFieldCalls)); + ShellTriangles.Add(static_cast(Diagnostics.farShellTrianglesAdded)); + GroundTriangles.Add(static_cast(Diagnostics.horizonGroundTriangles)); + GroundRemoved.Add(static_cast(Diagnostics.horizonGroundTrianglesRemoved)); + CapTriangles.Add(static_cast(Diagnostics.horizonCapTriangles)); + if(SampleCount>0.0) + { + ClassificationNsPerSample.Add(Diagnostics.classificationExclusiveMilliseconds + *1000000.0/SampleCount); + ColumnNsPerSample.Add(Diagnostics.columnEvaluationExclusiveMilliseconds + *1000000.0/SampleCount); + MarchingColumnNsPerSample.Add(Diagnostics.marchingCubesColumnComputeMilliseconds + *1000000.0/SampleCount); + MarchingNsPerSample.Add(Diagnostics.marchingCubesExclusiveMilliseconds + *1000000.0/SampleCount); + GroundColumnNsPerSample.Add(Diagnostics.groundRefinementColumnComputeMilliseconds + *1000000.0/SampleCount); + GroundNsPerSample.Add(Diagnostics.groundRefinementExclusiveMilliseconds + *1000000.0/SampleCount); + CapColumnNsPerSample.Add(Diagnostics.capRefinementColumnComputeMilliseconds + *1000000.0/SampleCount); + CapNsPerSample.Add(Diagnostics.capRefinementExclusiveMilliseconds + *1000000.0/SampleCount); + AssemblyNsPerSample.Add(Diagnostics.meshAssemblyMilliseconds + *1000000.0/SampleCount); + ListNsPerSample.Add(Diagnostics.featureListMilliseconds*1000000.0/SampleCount); + } } const auto P95=[](TArray Values) { Values.Sort(); return Values.IsEmpty() ? 0.0 : Values[FMath::Clamp( FMath::CeilToInt(Values.Num()*0.95f)-1,0,Values.Num()-1)]; }; UE_LOG(LogTemp,Display, - TEXT("[VoxelForgeSharedFeatureCost] label=%s lod=%d tiles=%d hashes_identical=1 full_p95_ms=%.5f candidate_p95_ms=%.5f list_p95_ms=%.5f classification_p95_ms=%.5f column_compute_p95_ms=%.5f column_eval_exclusive_p95_ms=%.5f mc_exclusive_p95_ms=%.5f ground_refine_exclusive_p95_ms=%.5f cap_refine_exclusive_p95_ms=%.5f mesh_assembly_p95_ms=%.5f meshing_p95_ms=%.5f horizon_p95_ms=%.5f full_shell_p95_ms=%.5f candidate_shell_p95_ms=%.5f repair_skipped_p95=%.0f unknown_boxes_p95=%.0f field_calls_p95=%.0f column_misses_p95=%.0f stratum_shell_p95_ms=%.5f stratum_field_calls_p95=%.0f connector_shell_p95_ms=%.5f connector_field_calls_p95=%.0f shell_triangles_p95=%.0f horizon_ground_triangles_p95=%.0f horizon_ground_removed_p95=%.0f horizon_cap_triangles_p95=%.0f"), + TEXT("[VoxelForgeSharedFeatureCost] label=%s lod=%d tiles=%d hashes_identical=1 full_p95_ms=%.5f candidate_p95_ms=%.5f list_p95_ms=%.5f classification_wall_p95_ms=%.5f classification_exclusive_p95_ms=%.5f column_compute_p95_ms=%.5f column_eval_exclusive_p95_ms=%.5f mc_column_compute_p95_ms=%.5f mc_exclusive_p95_ms=%.5f ground_column_compute_p95_ms=%.5f ground_refine_exclusive_p95_ms=%.5f cap_column_compute_p95_ms=%.5f cap_refine_exclusive_p95_ms=%.5f mesh_assembly_p95_ms=%.5f meshing_p95_ms=%.5f horizon_p95_ms=%.5f samples_p95=%.0f classification_ns_per_sample_p95=%.2f column_ns_per_sample_p95=%.2f mc_column_ns_per_sample_p95=%.2f mc_ns_per_sample_p95=%.2f ground_column_ns_per_sample_p95=%.2f ground_ns_per_sample_p95=%.2f cap_column_ns_per_sample_p95=%.2f cap_ns_per_sample_p95=%.2f assembly_ns_per_sample_p95=%.2f feature_list_ns_per_sample_p95=%.2f full_shell_p95_ms=%.5f candidate_shell_p95_ms=%.5f repair_skipped_p95=%.0f unknown_boxes_p95=%.0f field_calls_p95=%.0f column_misses_p95=%.0f stratum_shell_p95_ms=%.5f stratum_field_calls_p95=%.0f connector_shell_p95_ms=%.5f connector_field_calls_p95=%.0f shell_triangles_p95=%.0f horizon_ground_triangles_p95=%.0f horizon_ground_removed_p95=%.0f horizon_cap_triangles_p95=%.0f"), Label,LOD,FullTimes.Num(),P95(FullTimes),P95(CandidateTimes),P95(ListTimes), - P95(ClassificationTimes),P95(ColumnComputeTimes),P95(ColumnEvalExclusiveTimes), - P95(MarchingExclusiveTimes),P95(GroundRefineExclusiveTimes),P95(CapRefineExclusiveTimes), - P95(MeshAssemblyTimes),P95(MeshingTimes),P95(HorizonTimes), + P95(ClassificationTimes),P95(ClassificationExclusiveTimes),P95(ColumnComputeTimes), + P95(ColumnEvalExclusiveTimes),P95(MarchingColumnTimes),P95(MarchingExclusiveTimes), + P95(GroundColumnTimes),P95(GroundRefineExclusiveTimes),P95(CapColumnTimes), + P95(CapRefineExclusiveTimes), + P95(MeshAssemblyTimes),P95(MeshingTimes),P95(HorizonTimes),P95(DensitySamples), + P95(ClassificationNsPerSample),P95(ColumnNsPerSample),P95(MarchingColumnNsPerSample), + P95(MarchingNsPerSample),P95(GroundColumnNsPerSample),P95(GroundNsPerSample), + P95(CapColumnNsPerSample),P95(CapNsPerSample),P95(AssemblyNsPerSample),P95(ListNsPerSample), P95(FullShellTimes),P95(CandidateShellTimes),P95(RepairSkipped),P95(UnknownBoxes),P95(FieldCalls), P95(ColumnMisses),P95(StratumShellTimes),P95(StratumFieldCalls), P95(ConnectorShellTimes),P95(ConnectorFieldCalls),P95(ShellTriangles), diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 3bc6f02..cc98e35 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -126,6 +126,10 @@ namespace VoxelForgeCore double classificationMilliseconds = 0.0; double columnComputeMilliseconds = 0.0; double columnEvaluationExclusiveMilliseconds = 0.0; + double marchingCubesColumnComputeMilliseconds = 0.0; + double groundRefinementColumnComputeMilliseconds = 0.0; + double capRefinementColumnComputeMilliseconds = 0.0; + double classificationExclusiveMilliseconds = 0.0; double marchingCubesExclusiveMilliseconds = 0.0; double groundRefinementExclusiveMilliseconds = 0.0; double capRefinementExclusiveMilliseconds = 0.0; @@ -687,7 +691,8 @@ namespace VoxelForgeCore std::int64_t minimumYSubvoxels, std::int64_t minimumZSubvoxels, std::int64_t maximumXSubvoxels, std::int64_t maximumYSubvoxels, std::int64_t maximumZSubvoxels, std::int32_t stepVoxels) const noexcept; - static void resetThreadColumnCacheStats(bool captureCoordinates = false) noexcept; + static void resetThreadColumnCacheStats(bool captureCoordinates = false, + bool clearCache = false, std::uint32_t octaveDrop = 0) noexcept; static const HeightfieldColumnCacheStats& threadColumnCacheStats() noexcept; FieldInterval interval(std::int64_t minimumXSubvoxels, std::int64_t minimumYSubvoxels, diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 505f032..392129c 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -1479,12 +1479,18 @@ namespace VoxelForgeCore const bool bSkipFarShellRepair=key.lod>=7&&bUseNativeSurfaceHorizon; FStandaloneShapeWorld::ResetThreadClassificationStats(); VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats( - options.captureColumnCoordinates&&key.lod==8); + options.captureColumnCoordinates); bool bNativeSurfaceTile=false; bool bNativeSurfaceBandSafe=false; + double classificationColumnComputeMilliseconds=0.0; if(bHasWorldAssembly) { + const double columnMillisecondsBeforeClassification= + VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds; const auto Classification=tileShape.ClassifyBoxDetailed(origin,step,cells); + classificationColumnComputeMilliseconds=std::max(0.0, + VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds + -columnMillisecondsBeforeClassification); bNativeSurfaceTile=Classification.bNativeSurface; // NativeSurfaceDensityAt consumes only the bounded manifest heightfield and its // bounded type-2/type-4 features. Unknown tile occupancy does not make that Z support @@ -1518,7 +1524,16 @@ namespace VoxelForgeCore out.diagnostics.columnCacheHits=Columns.columnHits; out.diagnostics.columnCacheMisses=Columns.columnMisses; out.diagnostics.classificationMilliseconds=Counts.Milliseconds; + out.diagnostics.classificationExclusiveMilliseconds=std::max(0.0, + Counts.Milliseconds-classificationColumnComputeMilliseconds); out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds; + if(options.captureColumnCoordinates) + { + out.diagnostics.columnEvaluationWorkerHash=static_cast( + std::hash{}(std::this_thread::get_id()))+1u; + out.diagnostics.columnCoordinateCaptureComplete=Columns.coordinateCaptureComplete; + out.diagnostics.evaluatedColumnCoordinates=Columns.evaluatedCoordinates; + } out.densitySampleCount=0; return out; } @@ -1662,6 +1677,8 @@ namespace VoxelForgeCore VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds; out.diagnostics.marchingCubesExclusiveMilliseconds=std::max(0.0, MarchingCubesWallMilliseconds-(ColumnMillisecondsAfterMeshing-ColumnMillisecondsBeforeMeshing)); + out.diagnostics.marchingCubesColumnComputeMilliseconds=std::max(0.0, + ColumnMillisecondsAfterMeshing-ColumnMillisecondsBeforeMeshing); const double MeshingMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-MeshingStart).count(); out.diagnostics.farHorizonVersion=key.lod>=4?options.farHorizonVersion:0; @@ -1712,6 +1729,8 @@ namespace VoxelForgeCore VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds; out.diagnostics.groundRefinementExclusiveMilliseconds=std::max(0.0, GroundWallMilliseconds-(ColumnMillisecondsAfterGround-ColumnMillisecondsBeforeGround)); + out.diagnostics.groundRefinementColumnComputeMilliseconds=std::max(0.0, + ColumnMillisecondsAfterGround-ColumnMillisecondsBeforeGround); const double ColumnMillisecondsBeforeCap= VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds; const auto CapStart=std::chrono::steady_clock::now(); @@ -1723,6 +1742,8 @@ namespace VoxelForgeCore VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds; out.diagnostics.capRefinementExclusiveMilliseconds=std::max(0.0, CapWallMilliseconds-(ColumnMillisecondsAfterCap-ColumnMillisecondsBeforeCap)); + out.diagnostics.capRefinementColumnComputeMilliseconds=std::max(0.0, + ColumnMillisecondsAfterCap-ColumnMillisecondsBeforeCap); out.diagnostics.farHorizonMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-HorizonStart).count(); out.diagnostics.capRefined=CapStats.Refined; @@ -1747,7 +1768,9 @@ namespace VoxelForgeCore out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds; out.diagnostics.columnEvaluationExclusiveMilliseconds=Columns.columnComputeMilliseconds; out.diagnostics.classificationMilliseconds=Counts.Milliseconds; - if(options.captureColumnCoordinates&&key.lod==8) + out.diagnostics.classificationExclusiveMilliseconds=std::max(0.0, + Counts.Milliseconds-classificationColumnComputeMilliseconds); + if(options.captureColumnCoordinates) { out.diagnostics.columnEvaluationWorkerHash=static_cast( std::hash{}(std::this_thread::get_id()))+1u; diff --git a/Standalone/Core/src/WorldAssemblerInternal.h b/Standalone/Core/src/WorldAssemblerInternal.h index d76eeae..7cc66b1 100644 --- a/Standalone/Core/src/WorldAssemblerInternal.h +++ b/Standalone/Core/src/WorldAssemblerInternal.h @@ -1027,11 +1027,18 @@ namespace world_assembly_detail std::int64_t wavelengthSubvoxels) { return FloorDiv(positionSubvoxels * Q16, wavelengthSubvoxels); } + // Audit-only override; normal tile generation resets this to zero before evaluating a recipe. + // Surcharge reservee aux mesures; la generation remet zero avant d'evaluer une recette. + inline thread_local std::uint32_t HeightfieldColumnOctaveDrop = 0; + inline std::int32_t FractalNoiseQ16(std::int64_t xQ16, std::int64_t yQ16, std::int64_t zQ16, std::uint32_t octaves, bool ridged = false) { std::int64_t total = 0, weightSum = 0, weight = Q16, ridgeWeight = Q16; - for (std::uint32_t octave = 0; octave < octaves; ++octave) + std::uint32_t effectiveOctaves=octaves; + if(HeightfieldColumnOctaveDrop!=0&&octaves!=0) + effectiveOctaves=std::max(1u,octaves-std::min(octaves-1,HeightfieldColumnOctaveDrop)); + for (std::uint32_t octave = 0; octave < effectiveOctaves; ++octave) { const std::int64_t scale = 1ll << octave; const std::int64_t noise = Perlin3DQ16(xQ16 * scale,yQ16 * scale,zQ16 * scale); @@ -1965,9 +1972,15 @@ HeightfieldPointSample Heightfield2D::point(std::int64_t x, std::int64_t y, return out; } -void Heightfield2D::resetThreadColumnCacheStats(bool captureCoordinates) noexcept +void Heightfield2D::resetThreadColumnCacheStats(bool captureCoordinates, bool clearCache, + std::uint32_t octaveDrop) noexcept { + // Diagnostic probes may clear old tile entries before comparing isolated sample grids. + // Les sondes de mesure peuvent vider les entrees d'une tuile avant de comparer deux grilles. + if(clearCache)world_assembly_detail::HeightfieldColumnCache.fill( + world_assembly_detail::HeightfieldColumnCacheEntry{}); auto& Stats=world_assembly_detail::HeightfieldColumnStats; + world_assembly_detail::HeightfieldColumnOctaveDrop=octaveDrop; Stats.pointCalls=0; Stats.columnHits=0; Stats.columnMisses=0; diff --git a/Standalone/Research/WorldAssemblerM3/Round17/REPORT.md b/Standalone/Research/WorldAssemblerM3/Round17/REPORT.md new file mode 100644 index 0000000..0ea5787 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round17/REPORT.md @@ -0,0 +1,277 @@ +# World Assembler S13 — Round 17 + +**2026-10-19 · What does a tile actually cost?** + +## Result + +No generation optimization shipped. The measurements show that owner-plan tile time is mostly +column evaluation plus MC, that LOD4 is dominated by column-cache misses, and that cross-worker XY +reuse is common. The interpolation and global octave-drop probes fail the 2 cm quality bar. A shared +column page still needs a measured candidate with bounded memory, synchronization, and lifecycle +proof, so I did not add one. Density, mesh, collision, and recipe output are unchanged; no recipe +version bump is needed. + +The current BuildHost 40 m/s trace passed the support gate **75/75**. It reported a 4.825 s +initial-set ready time, 28.760 worker-seconds, and a 0.510 s stationary ring. The last initial LOD0 +tile applied at 1.146 s. At 40 m/s, `UWorld::Tick` was 3.133/5.426 ms p95/max at spawn and +30.314/43.142 ms while walking. The worker-second and full-set targets passed in this trace; the +LOD0 and strict S12 tick-tail targets did not. Startup-clock measurements and worker-task intervals +use different origins from the S12 actor report, so I do not treat 4.825 s as an apples-to-apples +replacement for S12's 4.911 s. + +## 1. Exclusive tile cost on the owner plan + +The BuildHost audit measures eight cold, no-hash tiles per requested LOD, with a separate hashed +full-feature-list versus per-tile-list parity check (`hashes_identical=1`). The table gives p95 +stage time and the p95 of each tile's `stage time / density samples`, in `ms / ns per sample`. +Percentiles are computed independently, so stage p95 values do not add to the full-tile p95. The +sample denominator includes the normal halo: LOD0–1 are 32 MC cells / 35³ = 42,875 samples; +LOD3–8 are 16 coarse cells / 19³ = 6,859 samples. The owner configuration is not a 32³ lattice at +every LOD. + +| LOD | Full scan → per-tile list p95 (ms) | Classify excl. | MC column eval. | MC excl. | Ground column eval. | Ground excl. | Cap column eval. | Cap excl. | Mesh assembly | +|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| 0 | 13.806 → 14.160 | 0.0064 / 0.15 | 3.708 / 86.48 | 10.436 / 243.40 | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | 0.0251 / 0.59 | +| 1 | 9.616 → 9.584 | 0.0821 / 0.02 | 4.096 / 95.52 | 7.191 / 167.72 | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | 0.0314 / 0.73 | +| 3 | 6.952 → 6.878 | 0.0286 / 9.90 | 3.989 / 1,090.54 | 2.879 / 419.80 | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | 0.0075 / 2.97 | +| 4 | 36.445 → 35.790 | 0.0015 / 0.22 | 29.361 / 4,280.71 | 2.948 / 429.84 | 0 / 0 | 0.0001 / 0.01 | 4.865 / 709.35 | 1.259 / 183.61 | 0.0231 / 3.37 | +| 5 | 10.586 → 10.420 | 0.0010 / 0.15 | 2.958 / 431.27 | 1.195 / 174.25 | 4.894 / 713.57 | 2.234 / 325.63 | 0.405 / 59.06 | 0.241 / 35.08 | 0.0054 / 0.79 | +| 8 | 14.778 → 14.507 | 0.0046 / 0.67 | 5.671 / 826.84 | 2.366 / 345.01 | 1.775 / 258.74 | 1.652 / 240.87 | 1.823 / 265.78 | 1.348 / 196.50 | 0.0252 / 3.67 | + +The paired full/candidate timings are the feature-query negative control: the full plan scans feature +bounds; the candidate builds the per-tile feature lists used by Core. All tested hashes matched. +Their timing differences are small and mixed in sign. Feature-list p95 itself was only 0.0009– +0.0015 ms on this owner sample. + +There is no separate volumetric 3D density-noise stage in native SurfaceWorld. Its surface recipe +does call `FractalNoiseQ16`/Perlin while evaluating 2D XY columns, so that work is included in +column evaluation. Material/color assignment is not a separate timer; it is included in meshing and +the final mesh/payload path. The current route has no tunnel, room, shaft, or route-density stage. +Classification-exclusive time subtracts column work done while classifying; MC/ground/cap column +time is reported beside the corresponding non-column exclusive time. The `horizon_p95_ms` counter +wraps ground and cap refinement and is inclusive, so it is not another exclusive stage. Far-shell +repair was negligible in this sample (at most 0.0003 ms p95). + +### Why LOD4 is expensive + +LOD4 full tile p95 is 35.790 ms, versus 6.878 ms at LOD3 and 10.420 ms at LOD5. LOD4 is the first +cap-refined level: it samples cap at 16 voxels (4 m); LOD3 has no cap refinement, and LOD5 cap uses +the 128-voxel (32 m) boundary grid. Ground refinement starts at LOD5. LOD4's cap work costs +4.865 ms of column evaluation and 1.259 ms exclusive at p95. + +That cap difference does not explain most of the spike. LOD4 MC performs 29.361 ms p95 of column +evaluation, and its sampled tile has 8,224 column misses versus 1,634 at LOD3 and 1,916 at LOD5. +Thus the measured anomaly is primarily poor column reuse at LOD4, with cap refinement adding a +smaller separate cost. The data does not isolate why the thread-local direct-mapped cache misses +rise at LOD4; tile extent, halo/normal probes, and XY spread all differ with LOD. + +## 2. Do far tiles evaluate detail they cannot show? + +The LOD8 tile p95 spends 9.203 ms in column evaluation out of 14.507 ms. There is no volumetric +cave-noise loop in this path, but the 2D heightfield recipe still evaluates its full configured +noise octaves at every requested XY column. A measurement-only probe dropped the highest one or two +octaves globally on two LOD8 tiles. It reported 26.50% / 40.31% less column time, but ground-height +p95 errors of 7,545 / 7,581 cm and cap-height errors of 2,427 / 7,859 cm; all 578 cap samples were +over 2 cm. Near LOD0–1 the same probe was slower than baseline, so its timing is not a reliable +frequency cost estimate. It also renormalizes the remaining octaves and does not implement a +per-recipe wavelength cutoff at half the MC cell size. + +This rejects global octave removal. A frequency-aware cutoff could change appearance while keeping +determinism, but this probe does not justify a cutoff or quantify its screen-space result. It was +measurement-only and did not affect tile hashes. + +## 3. Per-tile feature lists and 2D cache reuse + +Core's `World::generateTile` builds bounded per-tile query lists for overlapping +`Heightfield2D` strata and `FloatingIslandFieldV1` plans, including the normal halo. Those two +WorldAssembler families share the scoped feature-list route. The legacy Unreal `UVoxelGenerator` +room/tunnel/shaft families use their own room, tunnel, and generator caches; Round 1's Core tile list +is not one shared list for all legacy generator families. On the owner SurfaceWorld tiles, list-build +time is under 0.002 ms p95. + +`Heightfield2D::point(x,y,z)` may be called at many Z/normal samples, but the thread-local XY cache +normally evaluates a column once per worker-cache miss. The cold BuildHost sample shows: + +| LOD | Field-point calls p95 | Column misses p95 | Approx. hit rate | +|---:|---:|---:|---:| +| 0 | 44,100 | 2,244 | 94.9% | +| 1 | 44,100 | 2,289 | 94.8% | +| 3 | 4,191 | 1,634 | 61.0% | +| 4 | 9,405 | 8,224 | 12.6% | +| 5 | 8,197 | 1,916 | 76.6% | +| 8 | 4,441 | 2,708 | 39.0% | + +The Standalone W8 capture recorded all tiles and all LODs. `repeated` means an XY coordinate was +re-evaluated by a different same-LOD tile; the neighbor count includes any prior adjacent tile, +and the cross-worker column counts a neighbor repeat observed on a different worker. + +| LOD | Tiles | Unique XY | Repeated tile evaluations | Neighbor repeats | Cross-worker neighbor repeats | +|---:|---:|---:|---:|---:|---:| +| 0 | 512 | 128,240 | 347,280 | 280,233 | 276,658 | +| 1 | 448 | 103,302 | 300,242 | 249,825 | 244,941 | +| 2 | 448 | 32,274 | 102,713 | 84,318 | 79,402 | +| 3 | 384 | 33,279 | 87,158 | 81,680 | 78,272 | +| 4 | 320 | 91,325 | 230,492 | 229,811 | 202,684 | +| 5 | 144 | 71,950 | 118,630 | 118,580 | 112,191 | +| 6 | 96 | 280,313 | 63,523 | 63,523 | 63,435 | +| 7 | 48 | 71,529 | 7,780 | 7,780 | 7,138 | +| 8 | 153 | 106,219 | 32,879 | 32,879 | 30,956 | + +This makes immutable shared column pages worth a candidate A/B, especially for LOD0–5. The counts +do not measure synchronization, page compilation, eviction, memory, or resulting wall-time savings. +No page cache was added on counts alone. The column capture is opt-in, worker-local, and read back +after generation. + +## 4. Coarse-lattice interpolation probe + +The measurement-only probe evaluated exact ground/cap columns at every MC XY sample, then sampled +every second MC sample and bilinearly interpolated the rest. No near-zero or thin-feature fallback +was included, so this is a lower-cost bound and a failure screen, not a production candidate. The +table is two tiles per LOD. Errors are p95 over sampled columns; counts over 2 cm are explicit. + +| LOD | Measured column-compute time saved at stride 2 | Ground p95 / >2 cm | Cap p95 / >2 cm | +|---:|---:|---:|---:| +| 0 | 73.48% | 0.78 cm / 0 of 2,178 | 1.32 cm / 25 of 2,178 | +| 1 | 73.96% | 2.54 cm / 236 of 2,178 | 3.00 cm / 375 of 2,178 | +| 3 | 71.99% | 388.62 cm / 405 of 578 | 37.70 cm / 397 of 578 | +| 4 | 71.39% | 702.29 cm / 403 of 578 | 68.92 cm / 401 of 578 | +| 5 | 71.27% | 1,248.85 cm / 411 of 578 | 87.74 cm / 395 of 578 | +| 8 | 72.81% | 4,369.95 cm / 414 of 578 | 2,201.17 cm / 415 of 578 | + +At stride two, exact column evaluations fell from 2,178 to 578 at LOD0–1 and from 578 to 162 at +LOD3–8. The measured column-compute time reduction is shown above; it was 71–74%. Stride four +reduced exact evaluations from 2,178 to 162 at LOD0–1 and from 578 to 50 at LOD3–8, roughly +90–92%. It already gives LOD0 ground/cap p95 errors of 2.72/2.09 cm (293/2,178 and 123/2,178 +samples over 2 cm). At LOD1, stride two fails the +2 cm threshold in both fields; coarse LOD errors are much larger. A near-zero exact fallback could +recover surface crossings, but it was not simulated here and could erase much of the nominal saving. +This probe does not exercise volumetric tunnel/wall thickness or the arrival-to-departure route +law. No interpolation was enabled; exact sampled outputs, walkability, and route behavior are +unchanged. + +## 5. S12 worker-cap follow-up and current trace + +The matched cap comparison kept `voxel.StreamingReadyQueueDepth=8` and changed only +`voxel.StreamingBackgroundWorkerCap`. Both controls were recorded in the JSONL trace, as were the +effective slots and the eight actual background worker count. + +| Speed | Background cap | Effective slots / actual tile workers | Initial set ready | Spawn tick p95/max | Walk tick p95/max | Ring | +|---:|---:|---:|---:|---:|---:|---:| +| 6 m/s | 0 | 10 / 8 | 4.669 s | 3.483 / 5.184 ms | 2.708 / 28.581 ms | — | +| 6 m/s | 1 | 8 / 8 | 4.745 s | 3.283 / 14.945 ms | 2.696 / 28.535 ms | — | +| 40 m/s | 0 | 10 / 8 | 4.633 s | 3.455 / 5.468 ms | 32.345 / 55.115 ms | 75/75 | +| 40 m/s | 1, matched | 8 / 8 | 4.712 s | 3.031 / 5.364 ms | 30.541 / 41.598 ms | 75/75 | +| 40 m/s | 1, final build | 8 / 8 | 4.825 s | 3.133 / 5.426 ms | 30.314 / 43.142 ms | 75/75 | + +Cap 0 gained only 76 ms at 6 m/s and about 79 ms in the matched 40 m/s pair, while its 40 m/s +walk tick tail was substantially worse. The final cap-1 run confirms the ring at 75/75 gates. Keep +cap 1 as the default; the latest tick p95/max is still 0.752/3.915 ms above Round 16's 29.562 / +39.227 ms 40 m/s result, so the strict tick-tail target is not claimed. + +On the final cap-1 40 m/s trace, the initial set contains 2,578 tiles and totals 28.760 worker- +seconds. The worker-seconds by LOD were: LOD0 3.81, LOD1 3.94, LOD2 0.81, LOD3 2.36, LOD4 7.73, +LOD5 1.68, LOD6 2.17, LOD7 1.97, LOD8 4.31. The ring was ready at 0.510 s; full initial set +ready at 4.825 s. LOD0's last worker ended at 1.131 s and its last apply was at 1.146 s. + +## Builds, quality, determinism, and files + +Build commands: + +```powershell +.\Tools\BuildVoxelForgeCoreHost.ps1 -MaxParallelActions 12 -TimeoutSeconds 900 +.\Standalone\Build.ps1 -Target All -CompileWorkers 12 -BuildPriority Normal +``` + +Measurement commands (each was a fresh process; BuildHost runs were serial, Normal priority, and +limited to the 12-CPU affinity): + +```powershell +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -SharedFeatureAuditOnly -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -StreamingCollisionLookahead 1 -TestMoveDirection '0,1,0' -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 600 -StreamingReadyQueueDepth 8 -StreamingBackgroundWorkerCap 0 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -StreamingCollisionLookahead 1 -TestMoveDirection '0,1,0' -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 600 -StreamingReadyQueueDepth 8 -StreamingBackgroundWorkerCap 1 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -StreamingCollisionLookahead 1 -TestMoveDirection '0,1,0' -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 4000 -StreamingReadyQueueDepth 8 -StreamingBackgroundWorkerCap 0 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -StreamingCollisionLookahead 1 -TestMoveDirection '0,1,0' -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 4000 -StreamingReadyQueueDepth 8 -StreamingBackgroundWorkerCap 1 -TimeoutSeconds 900 +``` + +The final Standalone commands used the same options for each worker count; W8 also enabled the +all-LOD overlap capture: + +```powershell +.\Standalone\bin\VoxelForgeCoreStandalone.exe --workers=1 --world-assembler-prototype --stream-benchmark --stream-pipeline --stream-hash --force-generate --clip-radius=3 --max-clip-level=8 --camera=0,-35,-487.94,0,-22 --far-horizon-lod8-step=0 --process-priority=Normal --process-affinity=4095 --frame-limit=60 +.\Standalone\bin\VoxelForgeCoreStandalone.exe --workers=8 --world-assembler-prototype --stream-benchmark --stream-pipeline --stream-hash --profile-column-overlap --force-generate --clip-radius=3 --max-clip-level=8 --camera=0,-35,-487.94,0,-22 --far-horizon-lod8-step=0 --process-priority=Normal --process-affinity=4095 --frame-limit=60 +.\Standalone\bin\VoxelForgeCoreStandalone.exe --workers=12 --world-assembler-prototype --stream-benchmark --stream-pipeline --stream-hash --force-generate --clip-radius=3 --max-clip-level=8 --camera=0,-35,-487.94,0,-22 --far-horizon-lod8-step=0 --process-priority=Normal --process-affinity=4095 --frame-limit=60 +``` + +Current-build compatibility gates: + +```powershell +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -CoreOffTest -CoreVoxelSizeCm 12.5 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -HybridFixturesOnly -CoreMesherRecipe WorldAssemblerHybridV1 -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -HybridFixturesOnly -CoreMesherRecipe WorldAssemblerHybridV2 -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -FloatingIslandsOnly -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +``` + +The current BuildHost SurfaceWorld actor run passed and reported stable W1/W8/W16 hashes: +density/mesh/payload/topology `0cd2a1a4152ee4aa / a06243aee16b65d5 / c700162935c262d7 / +0af901aa42b452bc`. The current BuildHost cap audit passed S4 agreement (128 points, max 0.0504 cm +against 2 cm) and 1,584/1,584 cap rays (100%). The BuildHost actor run also passed S3 far-horizon +agreement at max 0.0427 cm against 2 cm. Current Standalone W1/W8/W12 each matched full-view hashes: +density/mesh/payload/topology `c10697a8f48380c7 / e75c9119a1498cd5 / 49a9a8d1835a6c8c / +edcb23288b5e8724`. W8's final full-view pipeline was 3.518 s for 2,553/2,553 tiles. + +The current-build Core-off gate passed `legacy_unchanged=PASS` at 12.5 cm. M2 Hybrid V1 and V2 +each passed all five analytic fixtures and the border suite at 25 cm. FloatingIslands passed at +25 cm with stable W1/W8/W16 hashes `4dcfb37585d7d986 / f14f76fbf065c6ed / 53035c45fd1a082e / +23550100654a4ec3`; its island and span capsule contacts, content anchors, and carve/fill checks +passed. These runs used the current BuildHost binaries. No density output changed, so paired +near/far captures and a recipe bump were not required. No runtime CVar or behavior change was +introduced; the cap-0 negative controls above were trace-read back. + +Full BuildHost logs are under +`Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/` with stems +`CoreWorldValidation-core_off_voxel_12.5_applyboost_default_legacy_order_nearreserve_auto`, +`CoreWorldValidation-owner_surface_applyboost_default_legacy_order_nearreserve_auto_worldassemblerhybridv1`, +`CoreWorldValidation-owner_surface_applyboost_default_legacy_order_nearreserve_auto_worldassemblerhybridv2`, +and `CoreWorldValidation-floating_islands_applyboost_default_legacy_order_nearreserve_auto`. + +Current binary SHA-256: + +| Binary | SHA-256 | +|---|---| +| BuildHost `UnrealEditor-VoxelForge.dll` | `811F83D43EDE47EE870DD608F25C9F902422E1B264901917726CDBECE0EC0A37` | +| BuildHost `UnrealEditor-VoxelForgeCore.dll` | `AA74A8BC8E1C9C59FB7EA94D5A6BA71341EF8208EDE2458A7B5FAC0045FF2520` | +| BuildHost `UnrealEditor-VoxelForgeEditor.dll` | `A851836ED435E8795AF860EBFB49AD84B603FF0D94600637D9B8CE52E0710EE3` | +| Standalone `VoxelForgeCoreStandalone.exe` | `704F433791AF0D483D082CCBB2043DB4194323F40A986DA72F338FC424BF82F3` | + +Round 17 changed these existing source files; no new source file was added: + +| File | Likely compile spot | +|---|---| +| `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/VoxelForgeCore.cpp`, `Standalone/Core/src/WorldAssemblerInternal.h` | BuildHost VoxelForgeCore and Standalone Core | +| `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp` | BuildHost VoxelForgeEditor commandlet | +| `Standalone/Source/StreamingWorld.h`, `Standalone/Source/StandaloneViewer.cpp` | Standalone viewer executable | + +Async-safety review: no actor queues or worker mutation code changed. `Epoch`, MPSC queue modes, +`EndPlay` draining, worker `bShuttingDown` checks, `ModsLock`, and shared region-page lifetime are +unchanged. Coordinate capture is opt-in thread-local diagnostic data copied into each tile result; +UObject and RMC mutation remains on the game thread. RealtimeMeshComponent was read-only. No +`.uproject`, owner editor, `.uasset`, generated header, `Binaries/`, or `Intermediate/` source was +edited. Files over 300 KB are under `Saved/BuildHost/`. No `0xC0000142` occurred and no new +`UECC-*` crash folder appeared. + +The first BuildHost compile failed because two probe lambdas omitted the configured tile-size +capture; that was fixed and the next build passed. The Standalone build passed with the existing +`vswhere.exe` lookup warning and `VoxelStrateManager.cpp` C4838 warnings. A final source check and +both current targets passed. No commit, push, or stash. + +### Dialogue + +**User:** Shouldn't a tile be way cheaper? Measure the stages and explain LOD4 before choosing a fix. + +**Codex:** The largest measured cost is XY column evaluation, and LOD4 has a sharp spike in cache +misses plus its first fine cap pass. Two-step interpolation reduced measured column-compute time by +roughly 72%, but moved far ground by tens of meters; global octave dropping also fails badly. +Cross-worker column +reuse is a strong candidate for a future measured page-cache experiment, so Round 17 keeps output +unchanged and records the floor instead of shipping an unproven approximation. diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index 4617bf6..5d07b57 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -6976,7 +6976,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; const FVector& CameraPositionVoxels,int32 WorkerCount,int32 MaxLevel, VoxelForgeCore::DetailVariant DetailVariant,const VoxelForgeCore::JobScheduler& Scheduler, bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,bool bSkipUV,bool bSkipColors, - std::chrono::steady_clock::time_point ColdStart,bool bProfileFarColumnOverlap, + std::chrono::steady_clock::time_point ColdStart,bool bProfileColumnOverlap, int32 FarHorizonLOD8InteriorStepVoxels) { using Clock=std::chrono::steady_clock; @@ -7045,7 +7045,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; const auto ShapeReady=Clock::now(); const double ShapeSeconds=std::chrono::duration(ShapeReady-ShapeBuildStart).count(); FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel, - bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileFarColumnOverlap, + bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap, FarHorizonLOD8InteriorStepVoxels); const auto StreamStart=Clock::now(); const FStandaloneStreamingUpdate Update=Streamer.StartWithDesired(Desired); @@ -7078,19 +7078,23 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; FVoxelTileKey Tile; std::uint64_t WorkerHash=0; }; - std::unordered_map LOD8ColumnFirstUse; - std::uint64_t LOD8ColumnDuplicateEvents=0,LOD8NeighbourDuplicateEvents=0; - std::uint64_t LOD8CrossWorkerNeighbourDuplicateEvents=0; - std::size_t LOD8ColumnCaptureTiles=0; - bool bLOD8ColumnCaptureComplete=true; + std::array,FColumnXYHash>,9> ColumnUsesByLOD; + std::array ColumnDuplicateEventsByLOD{},NeighbourDuplicateEventsByLOD{}; + std::array CrossWorkerNeighbourDuplicateEventsByLOD{}; + std::array ColumnCaptureTilesByLOD{},ExpectedTilesByLOD{}; + std::array ColumnCaptureCompleteByLOD{}; + ColumnCaptureCompleteByLOD.fill(true); std::array WorkerSecondsByLOD{}; std::array ColumnEvalMsByLOD{},MarchingExclusiveMsByLOD{}; std::array GroundRefineExclusiveMsByLOD{},CapRefineExclusiveMsByLOD{}; std::array MeshAssemblyMsByLOD{}; std::array TileCountByLOD{}; - const auto AreLOD8Neighbours=[](const FVoxelTileKey& A,const FVoxelTileKey& B) + for(const FVoxelTileKey& Tile:Desired.Tiles) + if(Tile.Level>=0&&Tile.Level(ExpectedTilesByLOD.size())) + ++ExpectedTilesByLOD[static_cast(Tile.Level)]; + const auto AreSameLODNeighbours=[](const FVoxelTileKey& A,const FVoxelTileKey& B) { - if(A.Level!=8||B.Level!=8)return false; + if(A.Level!=B.Level)return false; const std::int64_t DX=std::abs(static_cast(A.Coord.X)-B.Coord.X); const std::int64_t DY=std::abs(static_cast(A.Coord.Y)-B.Coord.Y); const std::int64_t DZ=std::abs(static_cast(A.Coord.Z)-B.Coord.Z); @@ -7122,10 +7126,12 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; CapRefineExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.capRefinementExclusiveMilliseconds; MeshAssemblyMsByLOD[LOD]+=Tile.CoreDiagnostics.meshAssemblyMilliseconds; } - if(bProfileFarColumnOverlap&&Tile.Key.Level==8) + if(bProfileColumnOverlap&&Tile.Key.Level>=0 + &&Tile.Key.Level(ColumnCaptureTilesByLOD.size())) { - ++LOD8ColumnCaptureTiles; - bLOD8ColumnCaptureComplete=bLOD8ColumnCaptureComplete + const std::size_t LOD=static_cast(Tile.Key.Level); + ++ColumnCaptureTilesByLOD[LOD]; + ColumnCaptureCompleteByLOD[LOD]=ColumnCaptureCompleteByLOD[LOD] &&Tile.CoreDiagnostics.columnCoordinateCaptureComplete; std::vector> Coordinates= Tile.CoreDiagnostics.evaluatedColumnCoordinates; @@ -7134,16 +7140,26 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; for(const auto& Coordinate:Coordinates) { const FColumnXY XY{Coordinate[0],Coordinate[1]}; - const auto [First,Inserted]=LOD8ColumnFirstUse.try_emplace(XY, - FFirstColumnUse{Tile.Key,Tile.CoreDiagnostics.columnEvaluationWorkerHash}); - if(Inserted)continue; - ++LOD8ColumnDuplicateEvents; - if(AreLOD8Neighbours(First->second.Tile,Tile.Key)) + auto [Uses,Inserted]=ColumnUsesByLOD[LOD].try_emplace(XY); + std::vector& PriorUses=Uses->second; + if(Inserted) { - ++LOD8NeighbourDuplicateEvents; - if(First->second.WorkerHash!=Tile.CoreDiagnostics.columnEvaluationWorkerHash) - ++LOD8CrossWorkerNeighbourDuplicateEvents; + PriorUses.push_back({Tile.Key,Tile.CoreDiagnostics.columnEvaluationWorkerHash}); + continue; } + ++ColumnDuplicateEventsByLOD[LOD]; + bool bHasNeighbourDuplicate=false,bHasCrossWorkerNeighbourDuplicate=false; + for(const FFirstColumnUse& Prior:PriorUses) + { + if(!AreSameLODNeighbours(Prior.Tile,Tile.Key))continue; + bHasNeighbourDuplicate=true; + bHasCrossWorkerNeighbourDuplicate=bHasCrossWorkerNeighbourDuplicate + ||Prior.WorkerHash!=Tile.CoreDiagnostics.columnEvaluationWorkerHash; + } + if(bHasNeighbourDuplicate)++NeighbourDuplicateEventsByLOD[LOD]; + if(bHasCrossWorkerNeighbourDuplicate) + ++CrossWorkerNeighbourDuplicateEventsByLOD[LOD]; + PriorUses.push_back({Tile.Key,Tile.CoreDiagnostics.columnEvaluationWorkerHash}); } } WholeViewReady=std::max(WholeViewReady,Tile.CompletedAt); @@ -7191,14 +7207,23 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; } } Streamer.Stop(); - if(bProfileFarColumnOverlap) + bool bColumnOverlapCapturePassing=true; + if(bProfileColumnOverlap) { - std::printf("[WorldAssemblerLOD8ColumnOverlap] tiles=%zu unique_xy_columns=%zu repeated_tile_evaluations=%llu neighbouring_duplicate_evaluations=%llu cross_worker_neighbouring_duplicates=%llu capture_complete=%d\n", - LOD8ColumnCaptureTiles,LOD8ColumnFirstUse.size(), - static_cast(LOD8ColumnDuplicateEvents), - static_cast(LOD8NeighbourDuplicateEvents), - static_cast(LOD8CrossWorkerNeighbourDuplicateEvents), - bLOD8ColumnCaptureComplete?1:0); + for(std::size_t LOD=0;LOD(ColumnCaptureTilesByLOD[LOD]), + static_cast(ExpectedTilesByLOD[LOD]), + ColumnUsesByLOD[LOD].size(), + static_cast(ColumnDuplicateEventsByLOD[LOD]), + static_cast(NeighbourDuplicateEventsByLOD[LOD]), + static_cast(CrossWorkerNeighbourDuplicateEventsByLOD[LOD]), + bLODComplete?1:0); + } } for(std::size_t LOD=0;LOD(Neighborhood.size()) &&FullDetailFinished==FullDetailExpected&&Finished==Update.DesiredTiles&&bHashesPassing - &&(!bProfileFarColumnOverlap||(bLOD8ColumnCaptureComplete&&LOD8ColumnCaptureTiles>0))?0:6; + &&(!bProfileColumnOverlap||bColumnOverlapCapturePassing)?0:6; } int RunStreamingBenchmark(FStandaloneOwnerWorld& World,VoxelForgeCore::World* CoreWorld, const FVector& CameraPositionVoxels, @@ -9601,7 +9626,7 @@ int main(int Argc,char** Argv) std::string WorldAssemblyManifestOutputPath; bool bStreamBenchmark=false; bool bStreamPipeline=false; - bool bProfileFarColumnOverlap=false; + bool bProfileColumnOverlap=false; bool bCoarseFirstStreamOrder=false; bool bProfileDetailBand=false; bool bPassageDetailAuditOnly=false; @@ -9739,8 +9764,8 @@ int main(int Argc,char** Argv) else if(Arg=="--coarse-first-stream-order")bCoarseFirstStreamOrder=true; else if(Arg=="--stream-pipeline") {bStreamPipeline=true;bStreamBenchmark=true;App.bShapeWorld=true;App.bForceGenerate=true;} - else if(Arg=="--profile-far-column-overlap") - {bProfileFarColumnOverlap=true;bStreamPipeline=true;bStreamBenchmark=true;App.bShapeWorld=true;App.bForceGenerate=true;} + else if(Arg=="--profile-column-overlap"||Arg=="--profile-far-column-overlap") + {bProfileColumnOverlap=true;bStreamPipeline=true;bStreamBenchmark=true;App.bShapeWorld=true;App.bForceGenerate=true;} else if(Arg=="--edit-benchmark") {bEditBenchmark=true;bStreamBenchmark=true;App.bShapeWorld=true;} else if(Arg=="--stream-hash")bStreamHash=true; @@ -9957,7 +9982,7 @@ int main(int Argc,char** Argv) FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0), App.WorkerCount,StreamBenchmarkMaxLevel,App.ShapeDetailVariant,App.CoreScheduler, bStreamHash||!BakeMeshCachePath.empty(),bSkipUvAndColors,bSkipMaterialGroups, - bSkipUV,bSkipColors,ColdProcessStart,bProfileFarColumnOverlap, + bSkipUV,bSkipColors,ColdProcessStart,bProfileColumnOverlap, App.FarHorizonLOD8InteriorStepVoxels); if(bEditBenchmark) return RunEditTransactionBenchmark(*App.World,*App.ShapeWorld, diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h index 764431e..a5853f6 100644 --- a/Standalone/Source/StreamingWorld.h +++ b/Standalone/Source/StreamingWorld.h @@ -424,13 +424,13 @@ public: bool bInSkipUV=false, bool bInSkipColors=false, VoxelForgeCore::World* InCoreWorld=nullptr, - bool bInCaptureFarColumnCoordinates=false, + bool bInCaptureColumnCoordinates=false, int32 InLod8HorizonInteriorStepVoxels=0) :World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,16)),bHashDensity(bInHashDensity), MeshCache(InMeshCache),MaxLevelFilter(InMaxLevelFilter), CoreWorld(InCoreWorld),bSkipUvAndColors(bInSkipUvAndColors),bSkipMaterialGroups(bInSkipMaterialGroups), bSkipUV(bInSkipUV),bSkipColors(bInSkipColors), - bCaptureFarColumnCoordinates(bInCaptureFarColumnCoordinates), + bCaptureColumnCoordinates(bInCaptureColumnCoordinates), Lod8HorizonInteriorStepVoxels(std::max(0,InLod8HorizonInteriorStepVoxels)) { VoxelGenLOD::InitializeConsoleSwitches(); @@ -799,7 +799,7 @@ private: Options.bandMinZVoxel=Job.BandZMin;Options.bandMaxZVoxel=Job.BandZMax; Options.hashDensity=bHashDensity;Options.skipUvAndColors=bSkipUvAndColors; Options.skipMaterialGroups=bSkipMaterialGroups;Options.skipUv=bSkipUV;Options.skipColors=bSkipColors; - Options.captureColumnCoordinates=bCaptureFarColumnCoordinates&&Level==8; + Options.captureColumnCoordinates=bCaptureColumnCoordinates; Options.lod8HorizonInteriorStepVoxels=Lod8HorizonInteriorStepVoxels; std::string Error; auto Result=CoreWorld->generateTile( @@ -971,7 +971,7 @@ private: bool bSkipMaterialGroups=false; bool bSkipUV=false; bool bSkipColors=false; - bool bCaptureFarColumnCoordinates=false; + bool bCaptureColumnCoordinates=false; int32 Lod8HorizonInteriorStepVoxels=0; std::atomic CacheHitCount{0}; mutable std::mutex Mutex;