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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-19 15:40:25 +02:00
1 parent 212814a75c
commit 87b0e5d8cd
9 files changed
+763 -76

No files matched your search

+22
View File
@@ -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. triangulation, collision, or Core output, so no recipe version changes.
See [`Standalone/Research/WorldAssemblerM3/Round16/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round16/REPORT.md). 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) ## 9. Multiplayer model (listen-server first, dedicated-friendly)
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
+12
View File
@@ -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 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). [`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. The density pipeline (most-edited hot path)
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
@@ -15940,13 +15940,282 @@ bool RunCoreSharedFeatureCostAudit(VoxelForgeCore::World& World,
const std::vector<VoxelForgeCore::TileKey>& Keys,const TCHAR* Label, const std::vector<VoxelForgeCore::TileKey>& Keys,const TCHAR* Label,
const VoxelForgeCore::WorldAssemblyManifest* Assembly,FString& OutError) 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<double> 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<double>& Values)
{ return Values.IsEmpty() ? 0.0 : *std::max_element(Values.GetData(),Values.GetData()+Values.Num()); };
const std::int32_t TileVoxelCount=static_cast<std::int32_t>(
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<World.configuration().fullResolutionLodCount
?TileVoxelCount:std::max(1,World.configuration().coarseTileCells);
TArray<double> 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<std::int64_t>(TileVoxelCount)<<LOD;
const std::int64_t TileStep=TileExtent/Cells;
const std::int64_t OriginX=static_cast<std::int64_t>(Key.coordinate.x)*TileExtent;
const std::int64_t OriginY=static_cast<std::int64_t>(Key.coordinate.y)*TileExtent;
const std::int64_t OriginZ=static_cast<std::int64_t>(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::int64_t>(
std::min(Plan.bottomChunkZ,Plan.topChunkZ))*TileVoxelCount;
const std::int64_t PlanMaximumZ=(static_cast<std::int64_t>(
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<std::int64_t,3> Minimum={
(OriginX-TileStep)*Subvoxels,(OriginY-TileStep)*Subvoxels,
(OriginZ-TileStep)*Subvoxels};
const std::array<std::int64_t,3> Maximum={
(OriginX+TileExtent+TileStep)*Subvoxels,
(OriginY+TileExtent+TileStep)*Subvoxels,
(OriginZ+TileExtent+TileStep)*Subvoxels};
std::vector<VoxelForgeCore::WorldAssemblyFeatureQueryV1> Queries;
Queries.push_back({Field.recipe(),Field.plan(),Field.buildFeatureTileList(Minimum,Maximum)});
const VoxelForgeCore::ScopedWorldAssemblyFeatureQueriesV1 QueryScope{
std::span<const VoxelForgeCore::WorldAssemblyFeatureQueryV1>(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<FProbeColumn> Exact(static_cast<std::size_t>(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<std::size_t>(Y)*(Cells+1)+X]={
Sample.groundHeightSubvoxels,Sample.capHeightSubvoxels};
}
const auto& BaselineStats=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
BaselineColumnMs.Add(BaselineStats.columnComputeMilliseconds);
BaselineColumns+=BaselineStats.columnMisses;
std::vector<std::int32_t> 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<FProbeColumn> Coarse(static_cast<std::size_t>(Axis.size())*Axis.size());
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true);
for(std::size_t Y=0;Y<Axis.size();++Y)
for(std::size_t X=0;X<Axis.size();++X)
{
const auto Sample=Field.point((OriginX+Axis[X]*TileStep)*Subvoxels,
(OriginY+Axis[Y]*TileStep)*Subvoxels,0);
Coarse[Y*Axis.size()+X]={Sample.groundHeightSubvoxels,
Sample.capHeightSubvoxels};
}
const auto& CandidateStats=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
CandidateColumnMs.Add(CandidateStats.columnComputeMilliseconds);
CandidateColumns+=CandidateStats.columnMisses;
const auto Bracket=[&Axis](std::int32_t Value)
{
const auto Upper=std::upper_bound(Axis.begin(),Axis.end(),Value);
const std::size_t High=Upper==Axis.end()?Axis.size()-1
:static_cast<std::size_t>(Upper-Axis.begin());
const std::size_t Low=High==0?0:High-1;
return std::pair<std::size_t,std::size_t>{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<double>(X-Axis[X0])/(Axis[X1]-Axis[X0]);
const double TY=Axis[Y1]==Axis[Y0]?0.0
:static_cast<double>(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<double>(C00.Ground),
static_cast<double>(C10.Ground),TX),Lerp(static_cast<double>(C01.Ground),
static_cast<double>(C11.Ground),TX),TY);
const double Cap=Lerp(Lerp(static_cast<double>(C00.Cap),
static_cast<double>(C10.Cap),TX),Lerp(static_cast<double>(C01.Cap),
static_cast<double>(C11.Cap),TX),TY);
const auto& Reference=Exact[static_cast<std::size_t>(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<unsigned long long>(BaselineColumns),
static_cast<unsigned long long>(CandidateColumns),
BaselineAverageMs>0.0?100.0*(1.0-CandidateAverageMs/BaselineAverageMs):0.0,
BaselineAverageMs,CandidateAverageMs,
P95(GroundErrorsCm),MaxValue(GroundErrorsCm),
static_cast<unsigned long long>(GroundOver2Cm),GroundErrorsCm.Num(),
P95(CapErrorsCm),MaxValue(CapErrorsCm),
static_cast<unsigned long long>(CapOver2Cm),CapErrorsCm.Num(),
static_cast<unsigned long long>(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<World.configuration().fullResolutionLodCount
?TileVoxelCount:std::max(1,World.configuration().coarseTileCells);
TArray<double> 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<std::int64_t>(TileVoxelCount)<<LOD;
const std::int64_t TileStep=TileExtent/Cells;
const std::int64_t OriginX=static_cast<std::int64_t>(Key.coordinate.x)*TileExtent;
const std::int64_t OriginY=static_cast<std::int64_t>(Key.coordinate.y)*TileExtent;
const std::int64_t OriginZ=static_cast<std::int64_t>(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::int64_t>(
std::min(Plan.bottomChunkZ,Plan.topChunkZ))*TileVoxelCount;
const std::int64_t PlanMaximumZ=(static_cast<std::int64_t>(
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<std::int64_t,3> Minimum={
(OriginX-TileStep)*Subvoxels,(OriginY-TileStep)*Subvoxels,
(OriginZ-TileStep)*Subvoxels};
const std::array<std::int64_t,3> Maximum={
(OriginX+TileExtent+TileStep)*Subvoxels,
(OriginY+TileExtent+TileStep)*Subvoxels,
(OriginZ+TileExtent+TileStep)*Subvoxels};
std::vector<VoxelForgeCore::WorldAssemblyFeatureQueryV1> Queries;
Queries.push_back({Field.recipe(),Field.plan(),Field.buildFeatureTileList(Minimum,Maximum)});
const VoxelForgeCore::ScopedWorldAssemblyFeatureQueriesV1 QueryScope{
std::span<const VoxelForgeCore::WorldAssemblyFeatureQueryV1>(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<FProbeColumn> Exact(static_cast<std::size_t>(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<std::size_t>(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<double>(Sample.groundHeightSubvoxels)
-Exact[static_cast<std::size_t>(Y)*(Cells+1)+X].Ground)
*CoreVoxelSizeCm/Subvoxels);
CapErrorsCm.Add(std::abs(static_cast<double>(Sample.capHeightSubvoxels)
-Exact[static_cast<std::size_t>(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<std::int32_t>(Drop),
static_cast<unsigned long long>(BaselineColumns),
static_cast<unsigned long long>(CandidateColumns),
BaselineAverageMs>0.0?100.0*(1.0-CandidateAverageMs/BaselineAverageMs):0.0,
BaselineAverageMs,CandidateAverageMs,
P95(GroundErrorsCm),MaxValue(GroundErrorsCm),
static_cast<unsigned long long>(GroundOver2Cm),GroundErrorsCm.Num(),
P95(CapErrorsCm),MaxValue(CapErrorsCm),
static_cast<unsigned long long>(CapOver2Cm),CapErrorsCm.Num(),
static_cast<unsigned long long>(FiniteFeatureTiles));
}
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true,0);
}
for (const int32 LOD : {0,1,3,4,5,8})
{ {
TArray<double> FullTimes,CandidateTimes,FullShellTimes,CandidateShellTimes,ListTimes; TArray<double> FullTimes,CandidateTimes,FullShellTimes,CandidateShellTimes,ListTimes;
TArray<double> UnknownBoxes,FieldCalls,ColumnMisses,StratumShellTimes,ConnectorShellTimes; TArray<double> UnknownBoxes,FieldCalls,ColumnMisses,StratumShellTimes,ConnectorShellTimes;
TArray<double> ClassificationTimes,ColumnComputeTimes,MeshingTimes,HorizonTimes,RepairSkipped; TArray<double> DensitySamples,ClassificationTimes,ClassificationExclusiveTimes;
TArray<double> ColumnComputeTimes,MeshingTimes,HorizonTimes,RepairSkipped;
TArray<double> ColumnEvalExclusiveTimes,MarchingExclusiveTimes,GroundRefineExclusiveTimes; TArray<double> ColumnEvalExclusiveTimes,MarchingExclusiveTimes,GroundRefineExclusiveTimes;
TArray<double> CapRefineExclusiveTimes,MeshAssemblyTimes; TArray<double> CapRefineExclusiveTimes,MeshAssemblyTimes;
TArray<double> MarchingColumnTimes,GroundColumnTimes,CapColumnTimes;
TArray<double> ClassificationNsPerSample,ColumnNsPerSample,MarchingNsPerSample;
TArray<double> GroundNsPerSample,CapNsPerSample,AssemblyNsPerSample,ListNsPerSample;
TArray<double> MarchingColumnNsPerSample,GroundColumnNsPerSample,CapColumnNsPerSample;
TArray<double> StratumFieldCalls,ConnectorFieldCalls,ShellTriangles,GroundTriangles,GroundRemoved,CapTriangles; TArray<double> StratumFieldCalls,ConnectorFieldCalls,ShellTriangles,GroundTriangles,GroundRemoved,CapTriangles;
for (const auto& Key : Keys) for (const auto& Key : Keys)
{ {
@@ -15955,57 +16224,98 @@ bool RunCoreSharedFeatureCostAudit(VoxelForgeCore::World& World,
Options.hashDensity=true; Options.hashDensity=true;
Options.useAssemblyFeatureQueries=false; Options.useAssemblyFeatureQueries=false;
std::string Error; std::string Error;
// Warm both routes before measuring: column/page caches must not favor the second route. // Check density and mesh parity with hashes, then time uncached runtime generation.
// Chauffer les deux parcours : les caches ne doivent pas favoriser le second. // Verifier la parite avec hashes, puis mesurer la generation sans cache de colonnes.
if (!World.generateTile(Key,Options,Error)) return false; const auto FullParity=World.generateTile(Key,Options,Error);
if (!FullParity) return false;
Options.useAssemblyFeatureQueries=true; 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; Options.useAssemblyFeatureQueries=false;
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true);
const double FullStart=FPlatformTime::Seconds(); const double FullStart=FPlatformTime::Seconds();
auto Full=World.generateTile(Key,Options,Error); auto Full=World.generateTile(Key,Options,Error);
FullTimes.Add((FPlatformTime::Seconds()-FullStart)*1000.0); FullTimes.Add((FPlatformTime::Seconds()-FullStart)*1000.0);
Options.useAssemblyFeatureQueries=true; Options.useAssemblyFeatureQueries=true;
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true);
const double CandidateStart=FPlatformTime::Seconds(); const double CandidateStart=FPlatformTime::Seconds();
auto Candidate=World.generateTile(Key,Options,Error); auto Candidate=World.generateTile(Key,Options,Error);
CandidateTimes.Add((FPlatformTime::Seconds()-CandidateStart)*1000.0); CandidateTimes.Add((FPlatformTime::Seconds()-CandidateStart)*1000.0);
if (!Full||!Candidate||Full->densityHash!=Candidate->densityHash if (!Full||!Candidate)
||Full->meshHash!=Candidate->meshHash||Full->payloadHash!=Candidate->payloadHash { OutError=TEXT("Shared feature full/candidate measured generation failed."); return false; }
||Full->topologyHash!=Candidate->topologyHash)
{ OutError=TEXT("Shared feature full/candidate tile hashes differ."); return false; }
FullShellTimes.Add(Full->diagnostics.farShellMilliseconds); FullShellTimes.Add(Full->diagnostics.farShellMilliseconds);
CandidateShellTimes.Add(Candidate->diagnostics.farShellMilliseconds); CandidateShellTimes.Add(Candidate->diagnostics.farShellMilliseconds);
ListTimes.Add(Candidate->diagnostics.featureListMilliseconds); ListTimes.Add(Candidate->diagnostics.featureListMilliseconds);
UnknownBoxes.Add(static_cast<double>(Full->diagnostics.unknownBoxes)); const auto& Diagnostics=Candidate->diagnostics;
FieldCalls.Add(static_cast<double>(Full->diagnostics.fieldCalls)); const double SampleCount=static_cast<double>(Candidate->densitySampleCount);
ColumnMisses.Add(static_cast<double>(Full->diagnostics.columnCacheMisses)); DensitySamples.Add(static_cast<double>(Candidate->densitySampleCount));
ClassificationTimes.Add(Full->diagnostics.classificationMilliseconds); UnknownBoxes.Add(static_cast<double>(Diagnostics.unknownBoxes));
ColumnComputeTimes.Add(Full->diagnostics.columnComputeMilliseconds); FieldCalls.Add(static_cast<double>(Diagnostics.fieldCalls));
ColumnEvalExclusiveTimes.Add(Full->diagnostics.columnEvaluationExclusiveMilliseconds); ColumnMisses.Add(static_cast<double>(Diagnostics.columnCacheMisses));
MarchingExclusiveTimes.Add(Full->diagnostics.marchingCubesExclusiveMilliseconds); ClassificationTimes.Add(Diagnostics.classificationMilliseconds);
GroundRefineExclusiveTimes.Add(Full->diagnostics.groundRefinementExclusiveMilliseconds); ClassificationExclusiveTimes.Add(Diagnostics.classificationExclusiveMilliseconds);
CapRefineExclusiveTimes.Add(Full->diagnostics.capRefinementExclusiveMilliseconds); ColumnComputeTimes.Add(Diagnostics.columnComputeMilliseconds);
MeshAssemblyTimes.Add(Full->diagnostics.meshAssemblyMilliseconds); ColumnEvalExclusiveTimes.Add(Diagnostics.columnEvaluationExclusiveMilliseconds);
MeshingTimes.Add(Full->diagnostics.meshingMilliseconds); MarchingExclusiveTimes.Add(Diagnostics.marchingCubesExclusiveMilliseconds);
HorizonTimes.Add(Full->diagnostics.farHorizonMilliseconds); MarchingColumnTimes.Add(Diagnostics.marchingCubesColumnComputeMilliseconds);
RepairSkipped.Add(Full->diagnostics.farShellRepairSkipped?1.0:0.0); GroundRefineExclusiveTimes.Add(Diagnostics.groundRefinementExclusiveMilliseconds);
StratumShellTimes.Add(Full->diagnostics.farStratumShellMilliseconds); GroundColumnTimes.Add(Diagnostics.groundRefinementColumnComputeMilliseconds);
ConnectorShellTimes.Add(Full->diagnostics.farConnectorShellMilliseconds); CapRefineExclusiveTimes.Add(Diagnostics.capRefinementExclusiveMilliseconds);
StratumFieldCalls.Add(static_cast<double>(Full->diagnostics.farStratumFieldCalls)); CapColumnTimes.Add(Diagnostics.capRefinementColumnComputeMilliseconds);
ConnectorFieldCalls.Add(static_cast<double>(Full->diagnostics.farConnectorFieldCalls)); MeshAssemblyTimes.Add(Diagnostics.meshAssemblyMilliseconds);
ShellTriangles.Add(static_cast<double>(Full->diagnostics.farShellTrianglesAdded)); MeshingTimes.Add(Diagnostics.meshingMilliseconds);
GroundTriangles.Add(static_cast<double>(Full->diagnostics.horizonGroundTriangles)); HorizonTimes.Add(Diagnostics.farHorizonMilliseconds);
GroundRemoved.Add(static_cast<double>(Full->diagnostics.horizonGroundTrianglesRemoved)); RepairSkipped.Add(Diagnostics.farShellRepairSkipped?1.0:0.0);
CapTriangles.Add(static_cast<double>(Full->diagnostics.horizonCapTriangles)); StratumShellTimes.Add(Diagnostics.farStratumShellMilliseconds);
ConnectorShellTimes.Add(Diagnostics.farConnectorShellMilliseconds);
StratumFieldCalls.Add(static_cast<double>(Diagnostics.farStratumFieldCalls));
ConnectorFieldCalls.Add(static_cast<double>(Diagnostics.farConnectorFieldCalls));
ShellTriangles.Add(static_cast<double>(Diagnostics.farShellTrianglesAdded));
GroundTriangles.Add(static_cast<double>(Diagnostics.horizonGroundTriangles));
GroundRemoved.Add(static_cast<double>(Diagnostics.horizonGroundTrianglesRemoved));
CapTriangles.Add(static_cast<double>(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<double> Values) const auto P95=[](TArray<double> Values)
{ Values.Sort(); return Values.IsEmpty() ? 0.0 : Values[FMath::Clamp( { Values.Sort(); return Values.IsEmpty() ? 0.0 : Values[FMath::Clamp(
FMath::CeilToInt(Values.Num()*0.95f)-1,0,Values.Num()-1)]; }; FMath::CeilToInt(Values.Num()*0.95f)-1,0,Values.Num()-1)]; };
UE_LOG(LogTemp,Display, 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), Label,LOD,FullTimes.Num(),P95(FullTimes),P95(CandidateTimes),P95(ListTimes),
P95(ClassificationTimes),P95(ColumnComputeTimes),P95(ColumnEvalExclusiveTimes), P95(ClassificationTimes),P95(ClassificationExclusiveTimes),P95(ColumnComputeTimes),
P95(MarchingExclusiveTimes),P95(GroundRefineExclusiveTimes),P95(CapRefineExclusiveTimes), P95(ColumnEvalExclusiveTimes),P95(MarchingColumnTimes),P95(MarchingExclusiveTimes),
P95(MeshAssemblyTimes),P95(MeshingTimes),P95(HorizonTimes), 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(FullShellTimes),P95(CandidateShellTimes),P95(RepairSkipped),P95(UnknownBoxes),P95(FieldCalls),
P95(ColumnMisses),P95(StratumShellTimes),P95(StratumFieldCalls), P95(ColumnMisses),P95(StratumShellTimes),P95(StratumFieldCalls),
P95(ConnectorShellTimes),P95(ConnectorFieldCalls),P95(ShellTriangles), P95(ConnectorShellTimes),P95(ConnectorFieldCalls),P95(ShellTriangles),
@@ -126,6 +126,10 @@ namespace VoxelForgeCore
double classificationMilliseconds = 0.0; double classificationMilliseconds = 0.0;
double columnComputeMilliseconds = 0.0; double columnComputeMilliseconds = 0.0;
double columnEvaluationExclusiveMilliseconds = 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 marchingCubesExclusiveMilliseconds = 0.0;
double groundRefinementExclusiveMilliseconds = 0.0; double groundRefinementExclusiveMilliseconds = 0.0;
double capRefinementExclusiveMilliseconds = 0.0; double capRefinementExclusiveMilliseconds = 0.0;
@@ -687,7 +691,8 @@ namespace VoxelForgeCore
std::int64_t minimumYSubvoxels, std::int64_t minimumZSubvoxels, std::int64_t minimumYSubvoxels, std::int64_t minimumZSubvoxels,
std::int64_t maximumXSubvoxels, std::int64_t maximumYSubvoxels, std::int64_t maximumXSubvoxels, std::int64_t maximumYSubvoxels,
std::int64_t maximumZSubvoxels, std::int32_t stepVoxels) const noexcept; 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; static const HeightfieldColumnCacheStats& threadColumnCacheStats() noexcept;
FieldInterval interval(std::int64_t minimumXSubvoxels, FieldInterval interval(std::int64_t minimumXSubvoxels,
std::int64_t minimumYSubvoxels, std::int64_t minimumYSubvoxels,
+25 -2
View File
@@ -1479,12 +1479,18 @@ namespace VoxelForgeCore
const bool bSkipFarShellRepair=key.lod>=7&&bUseNativeSurfaceHorizon; const bool bSkipFarShellRepair=key.lod>=7&&bUseNativeSurfaceHorizon;
FStandaloneShapeWorld::ResetThreadClassificationStats(); FStandaloneShapeWorld::ResetThreadClassificationStats();
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats( VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(
options.captureColumnCoordinates&&key.lod==8); options.captureColumnCoordinates);
bool bNativeSurfaceTile=false; bool bNativeSurfaceTile=false;
bool bNativeSurfaceBandSafe=false; bool bNativeSurfaceBandSafe=false;
double classificationColumnComputeMilliseconds=0.0;
if(bHasWorldAssembly) if(bHasWorldAssembly)
{ {
const double columnMillisecondsBeforeClassification=
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
const auto Classification=tileShape.ClassifyBoxDetailed(origin,step,cells); const auto Classification=tileShape.ClassifyBoxDetailed(origin,step,cells);
classificationColumnComputeMilliseconds=std::max(0.0,
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds
-columnMillisecondsBeforeClassification);
bNativeSurfaceTile=Classification.bNativeSurface; bNativeSurfaceTile=Classification.bNativeSurface;
// NativeSurfaceDensityAt consumes only the bounded manifest heightfield and its // NativeSurfaceDensityAt consumes only the bounded manifest heightfield and its
// bounded type-2/type-4 features. Unknown tile occupancy does not make that Z support // 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.columnCacheHits=Columns.columnHits;
out.diagnostics.columnCacheMisses=Columns.columnMisses; out.diagnostics.columnCacheMisses=Columns.columnMisses;
out.diagnostics.classificationMilliseconds=Counts.Milliseconds; out.diagnostics.classificationMilliseconds=Counts.Milliseconds;
out.diagnostics.classificationExclusiveMilliseconds=std::max(0.0,
Counts.Milliseconds-classificationColumnComputeMilliseconds);
out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds; out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds;
if(options.captureColumnCoordinates)
{
out.diagnostics.columnEvaluationWorkerHash=static_cast<std::uint64_t>(
std::hash<std::thread::id>{}(std::this_thread::get_id()))+1u;
out.diagnostics.columnCoordinateCaptureComplete=Columns.coordinateCaptureComplete;
out.diagnostics.evaluatedColumnCoordinates=Columns.evaluatedCoordinates;
}
out.densitySampleCount=0; out.densitySampleCount=0;
return out; return out;
} }
@@ -1662,6 +1677,8 @@ namespace VoxelForgeCore
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds; VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
out.diagnostics.marchingCubesExclusiveMilliseconds=std::max(0.0, out.diagnostics.marchingCubesExclusiveMilliseconds=std::max(0.0,
MarchingCubesWallMilliseconds-(ColumnMillisecondsAfterMeshing-ColumnMillisecondsBeforeMeshing)); MarchingCubesWallMilliseconds-(ColumnMillisecondsAfterMeshing-ColumnMillisecondsBeforeMeshing));
out.diagnostics.marchingCubesColumnComputeMilliseconds=std::max(0.0,
ColumnMillisecondsAfterMeshing-ColumnMillisecondsBeforeMeshing);
const double MeshingMilliseconds=std::chrono::duration<double,std::milli>( const double MeshingMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-MeshingStart).count(); std::chrono::steady_clock::now()-MeshingStart).count();
out.diagnostics.farHorizonVersion=key.lod>=4?options.farHorizonVersion:0; out.diagnostics.farHorizonVersion=key.lod>=4?options.farHorizonVersion:0;
@@ -1712,6 +1729,8 @@ namespace VoxelForgeCore
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds; VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
out.diagnostics.groundRefinementExclusiveMilliseconds=std::max(0.0, out.diagnostics.groundRefinementExclusiveMilliseconds=std::max(0.0,
GroundWallMilliseconds-(ColumnMillisecondsAfterGround-ColumnMillisecondsBeforeGround)); GroundWallMilliseconds-(ColumnMillisecondsAfterGround-ColumnMillisecondsBeforeGround));
out.diagnostics.groundRefinementColumnComputeMilliseconds=std::max(0.0,
ColumnMillisecondsAfterGround-ColumnMillisecondsBeforeGround);
const double ColumnMillisecondsBeforeCap= const double ColumnMillisecondsBeforeCap=
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds; VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
const auto CapStart=std::chrono::steady_clock::now(); const auto CapStart=std::chrono::steady_clock::now();
@@ -1723,6 +1742,8 @@ namespace VoxelForgeCore
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds; VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
out.diagnostics.capRefinementExclusiveMilliseconds=std::max(0.0, out.diagnostics.capRefinementExclusiveMilliseconds=std::max(0.0,
CapWallMilliseconds-(ColumnMillisecondsAfterCap-ColumnMillisecondsBeforeCap)); CapWallMilliseconds-(ColumnMillisecondsAfterCap-ColumnMillisecondsBeforeCap));
out.diagnostics.capRefinementColumnComputeMilliseconds=std::max(0.0,
ColumnMillisecondsAfterCap-ColumnMillisecondsBeforeCap);
out.diagnostics.farHorizonMilliseconds=std::chrono::duration<double,std::milli>( out.diagnostics.farHorizonMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-HorizonStart).count(); std::chrono::steady_clock::now()-HorizonStart).count();
out.diagnostics.capRefined=CapStats.Refined; out.diagnostics.capRefined=CapStats.Refined;
@@ -1747,7 +1768,9 @@ namespace VoxelForgeCore
out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds; out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds;
out.diagnostics.columnEvaluationExclusiveMilliseconds=Columns.columnComputeMilliseconds; out.diagnostics.columnEvaluationExclusiveMilliseconds=Columns.columnComputeMilliseconds;
out.diagnostics.classificationMilliseconds=Counts.Milliseconds; 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::uint64_t>( out.diagnostics.columnEvaluationWorkerHash=static_cast<std::uint64_t>(
std::hash<std::thread::id>{}(std::this_thread::get_id()))+1u; std::hash<std::thread::id>{}(std::this_thread::get_id()))+1u;
+15 -2
View File
@@ -1027,11 +1027,18 @@ namespace world_assembly_detail
std::int64_t wavelengthSubvoxels) std::int64_t wavelengthSubvoxels)
{ return FloorDiv(positionSubvoxels * Q16, 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, 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 zQ16, std::uint32_t octaves, bool ridged = false)
{ {
std::int64_t total = 0, weightSum = 0, weight = Q16, ridgeWeight = Q16; 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 scale = 1ll << octave;
const std::int64_t noise = Perlin3DQ16(xQ16 * scale,yQ16 * scale,zQ16 * scale); 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; 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; auto& Stats=world_assembly_detail::HeightfieldColumnStats;
world_assembly_detail::HeightfieldColumnOctaveDrop=octaveDrop;
Stats.pointCalls=0; Stats.pointCalls=0;
Stats.columnHits=0; Stats.columnHits=0;
Stats.columnMisses=0; Stats.columnMisses=0;
@@ -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.
+57 -32
View File
@@ -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, const FVector& CameraPositionVoxels,int32 WorkerCount,int32 MaxLevel,
VoxelForgeCore::DetailVariant DetailVariant,const VoxelForgeCore::JobScheduler& Scheduler, VoxelForgeCore::DetailVariant DetailVariant,const VoxelForgeCore::JobScheduler& Scheduler,
bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,bool bSkipUV,bool bSkipColors, 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) int32 FarHorizonLOD8InteriorStepVoxels)
{ {
using Clock=std::chrono::steady_clock; 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 auto ShapeReady=Clock::now();
const double ShapeSeconds=std::chrono::duration<double>(ShapeReady-ShapeBuildStart).count(); const double ShapeSeconds=std::chrono::duration<double>(ShapeReady-ShapeBuildStart).count();
FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel, FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileFarColumnOverlap, bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap,
FarHorizonLOD8InteriorStepVoxels); FarHorizonLOD8InteriorStepVoxels);
const auto StreamStart=Clock::now(); const auto StreamStart=Clock::now();
const FStandaloneStreamingUpdate Update=Streamer.StartWithDesired(Desired); 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; FVoxelTileKey Tile;
std::uint64_t WorkerHash=0; std::uint64_t WorkerHash=0;
}; };
std::unordered_map<FColumnXY,FFirstColumnUse,FColumnXYHash> LOD8ColumnFirstUse; std::array<std::unordered_map<FColumnXY,std::vector<FFirstColumnUse>,FColumnXYHash>,9> ColumnUsesByLOD;
std::uint64_t LOD8ColumnDuplicateEvents=0,LOD8NeighbourDuplicateEvents=0; std::array<std::uint64_t,9> ColumnDuplicateEventsByLOD{},NeighbourDuplicateEventsByLOD{};
std::uint64_t LOD8CrossWorkerNeighbourDuplicateEvents=0; std::array<std::uint64_t,9> CrossWorkerNeighbourDuplicateEventsByLOD{};
std::size_t LOD8ColumnCaptureTiles=0; std::array<std::uint64_t,9> ColumnCaptureTilesByLOD{},ExpectedTilesByLOD{};
bool bLOD8ColumnCaptureComplete=true; std::array<bool,9> ColumnCaptureCompleteByLOD{};
ColumnCaptureCompleteByLOD.fill(true);
std::array<double,9> WorkerSecondsByLOD{}; std::array<double,9> WorkerSecondsByLOD{};
std::array<double,9> ColumnEvalMsByLOD{},MarchingExclusiveMsByLOD{}; std::array<double,9> ColumnEvalMsByLOD{},MarchingExclusiveMsByLOD{};
std::array<double,9> GroundRefineExclusiveMsByLOD{},CapRefineExclusiveMsByLOD{}; std::array<double,9> GroundRefineExclusiveMsByLOD{},CapRefineExclusiveMsByLOD{};
std::array<double,9> MeshAssemblyMsByLOD{}; std::array<double,9> MeshAssemblyMsByLOD{};
std::array<std::uint64_t,9> TileCountByLOD{}; std::array<std::uint64_t,9> TileCountByLOD{};
const auto AreLOD8Neighbours=[](const FVoxelTileKey& A,const FVoxelTileKey& B) for(const FVoxelTileKey& Tile:Desired.Tiles)
if(Tile.Level>=0&&Tile.Level<static_cast<int32>(ExpectedTilesByLOD.size()))
++ExpectedTilesByLOD[static_cast<std::size_t>(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<std::int64_t>(A.Coord.X)-B.Coord.X); const std::int64_t DX=std::abs(static_cast<std::int64_t>(A.Coord.X)-B.Coord.X);
const std::int64_t DY=std::abs(static_cast<std::int64_t>(A.Coord.Y)-B.Coord.Y); const std::int64_t DY=std::abs(static_cast<std::int64_t>(A.Coord.Y)-B.Coord.Y);
const std::int64_t DZ=std::abs(static_cast<std::int64_t>(A.Coord.Z)-B.Coord.Z); const std::int64_t DZ=std::abs(static_cast<std::int64_t>(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; CapRefineExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.capRefinementExclusiveMilliseconds;
MeshAssemblyMsByLOD[LOD]+=Tile.CoreDiagnostics.meshAssemblyMilliseconds; MeshAssemblyMsByLOD[LOD]+=Tile.CoreDiagnostics.meshAssemblyMilliseconds;
} }
if(bProfileFarColumnOverlap&&Tile.Key.Level==8) if(bProfileColumnOverlap&&Tile.Key.Level>=0
&&Tile.Key.Level<static_cast<int32>(ColumnCaptureTilesByLOD.size()))
{ {
++LOD8ColumnCaptureTiles; const std::size_t LOD=static_cast<std::size_t>(Tile.Key.Level);
bLOD8ColumnCaptureComplete=bLOD8ColumnCaptureComplete ++ColumnCaptureTilesByLOD[LOD];
ColumnCaptureCompleteByLOD[LOD]=ColumnCaptureCompleteByLOD[LOD]
&&Tile.CoreDiagnostics.columnCoordinateCaptureComplete; &&Tile.CoreDiagnostics.columnCoordinateCaptureComplete;
std::vector<std::array<std::int64_t,2>> Coordinates= std::vector<std::array<std::int64_t,2>> Coordinates=
Tile.CoreDiagnostics.evaluatedColumnCoordinates; 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) for(const auto& Coordinate:Coordinates)
{ {
const FColumnXY XY{Coordinate[0],Coordinate[1]}; const FColumnXY XY{Coordinate[0],Coordinate[1]};
const auto [First,Inserted]=LOD8ColumnFirstUse.try_emplace(XY, auto [Uses,Inserted]=ColumnUsesByLOD[LOD].try_emplace(XY);
FFirstColumnUse{Tile.Key,Tile.CoreDiagnostics.columnEvaluationWorkerHash}); std::vector<FFirstColumnUse>& PriorUses=Uses->second;
if(Inserted)continue; if(Inserted)
++LOD8ColumnDuplicateEvents;
if(AreLOD8Neighbours(First->second.Tile,Tile.Key))
{ {
++LOD8NeighbourDuplicateEvents; PriorUses.push_back({Tile.Key,Tile.CoreDiagnostics.columnEvaluationWorkerHash});
if(First->second.WorkerHash!=Tile.CoreDiagnostics.columnEvaluationWorkerHash) continue;
++LOD8CrossWorkerNeighbourDuplicateEvents;
} }
++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); 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(); 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", for(std::size_t LOD=0;LOD<ColumnCaptureTilesByLOD.size();++LOD)
LOD8ColumnCaptureTiles,LOD8ColumnFirstUse.size(), {
static_cast<unsigned long long>(LOD8ColumnDuplicateEvents), const bool bLODComplete=ColumnCaptureCompleteByLOD[LOD]
static_cast<unsigned long long>(LOD8NeighbourDuplicateEvents), &&ColumnCaptureTilesByLOD[LOD]==ExpectedTilesByLOD[LOD];
static_cast<unsigned long long>(LOD8CrossWorkerNeighbourDuplicateEvents), bColumnOverlapCapturePassing=bColumnOverlapCapturePassing&&bLODComplete;
bLOD8ColumnCaptureComplete?1:0); std::printf("[WorldAssemblerLODColumnOverlap] lod=%zu tiles=%llu expected_tiles=%llu unique_xy_columns=%zu repeated_tile_evaluations=%llu neighbouring_duplicate_evaluations=%llu cross_worker_neighbouring_duplicates=%llu capture_complete=%d\n",
LOD,static_cast<unsigned long long>(ColumnCaptureTilesByLOD[LOD]),
static_cast<unsigned long long>(ExpectedTilesByLOD[LOD]),
ColumnUsesByLOD[LOD].size(),
static_cast<unsigned long long>(ColumnDuplicateEventsByLOD[LOD]),
static_cast<unsigned long long>(NeighbourDuplicateEventsByLOD[LOD]),
static_cast<unsigned long long>(CrossWorkerNeighbourDuplicateEventsByLOD[LOD]),
bLODComplete?1:0);
}
} }
for(std::size_t LOD=0;LOD<TileCountByLOD.size();++LOD) for(std::size_t LOD=0;LOD<TileCountByLOD.size();++LOD)
{ {
@@ -7253,7 +7278,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
} }
return NeighborhoodFinished==static_cast<int32>(Neighborhood.size()) return NeighborhoodFinished==static_cast<int32>(Neighborhood.size())
&&FullDetailFinished==FullDetailExpected&&Finished==Update.DesiredTiles&&bHashesPassing &&FullDetailFinished==FullDetailExpected&&Finished==Update.DesiredTiles&&bHashesPassing
&&(!bProfileFarColumnOverlap||(bLOD8ColumnCaptureComplete&&LOD8ColumnCaptureTiles>0))?0:6; &&(!bProfileColumnOverlap||bColumnOverlapCapturePassing)?0:6;
} }
int RunStreamingBenchmark(FStandaloneOwnerWorld& World,VoxelForgeCore::World* CoreWorld, int RunStreamingBenchmark(FStandaloneOwnerWorld& World,VoxelForgeCore::World* CoreWorld,
const FVector& CameraPositionVoxels, const FVector& CameraPositionVoxels,
@@ -9601,7 +9626,7 @@ int main(int Argc,char** Argv)
std::string WorldAssemblyManifestOutputPath; std::string WorldAssemblyManifestOutputPath;
bool bStreamBenchmark=false; bool bStreamBenchmark=false;
bool bStreamPipeline=false; bool bStreamPipeline=false;
bool bProfileFarColumnOverlap=false; bool bProfileColumnOverlap=false;
bool bCoarseFirstStreamOrder=false; bool bCoarseFirstStreamOrder=false;
bool bProfileDetailBand=false; bool bProfileDetailBand=false;
bool bPassageDetailAuditOnly=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=="--coarse-first-stream-order")bCoarseFirstStreamOrder=true;
else if(Arg=="--stream-pipeline") else if(Arg=="--stream-pipeline")
{bStreamPipeline=true;bStreamBenchmark=true;App.bShapeWorld=true;App.bForceGenerate=true;} {bStreamPipeline=true;bStreamBenchmark=true;App.bShapeWorld=true;App.bForceGenerate=true;}
else if(Arg=="--profile-far-column-overlap") else if(Arg=="--profile-column-overlap"||Arg=="--profile-far-column-overlap")
{bProfileFarColumnOverlap=true;bStreamPipeline=true;bStreamBenchmark=true;App.bShapeWorld=true;App.bForceGenerate=true;} {bProfileColumnOverlap=true;bStreamPipeline=true;bStreamBenchmark=true;App.bShapeWorld=true;App.bForceGenerate=true;}
else if(Arg=="--edit-benchmark") else if(Arg=="--edit-benchmark")
{bEditBenchmark=true;bStreamBenchmark=true;App.bShapeWorld=true;} {bEditBenchmark=true;bStreamBenchmark=true;App.bShapeWorld=true;}
else if(Arg=="--stream-hash")bStreamHash=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), FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),
App.WorkerCount,StreamBenchmarkMaxLevel,App.ShapeDetailVariant,App.CoreScheduler, App.WorkerCount,StreamBenchmarkMaxLevel,App.ShapeDetailVariant,App.CoreScheduler,
bStreamHash||!BakeMeshCachePath.empty(),bSkipUvAndColors,bSkipMaterialGroups, bStreamHash||!BakeMeshCachePath.empty(),bSkipUvAndColors,bSkipMaterialGroups,
bSkipUV,bSkipColors,ColdProcessStart,bProfileFarColumnOverlap, bSkipUV,bSkipColors,ColdProcessStart,bProfileColumnOverlap,
App.FarHorizonLOD8InteriorStepVoxels); App.FarHorizonLOD8InteriorStepVoxels);
if(bEditBenchmark) if(bEditBenchmark)
return RunEditTransactionBenchmark(*App.World,*App.ShapeWorld, return RunEditTransactionBenchmark(*App.World,*App.ShapeWorld,
+4 -4
View File
@@ -424,13 +424,13 @@ public:
bool bInSkipUV=false, bool bInSkipUV=false,
bool bInSkipColors=false, bool bInSkipColors=false,
VoxelForgeCore::World* InCoreWorld=nullptr, VoxelForgeCore::World* InCoreWorld=nullptr,
bool bInCaptureFarColumnCoordinates=false, bool bInCaptureColumnCoordinates=false,
int32 InLod8HorizonInteriorStepVoxels=0) int32 InLod8HorizonInteriorStepVoxels=0)
:World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,16)),bHashDensity(bInHashDensity), :World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,16)),bHashDensity(bInHashDensity),
MeshCache(InMeshCache),MaxLevelFilter(InMaxLevelFilter), MeshCache(InMeshCache),MaxLevelFilter(InMaxLevelFilter),
CoreWorld(InCoreWorld),bSkipUvAndColors(bInSkipUvAndColors),bSkipMaterialGroups(bInSkipMaterialGroups), CoreWorld(InCoreWorld),bSkipUvAndColors(bInSkipUvAndColors),bSkipMaterialGroups(bInSkipMaterialGroups),
bSkipUV(bInSkipUV),bSkipColors(bInSkipColors), bSkipUV(bInSkipUV),bSkipColors(bInSkipColors),
bCaptureFarColumnCoordinates(bInCaptureFarColumnCoordinates), bCaptureColumnCoordinates(bInCaptureColumnCoordinates),
Lod8HorizonInteriorStepVoxels(std::max(0,InLod8HorizonInteriorStepVoxels)) Lod8HorizonInteriorStepVoxels(std::max(0,InLod8HorizonInteriorStepVoxels))
{ {
VoxelGenLOD::InitializeConsoleSwitches(); VoxelGenLOD::InitializeConsoleSwitches();
@@ -799,7 +799,7 @@ private:
Options.bandMinZVoxel=Job.BandZMin;Options.bandMaxZVoxel=Job.BandZMax; Options.bandMinZVoxel=Job.BandZMin;Options.bandMaxZVoxel=Job.BandZMax;
Options.hashDensity=bHashDensity;Options.skipUvAndColors=bSkipUvAndColors; Options.hashDensity=bHashDensity;Options.skipUvAndColors=bSkipUvAndColors;
Options.skipMaterialGroups=bSkipMaterialGroups;Options.skipUv=bSkipUV;Options.skipColors=bSkipColors; Options.skipMaterialGroups=bSkipMaterialGroups;Options.skipUv=bSkipUV;Options.skipColors=bSkipColors;
Options.captureColumnCoordinates=bCaptureFarColumnCoordinates&&Level==8; Options.captureColumnCoordinates=bCaptureColumnCoordinates;
Options.lod8HorizonInteriorStepVoxels=Lod8HorizonInteriorStepVoxels; Options.lod8HorizonInteriorStepVoxels=Lod8HorizonInteriorStepVoxels;
std::string Error; std::string Error;
auto Result=CoreWorld->generateTile( auto Result=CoreWorld->generateTile(
@@ -971,7 +971,7 @@ private:
bool bSkipMaterialGroups=false; bool bSkipMaterialGroups=false;
bool bSkipUV=false; bool bSkipUV=false;
bool bSkipColors=false; bool bSkipColors=false;
bool bCaptureFarColumnCoordinates=false; bool bCaptureColumnCoordinates=false;
int32 Lod8HorizonInteriorStepVoxels=0; int32 Lod8HorizonInteriorStepVoxels=0;
std::atomic<uint64> CacheHitCount{0}; std::atomic<uint64> CacheHitCount{0};
mutable std::mutex Mutex; mutable std::mutex Mutex;