measure(perf): the remaining 4.3x is ALL density evaluation -- and the classifier costs 3.5x in game

Measurement only; no density or meshing decision changed. Full table in
PERF-ATTRIBUTION.md. The profiler self-check finally passes honestly: profiled 606.128
vs unprofiled 602.341 ms, **+0.63%** (it used to inflate 11.5x).

THE ATTRIBUTION, and it is almost embarrassingly one-sided

  mesh ledger, additive, sums to the clock exactly (sum_matches_mesh = true)
    shared density-grid fill    590.923 ms   97.4913%
    tile parallel wall           10.972 ms    1.8102%   cells, interp, normals, streams
    tile merge                    4.232 ms    0.6982%
    pre-tile setup / prep         0.002 ms    ~0%
    remainder                     0.0001 ms   ~0%
    ------------------------------------------------
    total                       606.128 ms

  GetDensityAt parent: 580.184 ms worker-wall, nested inside that 590.923 ms.

⇒ **The mesher is exonerated. Marching cubes, vertex interpolation, normals and stream
building are 2.5% combined.** The whole 4.3x is density evaluation, which is where three
of us have been looking and none of us had proven it.

WHAT IS ACTUALLY BEING DONE, per 2,248,091 samples
  rooms        11,099,891 candidates ->  1,730,920 evaluated
  tunnels      28,951,420 candidates ->  3,462,565 evaluated
  tunnel-core  28,951,420 candidates ->  1,881,812 evaluated
  ~31 candidates SCANNED per sample, ~3.1 evaluated
  fused detail ran on 1,079,777 and skipped 1,168,314 samples (52.0%)
  software tunnel cache: 2,248,043 hits / 48 misses -- not the problem

The post-stack structural tail, which I have suspected twice, is small: StructuralTail
20.9 ms CPU / 1.9 ms wall, DensityStructuralPosts 21.9 / 2.0, PassageStructuralPosts
10.5 / 1.0 -- all nested samples against a 590.9 ms wall.

⭐⭐ AND THE FINDING THAT MATTERS MOST FOR THE GAME
  clean export, classifier absent       587.991 ms
  same export with the classifier       2,056.596 ms
The classifier makes generation **3.5x slower**, it is ABSENT from the export path we
have benchmarked for five days, and the GAME PATH USES IT. Its control route runs
211,289 support-column builds, 2,171,763 candidates, 1,370,314 floor queries and
2,296,243 checks -- none of which the fused export route executes at all.

⇒ Every optimisation this week was measured on a path that skips the single largest
cost the player actually pays. That is a measurement-design failure and it is mine.

Also newly visible: the full export wall is 1,004.557 ms, of which mesh is 606.128 ms
(60.3%) and OBJ WRITING is 394.273 ms (39.2%). My "0.556 s" figures have always been
mesh_seconds alone.

One real hot-path improvement landed with the instrumentation: AreCountersEnabled()
returned an unconditional `true`, so counter hooks were live in shipping builds. It is
now a relaxed load gated on diagnostic mode.

VERIFIED BY ME: build 0 errors; export 0.552 / 0.555 / 0.566 s (cf0319d was
0.563-0.566, unchanged); geometry SHA-256 b3e5f4c3..9b377 UNCHANGED.

Honest gaps, as reported: exact per-operation shares inside the density stage, physical
cache-miss counters, and a rebuilt 34f06df binary for a true A/B were all unavailable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-12 08:45:26 +02:00
1 parent cf0319d480
commit bb8ec51939
9 files changed
+917 -74

No files matched your search

