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
@@ -0,0 +1,129 @@
|
||||
# VoxelForge Task 1: profiler, batch explorer, and research handoff
|
||||
|
||||
Research date: 2026-09-09. This note records the implementation and the read-only web research for Task 1. No RealtimeMeshComponent files were changed.
|
||||
|
||||
## 1. Measurement result
|
||||
|
||||
The old result in the task brief was 14.74 us/sample on the profiled density path versus 1.28 us/voxel without instrumentation: about 11.5x apparent overhead. That is consistent with putting `FPlatformTime::Cycles64()` around a roughly 100 ns operation. The timer call is not free, and at that granularity the measurement is mostly the measurement.
|
||||
|
||||
The new default is sampled profiling:
|
||||
|
||||
- Coarse calls and counters stay cheap and are collected in thread-local state.
|
||||
- `GetDensityAt` uses a deterministic count interval of 64 calls and one amortized block timer, rather than one timer pair per density call.
|
||||
- Full cycle timing remains an explicit opt-in (`-profiledensityfull`) for a small investigation, not the normal batch mode.
|
||||
- The default sampled mode reports the parent density/mesher wall estimate. Fine per-operation scopes are disabled in that mode; use Full or Unreal Insights for a detailed phase breakdown.
|
||||
- Sampling and timers only write report counters. They do not choose cells, alter density values, alter task partitioning, or participate in any reduction, so they cannot affect generated geometry.
|
||||
|
||||
The timer calibration in the validation run measured one `Cycles64` pair as approximately one platform cycle, or 0.1 us at the reported clock conversion. That is the direct self-check for the suspected timer-dominates-work failure mode.
|
||||
|
||||
Validation on the same 64^3 `TunnelNetwork` export:
|
||||
|
||||
| measurement | seconds |
|
||||
| --- | ---: |
|
||||
| profiled sampled export | 0.4630500004 |
|
||||
| fresh unprofiled reference export | 0.5584703982 |
|
||||
| discrepancy | -17.09% |
|
||||
| profiled/reference ratio | 0.82914 |
|
||||
|
||||
Both exports produced geometry hash `235AC2DC`. The commandlet now puts all four values in every profiled report and fails with exit code 2 when the absolute discrepancy exceeds 20% or the reference hash differs. The report also exposes `cpu_us` (sum of worker CPU estimates) separately from `wall_us`/`total_us` (largest-worker wall estimate). Summing CPU time across 15 workers is not a valid wall-clock parent; collapsing those fields was one way to make a parallel profile lie.
|
||||
|
||||
The density report has an explicit ledger. In sampled mode, the parent estimate is the measured quantity, fine phase buckets are marked unavailable, and the unattributed remainder makes the ledger sum exactly to the parent. This is honest about what was measured instead of inventing a fine breakdown from disabled timers.
|
||||
|
||||
## 2. Batch explorer
|
||||
|
||||
The explorer commandlet accepts one JSON file and keeps one editor process alive for every case:
|
||||
|
||||
```text
|
||||
UnrealEditor-Cmd.exe "<HostProject>.uproject" -run=VoxelForgeExplore -batch="E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BatchValidation.json" -out="E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BatchValidationRun2" -trace=cpu,counters
|
||||
```
|
||||
|
||||
`-out` must be an absolute directory below this plugin's `Saved` directory. The commandlet writes one `batch.json` below that directory. The input supports, per case: `id`, `seed`, `archetype`, `slot`, `export_size`, `export_step`/`step`/`lod`, `modes`, `operator_stack`, `profile_density`, `profile_density_full`, `profile_lod`, render/failure-focus settings, and a per-case budget. The parser caps one batch at 256 cases and clamps the total budget to 30 minutes; each case also has a bounded budget.
|
||||
|
||||
Before every case, the commandlet resets the density profile and all per-case report state, then applies the case's profile settings. The output contains per case: mesh seconds, microseconds per voxel, triangle count, geometry hash, counters, profile mode, and the case exit code. Invalid or timed-out cases are represented in the same machine-readable output rather than silently disappearing.
|
||||
|
||||
The final three-case confirmation completed in 6.59 seconds of commandlet case time after the single process launch:
|
||||
|
||||
| case | triangles | mesh seconds | hash | result |
|
||||
| --- | ---: | ---: | --- | --- |
|
||||
| repeat_unprofiled | 2,461 | 0.391420 | `235AC2DC` | pass |
|
||||
| repeat_profiled | 2,461 | 0.515374 | `235AC2DC` | self-check pass |
|
||||
| maze_lod1 | 5,218 | 0.277830 | `784EBF92` | pass |
|
||||
|
||||
The standalone profiled 64^3 case and the `repeat_profiled` case inside the batch both produced `235AC2DC`, 2,461 triangles, 343,000 tunnel lookups, and 554,381 passage evaluations. That is the contamination check: same final build, seed, archetype, slot, export size, step, and modes produced the same geometry and coarse counters alone and in a reused process. The standalone profiled export also matched its fresh unprofiled reference hash.
|
||||
|
||||
## 3. Cache footprint answer
|
||||
|
||||
The historical 15-worker report that prompted the question was:
|
||||
|
||||
```text
|
||||
capacity_entries=3840 valid_entries=1542 static_bytes=2534400
|
||||
dynamic_bytes=428627716 total_bytes=431162116
|
||||
avg_valid_entry_bytes=278703.6 largest_entry_bytes=541344
|
||||
tunnel_valid=1054 tunnel_dynamic=345243636
|
||||
roomgraph_valid=488 roomgraph_dynamic=83384080
|
||||
```
|
||||
|
||||
That is 431,162,116 bytes (411.2 MiB) in aggregate and 278,703.6 bytes per valid entry on average (278.7 kB decimal, about 265.7 KiB). The number is not one monolithic 272 kB struct. Each worker owns cache slots, and valid entries contain variable-length arrays.
|
||||
|
||||
A representative 128^3 breakdown added by this task was 360,063,124 bytes total:
|
||||
|
||||
| member family | tunnel cache | room-graph cache |
|
||||
| --- | ---: | ---: |
|
||||
| slot storage | 1,658,880 | 875,520 |
|
||||
| op stack | 8,264,000 | 0 |
|
||||
| rooms | 47,876,912 | 31,822,512 |
|
||||
| room-floor joins | 4,936,512 | 3,278,976 |
|
||||
| tunnels and control arrays | 5,560,248 | 3,591,584 |
|
||||
| support-column entries | 7,153,328 | 0 |
|
||||
| support columns | 30,699,008 | 0 |
|
||||
| room index bins + candidates | 30,759,720 | 22,700,380 |
|
||||
| tunnel index bins + candidates | 60,972,480 | 41,583,988 |
|
||||
| tunnel-world index bins + candidates | 34,635,272 | 23,693,804 |
|
||||
| dynamic total | 230,857,480 | 126,671,244 |
|
||||
| static + dynamic total | 232,516,360 | 127,546,764 |
|
||||
|
||||
The dominant allocation is the spatial index bin/candidate storage, followed by rooms and support-column arrays. Slot storage is comparatively small. The exact per-entry average moves with the generated case and cache occupancy, which explains why the 128^3 confirmation is not numerically identical to the historical 411 MiB run.
|
||||
|
||||
## 4. Research findings
|
||||
|
||||
### RealtimeMeshComponent, read-only
|
||||
|
||||
At research time the upstream releases page lists v5.3.2 as the latest tagged release: [RMC releases](https://github.com/TriAxis-Games/RealtimeMeshComponent/releases). The disposable build host's copy identifies itself as `VersionName: 5.4`, so the upstream tag number is not a safe linear comparison; this project also carries custom changes. The visible upstream history includes a 5.1 game-thread/render-thread restructuring and a 5.0.5 release mentioning mesh-update and memory-overuse fixes. The public release page does not provide a complete API diff against this custom 5.4 copy, so I am treating those as compatibility signals rather than promising source compatibility. The local copy was not upgraded, diffed destructively, or otherwise modified.
|
||||
|
||||
The current upstream guidance is to build an `FRealtimeMeshStreamSet` as a CPU-side value on a worker, then cross to the game thread to create/update the component section group; use a weak object pointer for the handoff. The structure documentation describes a game/render proxy mirror and distinguishes in-place dynamic/compute updates from static proxy recreation. See [updating mesh data](https://triaxis.games/realtime-mesh/docs/component-core/updating-mesh-data/) and [RMC component structure](https://triaxis.games/realtime-mesh/docs/component-core/structure/). The practical candidate is therefore worker-side stream construction plus the smallest possible section/buffer update, not touching a `URealtimeMeshComponent` from a worker. Custom RMC changes make this a high-risk follow-up.
|
||||
|
||||
### UE 5.7 tasking and tracing
|
||||
|
||||
Epic's UE 5.7 API documentation says `ParallelForWithExistingTaskContext` gives each spawned task a task-local workspace, and `MinBatchSize` limits launches to `DivUp(Num, MinBatchSize)` workers. The pre-work variant runs caller work before that thread helps the parallel loop. Both pages explicitly warn against clogging the task graph with long-running or blocking work: [existing task context](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Core/ParallelForWithExistingTaskConte-) and [pre-work task context](https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Core/ParallelForWithPreWorkWithTaskCo-). The next tuning target should be measured batch size and task-local scratch reuse for short density/cell jobs, not more tiny tasks.
|
||||
|
||||
UE's built-in tracing is the better fine-grained instrument. The documented channels include CPU, Counters, Task, and StackSampling; `-trace=cpu,counters` is a valid launch selection. `TRACE_CPUPROFILER_EVENT_SCOPE` with static scope names and `CountersTrace.h`/`TRACE_COUNTER_*` are the low-cost native hooks. See [Trace channels](https://dev.epicgames.com/documentation/en-us/unreal-engine/trace-in-unreal-engine-5), [the tracing guide](https://dev.epicgames.com/documentation/en-us/unreal-engine/developer-guide-to-tracing-in-unreal-engine?lang=en-US), and the [Unreal Insights reference](https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-insights-reference-in-unreal-engine-5). Timing Insights supplies timers, counters, callers/callees, and task execution views; Task Graph Insights exposes execution paths and critical paths: [Timing Insights](https://dev.epicgames.com/documentation/en-us/unreal-engine/timing-insights-in-unreal-engine?lang=en-US) and [Task Graph Insights](https://dev.epicgames.com/documentation/unreal-engine/task-graph-insights-in-unreal-engine-5?lang=en-US).
|
||||
|
||||
This task therefore keeps the lightweight sampled report for automated self-checks and adds native CPU scopes/counters for investigation. Insights should own future fine per-scope and task scheduling analysis; the hand-rolled profiler should not grow back into a timer around every density call.
|
||||
|
||||
### Marching cubes and density generation
|
||||
|
||||
The upstream [Transvoxel overview](https://transvoxel.org/) confirms that transition cells use local voxel data and a bounded lookup table to stitch different-resolution meshes without cracks. It is relevant to LOD boundary work and dynamic retriangulation, not a reason to change the current mesh in this task. A classic SIMD study reports nearly 4x over an unoptimized serial Marching Cubes implementation and about 2x over compiler-optimized C: [Newman et al., *High performance SIMD marching cubes*](https://www.sciencedirect.com/science/article/abs/pii/S0097849303002760). Those are historical results on older x86 and should be treated as an upper-bound signal, not a VoxelForge forecast.
|
||||
|
||||
## 5. Ranked next-task candidates (not implemented here)
|
||||
|
||||
The magnitude estimates are expected ranges for the affected stage, inferred from the sources and this run's counters; they are not promises for end-to-end time.
|
||||
|
||||
| rank | candidate | expected magnitude | risk |
|
||||
| ---: | --- | --- | --- |
|
||||
| 1 | Conservative macrocell/cell empty-space early-out using deterministic density bounds, with a fallback on boundary uncertainty | 2–10x on empty or mostly empty chunks; smaller on surface-heavy chunks | Medium-high: bounds must never skip a crossing cell or change traversal order |
|
||||
| 2 | Reuse one canonical density grid/field across meshing, LOD, export, and renderer preparation; invalidate by seed/slot/parameters | 20–60% less repeated density work and memory bandwidth where consumers overlap | Medium: cache lifetime and invalidation |
|
||||
| 3 | Tune `ParallelFor` granularity and task-local scratch for short jobs; measure `MinBatchSize`, worker count, and critical path in Insights | 5–25% less scheduling/coordination cost | Low-medium: load imbalance and contention can reverse the win |
|
||||
| 4 | SIMD/SoA batches for density and Marching Cubes table work | 1.5–4x for the vectorized stage in favorable workloads | High: exact cross-machine geometry and floating-point contraction; no runtime CPU dispatch is allowed |
|
||||
| 5 | Hierarchical sparse/occupancy metadata or distance bounds to skip whole regions before field evaluation | 2–20x in sparse worlds | High: build/update cost, conservative topology, and invalidation |
|
||||
| 6 | RMC partial section/buffer updates after worker-side stream construction | 10–50% less renderer update/bandwidth cost when only part of a section changes | High: custom RMC modifications and game/render-thread ownership |
|
||||
| 7 | Transvoxel transition cells for LOD boundaries | 10–30% less boundary patch/rebuild work in LOD-heavy scenes; also removes crack workarounds | Medium-high: transition topology and hash stability |
|
||||
|
||||
The first safe experiment should be the deterministic early-out or field reuse, after a baseline with the corrected profiler. SIMD is deliberately below those despite its attractive paper result because the project's cross-machine geometry requirement is absolute.
|
||||
|
||||
## 6. Pushback and constraints
|
||||
|
||||
The requirement that buckets sum to their parent needs a definition in a parallel program. Raw worker CPU time can exceed wall time by the worker count; the report now exposes both and makes the wall estimate the parent used for the self-check. A fine raw-bucket sum cannot be claimed when sampled mode intentionally does not time those scopes, so the report says that explicitly and accounts for the remainder.
|
||||
|
||||
The 20% self-check threshold is a practical alarm, not a proof of zero measurement cost: OS scheduling, cache state, DDC state, and editor startup can move two exports. The geometry hash check is exact; repeated cases or a median of several runs should be used when a performance regression is close to the threshold.
|
||||
|
||||
The sampling decision is non-deterministic only in its elapsed-time observation. It is isolated from the generation inputs and outputs. The batch commandlet resets profile/cache-report state per case, but it intentionally reuses the engine process to remove startup cost; the standalone-versus-batch geometry hash is the proof that this reuse did not contaminate the tested case.
|
||||
@@ -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