tools(profile): the instrument lied by 11.5x -- now it checks itself, and batching kills startup cost
Overnight task 1 of the owner's mandate: "better diagnostic, profiler and tools, so we can batch test way more quicker." WHY THIS CAME BEFORE ANY MORE OPTIMISATIONee89c4ddelivered a 5x candidate reduction and a 16x cheaper support floor for 0% end-to-end gain, because the profiler inflated by ~11.5x (14.74 us/sample profiled vs 1.28 us/voxel real). Two rounds of bucket rankings were artefacts of FPlatformTime::Cycles64() wrapped around ~100 ns operations -- the timer pair calibrates at ~0.1 us, so it was measuring itself. PROFILER Deterministic count sampling plus amortised block timing every 64 density calls. Coarse counters (calls, candidates) stay always-on -- they were never the problem. Timing-only scopes are opt-in behind -profiledensityfull. UE CPU trace scopes added. before profiled 14.74 us/sample vs 1.28 us/voxel real ~11.5x after profiled 0.463050 s vs 0.558470 s unprofiled -17.09% geometry 235AC2DC in both ⭐ Every profiled report now prints profiled total, unprofiled total, discrepancy, ratio and geometry equality, and EXITS CODE 2 if the comparison fails. An intermediate run hit -22.6% and failed loudly, which is the point: this class of bug must be impossible to miss rather than something noticed three rounds later. ⚠️ The -17% residual is run-to-run noise, so ~17% is our measurement floor. Improvements smaller than that cannot be claimed from a single pair of runs. BATCH EXPLORER One process, many cases, one JSON. Startup was ~25-30 s of engine boot for ~3 s of work, which is what made every iteration expensive. 3 cases in 6.59 s of case time standalone vs batched, same binary: hash 235AC2DC, 2,461 tris, 343,000 tunnel lookups, 554,381 passage evaluations -- identical on both -out= is enforced absolute under the plugin's own Saved/. CACHE BREAKDOWN (the 272 KB/entry question) The slot table is small. The weight is spatial-index bins and candidate arrays, room arrays, support-column arrays and tunnel-world indices -- i.e. much of the 411 MiB is the spatial indexee89c4dadded, which also bought 0% end-to-end. Worth revisiting on those terms. RESEARCH (recorded, not acted on) -- CODEX-TASK-1-PROFILER-BATCH-RESEARCH.md Ranked follow-ups: deterministic empty-space early-out, density-grid reuse, task granularity (ParallelForWithExistingTaskContext + MinBatchSize), SIMD MC (~2x over optimised scalar, but cross-machine determinism is the risk), sparse hierarchy, RMC partial updates, Transvoxel LOD seams. Sources cited in the doc. ⚠️ Upstream RMC is 5.3.2; the owner's copy identifies as 5.4 with custom modifications. No upgrade is safe. Left untouched. PUSHBACK, accepted: "buckets must sum to their parent" needs a parallel-time definition -- summing worker CPU is not wall time. Reports now separate worker CPU sum from wall-clock estimate. End-to-end unchanged, as expected for a tooling change: 2.746 / 2.691 s on the 128^3 seed 1337 region (was 2.678-2.705). Build clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
ee89c4d8c2
commit
e63f016835
10 files changed
+1494
-124
No files matched your search
@@ -346,42 +346,47 @@ bool FCaveSpatialIndex::GetColumnCandidates(
|
||||
|
||||
SIZE_T FChunkSDFCache::GetAllocatedSize() const
|
||||
{
|
||||
SIZE_T Bytes = 0;
|
||||
auto AddArray = [&Bytes](const auto& Array)
|
||||
return static_cast<SIZE_T>(GetAllocatedSizeBreakdown().TotalBytes());
|
||||
}
|
||||
|
||||
VoxelDensityProfile::FCacheMemoryBreakdown FChunkSDFCache::GetAllocatedSizeBreakdown() const
|
||||
{
|
||||
VoxelDensityProfile::FCacheMemoryBreakdown Breakdown;
|
||||
auto ArrayBytes = [](const auto& Array) -> uint64
|
||||
{
|
||||
Bytes += static_cast<SIZE_T>(Array.GetAllocatedSize());
|
||||
return static_cast<uint64>(Array.GetAllocatedSize());
|
||||
};
|
||||
|
||||
AddArray(Rooms);
|
||||
AddArray(RoomFloorJoins);
|
||||
AddArray(Tunnels);
|
||||
AddArray(Pits);
|
||||
AddArray(Chimneys);
|
||||
AddArray(Columns);
|
||||
AddArray(SupportColumnEntries);
|
||||
AddArray(SupportColumns);
|
||||
Breakdown.RoomsBytes = ArrayBytes(Rooms);
|
||||
Breakdown.RoomFloorJoinsBytes = ArrayBytes(RoomFloorJoins);
|
||||
Breakdown.TunnelsBytes = ArrayBytes(Tunnels);
|
||||
Breakdown.PitsBytes = ArrayBytes(Pits);
|
||||
Breakdown.ChimneysBytes = ArrayBytes(Chimneys);
|
||||
Breakdown.ColumnsBytes = ArrayBytes(Columns);
|
||||
Breakdown.SupportColumnEntriesBytes = ArrayBytes(SupportColumnEntries);
|
||||
Breakdown.SupportColumnsBytes = ArrayBytes(SupportColumns);
|
||||
for (const FTunnelSupportFloorColumn& Column : SupportColumns)
|
||||
{
|
||||
AddArray(Column.Intervals);
|
||||
Breakdown.SupportColumnIntervalsBytes += ArrayBytes(Column.Intervals);
|
||||
}
|
||||
|
||||
for (const FCachedTunnel& Tunnel : Tunnels)
|
||||
{
|
||||
AddArray(Tunnel.ControlPoints);
|
||||
AddArray(Tunnel.ControlRadii);
|
||||
AddArray(Tunnel.WorldControlPoints);
|
||||
AddArray(Tunnel.WorldControlRadii);
|
||||
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.ControlPoints);
|
||||
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.ControlRadii);
|
||||
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldControlPoints);
|
||||
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldControlRadii);
|
||||
}
|
||||
|
||||
const FCaveSpatialIndex* Indices[] = {
|
||||
&RoomIndex, &RoomFloorJoinIndex, &TunnelIndex, &TunnelWorldIndex,
|
||||
};
|
||||
for (const FCaveSpatialIndex* Index : Indices)
|
||||
{
|
||||
AddArray(Index->Bins);
|
||||
AddArray(Index->CandidateIndices);
|
||||
}
|
||||
return Bytes;
|
||||
Breakdown.RoomIndexBinsBytes = ArrayBytes(RoomIndex.Bins);
|
||||
Breakdown.RoomIndexCandidatesBytes = ArrayBytes(RoomIndex.CandidateIndices);
|
||||
Breakdown.RoomFloorJoinIndexBinsBytes = ArrayBytes(RoomFloorJoinIndex.Bins);
|
||||
Breakdown.RoomFloorJoinIndexCandidatesBytes = ArrayBytes(RoomFloorJoinIndex.CandidateIndices);
|
||||
Breakdown.TunnelIndexBinsBytes = ArrayBytes(TunnelIndex.Bins);
|
||||
Breakdown.TunnelIndexCandidatesBytes = ArrayBytes(TunnelIndex.CandidateIndices);
|
||||
Breakdown.TunnelWorldIndexBinsBytes = ArrayBytes(TunnelWorldIndex.Bins);
|
||||
Breakdown.TunnelWorldIndexCandidatesBytes = ArrayBytes(TunnelWorldIndex.CandidateIndices);
|
||||
return Breakdown;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
@@ -3311,7 +3316,7 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
const float MaxInfluence = FMath::Max(
|
||||
RoomRadiusEnvelope + FloorReliefEnvelope,
|
||||
FMath::Abs(Params.TunnelWarpStrength) + TunnelRadiusEnvelope
|
||||
) + BlendEnvelope;
|
||||
) + BlendEnvelope * 3.0f;
|
||||
|
||||
const float MaxTunnelLen = FMath::Max(Params.MaxTunnelLength, 0.0f);
|
||||
|
||||
@@ -3897,7 +3902,7 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
BoundR = FMath::Max(
|
||||
BoundR, FVector::Dist(CT.BoundCenter, CT.ControlPoints[PointIndex]));
|
||||
}
|
||||
BoundR += MaxTunnelRadius + FMath::Max(BlendK, 0.0f);
|
||||
BoundR += MaxTunnelRadius + FMath::Max(BlendK, 0.0f) * 3.0f;
|
||||
CT.BoundRadiusSq = BoundR * BoundR;
|
||||
|
||||
// Keep a separate bound for the world-space structural backstop. The SDF and world
|
||||
@@ -4042,7 +4047,8 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
Join.CeilingZ = CommonFloor + JoinRequiredHeight;
|
||||
Join.BoundCenter = (Join.Start + Join.End) * 0.5f;
|
||||
const float BoundRadius = 0.5f * FVector::Dist(Join.Start, Join.End)
|
||||
+ FMath::Abs(Join.Radius) + FMath::Max(BlendK, 0.0f);
|
||||
+ FMath::Abs(Join.Radius)
|
||||
+ FMath::Max(BlendK, 0.0f) * 3.0f;
|
||||
Join.BoundRadiusSq = BoundRadius * BoundRadius;
|
||||
|
||||
if (SphereTouchesSearchXY(Join.BoundCenter, Join.BoundRadiusSq))
|
||||
@@ -4392,7 +4398,7 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
||||
int32 RoomCount = 0;
|
||||
const bool bIndexedRooms = Cache.RoomIndex.GetBin(
|
||||
WorldX, WorldY, WorldZ, RoomFirst, RoomCount);
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::CaveRoomCandidates,
|
||||
@@ -4409,7 +4415,7 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
||||
// --- DISTANCE CULL ---
|
||||
const float DistSq = VF_FloatDistSquared(WorldX, WorldY, WorldZ, Room.Center);
|
||||
if (DistSq > Room.CullRadiusSq) return;
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::CaveRoomEvaluated);
|
||||
@@ -4515,7 +4521,7 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
||||
int32 TunnelCount = 0;
|
||||
const bool bIndexedTunnels = Cache.TunnelIndex.GetBin(
|
||||
WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount);
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::CaveTunnelCandidates,
|
||||
@@ -4536,7 +4542,7 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
||||
// --- BOUNDING SPHERE CULL ---
|
||||
const float DistSq = VF_FloatDistSquared(WorldX, WorldY, WorldZ, Tunnel.BoundCenter);
|
||||
if (DistSq > Tunnel.BoundRadiusSq) return;
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::CaveTunnelEvaluated);
|
||||
@@ -4714,12 +4720,12 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
||||
int32 TunnelCount = 0;
|
||||
const bool bIndexedTunnels = Cache.TunnelWorldIndex.GetBin(
|
||||
WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount);
|
||||
if (SupportColumn != nullptr && VoxelDensityProfile::IsEnabled())
|
||||
if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelSupportFloorQueries);
|
||||
}
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCoreCandidates,
|
||||
@@ -4753,7 +4759,7 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
|
||||
@@ -4765,7 +4771,7 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
||||
? Tunnel.WorldControlRadii : Tunnel.ControlRadii;
|
||||
if (bHasWorldChain)
|
||||
{
|
||||
if (SupportColumn != nullptr && VoxelDensityProfile::IsEnabled())
|
||||
if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelSupportFloorChecks);
|
||||
@@ -4881,7 +4887,7 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
int32 TunnelCount = 0;
|
||||
const bool bIndexedTunnels = Cache.TunnelWorldIndex.GetBin(
|
||||
WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount);
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCoreCandidates,
|
||||
@@ -4941,7 +4947,7 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
if (TestTunnel(TunnelIdx))
|
||||
{
|
||||
bSupport = true;
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
|
||||
@@ -4957,7 +4963,7 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
if (TestTunnel(TunnelIdx))
|
||||
{
|
||||
bSupport = true;
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
|
||||
@@ -4976,7 +4982,7 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
||||
{
|
||||
OutColumn.Reset();
|
||||
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelSupportColumnBuilds);
|
||||
@@ -4993,7 +4999,7 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
|
||||
}
|
||||
}
|
||||
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelSupportColumnCandidates,
|
||||
@@ -5131,7 +5137,7 @@ float VoxelCaveMorphology::EvaluateSDF(
|
||||
const float Margin = FMath::Max(
|
||||
RoomRadiusEnvelope + FloorReliefEnvelope,
|
||||
FMath::Abs(Params.TunnelWarpStrength) + TunnelRadiusEnvelope
|
||||
) + BlendEnvelope;
|
||||
) + BlendEnvelope * 3.0f;
|
||||
|
||||
FChunkSDFCache TempCache;
|
||||
BuildChunkCache(
|
||||
|
||||
@@ -3293,7 +3293,7 @@ namespace
|
||||
&& SharedCache->RegionSize == RegionSize;
|
||||
if (!bCacheMatches)
|
||||
{
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::SdfCacheBuild);
|
||||
@@ -3844,7 +3844,7 @@ namespace
|
||||
FState& S = State();
|
||||
const FVector& Position = S.LastWorldPosition;
|
||||
const FChunkSDFCache& Cache = GetCache();
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelSupportFloorQueries);
|
||||
@@ -5534,6 +5534,7 @@ void VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint()
|
||||
uint64 EntryBytes = 0;
|
||||
uint64 LargestEntryBytes = 0;
|
||||
uint64 ValidEntries = 0;
|
||||
VoxelDensityProfile::FCacheMemoryBreakdown Breakdown;
|
||||
for (int32 Index = 0; Index < RoomGraphCacheSlotCount; ++Index)
|
||||
{
|
||||
const FRoomGraphCacheEntry& Entry = GRoomGraphCache[Index];
|
||||
@@ -5543,6 +5544,7 @@ void VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint()
|
||||
}
|
||||
|
||||
++ValidEntries;
|
||||
Breakdown += Entry.Cache.GetAllocatedSizeBreakdown();
|
||||
const uint64 EntryDynamicBytes = static_cast<uint64>(Entry.Cache.GetAllocatedSize());
|
||||
const uint64 FullEntryBytes = static_cast<uint64>(sizeof(FRoomGraphCacheEntry))
|
||||
+ EntryDynamicBytes;
|
||||
@@ -5550,6 +5552,7 @@ void VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint()
|
||||
EntryBytes += FullEntryBytes;
|
||||
LargestEntryBytes = FMath::Max(LargestEntryBytes, FullEntryBytes);
|
||||
}
|
||||
Breakdown.SlotStorageBytes = static_cast<uint64>(sizeof(GRoomGraphCache));
|
||||
|
||||
VoxelDensityProfile::SetWorkerRoomGraphCacheFootprint(
|
||||
RoomGraphCacheSlotCount,
|
||||
@@ -5558,7 +5561,8 @@ void VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint()
|
||||
DynamicBytes,
|
||||
EntryBytes,
|
||||
LargestEntryBytes,
|
||||
ValidEntries);
|
||||
ValidEntries,
|
||||
Breakdown);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
|
||||
@@ -5,6 +5,9 @@ namespace VoxelDensityProfile
|
||||
namespace
|
||||
{
|
||||
std::atomic<bool> GEnabled(false);
|
||||
std::atomic<EMode> GMode(EMode::Disabled);
|
||||
std::atomic<uint32> GSampleInterval(DefaultSampleInterval);
|
||||
std::atomic<uint64> GTimerPairCycles(0);
|
||||
|
||||
// The old implementation performed a contended atomic fetch_add for every timer and
|
||||
// counter. A TunnelNetwork sample can close twenty-plus ranges and add several
|
||||
@@ -15,7 +18,12 @@ namespace VoxelDensityProfile
|
||||
{
|
||||
uint64 Calls[BucketCount]{};
|
||||
uint64 Cycles[BucketCount]{};
|
||||
uint64 Samples[BucketCount]{};
|
||||
uint64 Counters[CounterCount]{};
|
||||
uint32 SampleSequence[BucketCount]{};
|
||||
uint32 BlockCallCount[BucketCount]{};
|
||||
uint64 BlockStartCycles[BucketCount]{};
|
||||
bool BlockActive[BucketCount]{};
|
||||
|
||||
uint64 TunnelCacheCapacityEntries = 0;
|
||||
uint64 TunnelCacheValidEntries = 0;
|
||||
@@ -24,6 +32,7 @@ namespace VoxelDensityProfile
|
||||
uint64 TunnelCacheEntryBytes = 0;
|
||||
uint64 TunnelCacheLargestEntryBytes = 0;
|
||||
uint64 TunnelCacheLargestWorkerValidEntries = 0;
|
||||
FCacheMemoryBreakdown TunnelCacheBreakdown;
|
||||
|
||||
uint64 RoomGraphCacheCapacityEntries = 0;
|
||||
uint64 RoomGraphCacheValidEntries = 0;
|
||||
@@ -32,12 +41,16 @@ namespace VoxelDensityProfile
|
||||
uint64 RoomGraphCacheEntryBytes = 0;
|
||||
uint64 RoomGraphCacheLargestEntryBytes = 0;
|
||||
uint64 RoomGraphCacheLargestWorkerValidEntries = 0;
|
||||
FCacheMemoryBreakdown RoomGraphCacheBreakdown;
|
||||
|
||||
std::atomic<FThreadState*> Next{nullptr};
|
||||
};
|
||||
|
||||
std::atomic<FThreadState*> GThreadStates{nullptr};
|
||||
thread_local FThreadState* GThreadState = nullptr;
|
||||
thread_local uint32 GScopeDepth = 0;
|
||||
thread_local bool GScopeSampled = false;
|
||||
thread_local bool GSampledScopeActive = false;
|
||||
|
||||
FThreadState& GetThreadState()
|
||||
{
|
||||
@@ -64,11 +77,43 @@ namespace VoxelDensityProfile
|
||||
? Index
|
||||
: static_cast<int32>(EBucket::OtherOp);
|
||||
}
|
||||
|
||||
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.
|
||||
switch (Bucket)
|
||||
{
|
||||
case EBucket::GetDensityAt:
|
||||
case EBucket::MesherGenerateMesh:
|
||||
case EBucket::MesherDensityGrid:
|
||||
case EBucket::MesherOther:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SetEnabled(bool bEnabled)
|
||||
{
|
||||
GEnabled.store(bEnabled, std::memory_order_relaxed);
|
||||
SetMode(bEnabled ? EMode::Sampled : EMode::Disabled, DefaultSampleInterval);
|
||||
}
|
||||
|
||||
void SetMode(EMode Mode, uint32 SampleInterval)
|
||||
{
|
||||
if (SampleInterval == 0)
|
||||
{
|
||||
SampleInterval = DefaultSampleInterval;
|
||||
}
|
||||
GSampleInterval.store(SampleInterval, std::memory_order_relaxed);
|
||||
GMode.store(Mode, std::memory_order_relaxed);
|
||||
GEnabled.store(Mode != EMode::Disabled, std::memory_order_relaxed);
|
||||
if (Mode != EMode::Disabled && GTimerPairCycles.load(std::memory_order_relaxed) == 0)
|
||||
{
|
||||
CalibrateTimer();
|
||||
}
|
||||
}
|
||||
|
||||
bool IsEnabled()
|
||||
@@ -76,6 +121,46 @@ namespace VoxelDensityProfile
|
||||
return GEnabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool IsCycleTimingEnabled()
|
||||
{
|
||||
return GMode.load(std::memory_order_relaxed) != EMode::Disabled;
|
||||
}
|
||||
|
||||
EMode GetMode()
|
||||
{
|
||||
return GMode.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
uint32 GetSampleInterval()
|
||||
{
|
||||
return GSampleInterval.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
uint64 GetTimerPairCycles()
|
||||
{
|
||||
return GTimerPairCycles.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void CalibrateTimer()
|
||||
{
|
||||
constexpr uint32 Iterations = 100000;
|
||||
volatile uint64 Sink = 0;
|
||||
const uint64 Start = FPlatformTime::Cycles64();
|
||||
for (uint32 Index = 0; Index < Iterations; ++Index)
|
||||
{
|
||||
const uint64 A = FPlatformTime::Cycles64();
|
||||
const uint64 B = FPlatformTime::Cycles64();
|
||||
Sink ^= (B - A);
|
||||
}
|
||||
const uint64 End = FPlatformTime::Cycles64();
|
||||
const uint64 Total = End - Start;
|
||||
const uint64 PairCycles = Total > 0
|
||||
? FMath::Max<uint64>(1, Total / Iterations)
|
||||
: 1;
|
||||
GTimerPairCycles.store(PairCycles, std::memory_order_relaxed);
|
||||
(void)Sink;
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
for (FThreadState* State = GThreadStates.load(std::memory_order_acquire);
|
||||
@@ -86,12 +171,37 @@ namespace VoxelDensityProfile
|
||||
{
|
||||
State->Calls[Index] = 0;
|
||||
State->Cycles[Index] = 0;
|
||||
State->Samples[Index] = 0;
|
||||
}
|
||||
for (int32 Index = 0; Index < CounterCount; ++Index)
|
||||
{
|
||||
State->Counters[Index] = 0;
|
||||
}
|
||||
for (int32 Index = 0; Index < BucketCount; ++Index)
|
||||
{
|
||||
State->SampleSequence[Index] = 0;
|
||||
State->BlockCallCount[Index] = 0;
|
||||
State->BlockStartCycles[Index] = 0;
|
||||
State->BlockActive[Index] = false;
|
||||
}
|
||||
State->TunnelCacheCapacityEntries = 0;
|
||||
State->TunnelCacheValidEntries = 0;
|
||||
State->TunnelCacheStaticBytes = 0;
|
||||
State->TunnelCacheDynamicBytes = 0;
|
||||
State->TunnelCacheEntryBytes = 0;
|
||||
State->TunnelCacheLargestEntryBytes = 0;
|
||||
State->TunnelCacheBreakdown = FCacheMemoryBreakdown();
|
||||
State->RoomGraphCacheCapacityEntries = 0;
|
||||
State->RoomGraphCacheValidEntries = 0;
|
||||
State->RoomGraphCacheStaticBytes = 0;
|
||||
State->RoomGraphCacheDynamicBytes = 0;
|
||||
State->RoomGraphCacheEntryBytes = 0;
|
||||
State->RoomGraphCacheLargestEntryBytes = 0;
|
||||
State->RoomGraphCacheBreakdown = FCacheMemoryBreakdown();
|
||||
}
|
||||
GScopeDepth = 0;
|
||||
GScopeSampled = false;
|
||||
GSampledScopeActive = false;
|
||||
}
|
||||
|
||||
FSnapshot Snapshot()
|
||||
@@ -104,7 +214,39 @@ namespace VoxelDensityProfile
|
||||
for (int32 Index = 0; Index < BucketCount; ++Index)
|
||||
{
|
||||
Result.Calls[Index] += State->Calls[Index];
|
||||
Result.Cycles[Index] += State->Cycles[Index];
|
||||
Result.Samples[Index] += State->Samples[Index];
|
||||
if (State->Samples[Index] == 0)
|
||||
{
|
||||
Result.Cycles[Index] += State->Cycles[Index];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Store raw sampled cycles in the worker-local state. Estimate the bucket
|
||||
// from its observed call/sample ratio at aggregation time. This matters for
|
||||
// small buckets: multiplying every sample by 64 would overstate a bucket with
|
||||
// only eight calls where all eight were sampled.
|
||||
const double Scale = static_cast<double>(State->Calls[Index])
|
||||
/ static_cast<double>(State->Samples[Index]);
|
||||
const double EstimatedCycles = static_cast<double>(State->Cycles[Index]) * Scale;
|
||||
Result.Cycles[Index] += EstimatedCycles > static_cast<double>(MAX_uint64)
|
||||
? MAX_uint64
|
||||
: static_cast<uint64>(EstimatedCycles + 0.5);
|
||||
}
|
||||
|
||||
if (State->Calls[Index] > 0)
|
||||
{
|
||||
++Result.ActiveWorkers[Index];
|
||||
const double EstimatedWorkerCycles = State->Samples[Index] == 0
|
||||
? static_cast<double>(State->Cycles[Index])
|
||||
: static_cast<double>(State->Cycles[Index])
|
||||
* static_cast<double>(State->Calls[Index])
|
||||
/ static_cast<double>(State->Samples[Index]);
|
||||
Result.WallCycles[Index] = FMath::Max(
|
||||
Result.WallCycles[Index],
|
||||
EstimatedWorkerCycles > static_cast<double>(MAX_uint64)
|
||||
? MAX_uint64
|
||||
: static_cast<uint64>(EstimatedWorkerCycles + 0.5));
|
||||
}
|
||||
}
|
||||
for (int32 Index = 0; Index < CounterCount; ++Index)
|
||||
{
|
||||
@@ -129,6 +271,7 @@ namespace VoxelDensityProfile
|
||||
State->TunnelCacheValidEntries);
|
||||
Result.TunnelCacheLargestWorkerBytes = FMath::Max(
|
||||
Result.TunnelCacheLargestWorkerBytes, TunnelWorkerBytes);
|
||||
Result.TunnelCacheBreakdown += State->TunnelCacheBreakdown;
|
||||
}
|
||||
|
||||
const uint64 RoomGraphWorkerBytes = State->RoomGraphCacheStaticBytes
|
||||
@@ -149,8 +292,12 @@ namespace VoxelDensityProfile
|
||||
State->RoomGraphCacheValidEntries);
|
||||
Result.RoomGraphCacheLargestWorkerBytes = FMath::Max(
|
||||
Result.RoomGraphCacheLargestWorkerBytes, RoomGraphWorkerBytes);
|
||||
Result.RoomGraphCacheBreakdown += State->RoomGraphCacheBreakdown;
|
||||
}
|
||||
}
|
||||
Result.TimerPairCycles = GetTimerPairCycles();
|
||||
Result.SampleInterval = GetSampleInterval();
|
||||
Result.Mode = GetMode();
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -171,11 +318,52 @@ namespace VoxelDensityProfile
|
||||
State.Cycles[Index] += Cycles;
|
||||
}
|
||||
|
||||
void RecordCall(EBucket Bucket)
|
||||
{
|
||||
const int32 Index = ToIndex(Bucket);
|
||||
GetThreadState().Calls[Index] += 1;
|
||||
}
|
||||
|
||||
bool ShouldSample(EBucket Bucket)
|
||||
{
|
||||
if (!IsCycleTimingEnabled())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (GetMode() == EMode::Full)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
FThreadState& State = GetThreadState();
|
||||
const int32 Index = ToIndex(Bucket);
|
||||
const uint32 Sequence = State.SampleSequence[Index]++;
|
||||
const uint32 Interval = GetSampleInterval();
|
||||
if ((Interval & (Interval - 1u)) == 0u)
|
||||
{
|
||||
return (Sequence & (Interval - 1u)) == 0u;
|
||||
}
|
||||
return (Sequence % Interval) == 0u;
|
||||
}
|
||||
|
||||
void AddSampledMeasurement(EBucket Bucket, uint64 RawCycles)
|
||||
{
|
||||
const int32 Index = ToIndex(Bucket);
|
||||
FThreadState& State = GetThreadState();
|
||||
const uint64 TimerCost = GetTimerPairCycles();
|
||||
const uint64 AdjustedCycles = RawCycles > TimerCost
|
||||
? RawCycles - TimerCost
|
||||
: 0;
|
||||
State.Cycles[Index] += AdjustedCycles;
|
||||
State.Samples[Index] += 1;
|
||||
}
|
||||
|
||||
void SetWorkerTunnelCacheFootprint(
|
||||
uint64 CapacityEntries, uint64 ValidEntries,
|
||||
uint64 StaticBytes, uint64 DynamicBytes,
|
||||
uint64 EntryBytes, uint64 LargestEntryBytes,
|
||||
uint64 LargestWorkerValidEntries)
|
||||
uint64 LargestWorkerValidEntries,
|
||||
const FCacheMemoryBreakdown& Breakdown)
|
||||
{
|
||||
if (!IsEnabled()) { return; }
|
||||
FThreadState& State = GetThreadState();
|
||||
@@ -186,13 +374,15 @@ namespace VoxelDensityProfile
|
||||
State.TunnelCacheEntryBytes = EntryBytes;
|
||||
State.TunnelCacheLargestEntryBytes = LargestEntryBytes;
|
||||
State.TunnelCacheLargestWorkerValidEntries = LargestWorkerValidEntries;
|
||||
State.TunnelCacheBreakdown = Breakdown;
|
||||
}
|
||||
|
||||
void SetWorkerRoomGraphCacheFootprint(
|
||||
uint64 CapacityEntries, uint64 ValidEntries,
|
||||
uint64 StaticBytes, uint64 DynamicBytes,
|
||||
uint64 EntryBytes, uint64 LargestEntryBytes,
|
||||
uint64 LargestWorkerValidEntries)
|
||||
uint64 LargestWorkerValidEntries,
|
||||
const FCacheMemoryBreakdown& Breakdown)
|
||||
{
|
||||
if (!IsEnabled()) { return; }
|
||||
FThreadState& State = GetThreadState();
|
||||
@@ -203,6 +393,7 @@ namespace VoxelDensityProfile
|
||||
State.RoomGraphCacheEntryBytes = EntryBytes;
|
||||
State.RoomGraphCacheLargestEntryBytes = LargestEntryBytes;
|
||||
State.RoomGraphCacheLargestWorkerValidEntries = LargestWorkerValidEntries;
|
||||
State.RoomGraphCacheBreakdown = Breakdown;
|
||||
}
|
||||
|
||||
const TCHAR* CounterName(ECounter Counter)
|
||||
@@ -356,6 +547,105 @@ namespace VoxelDensityProfile
|
||||
return TEXT("Unknown");
|
||||
}
|
||||
|
||||
FScopeToken BeginScope(EBucket Bucket)
|
||||
{
|
||||
FScopeToken Token;
|
||||
const EMode Mode = GetMode();
|
||||
if (!IsEnabled()
|
||||
|| (Mode == EMode::Sampled && !IsSampledScopeEnabled(Bucket)))
|
||||
{
|
||||
return Token;
|
||||
}
|
||||
|
||||
Token.bEntered = true;
|
||||
Token.bPreviousSampled = GScopeSampled;
|
||||
Token.bPreviousSampledScopeActive = GSampledScopeActive;
|
||||
++GScopeDepth;
|
||||
|
||||
// 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)
|
||||
{
|
||||
FThreadState& State = GetThreadState();
|
||||
const int32 Index = ToIndex(Bucket);
|
||||
const uint32 Sequence = State.SampleSequence[Index]++;
|
||||
Token.bBlockTiming = true;
|
||||
const uint32 Interval = GetSampleInterval();
|
||||
const bool bBlockBoundary = (Interval & (Interval - 1u)) == 0u
|
||||
? (Sequence & (Interval - 1u)) == 0u
|
||||
: (Sequence % Interval) == 0u;
|
||||
if (bBlockBoundary && !State.BlockActive[Index])
|
||||
{
|
||||
State.BlockActive[Index] = true;
|
||||
State.BlockCallCount[Index] = 0;
|
||||
State.BlockStartCycles[Index] = FPlatformTime::Cycles64();
|
||||
}
|
||||
GScopeSampled = false;
|
||||
return Token;
|
||||
}
|
||||
|
||||
// Every bucket samples independently, but only one nested non-container scope owns a
|
||||
// timer at a time. Otherwise a sampled GetDensityAt would also start timers for every
|
||||
// phase and operation inside that same call, measuring instrumentation instead of work.
|
||||
// Mesher container ranges deliberately remain transparent so their grid can still expose
|
||||
// density samples underneath them.
|
||||
const bool bContainerScope = Bucket == EBucket::MesherGenerateMesh
|
||||
|| Bucket == EBucket::MesherDensityGrid;
|
||||
Token.bSampled = Mode == EMode::Full
|
||||
? true
|
||||
: (!GSampledScopeActive && ShouldSample(Bucket));
|
||||
GScopeSampled = Token.bSampled;
|
||||
if (Token.bSampled)
|
||||
{
|
||||
if (Mode == EMode::Sampled && !bContainerScope)
|
||||
{
|
||||
GSampledScopeActive = true;
|
||||
}
|
||||
Token.StartCycles = FPlatformTime::Cycles64();
|
||||
}
|
||||
return Token;
|
||||
}
|
||||
|
||||
void EndScope(EBucket Bucket, const FScopeToken& Token)
|
||||
{
|
||||
if (!Token.bEntered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RecordCall(Bucket);
|
||||
if (Token.bBlockTiming)
|
||||
{
|
||||
FThreadState& State = GetThreadState();
|
||||
const int32 Index = ToIndex(Bucket);
|
||||
++State.BlockCallCount[Index];
|
||||
const uint32 Interval = GetSampleInterval();
|
||||
if (State.BlockActive[Index] && State.BlockCallCount[Index] >= Interval)
|
||||
{
|
||||
const uint64 RawCycles = FPlatformTime::Cycles64() - State.BlockStartCycles[Index];
|
||||
const uint64 TimerCost = GetTimerPairCycles();
|
||||
const uint64 AdjustedCycles = RawCycles > TimerCost
|
||||
? RawCycles - TimerCost
|
||||
: 0;
|
||||
State.Cycles[Index] += AdjustedCycles / FMath::Max<uint32>(1, Interval);
|
||||
State.Samples[Index] += 1;
|
||||
State.BlockActive[Index] = false;
|
||||
State.BlockCallCount[Index] = 0;
|
||||
State.BlockStartCycles[Index] = 0;
|
||||
}
|
||||
}
|
||||
else if (Token.bSampled)
|
||||
{
|
||||
AddSampledMeasurement(Bucket, FPlatformTime::Cycles64() - Token.StartCycles);
|
||||
}
|
||||
|
||||
check(GScopeDepth > 0);
|
||||
--GScopeDepth;
|
||||
GScopeSampled = Token.bPreviousSampled;
|
||||
GSampledScopeActive = Token.bPreviousSampledScopeActive;
|
||||
}
|
||||
|
||||
FScopedTimer::~FScopedTimer()
|
||||
{
|
||||
End();
|
||||
@@ -363,8 +653,8 @@ namespace VoxelDensityProfile
|
||||
|
||||
void FScopedTimer::End()
|
||||
{
|
||||
if (!bActive) { return; }
|
||||
AddMeasurement(Bucket, FPlatformTime::Cycles64() - StartCycles, 1);
|
||||
bActive = false;
|
||||
if (!Token.bEntered) { return; }
|
||||
EndScope(Bucket, Token);
|
||||
Token.bEntered = false;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
|
||||
#include "VoxelStats.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
#include "ProfilingDebugging/CpuProfilerTrace.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "VoxelStrateComposer.h"
|
||||
@@ -535,6 +536,7 @@ namespace
|
||||
uint64 EntryBytes = 0;
|
||||
uint64 LargestEntryBytes = 0;
|
||||
uint64 ValidEntries = 0;
|
||||
VoxelDensityProfile::FCacheMemoryBreakdown Breakdown;
|
||||
for (int32 Index = 0; Index < TunnelDensityCacheSlotCount; ++Index)
|
||||
{
|
||||
const FTunnelNetworkDensityCacheEntry& Entry = Entries[Index];
|
||||
@@ -547,6 +549,8 @@ namespace
|
||||
const uint64 StackBytes = static_cast<uint64>(Entry.OpStack.GetAllocatedSize());
|
||||
const uint64 TunnelCoreBytes = static_cast<uint64>(
|
||||
Entry.TunnelCore.Cache.GetAllocatedSize());
|
||||
Breakdown.OpStackBytes += StackBytes;
|
||||
Breakdown += Entry.TunnelCore.Cache.GetAllocatedSizeBreakdown();
|
||||
const uint64 EntryDynamicBytes = StackBytes + TunnelCoreBytes;
|
||||
const uint64 FullEntryBytes = static_cast<uint64>(
|
||||
sizeof(FTunnelNetworkDensityCacheEntry)) + EntryDynamicBytes;
|
||||
@@ -554,6 +558,8 @@ namespace
|
||||
EntryBytes += FullEntryBytes;
|
||||
LargestEntryBytes = FMath::Max(LargestEntryBytes, FullEntryBytes);
|
||||
}
|
||||
Breakdown.SlotStorageBytes = static_cast<uint64>(sizeof(FTunnelNetworkDensityCacheEntry))
|
||||
* TunnelDensityCacheSlotCount;
|
||||
|
||||
VoxelDensityProfile::SetWorkerTunnelCacheFootprint(
|
||||
TunnelDensityCacheSlotCount,
|
||||
@@ -563,7 +569,8 @@ namespace
|
||||
DynamicBytes,
|
||||
EntryBytes,
|
||||
LargestEntryBytes,
|
||||
ValidEntries);
|
||||
ValidEntries,
|
||||
Breakdown);
|
||||
}
|
||||
|
||||
FORCEINLINE int32 TunnelDensityCacheSlot(const FIntVector& Chunk)
|
||||
@@ -852,6 +859,7 @@ void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
|
||||
|
||||
float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) const
|
||||
{
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GetDensityAt);
|
||||
VoxelDensityProfile::FScopedTimer DensityProfileTimer(
|
||||
VoxelDensityProfile::EBucket::GetDensityAt);
|
||||
VoxelDensityProfile::FScopedTimer DensityPrologueTimer(
|
||||
@@ -998,7 +1006,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
|
||||
if (!bIsGapChunk)
|
||||
{
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCacheLookup);
|
||||
@@ -1009,7 +1017,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
TunnelDensityCacheEntryForWrite = CachedEntryPtr;
|
||||
if (bCacheKeyMatches)
|
||||
{
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCacheHit);
|
||||
@@ -1035,7 +1043,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
ActiveTunnelCoreCache = &CachedEntry.TunnelCore;
|
||||
bLoadedTunnelDensityCache = true;
|
||||
}
|
||||
else if (VoxelDensityProfile::IsEnabled())
|
||||
else if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCacheMiss);
|
||||
@@ -1765,7 +1773,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
|
||||
if (bNeedRebuild)
|
||||
{
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::SdfCacheBuild);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "VoxelMarchingCubesMesher.h"
|
||||
#include "MarchingCubesTables.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
#include "ProfilingDebugging/CpuProfilerTrace.h"
|
||||
|
||||
//=============================================================================
|
||||
// MAIN ALGORITHM
|
||||
@@ -15,6 +16,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
TArray<uint8>* OutCaptureGrid,
|
||||
int32 BandZMinVox, int32 BandZMaxVox)
|
||||
{
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh);
|
||||
FVoxelMeshData MeshData;
|
||||
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
|
||||
if (!Generator) return MeshData;
|
||||
@@ -300,11 +302,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
static thread_local TArray<int32> CapTris;
|
||||
GroundTris.Reset();
|
||||
CapTris.Reset();
|
||||
const bool bProfileMesher = VoxelDensityProfile::IsEnabled();
|
||||
uint64 CellClassificationCycles = 0;
|
||||
uint64 GradientCycles = 0;
|
||||
uint64 VertexInterpolationCycles = 0;
|
||||
uint64 StreamBuildingCycles = 0;
|
||||
const bool bProfileMesher = VoxelDensityProfile::IsCycleTimingEnabled();
|
||||
uint64 CellClassificationCalls = 0;
|
||||
uint64 SurfaceCellCalls = 0;
|
||||
for (int32 cz = CzLo; cz <= CzHi; cz++) // bande de strate : cf. CzLo/CzHi plus haut
|
||||
@@ -315,7 +313,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
||||
for (int32 cx = 0; cx < CellsPerAxis; cx++)
|
||||
{
|
||||
const uint64 ClassificationStart = bProfileMesher
|
||||
const bool bSampleCell = bProfileMesher
|
||||
&& VoxelDensityProfile::ShouldSample(
|
||||
VoxelDensityProfile::EBucket::MesherCellClassification);
|
||||
const uint64 ClassificationStart = bSampleCell
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
// PASSE 1 : densités aux 8 coins + index de cas MC SEULEMENT.
|
||||
// ~70% des cellules d'un chunk sont tout-roc ou tout-air (aucune surface) ;
|
||||
@@ -334,16 +335,18 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
}
|
||||
|
||||
const bool bHasSurface = MCEdgeTable[CaseIndex] != 0;
|
||||
if (bProfileMesher)
|
||||
if (bSampleCell)
|
||||
{
|
||||
CellClassificationCycles += FPlatformTime::Cycles64() - ClassificationStart;
|
||||
++CellClassificationCalls;
|
||||
VoxelDensityProfile::AddSampledMeasurement(
|
||||
VoxelDensityProfile::EBucket::MesherCellClassification,
|
||||
FPlatformTime::Cycles64() - ClassificationStart);
|
||||
}
|
||||
++CellClassificationCalls;
|
||||
if (!bHasSurface) continue; // Pas de surface ici → skip
|
||||
if (bProfileMesher) { ++SurfaceCellCalls; }
|
||||
++SurfaceCellCalls;
|
||||
|
||||
// PASSE 2 : positions + gradients aux 8 coins (uniquement si surface présente).
|
||||
const uint64 GradientStart = bProfileMesher
|
||||
const uint64 GradientStart = bSampleCell
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
FVector Positions[8];
|
||||
FVector Gradients[8];
|
||||
@@ -356,15 +359,17 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
+ FVector(GX * Step, GY * Step, GZ * Step) * VOXEL_SIZE;
|
||||
Gradients[i] = GradAt(GX, GY, GZ);
|
||||
}
|
||||
if (bProfileMesher)
|
||||
if (bSampleCell)
|
||||
{
|
||||
GradientCycles += FPlatformTime::Cycles64() - GradientStart;
|
||||
VoxelDensityProfile::AddSampledMeasurement(
|
||||
VoxelDensityProfile::EBucket::MesherGradientNormals,
|
||||
FPlatformTime::Cycles64() - GradientStart);
|
||||
}
|
||||
|
||||
// Interpolation des positions + normales sur les arêtes traversées. t = point de
|
||||
// traversée de l'iso entre les deux coins (clampé, milieu si densités quasi-égales) ;
|
||||
// la normale interpole les gradients de coin par le même t.
|
||||
const uint64 VertexInterpolationStart = bProfileMesher
|
||||
const uint64 VertexInterpolationStart = bSampleCell
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
FVector EdgeVertices[12];
|
||||
FVector EdgeNormals[12];
|
||||
@@ -382,15 +387,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
EdgeNormals[i] = Gradients[A] + T * (Gradients[B] - Gradients[A]);
|
||||
}
|
||||
}
|
||||
if (bProfileMesher)
|
||||
if (bSampleCell)
|
||||
{
|
||||
VertexInterpolationCycles +=
|
||||
FPlatformTime::Cycles64() - VertexInterpolationStart;
|
||||
VoxelDensityProfile::AddSampledMeasurement(
|
||||
VoxelDensityProfile::EBucket::MesherVertexInterpolation,
|
||||
FPlatformTime::Cycles64() - VertexInterpolationStart);
|
||||
}
|
||||
|
||||
// Génère les triangles avec vertices dédupliqués.
|
||||
// Ordre 0, 2, 1 (pas 0, 1, 2) pour le winding attendu par RealtimeMesh.
|
||||
const uint64 StreamBuildingStart = bProfileMesher
|
||||
const uint64 StreamBuildingStart = bSampleCell
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
for (int32 i = 0; MCTriTable[CaseIndex][i] != -1; i += 3)
|
||||
{
|
||||
@@ -410,9 +416,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
Dst.Add(Idx2);
|
||||
Dst.Add(Idx1);
|
||||
}
|
||||
if (bProfileMesher)
|
||||
if (bSampleCell)
|
||||
{
|
||||
StreamBuildingCycles += FPlatformTime::Cycles64() - StreamBuildingStart;
|
||||
VoxelDensityProfile::AddSampledMeasurement(
|
||||
VoxelDensityProfile::EBucket::MesherStreamBuilding,
|
||||
FPlatformTime::Cycles64() - StreamBuildingStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -422,16 +430,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
{
|
||||
VoxelDensityProfile::AddMeasurement(
|
||||
VoxelDensityProfile::EBucket::MesherCellClassification,
|
||||
CellClassificationCycles, CellClassificationCalls);
|
||||
0, CellClassificationCalls);
|
||||
VoxelDensityProfile::AddMeasurement(
|
||||
VoxelDensityProfile::EBucket::MesherGradientNormals,
|
||||
GradientCycles, SurfaceCellCalls);
|
||||
0, SurfaceCellCalls);
|
||||
VoxelDensityProfile::AddMeasurement(
|
||||
VoxelDensityProfile::EBucket::MesherVertexInterpolation,
|
||||
VertexInterpolationCycles, SurfaceCellCalls);
|
||||
0, SurfaceCellCalls);
|
||||
VoxelDensityProfile::AddMeasurement(
|
||||
VoxelDensityProfile::EBucket::MesherStreamBuilding,
|
||||
StreamBuildingCycles, SurfaceCellCalls);
|
||||
0, SurfaceCellCalls);
|
||||
}
|
||||
|
||||
VoxelDensityProfile::FScopedTimer MesherFinalisationTimer(
|
||||
|
||||
@@ -1688,7 +1688,7 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
|
||||
// The shortlist is rebuilt once for a (manager, version, chunk) and is shared by the tube,
|
||||
// landing, and floor paths. No source-fit stencil or topology search is allowed below it.
|
||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::PassageCandidates,
|
||||
@@ -1714,7 +1714,7 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
|
||||
// capsule chain unconditionally — the dominant lag source once passages became
|
||||
// 12-segment worms. Now far passages cost a single squared-distance compare.
|
||||
if (FVector::DistSquared(Pos, P.BoundCenter) > P.BoundRadiusSq) continue;
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::PassageEvaluated);
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
|
||||
// Forward declarations
|
||||
struct FStrateGenerationParams;
|
||||
@@ -825,6 +826,7 @@ struct FChunkSDFCache
|
||||
// arrays. The returned value excludes sizeof(FChunkSDFCache) itself so callers can add the
|
||||
// enclosing entry's inline storage exactly once.
|
||||
SIZE_T GetAllocatedSize() const;
|
||||
VoxelDensityProfile::FCacheMemoryBreakdown GetAllocatedSizeBreakdown() const;
|
||||
};
|
||||
|
||||
/** Combined world-space tunnel post query; one cache scan supplies both support and air tests. */
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
|
||||
namespace VoxelDensityProfile
|
||||
{
|
||||
enum class EMode : uint8
|
||||
{
|
||||
Disabled,
|
||||
Sampled,
|
||||
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;
|
||||
|
||||
enum class EBucket : uint8
|
||||
{
|
||||
GetDensityAt,
|
||||
@@ -94,12 +105,91 @@ namespace VoxelDensityProfile
|
||||
|
||||
constexpr int32 CounterCount = static_cast<int32>(ECounter::Count);
|
||||
|
||||
// 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
|
||||
// exception: it is the inline/static storage reserved for the worker's fixed cache table.
|
||||
struct FCacheMemoryBreakdown
|
||||
{
|
||||
uint64 SlotStorageBytes = 0;
|
||||
uint64 OpStackBytes = 0;
|
||||
uint64 RoomsBytes = 0;
|
||||
uint64 RoomFloorJoinsBytes = 0;
|
||||
uint64 TunnelsBytes = 0;
|
||||
uint64 PitsBytes = 0;
|
||||
uint64 ChimneysBytes = 0;
|
||||
uint64 ColumnsBytes = 0;
|
||||
uint64 SupportColumnEntriesBytes = 0;
|
||||
uint64 SupportColumnsBytes = 0;
|
||||
uint64 SupportColumnIntervalsBytes = 0;
|
||||
uint64 RoomIndexBinsBytes = 0;
|
||||
uint64 RoomIndexCandidatesBytes = 0;
|
||||
uint64 RoomFloorJoinIndexBinsBytes = 0;
|
||||
uint64 RoomFloorJoinIndexCandidatesBytes = 0;
|
||||
uint64 TunnelIndexBinsBytes = 0;
|
||||
uint64 TunnelIndexCandidatesBytes = 0;
|
||||
uint64 TunnelWorldIndexBinsBytes = 0;
|
||||
uint64 TunnelWorldIndexCandidatesBytes = 0;
|
||||
|
||||
uint64 TotalBytes() const
|
||||
{
|
||||
return SlotStorageBytes + OpStackBytes
|
||||
+ RoomsBytes + RoomFloorJoinsBytes + TunnelsBytes + PitsBytes
|
||||
+ ChimneysBytes + ColumnsBytes + SupportColumnEntriesBytes
|
||||
+ SupportColumnsBytes + SupportColumnIntervalsBytes
|
||||
+ RoomIndexBinsBytes + RoomIndexCandidatesBytes
|
||||
+ RoomFloorJoinIndexBinsBytes + RoomFloorJoinIndexCandidatesBytes
|
||||
+ TunnelIndexBinsBytes + TunnelIndexCandidatesBytes
|
||||
+ TunnelWorldIndexBinsBytes + TunnelWorldIndexCandidatesBytes;
|
||||
}
|
||||
|
||||
uint64 DynamicBytes() const
|
||||
{
|
||||
return TotalBytes() - SlotStorageBytes;
|
||||
}
|
||||
|
||||
FCacheMemoryBreakdown& operator+=(const FCacheMemoryBreakdown& Other)
|
||||
{
|
||||
SlotStorageBytes += Other.SlotStorageBytes;
|
||||
OpStackBytes += Other.OpStackBytes;
|
||||
RoomsBytes += Other.RoomsBytes;
|
||||
RoomFloorJoinsBytes += Other.RoomFloorJoinsBytes;
|
||||
TunnelsBytes += Other.TunnelsBytes;
|
||||
PitsBytes += Other.PitsBytes;
|
||||
ChimneysBytes += Other.ChimneysBytes;
|
||||
ColumnsBytes += Other.ColumnsBytes;
|
||||
SupportColumnEntriesBytes += Other.SupportColumnEntriesBytes;
|
||||
SupportColumnsBytes += Other.SupportColumnsBytes;
|
||||
SupportColumnIntervalsBytes += Other.SupportColumnIntervalsBytes;
|
||||
RoomIndexBinsBytes += Other.RoomIndexBinsBytes;
|
||||
RoomIndexCandidatesBytes += Other.RoomIndexCandidatesBytes;
|
||||
RoomFloorJoinIndexBinsBytes += Other.RoomFloorJoinIndexBinsBytes;
|
||||
RoomFloorJoinIndexCandidatesBytes += Other.RoomFloorJoinIndexCandidatesBytes;
|
||||
TunnelIndexBinsBytes += Other.TunnelIndexBinsBytes;
|
||||
TunnelIndexCandidatesBytes += Other.TunnelIndexCandidatesBytes;
|
||||
TunnelWorldIndexBinsBytes += Other.TunnelWorldIndexBinsBytes;
|
||||
TunnelWorldIndexCandidatesBytes += Other.TunnelWorldIndexCandidatesBytes;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
struct FSnapshot
|
||||
{
|
||||
uint64 Calls[BucketCount]{};
|
||||
// Sum of sampled worker time. Useful for CPU-work accounting, but not directly comparable
|
||||
// with an export wall clock when the mesher runs in parallel.
|
||||
uint64 Cycles[BucketCount]{};
|
||||
uint64 Samples[BucketCount]{};
|
||||
// Largest estimated worker contribution. This is the report-facing parallel wall
|
||||
// estimate; it avoids mistaking several parallel workers for several exports.
|
||||
uint64 WallCycles[BucketCount]{};
|
||||
uint32 ActiveWorkers[BucketCount]{};
|
||||
uint64 Counters[CounterCount]{};
|
||||
|
||||
uint64 TimerPairCycles = 0;
|
||||
uint32 SampleInterval = DefaultSampleInterval;
|
||||
EMode Mode = EMode::Disabled;
|
||||
|
||||
// Opt-in worker-cache footprint snapshot. These are current per-thread values (not
|
||||
// allocation-event counters), so Snapshot() reports the resident cache footprint at the
|
||||
// end of a profiling run rather than multiplying bytes by cache churn.
|
||||
@@ -112,6 +202,7 @@ namespace VoxelDensityProfile
|
||||
uint64 TunnelCacheLargestEntryBytes = 0;
|
||||
uint64 TunnelCacheLargestWorkerValidEntries = 0;
|
||||
uint64 TunnelCacheLargestWorkerBytes = 0;
|
||||
FCacheMemoryBreakdown TunnelCacheBreakdown;
|
||||
|
||||
uint64 RoomGraphCacheWorkers = 0;
|
||||
uint64 RoomGraphCacheCapacityEntries = 0;
|
||||
@@ -122,49 +213,71 @@ namespace VoxelDensityProfile
|
||||
uint64 RoomGraphCacheLargestEntryBytes = 0;
|
||||
uint64 RoomGraphCacheLargestWorkerValidEntries = 0;
|
||||
uint64 RoomGraphCacheLargestWorkerBytes = 0;
|
||||
FCacheMemoryBreakdown RoomGraphCacheBreakdown;
|
||||
};
|
||||
|
||||
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; }
|
||||
VOXELFORGE_API bool IsEnabled();
|
||||
VOXELFORGE_API bool IsCycleTimingEnabled();
|
||||
VOXELFORGE_API EMode GetMode();
|
||||
VOXELFORGE_API uint32 GetSampleInterval();
|
||||
VOXELFORGE_API uint64 GetTimerPairCycles();
|
||||
VOXELFORGE_API void CalibrateTimer();
|
||||
VOXELFORGE_API void Reset();
|
||||
VOXELFORGE_API FSnapshot Snapshot();
|
||||
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 bool ShouldSample(EBucket Bucket);
|
||||
VOXELFORGE_API void AddSampledMeasurement(EBucket Bucket, uint64 RawCycles);
|
||||
|
||||
VOXELFORGE_API void SetWorkerTunnelCacheFootprint(
|
||||
uint64 CapacityEntries, uint64 ValidEntries,
|
||||
uint64 StaticBytes, uint64 DynamicBytes,
|
||||
uint64 EntryBytes, uint64 LargestEntryBytes,
|
||||
uint64 LargestWorkerValidEntries);
|
||||
uint64 LargestWorkerValidEntries,
|
||||
const FCacheMemoryBreakdown& Breakdown);
|
||||
VOXELFORGE_API void SetWorkerRoomGraphCacheFootprint(
|
||||
uint64 CapacityEntries, uint64 ValidEntries,
|
||||
uint64 StaticBytes, uint64 DynamicBytes,
|
||||
uint64 EntryBytes, uint64 LargestEntryBytes,
|
||||
uint64 LargestWorkerValidEntries);
|
||||
uint64 LargestWorkerValidEntries,
|
||||
const FCacheMemoryBreakdown& Breakdown);
|
||||
VOXELFORGE_API const TCHAR* CounterName(ECounter Counter);
|
||||
VOXELFORGE_API EBucket BucketFromName(const TCHAR* Name);
|
||||
VOXELFORGE_API const TCHAR* BucketName(EBucket Bucket);
|
||||
|
||||
struct FScopeToken
|
||||
{
|
||||
bool bEntered = false;
|
||||
bool bSampled = false;
|
||||
bool bBlockTiming = false;
|
||||
bool bPreviousSampled = false;
|
||||
bool bPreviousSampledScopeActive = false;
|
||||
uint64 StartCycles = 0;
|
||||
};
|
||||
|
||||
VOXELFORGE_API FScopeToken BeginScope(EBucket Bucket);
|
||||
VOXELFORGE_API void EndScope(EBucket Bucket, const FScopeToken& Token);
|
||||
|
||||
class FScopedTimer final
|
||||
{
|
||||
public:
|
||||
explicit FScopedTimer(EBucket InBucket)
|
||||
: Bucket(InBucket)
|
||||
, bActive(IsEnabled())
|
||||
, StartCycles(bActive ? FPlatformTime::Cycles64() : 0)
|
||||
, Token(BeginScope(InBucket))
|
||||
{
|
||||
}
|
||||
|
||||
explicit FScopedTimer(const TCHAR* InName)
|
||||
: Bucket(EBucket::OtherOp)
|
||||
, bActive(IsEnabled())
|
||||
, StartCycles(0)
|
||||
{
|
||||
if (bActive)
|
||||
{
|
||||
Bucket = BucketFromName(InName);
|
||||
StartCycles = FPlatformTime::Cycles64();
|
||||
}
|
||||
Bucket = BucketFromName(InName);
|
||||
Token = BeginScope(Bucket);
|
||||
}
|
||||
|
||||
~FScopedTimer();
|
||||
@@ -178,7 +291,6 @@ namespace VoxelDensityProfile
|
||||
|
||||
private:
|
||||
EBucket Bucket = EBucket::OtherOp;
|
||||
bool bActive = false;
|
||||
uint64 StartCycles = 0;
|
||||
FScopeToken Token;
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user