@@ -4729,6 +4729,8 @@ void VoxelCaveMorphology::BuildChunkCache(
uint32 Seed, int32 StrateIndex,
const TArray<FStrateTerrainOpEntry>* TerrainOps)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::RoomGraphBuild);
// Clear previous data (arrays keep their allocation for reuse)
OutCache.Reset();
OutCache.SDFBlendRadius = Params.SDFBlendRadius;
@@ -6025,6 +6027,8 @@ float VoxelCaveMorphology::EvaluateSDFCached(
float SDFBlendRadius,
int32* OutNearestRoomIdx)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::RoomGraphSdf);
float MinSDF = FLT_MAX;
const float BlendK = SDFBlendRadius;
const FVector Pos(WorldX, WorldY, WorldZ);
@@ -6273,6 +6277,8 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
const FChunkSDFCache& Cache,
const FTunnelSupportFloorColumn* SupportColumn)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCoreWorld);
const FVector Pos(WorldX, WorldY, WorldZ);
FTunnelCoreWorldEvaluation Result;
@@ -6491,6 +6497,8 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
const FChunkSDFCache& Cache,
FTunnelSupportFloorColumn& OutColumn)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCoreSupport);
OutColumn.Reset();
if (VoxelDensityProfile::AreCountersEnabled())
@@ -4009,6 +4009,8 @@ namespace
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::RoomGraphSource);
FState& S = State();
S.LastWorldPosition = FVector(WorldX, WorldY, WorldZ);
S.bLastTunnelCoreWorldEvaluationValid = false;
+263 -12
View File
@@ -2,10 +2,11 @@
namespace VoxelDensityProfile
{
std::atomic<bool> GEnabled(false);
std::atomic<EMode> GMode(EMode::Disabled);
namespace
{
std::atomic<bool> GEnabled(false);
std::atomic<EMode> GMode(EMode::Disabled);
std::atomic<uint32> GSampleInterval(DefaultSampleInterval);
std::atomic<uint64> GTimerPairCycles(0);
@@ -24,6 +25,7 @@ namespace VoxelDensityProfile
uint32 BlockCallCount[BucketCount]{};
uint64 BlockStartCycles[BucketCount]{};
bool BlockActive[BucketCount]{};
FSnapshot::FClassifierStats Classifier;
uint64 TunnelCacheCapacityEntries = 0;
uint64 TunnelCacheValidEntries = 0;
@@ -50,7 +52,6 @@ namespace VoxelDensityProfile
thread_local FThreadState* GThreadState = nullptr;
thread_local uint32 GScopeDepth = 0;
thread_local bool GScopeSampled = false;
thread_local bool GSampledScopeActive = false;
FThreadState& GetThreadState()
{
@@ -80,9 +81,9 @@ namespace VoxelDensityProfile
bool IsSampledScopeEnabled(EBucket Bucket)
{
// Sampled mode pays for the trustworthy parent and mesh containers only. Fine
// operation scopes remain available in Full mode, or through UE Insights scopes,
// without putting a Begin/End and thread-local bookkeeping on every density call.
// Sampled mode pays for the trustworthy parent and mesh containers only. Attribution
// additionally exposes the component scopes, but still samples them; Full remains the
// deliberately invasive mode for per-call scope timing.
switch (Bucket)
{
case EBucket::GetDensityAt:
@@ -94,6 +95,127 @@ namespace VoxelDensityProfile
return false;
}
}
bool IsAttributionScope(EBucket Bucket)
{
switch (Bucket)
{
case EBucket::GetDensityAt:
case EBucket::ApplyDisturbances:
case EBucket::PassageModifier:
case EBucket::PassageLandingAir:
case EBucket::PassageLandingFloor:
case EBucket::PassageLandingRoomFloor:
case EBucket::PassageTunnelAir:
case EBucket::PassageStructuralPosts:
case EBucket::TunnelCoreSupport:
case EBucket::TunnelCoreSdf:
case EBucket::TunnelCorePosts:
case EBucket::StructuralTail:
case EBucket::RoomGraphSource:
case EBucket::SdfCarve:
case EBucket::FusedEvaluator:
case EBucket::InterpretedOpStack:
case EBucket::OperatorBlock:
case EBucket::RoomGraphBuild:
case EBucket::RoomGraphSdf:
case EBucket::TunnelCoreWorld:
case EBucket::FusedDetail:
case EBucket::FusedWorm:
case EBucket::FusedStructural:
case EBucket::ClassifierTotal:
case EBucket::ClassifierIntervalProof:
case EBucket::ClassifierExactCore:
case EBucket::ClassifierExactFinal:
case EBucket::ClassifierRoomTail:
case EBucket::RuntimeStreamBuilding:
case EBucket::DensityPrologue:
case EBucket::DensityCore:
case EBucket::DensityDisturbances:
case EBucket::DensityStructuralPosts:
case EBucket::DensityBoundarySeal:
case EBucket::DensityDiffLayer:
case EBucket::DensityTail:
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherCellClassification:
case EBucket::MesherGradientNormals:
case EBucket::MesherVertexInterpolation:
case EBucket::MesherStreamBuilding:
case EBucket::MesherOther:
return true;
default:
return false;
}
}
bool IsAttributionAlwaysTimed(EBucket Bucket)
{
switch (Bucket)
{
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherOther:
case EBucket::RoomGraphBuild:
return true;
default:
return false;
}
}
uint64 ScaleSampledValue(uint64 Value, uint64 Calls, uint64 Samples)
{
if (Samples == 0 || Calls <= Samples)
{
return Value;
}
const double Scaled = static_cast<double>(Value)
* static_cast<double>(Calls) / static_cast<double>(Samples);
return Scaled > static_cast<double>(MAX_uint64)
? MAX_uint64
: static_cast<uint64>(Scaled + 0.5);
}
void AddClassifierStatsScaled(
FSnapshot::FClassifierStats& Destination,
const FSnapshot::FClassifierStats& Source,
uint64 Calls,
uint64 Samples)
{
Destination.RefineNodes += ScaleSampledValue(Source.RefineNodes, Calls, Samples);
Destination.StackBoxCalls += ScaleSampledValue(Source.StackBoxCalls, Calls, Samples);
Destination.WholeMixedNodes += ScaleSampledValue(Source.WholeMixedNodes, Calls, Samples);
Destination.WholeSolidNodes += ScaleSampledValue(Source.WholeSolidNodes, Calls, Samples);
Destination.WholeAirNodes += ScaleSampledValue(Source.WholeAirNodes, Calls, Samples);
Destination.NeedsFinalFieldNodes += ScaleSampledValue(Source.NeedsFinalFieldNodes, Calls, Samples);
Destination.SplitNodes += ScaleSampledValue(Source.SplitNodes, Calls, Samples);
Destination.MaxRefinementDepth = FMath::Max(
Destination.MaxRefinementDepth, Source.MaxRefinementDepth);
Destination.ExactCoreSamples += ScaleSampledValue(Source.ExactCoreSamples, Calls, Samples);
Destination.ExactFinalSamples += ScaleSampledValue(Source.ExactFinalSamples, Calls, Samples);
Destination.ExactCoreCacheHits += ScaleSampledValue(Source.ExactCoreCacheHits, Calls, Samples);
Destination.ExactFinalCacheHits += ScaleSampledValue(Source.ExactFinalCacheHits, Calls, Samples);
Destination.ExactCoreLeaves += ScaleSampledValue(Source.ExactCoreLeaves, Calls, Samples);
Destination.ExactFinalLeaves += ScaleSampledValue(Source.ExactFinalLeaves, Calls, Samples);
Destination.StackBoxCycles += ScaleSampledValue(Source.StackBoxCycles, Calls, Samples);
Destination.ExactCoreCycles += ScaleSampledValue(Source.ExactCoreCycles, Calls, Samples);
Destination.ExactFinalCycles += ScaleSampledValue(Source.ExactFinalCycles, Calls, Samples);
Destination.RoomTailQueries += ScaleSampledValue(Source.RoomTailQueries, Calls, Samples);
Destination.RoomTailEvaluated += ScaleSampledValue(Source.RoomTailEvaluated, Calls, Samples);
Destination.RoomTailCycles += ScaleSampledValue(Source.RoomTailCycles, Calls, Samples);
Destination.RoomPropagateCycles += ScaleSampledValue(Source.RoomPropagateCycles, Calls, Samples);
Destination.RoomExactPrimitiveCycles += ScaleSampledValue(Source.RoomExactPrimitiveCycles, Calls, Samples);
Destination.RoomCacheWindowCycles += ScaleSampledValue(Source.RoomCacheWindowCycles, Calls, Samples);
// These are topology descriptors, not per-call work counts. Keep the largest observed
// descriptor instead of multiplying one sampled graph description by the classifier
// call expansion factor.
Destination.RoomNumRooms = FMath::Max(Destination.RoomNumRooms, Source.RoomNumRooms);
Destination.RoomNumTunnels = FMath::Max(Destination.RoomNumTunnels, Source.RoomNumTunnels);
Destination.RoomNumRoomFloorJoins = FMath::Max(
Destination.RoomNumRoomFloorJoins, Source.RoomNumRoomFloorJoins);
Destination.RoomNumPits = FMath::Max(Destination.RoomNumPits, Source.RoomNumPits);
Destination.RoomNumChimneys = FMath::Max(Destination.RoomNumChimneys, Source.RoomNumChimneys);
}
}
void SetEnabled(bool bEnabled)
@@ -184,6 +306,7 @@ namespace VoxelDensityProfile
State->BlockStartCycles[Index] = 0;
State->BlockActive[Index] = false;
}
State->Classifier = FSnapshot::FClassifierStats();
State->TunnelCacheCapacityEntries = 0;
State->TunnelCacheValidEntries = 0;
State->TunnelCacheStaticBytes = 0;
@@ -253,6 +376,14 @@ namespace VoxelDensityProfile
Result.Counters[Index] += State->Counters[Index];
}
Result.Classifier.Calls += State->Classifier.Calls;
Result.Classifier.SampledCalls += State->Classifier.SampledCalls;
AddClassifierStatsScaled(
Result.Classifier,
State->Classifier,
State->Classifier.Calls,
State->Classifier.SampledCalls);
const uint64 TunnelWorkerBytes = State->TunnelCacheStaticBytes
+ State->TunnelCacheDynamicBytes;
if (State->TunnelCacheCapacityEntries > 0)
@@ -337,6 +468,19 @@ namespace VoxelDensityProfile
{
Result.Counters[Index] = State->Counters[Index];
}
Result.Classifier = State->Classifier;
if (State->Classifier.SampledCalls > 0)
{
FSnapshot::FClassifierStats Raw = State->Classifier;
Result.Classifier = FSnapshot::FClassifierStats();
Result.Classifier.Calls = State->Classifier.Calls;
Result.Classifier.SampledCalls = State->Classifier.SampledCalls;
AddClassifierStatsScaled(
Result.Classifier,
Raw,
State->Classifier.Calls,
State->Classifier.SampledCalls);
}
Result.TimerPairCycles = GetTimerPairCycles();
Result.SampleInterval = GetSampleInterval();
Result.Mode = GetMode();
@@ -345,6 +489,10 @@ namespace VoxelDensityProfile
void AddCounter(ECounter Counter, uint64 Amount)
{
if (!IsEnabledFast())
{
return;
}
const int32 Index = static_cast<int32>(Counter);
if (Index >= 0 && Index < CounterCount)
{
@@ -366,6 +514,56 @@ namespace VoxelDensityProfile
GetThreadState().Calls[Index] += 1;
}
void RecordClassifierCall(bool bSampled)
{
FThreadState& State = GetThreadState();
++State.Classifier.Calls;
if (bSampled)
{
++State.Classifier.SampledCalls;
}
++State.Calls[ToIndex(EBucket::ClassifierTotal)];
}
void AddClassifierStats(const FSnapshot::FClassifierStats& Stats)
{
FThreadState& State = GetThreadState();
State.Classifier.RefineNodes += Stats.RefineNodes;
State.Classifier.StackBoxCalls += Stats.StackBoxCalls;
State.Classifier.WholeMixedNodes += Stats.WholeMixedNodes;
State.Classifier.WholeSolidNodes += Stats.WholeSolidNodes;
State.Classifier.WholeAirNodes += Stats.WholeAirNodes;
State.Classifier.NeedsFinalFieldNodes += Stats.NeedsFinalFieldNodes;
State.Classifier.SplitNodes += Stats.SplitNodes;
State.Classifier.MaxRefinementDepth = FMath::Max(
State.Classifier.MaxRefinementDepth, Stats.MaxRefinementDepth);
State.Classifier.ExactCoreSamples += Stats.ExactCoreSamples;
State.Classifier.ExactFinalSamples += Stats.ExactFinalSamples;
State.Classifier.ExactCoreCacheHits += Stats.ExactCoreCacheHits;
State.Classifier.ExactFinalCacheHits += Stats.ExactFinalCacheHits;
State.Classifier.ExactCoreLeaves += Stats.ExactCoreLeaves;
State.Classifier.ExactFinalLeaves += Stats.ExactFinalLeaves;
State.Classifier.StackBoxCycles += Stats.StackBoxCycles;
State.Classifier.ExactCoreCycles += Stats.ExactCoreCycles;
State.Classifier.ExactFinalCycles += Stats.ExactFinalCycles;
State.Classifier.RoomTailQueries += Stats.RoomTailQueries;
State.Classifier.RoomTailEvaluated += Stats.RoomTailEvaluated;
State.Classifier.RoomTailCycles += Stats.RoomTailCycles;
State.Classifier.RoomPropagateCycles += Stats.RoomPropagateCycles;
State.Classifier.RoomExactPrimitiveCycles += Stats.RoomExactPrimitiveCycles;
State.Classifier.RoomCacheWindowCycles += Stats.RoomCacheWindowCycles;
State.Classifier.RoomNumRooms = FMath::Max(
State.Classifier.RoomNumRooms, Stats.RoomNumRooms);
State.Classifier.RoomNumTunnels = FMath::Max(
State.Classifier.RoomNumTunnels, Stats.RoomNumTunnels);
State.Classifier.RoomNumRoomFloorJoins = FMath::Max(
State.Classifier.RoomNumRoomFloorJoins, Stats.RoomNumRoomFloorJoins);
State.Classifier.RoomNumPits = FMath::Max(
State.Classifier.RoomNumPits, Stats.RoomNumPits);
State.Classifier.RoomNumChimneys = FMath::Max(
State.Classifier.RoomNumChimneys, Stats.RoomNumChimneys);
}
bool ShouldSample(EBucket Bucket)
{
if (!IsCycleTimingEnabled())
@@ -488,6 +686,12 @@ namespace VoxelDensityProfile
case ECounter::OpBlockOperators: return TEXT("OpBlockOperators");
case ECounter::OpBlockActiveOperators: return TEXT("OpBlockActiveOperators");
case ECounter::OpBlockPrunedOperators: return TEXT("OpBlockPrunedOperators");
case ECounter::MesherDensityGridBytes: return TEXT("MesherDensityGridBytes");
case ECounter::MesherSharedGridReadBytes: return TEXT("MesherSharedGridReadBytes");
case ECounter::MesherOutputArrayBytes: return TEXT("MesherOutputArrayBytes");
case ECounter::MesherOutputAllocatedBytes: return TEXT("MesherOutputAllocatedBytes");
case ECounter::OpBlockScratchBytes: return TEXT("OpBlockScratchBytes");
case ECounter::OpBlockCopyBytes: return TEXT("OpBlockCopyBytes");
case ECounter::Count: break;
}
return TEXT("Unknown");
@@ -535,6 +739,21 @@ namespace VoxelDensityProfile
{ TEXT("BoundarySealOp"), EBucket::BoundarySealOp },
{ TEXT("PassageCarveOp"), EBucket::PassageCarveOp },
{ TEXT("XYEdgeSealOp"), EBucket::XYEdgeSealOp },
{ TEXT("FusedEvaluator"), EBucket::FusedEvaluator },
{ TEXT("InterpretedOpStack"),EBucket::InterpretedOpStack },
{ TEXT("OperatorBlock"), EBucket::OperatorBlock },
{ TEXT("RoomGraphBuild"), EBucket::RoomGraphBuild },
{ TEXT("RoomGraphSdf"), EBucket::RoomGraphSdf },
{ TEXT("TunnelCoreWorld"), EBucket::TunnelCoreWorld },
{ TEXT("FusedDetail"), EBucket::FusedDetail },
{ TEXT("FusedWorm"), EBucket::FusedWorm },
{ TEXT("FusedStructural"), EBucket::FusedStructural },
{ TEXT("ClassifierTotal"), EBucket::ClassifierTotal },
{ TEXT("ClassifierIntervalProof"), EBucket::ClassifierIntervalProof },
{ TEXT("ClassifierExactCore"), EBucket::ClassifierExactCore },
{ TEXT("ClassifierExactFinal"), EBucket::ClassifierExactFinal },
{ TEXT("ClassifierRoomTail"), EBucket::ClassifierRoomTail },
{ TEXT("RuntimeStreamBuilding"), EBucket::RuntimeStreamBuilding },
{ TEXT("DensityPrologue"), EBucket::DensityPrologue },
{ TEXT("DensityCore"), EBucket::DensityCore },
{ TEXT("DensityDisturbances"), EBucket::DensityDisturbances },
@@ -596,6 +815,21 @@ namespace VoxelDensityProfile
case EBucket::PassageCarveOp: return TEXT("PassageCarveOp");
case EBucket::XYEdgeSealOp: return TEXT("XYEdgeSealOp");
case EBucket::OtherOp: return TEXT("OtherOp");
case EBucket::FusedEvaluator: return TEXT("FusedEvaluator");
case EBucket::InterpretedOpStack:return TEXT("InterpretedOpStack");
case EBucket::OperatorBlock: return TEXT("OperatorBlock");
case EBucket::RoomGraphBuild: return TEXT("RoomGraphBuild");
case EBucket::RoomGraphSdf: return TEXT("RoomGraphSdf");
case EBucket::TunnelCoreWorld: return TEXT("TunnelCoreWorld");
case EBucket::FusedDetail: return TEXT("FusedDetail");
case EBucket::FusedWorm: return TEXT("FusedWorm");
case EBucket::FusedStructural: return TEXT("FusedStructural");
case EBucket::ClassifierTotal: return TEXT("ClassifierTotal");
case EBucket::ClassifierIntervalProof:return TEXT("ClassifierIntervalProof");
case EBucket::ClassifierExactCore:return TEXT("ClassifierExactCore");
case EBucket::ClassifierExactFinal:return TEXT("ClassifierExactFinal");
case EBucket::ClassifierRoomTail:return TEXT("ClassifierRoomTail");
case EBucket::RuntimeStreamBuilding:return TEXT("RuntimeStreamBuilding");
case EBucket::DensityPrologue: return TEXT("DensityPrologue");
case EBucket::DensityCore: return TEXT("DensityCore");
case EBucket::DensityDisturbances:return TEXT("DensityDisturbances");
@@ -618,9 +852,14 @@ namespace VoxelDensityProfile
FScopeToken BeginScope(EBucket Bucket)
{
FScopeToken Token;
if (!IsEnabledFast())
{
return Token;
}
const EMode Mode = GetMode();
if (!IsEnabled()
|| (Mode == EMode::Sampled && !IsSampledScopeEnabled(Bucket)))
const bool bSampledMode = Mode == EMode::Sampled || Mode == EMode::Attribution;
if ((Mode == EMode::Sampled && !IsSampledScopeEnabled(Bucket))
|| (Mode == EMode::Attribution && !IsAttributionScope(Bucket)))
{
return Token;
}
@@ -633,7 +872,7 @@ namespace VoxelDensityProfile
// For the hot parent, amortise the timer over a deterministic block of calls. A timer
// pair around one ~100 ns operation is mostly the timer; a pair around 64 calls is cheap
// and estimates the parent from real elapsed work rather than instrumentation latency.
if (Mode == EMode::Sampled && Bucket == EBucket::GetDensityAt)
if (bSampledMode && Bucket == EBucket::GetDensityAt)
{
FThreadState& State = GetThreadState();
const int32 Index = ToIndex(Bucket);
@@ -649,7 +888,11 @@ namespace VoxelDensityProfile
State.BlockCallCount[Index] = 0;
State.BlockStartCycles[Index] = FPlatformTime::Cycles64();
}
GScopeSampled = false;
// The selected call admits all nested component scopes. Every other call exits the
// inline FScopedTimer fast path before crossing into this translation unit.
GScopeSampled = bBlockBoundary;
GSampledScopeActive = bBlockBoundary;
Token.bSampled = bBlockBoundary;
return Token;
}
@@ -660,13 +903,21 @@ namespace VoxelDensityProfile
// density samples underneath them.
const bool bContainerScope = Bucket == EBucket::MesherGenerateMesh
|| Bucket == EBucket::MesherDensityGrid;
const bool bInheritedAttributionSample = Mode == EMode::Attribution
&& GSampledScopeActive;
Token.bSampled = Mode == EMode::Full
? true
: (!GSampledScopeActive && ShouldSample(Bucket));
: ((Mode == EMode::Attribution && IsAttributionAlwaysTimed(Bucket))
? true
: (bInheritedAttributionSample
? true
: (Mode == EMode::Attribution && IsAttributionScope(Bucket)
? ShouldSample(Bucket)
: (!GSampledScopeActive && ShouldSample(Bucket)))));
GScopeSampled = Token.bSampled;
if (Token.bSampled)
{
if (Mode == EMode::Sampled && !bContainerScope)
if ((Mode == EMode::Sampled || Mode == EMode::Attribution) && !bContainerScope)
{
GSampledScopeActive = true;
}
+126 -39
View File
@@ -330,7 +330,15 @@ struct FVoxelDensityBlockSession
const int32 BlockSizeY = MaxY - MinY + 1;
const int32 BlockSizeZ = MaxZ - MinZ + 1;
const int32 BlockCount = BlockSizeX * BlockSizeY * BlockSizeZ;
VoxelDensityProfile::FScopedTimer OperatorBlockTimer(
VoxelDensityProfile::EBucket::OperatorBlock);
BlockScratch.SetNum(BlockCount);
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::OpBlockScratchBytes,
static_cast<uint64>(BlockCount) * sizeof(FVoxelOpSample));
}
for (FVoxelOpSample& Sample : BlockScratch)
{
Sample = FVoxelOpSample();
@@ -371,6 +379,13 @@ struct FVoxelDensityBlockSession
}
}
}
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::OpBlockCopyBytes,
static_cast<uint64>(BlockCount)
* (sizeof(FVoxelOpSample) + sizeof(uint8)));
}
if (VoxelDensityProfile::AreCountersEnabled())
{
@@ -2053,6 +2068,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// interpreted stack, plus the source's structural hand-off, without allocating
// FVoxelOpSample or traversing the graph one operator at a time for this voxel.
bUsedFusedEvaluator = true;
VoxelDensityProfile::FScopedTimer FusedEvaluatorTimer(
VoxelDensityProfile::EBucket::FusedEvaluator);
Result = GetDensityWithParams(
WorldX, WorldY, WorldZ, CP_Tunnel, CP_TunnelFP, LayoutVersion,
/*bApplyLegacyStructuralPosts=*/false,
@@ -2074,14 +2091,18 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// must not construct a per-sample FVoxelOpSample merely because this compatibility
// path still needs one when the block session is enabled.
FVoxelOpSample BlockCoreSample;
bUsedOpBlockSample = GVoxelDensityBlockSession.FillChunk(
*ActiveOpStack,
ChunkCoord,
FIntVector(
FMath::RoundToInt(WorldX),
FMath::RoundToInt(WorldY),
FMath::RoundToInt(WorldZ)),
BlockCoreSample);
{
VoxelDensityProfile::FScopedTimer OperatorBlockTimer(
VoxelDensityProfile::EBucket::OperatorBlock);
bUsedOpBlockSample = GVoxelDensityBlockSession.FillChunk(
*ActiveOpStack,
ChunkCoord,
FIntVector(
FMath::RoundToInt(WorldX),
FMath::RoundToInt(WorldY),
FMath::RoundToInt(WorldZ)),
BlockCoreSample);
}
if (bUsedOpBlockSample)
{
BlockDensity = BlockCoreSample.Density;
@@ -2093,9 +2114,16 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
}
if (!bUsedFusedEvaluator)
{
Result = bUsedOpBlockSample
? -BlockDensity
: ActiveOpStack->EvalMC(WorldX, WorldY, WorldZ);
if (bUsedOpBlockSample)
{
Result = -BlockDensity;
}
else
{
VoxelDensityProfile::FScopedTimer InterpretedOpStackTimer(
VoxelDensityProfile::EBucket::InterpretedOpStack);
Result = ActiveOpStack->EvalMC(WorldX, WorldY, WorldZ);
}
}
}
else switch (CP_GenType)
@@ -2221,6 +2249,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// calls use the same landing geometry already evaluated by the legacy and op-stack post.
VoxelDensityProfile::FScopedTimer DensityStructuralPostsTimer(
VoxelDensityProfile::EBucket::DensityStructuralPosts);
VoxelDensityProfile::FScopedTimer StructuralTailTimer(
VoxelDensityProfile::EBucket::StructuralTail);
float LandingBaseDensity = CP_Dist.BaseDensity;
#if WITH_EDITOR
// A composer parent can deliberately use a different structural base than the authored
@@ -2288,17 +2318,29 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
}
// Origin rooms are structural too. Reassert their air before either support writer so a
// bridge/ridge cannot plug a landing, while the two floor writers remain last.
VF_ApplyOriginLandingAirMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
VF_ApplyOriginLandingFloorMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageStructuralPostsMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness, bProtectAuthoredTunnelFloor);
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingAir);
VF_ApplyOriginLandingAirMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
}
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingFloor);
VF_ApplyOriginLandingFloorMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
}
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageStructuralPosts);
StrateManager->ApplyPassageStructuralPostsMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness, bProtectAuthoredTunnelFloor);
}
// Disturbance features are authored as a generic MC-space post and may add a ridge or
// bridge over a graph tunnel. Reassert the native cached tunnel core here, after every
// solid floor writer but before the global XY seal. This cache is built once per chunk,
@@ -2393,22 +2435,39 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// backstop is allowed to reopen the tunnel, but the landing's proved support floor
// must own the final floor band; otherwise the graph post can erase the only support
// surface at the mouth and leave the player-fit graph with a disconnected pocket.
VF_ApplyOriginLandingFloorMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageLandingFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
StrateManager->ApplyPassageLandingRoomFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingFloor);
VF_ApplyOriginLandingFloorMC(
Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
}
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingFloor);
StrateManager->ApplyPassageLandingFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
}
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingRoomFloor);
StrateManager->ApplyPassageLandingRoomFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
}
}
}
// Inter-strate passages own the same final D-floor contract as graph tunnels. This is
// composed from the immutable construction-time profile after generic writers and the
// graph overlap, so the legacy passage support slab can be disabled without losing the
// floor to a disturbance refill.
StrateManager->ApplyPassageNativeFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingFloor);
StrateManager->ApplyPassageNativeFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
}
StructuralTailTimer.End();
DensityStructuralPostsTimer.End();
}
else
@@ -2434,7 +2493,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// below and are deliberately still the user override path.
VoxelDensityProfile::FScopedTimer DensityBoundarySealTimer(
VoxelDensityProfile::EBucket::DensityBoundarySeal);
VF_ApplyXYEdgeSealMC(Result, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, 8.0f);
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::XYEdgeSealOp);
VF_ApplyXYEdgeSealMC(Result, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, 8.0f);
}
DensityBoundarySealTimer.End();
//=========================================================================
@@ -2879,6 +2942,9 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
: VoxelDensityProfile::ECounter::FusedTunnelDetailSkipped);
}
VoxelDensityProfile::FScopedTimer FusedDetailTimer(
VoxelDensityProfile::EBucket::FusedDetail);
//=========================================================================
// STEP 4b: SURFACE ROUGHNESS (volumetric, SDF-based)
//=========================================================================
@@ -3502,6 +3568,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
}
} // end bNearCaveSurface (terrain ops)
FusedDetailTimer.End();
// Steps 4e / 4f (pits and chimneys) are now handled by the CaveSDF SmoothMin
// block inserted above (between EvaluateSDFCached and the CarveFactor section).
@@ -3524,6 +3591,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
// already computed by Step 4 — free): full strength at the network, smooth fade to zero
// at Range. Worms become braids/shortcuts hugging the cave system; no isolated speckle.
// Range = 0 → unmasked legacy behaviour. Bonus: fully-masked voxels skip both Perlins.
VoxelDensityProfile::FScopedTimer FusedWormTimer(
VoxelDensityProfile::EBucket::FusedWorm);
if (Params.WormStrength > 0.0f && Params.WormThreshold > 0.0f)
{
float NetworkMask = 1.0f;
@@ -3570,16 +3639,28 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
}
}
FusedWormTimer.End();
//=========================================================================
// STEP 6: STRATE BOUNDARY SEAL (haut + bas)
//=========================================================================
ApplyOriginSpine(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
VoxelDensityProfile::FScopedTimer FusedStructuralTimer(
VoxelDensityProfile::EBucket::FusedStructural);
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::OriginSpineOp);
ApplyOriginSpine(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
}
ApplyBoundarySeal(Density, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity);
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::BoundarySealOp);
ApplyBoundarySeal(Density, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity);
}
//=========================================================================
// STEP 7: INTER-STRATE PASSAGES (perce le seal)
@@ -3588,6 +3669,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
{
if (bApplyLegacyStructuralPosts)
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageModifier);
StrateManager->ApplyPassageModifier(
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
@@ -3597,6 +3680,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
// GetDensityAt owns one common MC-space structural tail after either the legacy or
// operator-stack branch. Keep the actual tube carve here, but do not repeat its
// landing air/floor/tunnel-air posts before the shared disturbance/post sequence.
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageCarveOp);
StrateManager->ApplyPassageCarvingOnly(
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
@@ -3646,6 +3731,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
}
FusedStructuralTimer.End();
// Convention MC: négatif = solide, positif = air.
// La logique interne utilise positif = solide (plus lisible), donc on négate.
return -Density;
@@ -300,8 +300,96 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherBlockTests);
}
const bool bAttributeClassifier =
VoxelDensityProfile::GetMode()
== VoxelDensityProfile::EMode::Attribution;
const bool bSampleClassifier = bAttributeClassifier
&& VoxelDensityProfile::ShouldSample(
VoxelDensityProfile::EBucket::ClassifierTotal);
const uint64 ClassifierStartCycles = bSampleClassifier
? FPlatformTime::Cycles64() : 0;
FVoxelTileClassificationStats ClassifierStats;
if (bAttributeClassifier)
{
VoxelDensityProfile::RecordClassifierCall(bSampleClassifier);
}
const EVoxelTileClass Verdict = Generator->ClassifyTile(
BlockOrigin, Step, BlockCells);
BlockOrigin, Step, BlockCells,
bSampleClassifier ? &ClassifierStats : nullptr);
if (bSampleClassifier)
{
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::ClassifierTotal,
FPlatformTime::Cycles64() - ClassifierStartCycles);
VoxelDensityProfile::FSnapshot::FClassifierStats ProfileStats;
ProfileStats.RefineNodes = ClassifierStats.RefineNodes;
ProfileStats.StackBoxCalls = ClassifierStats.StackBoxCalls;
ProfileStats.WholeMixedNodes = ClassifierStats.WholeMixedNodes;
ProfileStats.WholeSolidNodes = ClassifierStats.WholeSolidNodes;
ProfileStats.WholeAirNodes = ClassifierStats.WholeAirNodes;
ProfileStats.NeedsFinalFieldNodes = ClassifierStats.NeedsFinalFieldNodes;
ProfileStats.SplitNodes = ClassifierStats.SplitNodes;
ProfileStats.MaxRefinementDepth = ClassifierStats.MaxRefinementDepth;
ProfileStats.ExactCoreSamples = ClassifierStats.ExactCoreSamples;
ProfileStats.ExactFinalSamples = ClassifierStats.ExactFinalSamples;
ProfileStats.ExactCoreCacheHits = ClassifierStats.ExactCoreCacheHits;
ProfileStats.ExactFinalCacheHits = ClassifierStats.ExactFinalCacheHits;
ProfileStats.ExactCoreLeaves = ClassifierStats.ExactCoreLeaves;
ProfileStats.ExactFinalLeaves = ClassifierStats.ExactFinalLeaves;
ProfileStats.StackBoxCycles = ClassifierStats.StackBoxCycles;
ProfileStats.ExactCoreCycles = ClassifierStats.ExactCoreCycles;
ProfileStats.ExactFinalCycles = ClassifierStats.ExactFinalCycles;
ProfileStats.RoomTailQueries = ClassifierStats.RoomTailQueries;
ProfileStats.RoomTailEvaluated = ClassifierStats.RoomTailEvaluated;
ProfileStats.RoomTailCycles = ClassifierStats.RoomTailCycles;
ProfileStats.RoomPropagateCycles = ClassifierStats.RoomPropagateCycles;
ProfileStats.RoomExactPrimitiveCycles = ClassifierStats.RoomExactPrimitiveCycles;
ProfileStats.RoomCacheWindowCycles = ClassifierStats.RoomCacheWindowCycles;
ProfileStats.RoomNumRooms = FMath::Max(ClassifierStats.RoomNumRooms, 0);
ProfileStats.RoomNumTunnels = FMath::Max(ClassifierStats.RoomNumTunnels, 0);
ProfileStats.RoomNumRoomFloorJoins = FMath::Max(
ClassifierStats.RoomNumRoomFloorJoins, 0);
ProfileStats.RoomNumPits = FMath::Max(ClassifierStats.RoomNumPits, 0);
ProfileStats.RoomNumChimneys = FMath::Max(ClassifierStats.RoomNumChimneys, 0);
VoxelDensityProfile::AddClassifierStats(ProfileStats);
if (ClassifierStats.StackBoxCalls > 0)
{
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::ClassifierIntervalProof,
0, ClassifierStats.StackBoxCalls);
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::ClassifierIntervalProof,
ClassifierStats.StackBoxCycles);
}
if (ClassifierStats.ExactCoreLeaves > 0)
{
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::ClassifierExactCore,
0, ClassifierStats.ExactCoreLeaves);
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::ClassifierExactCore,
ClassifierStats.ExactCoreCycles);
}
if (ClassifierStats.ExactFinalLeaves > 0)
{
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::ClassifierExactFinal,
0, ClassifierStats.ExactFinalLeaves);
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::ClassifierExactFinal,
ClassifierStats.ExactFinalCycles);
}
if (ClassifierStats.RoomTailQueries > 0)
{
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::ClassifierRoomTail,
0, ClassifierStats.RoomTailQueries);
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::ClassifierRoomTail,
ClassifierStats.RoomTailCycles);
}
}
if (Verdict == EVoxelTileClass::Mixed)
{
MixedBlockIndices.Add(
@@ -490,6 +578,21 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
}
}
}
if (VoxelDensityProfile::AreCountersEnabled())
{
const uint64 GridSampleCount = static_cast<uint64>(GzHi - GzLo + 1)
* static_cast<uint64>(MDim)
* static_cast<uint64>(MDim);
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherDensityGridBytes,
GridSampleCount * sizeof(float));
if (bUseSharedDensityGrid)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherSharedGridReadBytes,
GridSampleCount * sizeof(float));
}
}
if (bUseOperatorBlock)
{
Generator->EndDensityBlock();
@@ -835,6 +938,26 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
MeshData.Triangles.Append(CapTris);
MeshData.NumCeilingTriangles = CapTris.Num() / 3;
if (VoxelDensityProfile::AreCountersEnabled())
{
const uint64 OutputArrayBytes = static_cast<uint64>(MeshData.Vertices.Num()) * sizeof(FVector)
+ static_cast<uint64>(MeshData.Normals.Num()) * sizeof(FVector)
+ static_cast<uint64>(MeshData.UVs.Num()) * sizeof(FVector2D)
+ static_cast<uint64>(MeshData.Colors.Num()) * sizeof(FColor)
+ static_cast<uint64>(MeshData.Triangles.Num()) * sizeof(int32);
const uint64 OutputAllocatedBytes = static_cast<uint64>(MeshData.Vertices.GetAllocatedSize())
+ static_cast<uint64>(MeshData.Normals.GetAllocatedSize())
+ static_cast<uint64>(MeshData.UVs.GetAllocatedSize())
+ static_cast<uint64>(MeshData.Colors.GetAllocatedSize())
+ static_cast<uint64>(MeshData.Triangles.GetAllocatedSize());
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherOutputArrayBytes,
OutputArrayBytes);
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherOutputAllocatedBytes,
OutputAllocatedBytes);
}
return MeshData;
}
+27 -3
View File
@@ -68,6 +68,11 @@ namespace
TEXT("voxel.ProfileDensityFull"),
GVoxelForgeProfileDensityFull,
TEXT("Collect full per-operator profiling for the game path."));
int32 GVoxelForgePerfAttribution = 0;
FAutoConsoleVariableRef CVarVoxelForgePerfAttribution(
TEXT("voxel.PerfAttribution"),
GVoxelForgePerfAttribution,
TEXT("Collect low-overhead component attribution for the game path."));
// Startup command-line cvars can arrive before the game world has spawned AVoxelWorld (or
// before its first tiles have become visible). Keep the diagnostic edit pending until Tick has
@@ -1038,6 +1043,7 @@ void AVoxelWorld::BeginPlay()
// the default mode because of startup ordering.
int32 CommandLineProfile = GVoxelForgeProfileDensity;
int32 CommandLineProfileFull = GVoxelForgeProfileDensityFull;
int32 CommandLinePerfAttribution = GVoxelForgePerfAttribution;
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ProfileDensity="), CommandLineProfile))
{
GVoxelForgeProfileDensity = CommandLineProfile;
@@ -1046,8 +1052,21 @@ void AVoxelWorld::BeginPlay()
{
GVoxelForgeProfileDensityFull = CommandLineProfileFull;
}
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.PerfAttribution="), CommandLinePerfAttribution))
{
GVoxelForgePerfAttribution = CommandLinePerfAttribution;
}
if (GVoxelForgeProfileDensityFull != 0)
if (GVoxelForgePerfAttribution != 0)
{
VoxelDensityProfile::SetMode(
VoxelDensityProfile::EMode::Attribution,
VoxelDensityProfile::DefaultSampleInterval);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeDensityProfile] game_mode=attribution sample_interval=%u"),
VoxelDensityProfile::DefaultSampleInterval);
}
else if (GVoxelForgeProfileDensityFull != 0)
{
VoxelDensityProfile::SetMode(
VoxelDensityProfile::EMode::Full,
@@ -2084,7 +2103,8 @@ void AVoxelWorld::LogStreamingLatencySummary() const
const double TravelP95Metres = RequestP95
* static_cast<double>(PeakObservedPawnSpeedCmPerSecond) / 100.0;
const bool bDiagnosticsEnabled = GVoxelForgeProfileTileGeneration != 0
|| VoxelForgeStartupTrace::IsActive();
|| VoxelForgeStartupTrace::IsActive()
|| VoxelDensityProfile::GetMode() != VoxelDensityProfile::EMode::Disabled;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStreamingLatency] mode=%s diagnostics=%s lod0_ready_samples=%d "
"request_to_ready_s[p50=%.6f p95=%.6f max=%.6f] "
@@ -3651,7 +3671,9 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
const std::atomic<bool>* ObsoleteFlag)
{
const bool bLogTileProfile = GVoxelForgeProfileTileGeneration != 0;
const bool bMeasureTile = bLogTileProfile || VoxelForgeStartupTrace::IsActive();
const bool bMeasureTile = bLogTileProfile
|| VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Attribution
|| VoxelForgeStartupTrace::IsActive();
const double TileStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0;
double ClassifySeconds = 0.0;
double MeshSeconds = 0.0;
@@ -3919,6 +3941,8 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_BuildStreams);
const double StreamStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0;
Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>();
VoxelDensityProfile::FScopedTimer RuntimeStreamTimer(
VoxelDensityProfile::EBucket::RuntimeStreamBuilding);
BuildTileStreamSet(*Result.Streams, MeshData);
StreamSeconds = bMeasureTile ? FPlatformTime::Seconds() - StreamStartSeconds : 0.0;
if (ShouldAbortWork(ObsoleteFlag))
+127 -7
View File
@@ -15,12 +15,15 @@ namespace VoxelDensityProfile
{
Disabled,
Sampled,
// Low-overhead attribution: keeps the parent and stage ledger trustworthy, samples
// hot component scopes, and collects classifier detail without the old full-scope storm.
Attribution,
Full,
};
// Count-based sampling is deterministic with respect to the profiler's own counters, but the
// report is intentionally diagnostic only. It never participates in density or mesh output.
constexpr uint32 DefaultSampleInterval = 64;
constexpr uint32 DefaultSampleInterval = 128;
enum class EBucket : uint8
{
@@ -56,6 +59,23 @@ namespace VoxelDensityProfile
PassageCarveOp,
XYEdgeSealOp,
OtherOp,
// Attribution components. These are nested diagnostics; the non-overlapping phase
// buckets below remain the density accounting ledger.
FusedEvaluator,
InterpretedOpStack,
OperatorBlock,
RoomGraphBuild,
RoomGraphSdf,
TunnelCoreWorld,
FusedDetail,
FusedWorm,
FusedStructural,
ClassifierTotal,
ClassifierIntervalProof,
ClassifierExactCore,
ClassifierExactFinal,
ClassifierRoomTail,
RuntimeStreamBuilding,
// Non-overlapping top-level phases. The named operation buckets above are intentionally
// nested diagnostics; these phase buckets are the accounting ledger that must add up to
// GetDensityAt without double-counting a stack op inside its enclosing phase.
@@ -128,11 +148,24 @@ namespace VoxelDensityProfile
OpBlockOperators,
OpBlockActiveOperators,
OpBlockPrunedOperators,
MesherDensityGridBytes,
MesherSharedGridReadBytes,
MesherOutputArrayBytes,
MesherOutputAllocatedBytes,
OpBlockScratchBytes,
OpBlockCopyBytes,
Count
};
constexpr int32 CounterCount = static_cast<int32>(ECounter::Count);
// Export the one fast-path bit so callers in the editor module can skip instrumentation
// without crossing the DLL boundary for every scoped timer or counter. It is diagnostic
// state only; it never participates in density or mesh decisions.
VOXELFORGE_API extern std::atomic<bool> GEnabled;
VOXELFORGE_API extern std::atomic<EMode> GMode;
inline thread_local bool GSampledScopeActive = false;
// Resident allocator-backed bytes for one worker's generation cache family. The profiler
// aggregates this by worker; the fields intentionally describe allocations owned by the
// cache, not the inline size of the enclosing C++ object. SlotStorageBytes is the one
@@ -193,6 +226,44 @@ namespace VoxelDensityProfile
uint32 ActiveWorkers[BucketCount]{};
uint64 Counters[CounterCount]{};
// Classifier detail is sampled separately from the timer buckets. Calls is the exact
// number of classifier invocations; the remaining fields are worker-local samples
// expanded at Snapshot() using Calls / SampledCalls.
struct FClassifierStats
{
uint64 Calls = 0;
uint64 SampledCalls = 0;
uint64 RefineNodes = 0;
uint64 StackBoxCalls = 0;
uint64 WholeMixedNodes = 0;
uint64 WholeSolidNodes = 0;
uint64 WholeAirNodes = 0;
uint64 NeedsFinalFieldNodes = 0;
uint64 SplitNodes = 0;
uint64 MaxRefinementDepth = 0;
uint64 ExactCoreSamples = 0;
uint64 ExactFinalSamples = 0;
uint64 ExactCoreCacheHits = 0;
uint64 ExactFinalCacheHits = 0;
uint64 ExactCoreLeaves = 0;
uint64 ExactFinalLeaves = 0;
uint64 StackBoxCycles = 0;
uint64 ExactCoreCycles = 0;
uint64 ExactFinalCycles = 0;
uint64 RoomTailQueries = 0;
uint64 RoomTailEvaluated = 0;
uint64 RoomTailCycles = 0;
uint64 RoomPropagateCycles = 0;
uint64 RoomExactPrimitiveCycles = 0;
uint64 RoomCacheWindowCycles = 0;
uint64 RoomNumRooms = 0;
uint64 RoomNumTunnels = 0;
uint64 RoomNumRoomFloorJoins = 0;
uint64 RoomNumPits = 0;
uint64 RoomNumChimneys = 0;
};
FClassifierStats Classifier;
uint64 TimerPairCycles = 0;
uint32 SampleInterval = DefaultSampleInterval;
EMode Mode = EMode::Disabled;
@@ -225,9 +296,47 @@ namespace VoxelDensityProfile
VOXELFORGE_API void SetEnabled(bool bEnabled);
VOXELFORGE_API void SetMode(EMode Mode, uint32 SampleInterval = DefaultSampleInterval);
// Coarse counters are intentionally always collected, even when cycle timing is disabled.
// They are the low-cost diagnostic path; only cycle timing is opt-in.
FORCEINLINE bool AreCountersEnabled() { return true; }
// Counters are collected only while a diagnostic mode is enabled. In the ordinary clean
// path this is a single relaxed load, so the measurement hooks stay dormant.
FORCEINLINE bool IsEnabledFast()
{
return GEnabled.load(std::memory_order_relaxed);
}
FORCEINLINE bool AreCountersEnabled() { return IsEnabledFast(); }
FORCEINLINE bool ShouldProbeScopeFast(EBucket Bucket)
{
const EMode Mode = GMode.load(std::memory_order_relaxed);
if (Mode == EMode::Full)
{
return true;
}
if (Mode == EMode::Sampled)
{
return Bucket == EBucket::GetDensityAt
|| Bucket == EBucket::MesherGenerateMesh
|| Bucket == EBucket::MesherDensityGrid
|| Bucket == EBucket::MesherOther;
}
if (Mode == EMode::Attribution)
{
switch (Bucket)
{
case EBucket::GetDensityAt:
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherCellClassification:
case EBucket::MesherGradientNormals:
case EBucket::MesherVertexInterpolation:
case EBucket::MesherStreamBuilding:
case EBucket::MesherOther:
case EBucket::RoomGraphBuild:
return true;
default:
return GSampledScopeActive;
}
}
return false;
}
VOXELFORGE_API bool IsEnabled();
VOXELFORGE_API bool IsCycleTimingEnabled();
VOXELFORGE_API EMode GetMode();
@@ -242,6 +351,8 @@ namespace VoxelDensityProfile
VOXELFORGE_API void AddCounter(ECounter Counter, uint64 Amount = 1);
VOXELFORGE_API void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls = 1);
VOXELFORGE_API void RecordCall(EBucket Bucket);
VOXELFORGE_API void RecordClassifierCall(bool bSampled);
VOXELFORGE_API void AddClassifierStats(const FSnapshot::FClassifierStats& Stats);
VOXELFORGE_API bool ShouldSample(EBucket Bucket);
VOXELFORGE_API void AddSampledMeasurement(EBucket Bucket, uint64 RawCycles);
@@ -279,15 +390,24 @@ namespace VoxelDensityProfile
public:
explicit FScopedTimer(EBucket InBucket)
: Bucket(InBucket)
, Token(BeginScope(InBucket))
{
if (IsEnabledFast() && ShouldProbeScopeFast(InBucket))
{
Token = BeginScope(InBucket);
}
}
explicit FScopedTimer(const TCHAR* InName)
: Bucket(EBucket::OtherOp)
{
Bucket = BucketFromName(InName);
Token = BeginScope(Bucket);
if (IsEnabledFast())
{
Bucket = BucketFromName(InName);
if (ShouldProbeScopeFast(Bucket))
{
Token = BeginScope(Bucket);
}
}
}
~FScopedTimer();
@@ -266,6 +266,7 @@ struct FExploreArguments
bool bUseOperatorStack = true;
bool bProfileDensity = false;
bool bProfileDensityFull = false;
bool bPerfAttribution = false;
bool bProfileLod = false;
bool bOpBounds = false;
bool bFailureFocusRender = false;
@@ -313,6 +314,11 @@ struct FExploreArguments
bool bRenderCameraOverride = false;
FVector RenderCamera = FVector::ZeroVector;
bool IsDensityProfilingEnabled() const
{
return bProfileDensity || bPerfAttribution;
}
FString CanonicalModes() const
{
FString Result;
@@ -358,6 +364,7 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
OutArguments.bProfileDensity = FParse::Param(*Params, TEXT("profiledensity"));
OutArguments.bProfileDensityFull = FParse::Param(*Params, TEXT("profiledensityfull"));
OutArguments.bProfileDensity |= OutArguments.bProfileDensityFull;
OutArguments.bPerfAttribution = FParse::Param(*Params, TEXT("perfattribution"));
OutArguments.bProfileLod = FParse::Param(*Params, TEXT("profilelod"));
OutArguments.bOpBounds = FParse::Param(*Params, TEXT("opbounds"));
FParse::Value(*Params, TEXT("out="), OutText);
@@ -741,6 +748,10 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
VoxelDensityProfile::EMode ExploreProfileMode(const FExploreArguments& Arguments)
{
if (Arguments.bPerfAttribution)
{
return VoxelDensityProfile::EMode::Attribution;
}
return Arguments.bProfileDensityFull
? VoxelDensityProfile::EMode::Full
: VoxelDensityProfile::EMode::Sampled;
@@ -850,6 +861,14 @@ struct FExploreWorld
int32 ExploreMeshTileCount = 0;
int32 ExploreMeshTilesCompleted = 0;
double ExploreMeshSeconds = 0.0;
// Exact wall-clock partition of ExploreMeshSeconds. The first three fields end at the
// parallel tile launch; merge closes the mesh clock. Remainder is intentionally retained
// instead of rounding the ledger to 100%.
double ExploreMeshPreparationSeconds = 0.0;
double ExploreMeshPreTileSetupSeconds = 0.0;
double ExploreMeshMergeSeconds = 0.0;
double ExploreMeshAccountingRemainderSeconds = 0.0;
double ExploreMeshHashSeconds = 0.0;
double ExploreDensityGridSeconds = 0.0;
int64 ExploreDensityGridTotalSamples = 0;
int64 ExploreDensityGridUniqueSamples = 0;
@@ -1235,6 +1254,11 @@ struct FExploreExportOutput
FExploreMeshMetrics SurfaceMetrics;
double MeshSeconds = 0.0;
double MeshUsPerVoxel = 0.0;
double MeasureSeconds = 0.0;
double ObjWriteSeconds = 0.0;
double ManifestSeconds = 0.0;
double ExportWallSeconds = 0.0;
double ExportAccountingRemainderSeconds = 0.0;
FString GeometryHash;
bool bTruncated = false;
};
@@ -1634,6 +1658,11 @@ bool EnsureExploreMesh(
FMath::FloorToInt(0.5f * static_cast<float>(
World.TargetBottomWorldZ + World.TargetTopWorldZ)));
World.ExploreMeshSeconds = 0.0;
World.ExploreMeshPreparationSeconds = 0.0;
World.ExploreMeshPreTileSetupSeconds = 0.0;
World.ExploreMeshMergeSeconds = 0.0;
World.ExploreMeshAccountingRemainderSeconds = 0.0;
World.ExploreMeshHashSeconds = 0.0;
World.ExploreDensityGridSeconds = 0.0;
World.ExploreDensityGridTotalSamples = 0;
World.ExploreDensityGridUniqueSamples = 0;
@@ -1694,6 +1723,7 @@ bool EnsureExploreMesh(
World.ExploreDensityGridDuplicateSamplesAfter =
Arguments.bReuseDensityGrid ? 0 : World.ExploreDensityGridDuplicateSamplesBefore;
const double DensityStageStartSeconds = FPlatformTime::Seconds();
if (Arguments.bReuseDensityGrid)
{
const double DensityGridStartSeconds = FPlatformTime::Seconds();
@@ -1780,6 +1810,8 @@ bool EnsureExploreMesh(
World.Mesher->SetSharedDensityGrid(&World.ExploreSharedDensityGrid);
World.bExploreDensityGridReused = true;
}
const double DensityStageEndSeconds = FPlatformTime::Seconds();
World.ExploreMeshPreparationSeconds = DensityStageStartSeconds - MeshStartSeconds;
// Generate each tile through the canonical mesher on worker threads, then append strictly in
// Z/Y/X order. The mesher's scratch buffers and the generator's hot caches are thread-local;
@@ -1802,6 +1834,7 @@ bool EnsureExploreMesh(
std::atomic<bool> bTileWorkCancelled(false);
const double TileWorkDeadline = Budget.StartSeconds + Budget.LimitSeconds - 0.25;
const double ParallelStartSeconds = FPlatformTime::Seconds();
World.ExploreMeshPreTileSetupSeconds = ParallelStartSeconds - DensityStageEndSeconds;
ParallelFor(
TEXT("VoxelForgeExploreMeshTiles"),
World.ExploreMeshTileCount,
@@ -1925,9 +1958,20 @@ bool EnsureExploreMesh(
return false;
}
World.ExploreMeshSeconds = FPlatformTime::Seconds() - MeshStartSeconds;
const double MeshEndSeconds = FPlatformTime::Seconds();
World.ExploreMeshSeconds = MeshEndSeconds - MeshStartSeconds;
World.ExploreMeshMergeSeconds = MeshEndSeconds - ParallelEndSeconds;
const double MeshStageSumSeconds = World.ExploreMeshPreparationSeconds
+ World.ExploreDensityGridSeconds
+ World.ExploreMeshPreTileSetupSeconds
+ World.ExploreMeshTaskWallSeconds
+ World.ExploreMeshMergeSeconds;
World.ExploreMeshAccountingRemainderSeconds =
World.ExploreMeshSeconds - MeshStageSumSeconds;
World.bExploreMeshComplete = true;
const double GeometryHashStartSeconds = FPlatformTime::Seconds();
World.ExploreGeometryHash = ComputeGeometryHash(World.ExploreMesh);
World.ExploreMeshHashSeconds = FPlatformTime::Seconds() - GeometryHashStartSeconds;
OutError.Reset();
return true;
}
@@ -4021,7 +4065,7 @@ FString BuildManifestJson(
TSharedRef<FExploreJsonWriter> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
Writer->WriteObjectStart();
Writer->WriteValue(TEXT("schema_version"), 6);
Writer->WriteValue(TEXT("schema_version"), 7);
Writer->WriteValue(TEXT("format"), TEXT("OBJ"));
Writer->WriteValue(TEXT("mesh_file"), Export.MeshFileName);
Writer->WriteValue(TEXT("mesh_positions_units"), TEXT("metres"));
@@ -4126,6 +4170,7 @@ bool RunExport(
FExploreBudget& Budget)
{
OutOutput = FExploreExportOutput();
const double ExportStartSeconds = FPlatformTime::Seconds();
OutOutput.RegionSize = Arguments.ExportSize;
OutOutput.EstimatedWorkingBytes = EstimateExportWorkingBytes(Arguments.ExportSize);
if (OutOutput.EstimatedWorkingBytes > OutOutput.WorkingMemoryCapBytes)
@@ -4163,6 +4208,7 @@ bool RunExport(
OutOutput.RefusalReason = FString::Printf(TEXT("Could not create OBJ '%s'."), *MeshPath);
return false;
}
const double ObjWriteStartSeconds = FPlatformTime::Seconds();
if (!WriteObjHeader(*Archive))
{
OutOutput.Status = TEXT("error");
@@ -4170,12 +4216,14 @@ bool RunExport(
return false;
}
const double MeasureStartSeconds = FPlatformTime::Seconds();
if (!MeasureExploreMesh(World.ExploreMesh, OutOutput.SurfaceMetrics, Error))
{
OutOutput.Status = TEXT("error");
OutOutput.RefusalReason = Error;
return false;
}
OutOutput.MeasureSeconds = FPlatformTime::Seconds() - MeasureStartSeconds;
OutOutput.MeshArrayBytes = static_cast<int64>(World.ExploreMesh.Vertices.Num()) * sizeof(FVector)
+ static_cast<int64>(World.ExploreMesh.Normals.Num()) * sizeof(FVector)
+ static_cast<int64>(World.ExploreMesh.UVs.Num()) * sizeof(FVector2D)
@@ -4221,6 +4269,7 @@ bool RunExport(
OutOutput.RefusalReason = TEXT("OBJ was written but its file size could not be read.");
return false;
}
OutOutput.ObjWriteSeconds = FPlatformTime::Seconds() - ObjWriteStartSeconds;
OutOutput.bTruncated = Budget.bTruncated
|| !World.bExploreMeshComplete
@@ -4235,6 +4284,7 @@ bool RunExport(
: TEXT("The wall-clock budget stopped export after writing a partial mesh.");
}
const double ManifestStartSeconds = FPlatformTime::Seconds();
const FString ManifestJson = BuildManifestJson(Arguments, OutOutput);
const FString ManifestJsonRepeat = BuildManifestJson(Arguments, OutOutput);
if (ManifestJson.IsEmpty() || ManifestJsonRepeat.IsEmpty())
@@ -4258,6 +4308,13 @@ bool RunExport(
OutOutput.RefusalReason = TEXT("Could not write export manifest.json.");
return false;
}
OutOutput.ManifestSeconds = FPlatformTime::Seconds() - ManifestStartSeconds;
OutOutput.ExportWallSeconds = FPlatformTime::Seconds() - ExportStartSeconds;
OutOutput.ExportAccountingRemainderSeconds = OutOutput.ExportWallSeconds
- (World.ExploreMeshSeconds
+ World.ExploreMeshHashSeconds
+ OutOutput.ObjWriteSeconds
+ OutOutput.ManifestSeconds);
if (!OutOutput.bTruncated)
{
OutOutput.Status = TEXT("ok");
@@ -4348,6 +4405,7 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("mesh_min_batch_size"), Arguments.MeshMinBatchSize);
Writer->WriteValue(TEXT("profile_density"), Arguments.bProfileDensity);
Writer->WriteValue(TEXT("profile_density_full"), Arguments.bProfileDensityFull);
Writer->WriteValue(TEXT("perf_attribution"), Arguments.bPerfAttribution);
Writer->WriteValue(TEXT("profile_lod"), Arguments.bProfileLod);
Writer->WriteValue(TEXT("op_bounds"), Arguments.bOpBounds);
Writer->WriteValue(TEXT("budget_minutes"), static_cast<double>(Arguments.BudgetMinutes));
@@ -4386,6 +4444,27 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("us_per_voxel"), SummaryUsPerVoxel);
Writer->WriteValue(TEXT("triangle_count"), SummaryTriangleCount);
Writer->WriteValue(TEXT("geometry_hash"), SummaryGeometryHash);
Writer->WriteObjectStart(TEXT("mesh_wall_ledger"));
Writer->WriteValue(TEXT("mesh_seconds"), World.ExploreMeshSeconds);
Writer->WriteValue(TEXT("preparation_seconds"), World.ExploreMeshPreparationSeconds);
Writer->WriteValue(TEXT("density_grid_seconds"), World.ExploreDensityGridSeconds);
Writer->WriteValue(TEXT("pre_tile_setup_seconds"), World.ExploreMeshPreTileSetupSeconds);
Writer->WriteValue(TEXT("tile_parallel_wall_seconds"), World.ExploreMeshTaskWallSeconds);
Writer->WriteValue(TEXT("tile_merge_seconds"), World.ExploreMeshMergeSeconds);
Writer->WriteValue(TEXT("accounting_remainder_seconds"),
World.ExploreMeshAccountingRemainderSeconds);
Writer->WriteValue(TEXT("stage_sum_seconds"),
World.ExploreMeshPreparationSeconds
+ World.ExploreDensityGridSeconds
+ World.ExploreMeshPreTileSetupSeconds
+ World.ExploreMeshTaskWallSeconds
+ World.ExploreMeshMergeSeconds);
Writer->WriteValue(TEXT("sum_matches_mesh"),
FMath::Abs(World.ExploreMeshAccountingRemainderSeconds) <= 0.000001);
Writer->WriteValue(TEXT("geometry_hash_seconds"), World.ExploreMeshHashSeconds);
Writer->WriteValue(TEXT("note"), TEXT(
"The stage rows are one additive wall-clock ledger. Profiler buckets below are nested worker diagnostics and are not added to this ledger."));
Writer->WriteObjectEnd();
Writer->WriteObjectStart(TEXT("density_grid_reuse"));
Writer->WriteValue(TEXT("enabled"), World.bExploreDensityGridReused);
Writer->WriteValue(TEXT("total_tile_grid_samples"), World.ExploreDensityGridTotalSamples);
@@ -4752,6 +4831,13 @@ FString BuildExploreJson(
WriteSurfaceReliefMetrics(*Writer, Output.Export.SurfaceMetrics);
Writer->WriteValue(TEXT("mesh_seconds"), Output.Export.MeshSeconds);
Writer->WriteValue(TEXT("mesh_us_per_voxel"), Output.Export.MeshUsPerVoxel);
Writer->WriteValue(TEXT("geometry_hash_seconds"), World.ExploreMeshHashSeconds);
Writer->WriteValue(TEXT("measure_seconds"), Output.Export.MeasureSeconds);
Writer->WriteValue(TEXT("obj_write_seconds"), Output.Export.ObjWriteSeconds);
Writer->WriteValue(TEXT("manifest_seconds"), Output.Export.ManifestSeconds);
Writer->WriteValue(TEXT("export_wall_seconds"), Output.Export.ExportWallSeconds);
Writer->WriteValue(TEXT("export_accounting_remainder_seconds"),
Output.Export.ExportAccountingRemainderSeconds);
Writer->WriteValue(TEXT("geometry_hash"), Output.Export.GeometryHash);
Writer->WriteValue(TEXT("density_grid_reuse"), World.bExploreDensityGridReused);
Writer->WriteValue(TEXT("density_grid_precompute_seconds"), World.ExploreDensityGridSeconds);
@@ -4777,7 +4863,9 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("mode"),
Output.bHasProfile && Output.Profile.Mode != VoxelDensityProfile::EMode::Disabled
? (Output.Profile.Mode == VoxelDensityProfile::EMode::Full
? TEXT("full") : TEXT("sampled"))
? TEXT("full")
: (Output.Profile.Mode == VoxelDensityProfile::EMode::Attribution
? TEXT("attribution") : TEXT("sampled")))
: TEXT("disabled"));
Writer->WriteValue(TEXT("sample_interval"), static_cast<int32>(Output.Profile.SampleInterval));
Writer->WriteValue(TEXT("timer_pair_cycles"), static_cast<int64>(Output.Profile.TimerPairCycles));
@@ -4788,9 +4876,12 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("generation_can_depend_on_profiler"), false);
Writer->WriteValue(TEXT("fine_scope_timing"),
Output.Profile.Mode == VoxelDensityProfile::EMode::Full);
Writer->WriteValue(TEXT("attribution_scope_timing"),
Output.Profile.Mode == VoxelDensityProfile::EMode::Attribution);
Writer->WriteValue(TEXT("sampled_scope_policy"), TEXT(
"Sampled mode times GetDensityAt and mesh container parents; fine operation scopes are "
"opt-in via -profiledensityfull or UE Insights."));
"Attribution mode keeps the parent and additive mesh ledger wall clocks, samples hot "
"component scopes, and records classifier detail; full mode remains the invasive per-scope "
"diagnostic."));
Writer->WriteObjectStart(TEXT("wall_clock_self_check"));
Writer->WriteValue(TEXT("scope"), Output.ProfilerComparison.Scope);
Writer->WriteValue(TEXT("profiled_total_seconds"), Output.ProfilerComparison.ProfiledSeconds);
@@ -4803,6 +4894,7 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("geometry_equal"), Output.ProfilerComparison.bGeometryEqual);
Writer->WriteValue(TEXT("self_check"), Output.ProfilerComparison.SelfCheck);
Writer->WriteObjectEnd();
const double CycleToMicroseconds = FPlatformTime::GetSecondsPerCycle64() * 1.0e6;
Writer->WriteObjectStart(TEXT("counters"));
for (int32 Index = 0; Index < VoxelDensityProfile::CounterCount; ++Index)
{
@@ -4811,6 +4903,42 @@ FString BuildExploreJson(
static_cast<int64>(Output.Profile.Counters[Index]));
}
Writer->WriteObjectEnd();
const VoxelDensityProfile::FSnapshot::FClassifierStats& Classifier = Output.Profile.Classifier;
Writer->WriteObjectStart(TEXT("classifier"));
Writer->WriteValue(TEXT("calls"), static_cast<int64>(Classifier.Calls));
Writer->WriteValue(TEXT("sampled_calls"), static_cast<int64>(Classifier.SampledCalls));
Writer->WriteValue(TEXT("sample_fraction"), Classifier.Calls > 0
? static_cast<double>(Classifier.SampledCalls) / static_cast<double>(Classifier.Calls)
: 0.0);
Writer->WriteValue(TEXT("refine_nodes"), static_cast<int64>(Classifier.RefineNodes));
Writer->WriteValue(TEXT("interval_stack_box_calls"), static_cast<int64>(Classifier.StackBoxCalls));
Writer->WriteValue(TEXT("whole_mixed_nodes"), static_cast<int64>(Classifier.WholeMixedNodes));
Writer->WriteValue(TEXT("whole_solid_nodes"), static_cast<int64>(Classifier.WholeSolidNodes));
Writer->WriteValue(TEXT("whole_air_nodes"), static_cast<int64>(Classifier.WholeAirNodes));
Writer->WriteValue(TEXT("needs_final_field_nodes"), static_cast<int64>(Classifier.NeedsFinalFieldNodes));
Writer->WriteValue(TEXT("split_nodes"), static_cast<int64>(Classifier.SplitNodes));
Writer->WriteValue(TEXT("max_refinement_depth"), static_cast<int64>(Classifier.MaxRefinementDepth));
Writer->WriteValue(TEXT("exact_core_samples"), static_cast<int64>(Classifier.ExactCoreSamples));
Writer->WriteValue(TEXT("exact_final_samples"), static_cast<int64>(Classifier.ExactFinalSamples));
Writer->WriteValue(TEXT("exact_core_cache_hits"), static_cast<int64>(Classifier.ExactCoreCacheHits));
Writer->WriteValue(TEXT("exact_final_cache_hits"), static_cast<int64>(Classifier.ExactFinalCacheHits));
Writer->WriteValue(TEXT("exact_core_leaves"), static_cast<int64>(Classifier.ExactCoreLeaves));
Writer->WriteValue(TEXT("exact_final_leaves"), static_cast<int64>(Classifier.ExactFinalLeaves));
Writer->WriteValue(TEXT("interval_stack_box_us"), static_cast<double>(Classifier.StackBoxCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("exact_core_us"), static_cast<double>(Classifier.ExactCoreCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("exact_final_us"), static_cast<double>(Classifier.ExactFinalCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_tail_queries"), static_cast<int64>(Classifier.RoomTailQueries));
Writer->WriteValue(TEXT("room_tail_evaluated"), static_cast<int64>(Classifier.RoomTailEvaluated));
Writer->WriteValue(TEXT("room_tail_us"), static_cast<double>(Classifier.RoomTailCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_propagate_us"), static_cast<double>(Classifier.RoomPropagateCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_exact_primitive_us"), static_cast<double>(Classifier.RoomExactPrimitiveCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_cache_window_us"), static_cast<double>(Classifier.RoomCacheWindowCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_count"), static_cast<int64>(Classifier.RoomNumRooms));
Writer->WriteValue(TEXT("tunnel_count"), static_cast<int64>(Classifier.RoomNumTunnels));
Writer->WriteValue(TEXT("room_floor_join_count"), static_cast<int64>(Classifier.RoomNumRoomFloorJoins));
Writer->WriteValue(TEXT("pit_count"), static_cast<int64>(Classifier.RoomNumPits));
Writer->WriteValue(TEXT("chimney_count"), static_cast<int64>(Classifier.RoomNumChimneys));
Writer->WriteObjectEnd();
auto WriteCacheBreakdown = [&Writer](
const TCHAR* Name,
const VoxelDensityProfile::FCacheMemoryBreakdown& Breakdown)
@@ -4836,7 +4964,6 @@ FString BuildExploreJson(
WriteCacheBreakdown(TEXT("room_graph"), Output.Profile.RoomGraphCacheBreakdown);
Writer->WriteObjectEnd();
Writer->WriteObjectStart(TEXT("buckets"));
const double CycleToMicroseconds = FPlatformTime::GetSecondsPerCycle64() * 1.0e6;
for (int32 Index = 0; Index < VoxelDensityProfile::BucketCount; ++Index)
{
Writer->WriteObjectStart(VoxelDensityProfile::BucketName(
@@ -4884,7 +5011,9 @@ FString BuildExploreJson(
Output.Profile.Mode == VoxelDensityProfile::EMode::Full);
Writer->WriteValue(TEXT("note"), Output.Profile.Mode == VoxelDensityProfile::EMode::Full
? TEXT("Phase buckets are the non-overlapping ledger. Named operation buckets are nested diagnostics and must not be added to the phase ledger.")
: TEXT("Sampled mode intentionally disables fine phase and operation timers so the parent survives contact with reality; use -profiledensityfull or UE Insights for fine scope timing."));
: (Output.Profile.Mode == VoxelDensityProfile::EMode::Attribution
? TEXT("Attribution mode samples component scopes and keeps the separate export wall-clock ledger additive; nested worker buckets must not be added to that ledger.")
: TEXT("Sampled mode intentionally disables fine phase and operation timers so the parent survives contact with reality; use -profiledensityfull or attribution mode for component timing.")));
Writer->WriteObjectEnd();
Writer->WriteObjectEnd();
@@ -5437,7 +5566,7 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
// serving the next case's generation.
VoxelDensityProfile::Reset();
VoxelDensityProfile::SetMode(
Arguments.bProfileDensity
Arguments.IsDensityProfilingEnabled()
? ExploreProfileMode(Arguments)
: VoxelDensityProfile::EMode::Disabled,
VoxelDensityProfile::DefaultSampleInterval);
@@ -5535,7 +5664,7 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
Mesh.Vertices.Num(), Mesh.Triangles.Num() / 3,
Mesh.IsEmpty() ? 1 : 0);
}
if (Arguments.bProfileDensity)
if (Arguments.IsDensityProfilingEnabled())
{
VoxelDensityProfile::Reset();
VoxelDensityProfile::SetMode(
@@ -5616,7 +5745,7 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
// The reference world is fresh, profiling is disabled for it, and its geometry hash is
// compared with the profiled world below. This is deliberately a diagnostic second pass;
// it cannot affect the profiled world's generation or its deterministic output.
if (Arguments.bProfileDensity
if (Arguments.IsDensityProfilingEnabled()
&& (Arguments.bRender || Arguments.bExport)
&& (CameraSeedWalk != nullptr || Arguments.bExport)
&& !Budget.bTruncated)
@@ -5748,7 +5877,7 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
Output.Profile = VoxelDensityProfile::Snapshot();
Output.bHasProfile = true;
if (Arguments.bProfileDensity)
if (Arguments.IsDensityProfilingEnabled())
{
if (Output.ProfilerComparison.bAvailable)
{
@@ -5870,7 +5999,7 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
*ReportPath,
(bPayloadRepeatEqual && bFinalRepeatEqual) ? TEXT("yes") : TEXT("no"));
if (Arguments.bProfileDensity)
if (Arguments.IsDensityProfilingEnabled())
{
const VoxelDensityProfile::FSnapshot Profile = VoxelDensityProfile::Snapshot();
const double CycleToMicroseconds = FPlatformTime::GetSecondsPerCycle64() * 1.0e6;
@@ -6168,6 +6297,8 @@ bool BuildBatchCaseParams(
bool bProfileDensityFull = false;
JsonBool(Case, TEXT("profile_density_full"), bProfileDensityFull);
bProfileDensity |= bProfileDensityFull;
bool bPerfAttribution = false;
JsonBool(Case, TEXT("perf_attribution"), bPerfAttribution);
bool bProfileLod = false;
JsonBool(Case, TEXT("profile_lod"), bProfileLod);
bool bOpBounds = false;
@@ -6208,6 +6339,10 @@ bool BuildBatchCaseParams(
? TEXT(" -profiledensityfull")
: TEXT(" -profiledensity");
}
if (bPerfAttribution)
{
OutParams += TEXT(" -perfattribution");
}
if (bProfileLod)
{
OutParams += TEXT(" -profilelod");