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

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

THE ATTRIBUTION, and it is almost embarrassingly one-sided

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

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

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

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

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

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

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

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

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

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

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

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

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
# PERF-ATTRIBUTION
Measured 2026-09-12 at `cf0319d` (`experimental`). This was measurement-only: the source changes add
diagnostic counters and clocks; they do not change density or meshing decisions. The primary case is
the 128^3 `TunnelNetwork`, seed 1337, slot 1, operator-stack setting 1, origin `(-64,-64,-224)`
voxels, step 1, 64 canonical 32^3 tiles, and 15 workers.
`clean` means diagnostics off and `-blockearlyout=0`, with fused evaluation and shared density-grid
reuse enabled. The three clean primary runs used an exact-HEAD DLL staged before instrumentation:
603.599, 590.746, and 569.627 ms mesh time, mean **587.991 ms**. The requested 0.556 s is therefore
within the observed run spread but was not reproduced exactly on this host. `attr` means the final
instrumented DLL, `-perfattribution`, sample interval 128. Its wall-clock self-check compares the
same export against an unprofiled run in the same process.
The prescribed build was run successfully with:
```text
dotnet "E:\Program Files\Epic Games\UE_5.7\Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.dll" UnrealEditor Win64 Development "-Project=E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BuildHost\HostProject\HostProject.uproject" -WaitMutex -FromMsBuild -architecture=x64 -NoUBA "-Log=E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\PerfAttributionBuild.log"
```
The measured process loaded the staged runtime DLL at
`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BuildHost\HostProject\Plugins\VoxelForge\Binaries\Win64\UnrealEditor-VoxelForge.dll`, SHA-256
`C0CB812BB25EFE8C836DC1C1718C408DA5FE9D9F258B4AE12A6CA846510CA0B2`, length 7,673,856 bytes. The
staged editor DLL was `E64C9E80869EFA53FA1556947718DB8E64975ABF6B546657BCFF5D775A2B679A`, length
502,784 bytes. The game harness recorded the same runtime path and hash. The primary attribution and
final classifier exports returned `status=ok`, geometry CRC `07C14005`, and the required OBJ SHA-256
`b3e5f4c398dd0547c6c55f415872058246d4a292c2f0d636ee1cd0829de9b377`.
The table is the attribution. Only rows marked **additive** are summed. Profiler component rows are
sampled nested diagnostics; their CPU and worker-wall figures are evidence about composition, not
additional time to add to the wall ledger.
| Bucket / question | Measurement and mode | Elapsed time, volume, or count | Whole-clock accounting / interpretation |
|---|---|---:|---|
| Historical comparison | `34f06df` value supplied in the request | 130.000 ms | Historical pre-op-stack reference; the exact old binary was not rebuilt because that commit predates the current editor commandlet and host harness. |
| Current clean primary | 3 exports, exact `cf0319d` DLL, diagnostics **off**, fused=1, reuse=1, classifier early-out=0 | **587.991 ms mean**; 569.627–603.599 ms | Clean headline mesh clock. Mean is 4.523x the supplied 130 ms reference. |
| Primary attribution self-check | Final DLL, `attr`; profiled 606.128 ms versus same-argument unprofiled 602.341 ms | 1.0063x; +0.629% | Passed. Geometry equal. The profiler did not reproduce the 11.5x inflation seen in the earlier diagnostic mode. |
| Full commandlet export | Final DLL, `attr`; mesh plus output and bookkeeping | **1,004.557 ms** | Full export wall, not just mesh generation. The OBJ writer is a material part of this clock. |
| Full export: mesh generation | Additive export ledger | 606.128 ms; 60.338% of export | Includes the complete additive mesh ledger below. |
| Full export: OBJ/output interval | Additive; includes nested `MeasureExploreMesh` | 394.273 ms; 39.249% | `measure_seconds` is 82.964 ms inside this interval and is not added again. |
| Full export: geometry hash | Additive | 2.946 ms; 0.293% | CRC/hash bookkeeping. |
| Full export: manifest | Additive | 0.984 ms; 0.098% | Manifest write. |
| Full export: remainder | Additive export remainder | 0.225 ms; 0.022% | Export ledger remainder. Mesh + OBJ interval + hash + manifest + remainder = 1,004.557 ms. |
| Mesh ledger: preparation | **Additive** | 0.000101 ms; 0.0000% of mesh | No meaningful cost. |
| Mesh ledger: shared density-grid fill | **Additive**; 2,248,091 unique shared samples | **590.923 ms; 97.4913% of mesh** | This is the dominant remaining cost. It is the caller-side density-grid stage and includes the canonical fused `GetDensityAt` work. |
| Mesh ledger: pre-tile setup | **Additive** | 0.001602 ms; 0.0003% | No meaningful cost. |
| Mesh ledger: tile parallel wall | **Additive**; 64 jobs, 15 workers | 10.972 ms; 1.8102% | Per-tile cell scan, interpolation, normals, stream assembly, and task scheduling after the shared grid exists. |
| Mesh ledger: tile merge | **Additive** | 4.232 ms; 0.6982% | Cross-tile output merge. |
| Mesh ledger: accounting remainder | **Additive** | 0.000097 ms; 0.0000% | Stage sum 606.128205 ms versus mesh clock 606.128301 ms; `sum_matches_mesh=true`. Nothing is hidden in this remainder. |
| Mesh ledger total | Final DLL, `attr` | **606.128301 ms** | Complete mesh wall clock. |
| `GetDensityAt` parent | Final primary, `attr`; 2,248,091 calls, 17,558 sampled parent intervals, 15 workers | 580.184 ms worker-wall estimate | Nested inside the 590.923 ms density-grid stage. The remaining 10.738 ms is grid-loop/task/read overhead outside the parent scope; it is accounted for by the additive density-grid row. |
| Fused evaluator itself | Final primary, `attr`; 17,571 sampled component scopes; 2,248,091 fused tunnel samples | 107.183 ms sampled CPU; 12.301 ms sampled worker wall | Diagnostic sample only, not additive. Detail ran for 1,079,777 samples and was skipped for 1,168,314, about 52.0% of fused lattice samples. |
| Fused density core | Final primary, `attr`; sampled nested scope | 108.371 ms sampled CPU; 12.377 ms sampled worker wall | Diagnostic sample only. It is not a 108 ms total bucket and must not be added to the mesh ledger. |
| Room/tunnel graph traversal | Final primary counters | Rooms: 11,099,891 candidates / 1,730,920 evaluated; tunnels: 28,951,420 candidates / 3,462,565 evaluated; tunnel-core: 28,951,420 candidates / 1,881,812 evaluated | Room and tunnel candidate traversal is repeated inside the common density work. This is evidence of work volume, not a separately additive clock. |
| Room graph build and SDF | Final primary, `attr` | `RoomGraphBuild`: 77 calls, 192.146 ms sampled CPU / 19.323 ms worker wall; `RoomGraphSdf`: 12.101 ms / 0.989 ms sampled CPU/wall | The build is cold/cache construction work; SDF figures are nested samples. The room graph is not a large independent wall bucket on the primary fused route. |
| Tunnel-core world evaluation | Final primary, `attr` | 8.857 ms sampled CPU; 0.872 ms sampled worker wall | Nested diagnostic sample. The core traversal is present, but the measured evidence does not support calling it the whole 4.3x. |
| Post-stack structural/passage tail | Final primary, `attr`; `GetDensityAt` tail | `StructuralTail`: 20.853 ms CPU / 1.896 ms wall; `DensityStructuralPosts`: 21.909 ms / 1.990 ms; `PassageStructuralPosts`: 10.476 ms / 0.997 ms sampled CPU/wall | This is real current-only post-stack work, but it is a small sampled component relative to the 590.923 ms density-grid wall. All rows are nested and non-additive. Passage modifier was 4.579 ms CPU / 0.404 ms wall; landing-floor and room-floor samples were 1.691 ms / 0.177 ms and 0.665 ms / 0.060 ms. |
| Floor/profile work | Final primary counters, fused route; classifier figures from the separate classifier control | Primary support-column builds/queries/checks: 0 / 0 / 0; authored-floor samples: 0; floor-profile builds 2,636; segments 12,624; backstops 6,509 tunnel and 49 room. Classifier control: 211,289 support-column builds, 2,171,763 candidates, 1,370,314 floor queries, 2,296,243 checks, 9,710 authored-floor samples, 97,950 profiles and 442,278 profile segments. | The fused primary route does not execute the classifier’s support-column path. The classifier does, and that work is part of its separate 2.057 s route, not the clean 0.588 s headline. |
| Software tunnel cache | Final primary, `attr` | 2,248,043 hits, 48 misses, 77 cache builds | Cache lookup is not the removed verdict cache. The current tunnel cache is overwhelmingly hit; no hardware cache-miss counter was available. |
| Classifier absent from the clean headline | Clean exact-HEAD control, diagnostics **off**, classifier early-out=0 | 0 classifier calls | The stated 0.556 s case does not pay interval proofs. It is incorrect to charge classifier time to that baseline. |
| Classifier control when enabled | 3 clean exact-HEAD controls, diagnostics **off**, classifier early-out=1 | **2,056.596 ms mean** | +1,468.605 ms versus the clean primary mean; 3.498x the primary. This is a separate, expensive route, not the source of the clean 0.556 s. |
| Classifier total self-check | Final DLL, `attr`, classifier early-out=1 | 1,980.835 ms profiled versus 2,013.547 ms unprofiled; 0.9838x | Passed; geometry equal. Profile overhead was -1.62% in this run. |
| Interval proofs | Final classifier attribution; 32 sampled calls out of 4,096 | 494,976 refine/box calls; 479,872 whole-mixed, 15,104 whole-solid, 0 whole-air; 63,488 splits; max depth 4 | Proof detail is present and conservative, but its route is costly. The classifier’s scaled diagnostic work was 14,113.190 ms interval-box, 62.221 ms exact-core, and 399.770 ms exact-final microseconds; these are diagnostic work estimates, not additive wall time. |
| Classifier repeated room traversal | Final classifier attribution | 2,041,600 room-tail queries, 933,120 evaluated; 6,030,720 exact-core cache hits; 612,864 exact-core samples; 768 exact-final samples | The classifier does repeat room-graph work. Scaled diagnostic work was 11,423.590 ms room propagation, 4,665.818 ms exact primitive, 5,815.002 ms cache-window, and 1,227.366 ms room tail microseconds. |
| Shared grid traffic | Final primary, `attr` | 2,744,000 tile-grid samples; 2,248,091 unique shared samples; 495,909 duplicate evaluations before reuse, 0 after | The shared lattice removes duplicate evaluations, but filling the remaining 2.248 M samples is still 590.923 ms. |
| Mesher cell classification | Final primary, `attr`; nested sample | 2,097,152 cell calls; 1.344 ms sampled worker wall | The cell loop is not the dominant wall bucket. |
| Mesher vertex interpolation | Final primary, `attr`; nested sample | 76,808 calls; 0.012 ms sampled worker wall | Negligible in the observed sample. |
| Mesher gradient normals | Final primary, `attr`; nested sample | 76,808 calls; 1.056 ms sampled worker wall | Small nested cost; gradient normals come from the density grid. |
| Mesher stream building | Final primary, `attr`; nested sample | 76,808 calls; 3.140 ms sampled worker wall | Small relative to density-grid fill. Runtime stream handoff is not executed by the export commandlet. |
| Mesher output and memory | Final primary, `attr` | 82,273 vertices, 153,346 triangles; logical output arrays 7,434,716 bytes; allocated output 8,449,820 bytes; OBJ 17,587,937 bytes; estimated working set 14,851,564 bytes | Byte counters are logical allocator/output traffic, not physical DRAM traffic. |
| Grid read/write bytes | Final primary, `attr` | Density-grid bytes 10,976,000; shared-grid read bytes 10,976,000 | The instrumentation sees logical grid traffic. It does not see hardware cache misses, NUMA traffic, or memory-controller events. |
| Operator-block scratch/copy | Final primary, `attr` | 0 scratch bytes, 0 copy bytes, 0 builds, 0 operators | The old interpreted block machinery is not on the canonical primary route. |
| Operator-block control | Clean diagnostics-off controls with reuse=0 and fused=0 | 2,332.186 ms; 1,566 block builds; 2,486,750 block samples; 31,320 operator visits; 99,470,000 scratch bytes; 101,956,750 copy bytes | This is a deliberately separate block-path control, not the clean fused baseline. It demonstrates the traffic that is absent from the primary route. |
| Scalar and fused no-reuse controls | Clean diagnostics-off controls with reuse=0 | Scalar 2,733.080 ms; fused 2,218.132 ms | These controls show the cost of removing shared-grid reuse, but they are not attribution buckets for the primary run. |
| What `34f06df` did not do | Read-only source comparison at the historical commit | No active TunnelNetwork operator-stack evaluation, no fused evaluator, no current post-stack tunnel-floor/structural tail, no current editor export commandlet | The old source already had a shared density grid, `ClassifyTile`, marching-cubes cell classification, interpolation, grid-gradient normals, and stream construction. Those are not new current-only work. The old source also had room/tunnel morphology and passage carving, so “rooms/tunnels exist” alone is not a delta. |
| Primary operation-level attribution gap | Final primary profiler’s own ledger | Sampled phase sum 14.443 ms versus sampled `GetDensityAt` parent 580.184 ms; sampling gap 565.741 ms | This is the honest limit: component scopes were sampled to keep profiled/unprofiled totals within 0.63%, so their expanded operation-level wall shares are not identifiable from this run. The full mesh wall is nevertheless closed by the additive stage ledger, with a 0.000097 ms remainder. |
| Headless game clean check | `PerfAttribGameCleanFinal_20260912`; diagnostics **off**, tile profiling off; 343 LOD0-ready samples | request-to-ready p95 **0.375470 s**; queue wait p95 0.082292 s; generation p95 0.309538 s; collision cook p95 0.052945 s | Secondary check agrees with the earlier conclusion that queue wait is not the main generation constraint. |
| Headless game attribution check | `PerfAttribGameAttrFinal_20260912`; diagnostics **on**, tile profiling on; 834 profile lines, 822 applied | request-to-ready p95 **0.395715 s**; queue wait p95 0.083363 s; generation p95 0.315281 s; collision cook p95 0.061387 s | Attribution overhead was modest in the end-to-end game summary. Tile-profile p95 was 1.295847 s, of which classifier was 0.106626 s, mesh 1.284040 s, stream building 0.000361 s, room work 0.008842 s, and stack work 0.012019 s; these game tiles are not the same workload as the single 128^3 export. |
The surprise is that the clean primary is almost entirely the shared density-grid fill: the ordinary
mesher stages after it total only about 15.204 ms. The fused evaluator is genuinely on the canonical
path, and the room/tunnel candidate counters show substantial repeated traversal, but the sampled
component timers cannot honestly turn that 590.923 ms into exact per-operation percentages without
reintroducing the profiler inflation this round was designed to avoid. The classifier is an even
larger cost when enabled, yet it is absent from the clean baseline. The old path already performed
the grid and marching-cubes phases, so those phases should not be described as newly introduced
work; the current-only items are the active fused route and the later structural/floor machinery.
What could not be attributed: physical allocation-event counts, physical memory/cache misses, and an
exact numeric per-operation delta against the old `34f06df` binary. The old commit has no compatible
current export commandlet, and rebuilding it would not be the same measurement harness. The complete
current clocks, deterministic geometry, clean/profiled comparison, current counters, and all logical
memory-byte counters are accounted for above. Existing logs and outputs under `Saved\` were retained.
@@ -4729,6 +4729,8 @@ void VoxelCaveMorphology::BuildChunkCache(
uint32 Seed, int32 StrateIndex, uint32 Seed, int32 StrateIndex,
const TArray<FStrateTerrainOpEntry>* TerrainOps) const TArray<FStrateTerrainOpEntry>* TerrainOps)
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::RoomGraphBuild);
// Clear previous data (arrays keep their allocation for reuse) // Clear previous data (arrays keep their allocation for reuse)
OutCache.Reset(); OutCache.Reset();
OutCache.SDFBlendRadius = Params.SDFBlendRadius; OutCache.SDFBlendRadius = Params.SDFBlendRadius;
@@ -6025,6 +6027,8 @@ float VoxelCaveMorphology::EvaluateSDFCached(
float SDFBlendRadius, float SDFBlendRadius,
int32* OutNearestRoomIdx) int32* OutNearestRoomIdx)
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::RoomGraphSdf);
float MinSDF = FLT_MAX; float MinSDF = FLT_MAX;
const float BlendK = SDFBlendRadius; const float BlendK = SDFBlendRadius;
const FVector Pos(WorldX, WorldY, WorldZ); const FVector Pos(WorldX, WorldY, WorldZ);
@@ -6273,6 +6277,8 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
const FChunkSDFCache& Cache, const FChunkSDFCache& Cache,
const FTunnelSupportFloorColumn* SupportColumn) const FTunnelSupportFloorColumn* SupportColumn)
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCoreWorld);
const FVector Pos(WorldX, WorldY, WorldZ); const FVector Pos(WorldX, WorldY, WorldZ);
FTunnelCoreWorldEvaluation Result; FTunnelCoreWorldEvaluation Result;
@@ -6491,6 +6497,8 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
const FChunkSDFCache& Cache, const FChunkSDFCache& Cache,
FTunnelSupportFloorColumn& OutColumn) FTunnelSupportFloorColumn& OutColumn)
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCoreSupport);
OutColumn.Reset(); OutColumn.Reset();
if (VoxelDensityProfile::AreCountersEnabled()) if (VoxelDensityProfile::AreCountersEnabled())
@@ -4009,6 +4009,8 @@ namespace
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::RoomGraphSource);
FState& S = State(); FState& S = State();
S.LastWorldPosition = FVector(WorldX, WorldY, WorldZ); S.LastWorldPosition = FVector(WorldX, WorldY, WorldZ);
S.bLastTunnelCoreWorldEvaluationValid = false; S.bLastTunnelCoreWorldEvaluationValid = false;
+263 -12
View File
@@ -2,10 +2,11 @@
namespace VoxelDensityProfile namespace VoxelDensityProfile
{ {
namespace
{
std::atomic<bool> GEnabled(false); std::atomic<bool> GEnabled(false);
std::atomic<EMode> GMode(EMode::Disabled); std::atomic<EMode> GMode(EMode::Disabled);
namespace
{
std::atomic<uint32> GSampleInterval(DefaultSampleInterval); std::atomic<uint32> GSampleInterval(DefaultSampleInterval);
std::atomic<uint64> GTimerPairCycles(0); std::atomic<uint64> GTimerPairCycles(0);
@@ -24,6 +25,7 @@ namespace VoxelDensityProfile
uint32 BlockCallCount[BucketCount]{}; uint32 BlockCallCount[BucketCount]{};
uint64 BlockStartCycles[BucketCount]{}; uint64 BlockStartCycles[BucketCount]{};
bool BlockActive[BucketCount]{}; bool BlockActive[BucketCount]{};
FSnapshot::FClassifierStats Classifier;
uint64 TunnelCacheCapacityEntries = 0; uint64 TunnelCacheCapacityEntries = 0;
uint64 TunnelCacheValidEntries = 0; uint64 TunnelCacheValidEntries = 0;
@@ -50,7 +52,6 @@ namespace VoxelDensityProfile
thread_local FThreadState* GThreadState = nullptr; thread_local FThreadState* GThreadState = nullptr;
thread_local uint32 GScopeDepth = 0; thread_local uint32 GScopeDepth = 0;
thread_local bool GScopeSampled = false; thread_local bool GScopeSampled = false;
thread_local bool GSampledScopeActive = false;
FThreadState& GetThreadState() FThreadState& GetThreadState()
{ {
@@ -80,9 +81,9 @@ namespace VoxelDensityProfile
bool IsSampledScopeEnabled(EBucket Bucket) bool IsSampledScopeEnabled(EBucket Bucket)
{ {
// Sampled mode pays for the trustworthy parent and mesh containers only. Fine // Sampled mode pays for the trustworthy parent and mesh containers only. Attribution
// operation scopes remain available in Full mode, or through UE Insights scopes, // additionally exposes the component scopes, but still samples them; Full remains the
// without putting a Begin/End and thread-local bookkeeping on every density call. // deliberately invasive mode for per-call scope timing.
switch (Bucket) switch (Bucket)
{ {
case EBucket::GetDensityAt: case EBucket::GetDensityAt:
@@ -94,6 +95,127 @@ namespace VoxelDensityProfile
return false; return false;
} }
} }
bool IsAttributionScope(EBucket Bucket)
{
switch (Bucket)
{
case EBucket::GetDensityAt:
case EBucket::ApplyDisturbances:
case EBucket::PassageModifier:
case EBucket::PassageLandingAir:
case EBucket::PassageLandingFloor:
case EBucket::PassageLandingRoomFloor:
case EBucket::PassageTunnelAir:
case EBucket::PassageStructuralPosts:
case EBucket::TunnelCoreSupport:
case EBucket::TunnelCoreSdf:
case EBucket::TunnelCorePosts:
case EBucket::StructuralTail:
case EBucket::RoomGraphSource:
case EBucket::SdfCarve:
case EBucket::FusedEvaluator:
case EBucket::InterpretedOpStack:
case EBucket::OperatorBlock:
case EBucket::RoomGraphBuild:
case EBucket::RoomGraphSdf:
case EBucket::TunnelCoreWorld:
case EBucket::FusedDetail:
case EBucket::FusedWorm:
case EBucket::FusedStructural:
case EBucket::ClassifierTotal:
case EBucket::ClassifierIntervalProof:
case EBucket::ClassifierExactCore:
case EBucket::ClassifierExactFinal:
case EBucket::ClassifierRoomTail:
case EBucket::RuntimeStreamBuilding:
case EBucket::DensityPrologue:
case EBucket::DensityCore:
case EBucket::DensityDisturbances:
case EBucket::DensityStructuralPosts:
case EBucket::DensityBoundarySeal:
case EBucket::DensityDiffLayer:
case EBucket::DensityTail:
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherCellClassification:
case EBucket::MesherGradientNormals:
case EBucket::MesherVertexInterpolation:
case EBucket::MesherStreamBuilding:
case EBucket::MesherOther:
return true;
default:
return false;
}
}
bool IsAttributionAlwaysTimed(EBucket Bucket)
{
switch (Bucket)
{
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherOther:
case EBucket::RoomGraphBuild:
return true;
default:
return false;
}
}
uint64 ScaleSampledValue(uint64 Value, uint64 Calls, uint64 Samples)
{
if (Samples == 0 || Calls <= Samples)
{
return Value;
}
const double Scaled = static_cast<double>(Value)
* static_cast<double>(Calls) / static_cast<double>(Samples);
return Scaled > static_cast<double>(MAX_uint64)
? MAX_uint64
: static_cast<uint64>(Scaled + 0.5);
}
void AddClassifierStatsScaled(
FSnapshot::FClassifierStats& Destination,
const FSnapshot::FClassifierStats& Source,
uint64 Calls,
uint64 Samples)
{
Destination.RefineNodes += ScaleSampledValue(Source.RefineNodes, Calls, Samples);
Destination.StackBoxCalls += ScaleSampledValue(Source.StackBoxCalls, Calls, Samples);
Destination.WholeMixedNodes += ScaleSampledValue(Source.WholeMixedNodes, Calls, Samples);
Destination.WholeSolidNodes += ScaleSampledValue(Source.WholeSolidNodes, Calls, Samples);
Destination.WholeAirNodes += ScaleSampledValue(Source.WholeAirNodes, Calls, Samples);
Destination.NeedsFinalFieldNodes += ScaleSampledValue(Source.NeedsFinalFieldNodes, Calls, Samples);
Destination.SplitNodes += ScaleSampledValue(Source.SplitNodes, Calls, Samples);
Destination.MaxRefinementDepth = FMath::Max(
Destination.MaxRefinementDepth, Source.MaxRefinementDepth);
Destination.ExactCoreSamples += ScaleSampledValue(Source.ExactCoreSamples, Calls, Samples);
Destination.ExactFinalSamples += ScaleSampledValue(Source.ExactFinalSamples, Calls, Samples);
Destination.ExactCoreCacheHits += ScaleSampledValue(Source.ExactCoreCacheHits, Calls, Samples);
Destination.ExactFinalCacheHits += ScaleSampledValue(Source.ExactFinalCacheHits, Calls, Samples);
Destination.ExactCoreLeaves += ScaleSampledValue(Source.ExactCoreLeaves, Calls, Samples);
Destination.ExactFinalLeaves += ScaleSampledValue(Source.ExactFinalLeaves, Calls, Samples);
Destination.StackBoxCycles += ScaleSampledValue(Source.StackBoxCycles, Calls, Samples);
Destination.ExactCoreCycles += ScaleSampledValue(Source.ExactCoreCycles, Calls, Samples);
Destination.ExactFinalCycles += ScaleSampledValue(Source.ExactFinalCycles, Calls, Samples);
Destination.RoomTailQueries += ScaleSampledValue(Source.RoomTailQueries, Calls, Samples);
Destination.RoomTailEvaluated += ScaleSampledValue(Source.RoomTailEvaluated, Calls, Samples);
Destination.RoomTailCycles += ScaleSampledValue(Source.RoomTailCycles, Calls, Samples);
Destination.RoomPropagateCycles += ScaleSampledValue(Source.RoomPropagateCycles, Calls, Samples);
Destination.RoomExactPrimitiveCycles += ScaleSampledValue(Source.RoomExactPrimitiveCycles, Calls, Samples);
Destination.RoomCacheWindowCycles += ScaleSampledValue(Source.RoomCacheWindowCycles, Calls, Samples);
// These are topology descriptors, not per-call work counts. Keep the largest observed
// descriptor instead of multiplying one sampled graph description by the classifier
// call expansion factor.
Destination.RoomNumRooms = FMath::Max(Destination.RoomNumRooms, Source.RoomNumRooms);
Destination.RoomNumTunnels = FMath::Max(Destination.RoomNumTunnels, Source.RoomNumTunnels);
Destination.RoomNumRoomFloorJoins = FMath::Max(
Destination.RoomNumRoomFloorJoins, Source.RoomNumRoomFloorJoins);
Destination.RoomNumPits = FMath::Max(Destination.RoomNumPits, Source.RoomNumPits);
Destination.RoomNumChimneys = FMath::Max(Destination.RoomNumChimneys, Source.RoomNumChimneys);
}
} }
void SetEnabled(bool bEnabled) void SetEnabled(bool bEnabled)
@@ -184,6 +306,7 @@ namespace VoxelDensityProfile
State->BlockStartCycles[Index] = 0; State->BlockStartCycles[Index] = 0;
State->BlockActive[Index] = false; State->BlockActive[Index] = false;
} }
State->Classifier = FSnapshot::FClassifierStats();
State->TunnelCacheCapacityEntries = 0; State->TunnelCacheCapacityEntries = 0;
State->TunnelCacheValidEntries = 0; State->TunnelCacheValidEntries = 0;
State->TunnelCacheStaticBytes = 0; State->TunnelCacheStaticBytes = 0;
@@ -253,6 +376,14 @@ namespace VoxelDensityProfile
Result.Counters[Index] += State->Counters[Index]; Result.Counters[Index] += State->Counters[Index];
} }
Result.Classifier.Calls += State->Classifier.Calls;
Result.Classifier.SampledCalls += State->Classifier.SampledCalls;
AddClassifierStatsScaled(
Result.Classifier,
State->Classifier,
State->Classifier.Calls,
State->Classifier.SampledCalls);
const uint64 TunnelWorkerBytes = State->TunnelCacheStaticBytes const uint64 TunnelWorkerBytes = State->TunnelCacheStaticBytes
+ State->TunnelCacheDynamicBytes; + State->TunnelCacheDynamicBytes;
if (State->TunnelCacheCapacityEntries > 0) if (State->TunnelCacheCapacityEntries > 0)
@@ -337,6 +468,19 @@ namespace VoxelDensityProfile
{ {
Result.Counters[Index] = State->Counters[Index]; Result.Counters[Index] = State->Counters[Index];
} }
Result.Classifier = State->Classifier;
if (State->Classifier.SampledCalls > 0)
{
FSnapshot::FClassifierStats Raw = State->Classifier;
Result.Classifier = FSnapshot::FClassifierStats();
Result.Classifier.Calls = State->Classifier.Calls;
Result.Classifier.SampledCalls = State->Classifier.SampledCalls;
AddClassifierStatsScaled(
Result.Classifier,
Raw,
State->Classifier.Calls,
State->Classifier.SampledCalls);
}
Result.TimerPairCycles = GetTimerPairCycles(); Result.TimerPairCycles = GetTimerPairCycles();
Result.SampleInterval = GetSampleInterval(); Result.SampleInterval = GetSampleInterval();
Result.Mode = GetMode(); Result.Mode = GetMode();
@@ -345,6 +489,10 @@ namespace VoxelDensityProfile
void AddCounter(ECounter Counter, uint64 Amount) void AddCounter(ECounter Counter, uint64 Amount)
{ {
if (!IsEnabledFast())
{
return;
}
const int32 Index = static_cast<int32>(Counter); const int32 Index = static_cast<int32>(Counter);
if (Index >= 0 && Index < CounterCount) if (Index >= 0 && Index < CounterCount)
{ {
@@ -366,6 +514,56 @@ namespace VoxelDensityProfile
GetThreadState().Calls[Index] += 1; GetThreadState().Calls[Index] += 1;
} }
void RecordClassifierCall(bool bSampled)
{
FThreadState& State = GetThreadState();
++State.Classifier.Calls;
if (bSampled)
{
++State.Classifier.SampledCalls;
}
++State.Calls[ToIndex(EBucket::ClassifierTotal)];
}
void AddClassifierStats(const FSnapshot::FClassifierStats& Stats)
{
FThreadState& State = GetThreadState();
State.Classifier.RefineNodes += Stats.RefineNodes;
State.Classifier.StackBoxCalls += Stats.StackBoxCalls;
State.Classifier.WholeMixedNodes += Stats.WholeMixedNodes;
State.Classifier.WholeSolidNodes += Stats.WholeSolidNodes;
State.Classifier.WholeAirNodes += Stats.WholeAirNodes;
State.Classifier.NeedsFinalFieldNodes += Stats.NeedsFinalFieldNodes;
State.Classifier.SplitNodes += Stats.SplitNodes;
State.Classifier.MaxRefinementDepth = FMath::Max(
State.Classifier.MaxRefinementDepth, Stats.MaxRefinementDepth);
State.Classifier.ExactCoreSamples += Stats.ExactCoreSamples;
State.Classifier.ExactFinalSamples += Stats.ExactFinalSamples;
State.Classifier.ExactCoreCacheHits += Stats.ExactCoreCacheHits;
State.Classifier.ExactFinalCacheHits += Stats.ExactFinalCacheHits;
State.Classifier.ExactCoreLeaves += Stats.ExactCoreLeaves;
State.Classifier.ExactFinalLeaves += Stats.ExactFinalLeaves;
State.Classifier.StackBoxCycles += Stats.StackBoxCycles;
State.Classifier.ExactCoreCycles += Stats.ExactCoreCycles;
State.Classifier.ExactFinalCycles += Stats.ExactFinalCycles;
State.Classifier.RoomTailQueries += Stats.RoomTailQueries;
State.Classifier.RoomTailEvaluated += Stats.RoomTailEvaluated;
State.Classifier.RoomTailCycles += Stats.RoomTailCycles;
State.Classifier.RoomPropagateCycles += Stats.RoomPropagateCycles;
State.Classifier.RoomExactPrimitiveCycles += Stats.RoomExactPrimitiveCycles;
State.Classifier.RoomCacheWindowCycles += Stats.RoomCacheWindowCycles;
State.Classifier.RoomNumRooms = FMath::Max(
State.Classifier.RoomNumRooms, Stats.RoomNumRooms);
State.Classifier.RoomNumTunnels = FMath::Max(
State.Classifier.RoomNumTunnels, Stats.RoomNumTunnels);
State.Classifier.RoomNumRoomFloorJoins = FMath::Max(
State.Classifier.RoomNumRoomFloorJoins, Stats.RoomNumRoomFloorJoins);
State.Classifier.RoomNumPits = FMath::Max(
State.Classifier.RoomNumPits, Stats.RoomNumPits);
State.Classifier.RoomNumChimneys = FMath::Max(
State.Classifier.RoomNumChimneys, Stats.RoomNumChimneys);
}
bool ShouldSample(EBucket Bucket) bool ShouldSample(EBucket Bucket)
{ {
if (!IsCycleTimingEnabled()) if (!IsCycleTimingEnabled())
@@ -488,6 +686,12 @@ namespace VoxelDensityProfile
case ECounter::OpBlockOperators: return TEXT("OpBlockOperators"); case ECounter::OpBlockOperators: return TEXT("OpBlockOperators");
case ECounter::OpBlockActiveOperators: return TEXT("OpBlockActiveOperators"); case ECounter::OpBlockActiveOperators: return TEXT("OpBlockActiveOperators");
case ECounter::OpBlockPrunedOperators: return TEXT("OpBlockPrunedOperators"); case ECounter::OpBlockPrunedOperators: return TEXT("OpBlockPrunedOperators");
case ECounter::MesherDensityGridBytes: return TEXT("MesherDensityGridBytes");
case ECounter::MesherSharedGridReadBytes: return TEXT("MesherSharedGridReadBytes");
case ECounter::MesherOutputArrayBytes: return TEXT("MesherOutputArrayBytes");
case ECounter::MesherOutputAllocatedBytes: return TEXT("MesherOutputAllocatedBytes");
case ECounter::OpBlockScratchBytes: return TEXT("OpBlockScratchBytes");
case ECounter::OpBlockCopyBytes: return TEXT("OpBlockCopyBytes");
case ECounter::Count: break; case ECounter::Count: break;
} }
return TEXT("Unknown"); return TEXT("Unknown");
@@ -535,6 +739,21 @@ namespace VoxelDensityProfile
{ TEXT("BoundarySealOp"), EBucket::BoundarySealOp }, { TEXT("BoundarySealOp"), EBucket::BoundarySealOp },
{ TEXT("PassageCarveOp"), EBucket::PassageCarveOp }, { TEXT("PassageCarveOp"), EBucket::PassageCarveOp },
{ TEXT("XYEdgeSealOp"), EBucket::XYEdgeSealOp }, { TEXT("XYEdgeSealOp"), EBucket::XYEdgeSealOp },
{ TEXT("FusedEvaluator"), EBucket::FusedEvaluator },
{ TEXT("InterpretedOpStack"),EBucket::InterpretedOpStack },
{ TEXT("OperatorBlock"), EBucket::OperatorBlock },
{ TEXT("RoomGraphBuild"), EBucket::RoomGraphBuild },
{ TEXT("RoomGraphSdf"), EBucket::RoomGraphSdf },
{ TEXT("TunnelCoreWorld"), EBucket::TunnelCoreWorld },
{ TEXT("FusedDetail"), EBucket::FusedDetail },
{ TEXT("FusedWorm"), EBucket::FusedWorm },
{ TEXT("FusedStructural"), EBucket::FusedStructural },
{ TEXT("ClassifierTotal"), EBucket::ClassifierTotal },
{ TEXT("ClassifierIntervalProof"), EBucket::ClassifierIntervalProof },
{ TEXT("ClassifierExactCore"), EBucket::ClassifierExactCore },
{ TEXT("ClassifierExactFinal"), EBucket::ClassifierExactFinal },
{ TEXT("ClassifierRoomTail"), EBucket::ClassifierRoomTail },
{ TEXT("RuntimeStreamBuilding"), EBucket::RuntimeStreamBuilding },
{ TEXT("DensityPrologue"), EBucket::DensityPrologue }, { TEXT("DensityPrologue"), EBucket::DensityPrologue },
{ TEXT("DensityCore"), EBucket::DensityCore }, { TEXT("DensityCore"), EBucket::DensityCore },
{ TEXT("DensityDisturbances"), EBucket::DensityDisturbances }, { TEXT("DensityDisturbances"), EBucket::DensityDisturbances },
@@ -596,6 +815,21 @@ namespace VoxelDensityProfile
case EBucket::PassageCarveOp: return TEXT("PassageCarveOp"); case EBucket::PassageCarveOp: return TEXT("PassageCarveOp");
case EBucket::XYEdgeSealOp: return TEXT("XYEdgeSealOp"); case EBucket::XYEdgeSealOp: return TEXT("XYEdgeSealOp");
case EBucket::OtherOp: return TEXT("OtherOp"); case EBucket::OtherOp: return TEXT("OtherOp");
case EBucket::FusedEvaluator: return TEXT("FusedEvaluator");
case EBucket::InterpretedOpStack:return TEXT("InterpretedOpStack");
case EBucket::OperatorBlock: return TEXT("OperatorBlock");
case EBucket::RoomGraphBuild: return TEXT("RoomGraphBuild");
case EBucket::RoomGraphSdf: return TEXT("RoomGraphSdf");
case EBucket::TunnelCoreWorld: return TEXT("TunnelCoreWorld");
case EBucket::FusedDetail: return TEXT("FusedDetail");
case EBucket::FusedWorm: return TEXT("FusedWorm");
case EBucket::FusedStructural: return TEXT("FusedStructural");
case EBucket::ClassifierTotal: return TEXT("ClassifierTotal");
case EBucket::ClassifierIntervalProof:return TEXT("ClassifierIntervalProof");
case EBucket::ClassifierExactCore:return TEXT("ClassifierExactCore");
case EBucket::ClassifierExactFinal:return TEXT("ClassifierExactFinal");
case EBucket::ClassifierRoomTail:return TEXT("ClassifierRoomTail");
case EBucket::RuntimeStreamBuilding:return TEXT("RuntimeStreamBuilding");
case EBucket::DensityPrologue: return TEXT("DensityPrologue"); case EBucket::DensityPrologue: return TEXT("DensityPrologue");
case EBucket::DensityCore: return TEXT("DensityCore"); case EBucket::DensityCore: return TEXT("DensityCore");
case EBucket::DensityDisturbances:return TEXT("DensityDisturbances"); case EBucket::DensityDisturbances:return TEXT("DensityDisturbances");
@@ -618,9 +852,14 @@ namespace VoxelDensityProfile
FScopeToken BeginScope(EBucket Bucket) FScopeToken BeginScope(EBucket Bucket)
{ {
FScopeToken Token; FScopeToken Token;
if (!IsEnabledFast())
{
return Token;
}
const EMode Mode = GetMode(); const EMode Mode = GetMode();
if (!IsEnabled() const bool bSampledMode = Mode == EMode::Sampled || Mode == EMode::Attribution;
|| (Mode == EMode::Sampled && !IsSampledScopeEnabled(Bucket))) if ((Mode == EMode::Sampled && !IsSampledScopeEnabled(Bucket))
|| (Mode == EMode::Attribution && !IsAttributionScope(Bucket)))
{ {
return Token; return Token;
} }
@@ -633,7 +872,7 @@ namespace VoxelDensityProfile
// For the hot parent, amortise the timer over a deterministic block of calls. A timer // 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 // 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. // and estimates the parent from real elapsed work rather than instrumentation latency.
if (Mode == EMode::Sampled && Bucket == EBucket::GetDensityAt) if (bSampledMode && Bucket == EBucket::GetDensityAt)
{ {
FThreadState& State = GetThreadState(); FThreadState& State = GetThreadState();
const int32 Index = ToIndex(Bucket); const int32 Index = ToIndex(Bucket);
@@ -649,7 +888,11 @@ namespace VoxelDensityProfile
State.BlockCallCount[Index] = 0; State.BlockCallCount[Index] = 0;
State.BlockStartCycles[Index] = FPlatformTime::Cycles64(); State.BlockStartCycles[Index] = FPlatformTime::Cycles64();
} }
GScopeSampled = false; // The selected call admits all nested component scopes. Every other call exits the
// inline FScopedTimer fast path before crossing into this translation unit.
GScopeSampled = bBlockBoundary;
GSampledScopeActive = bBlockBoundary;
Token.bSampled = bBlockBoundary;
return Token; return Token;
} }
@@ -660,13 +903,21 @@ namespace VoxelDensityProfile
// density samples underneath them. // density samples underneath them.
const bool bContainerScope = Bucket == EBucket::MesherGenerateMesh const bool bContainerScope = Bucket == EBucket::MesherGenerateMesh
|| Bucket == EBucket::MesherDensityGrid; || Bucket == EBucket::MesherDensityGrid;
const bool bInheritedAttributionSample = Mode == EMode::Attribution
&& GSampledScopeActive;
Token.bSampled = Mode == EMode::Full Token.bSampled = Mode == EMode::Full
? true ? true
: (!GSampledScopeActive && ShouldSample(Bucket)); : ((Mode == EMode::Attribution && IsAttributionAlwaysTimed(Bucket))
? true
: (bInheritedAttributionSample
? true
: (Mode == EMode::Attribution && IsAttributionScope(Bucket)
? ShouldSample(Bucket)
: (!GSampledScopeActive && ShouldSample(Bucket)))));
GScopeSampled = Token.bSampled; GScopeSampled = Token.bSampled;
if (Token.bSampled) if (Token.bSampled)
{ {
if (Mode == EMode::Sampled && !bContainerScope) if ((Mode == EMode::Sampled || Mode == EMode::Attribution) && !bContainerScope)
{ {
GSampledScopeActive = true; GSampledScopeActive = true;
} }
+90 -3
View File
@@ -330,7 +330,15 @@ struct FVoxelDensityBlockSession
const int32 BlockSizeY = MaxY - MinY + 1; const int32 BlockSizeY = MaxY - MinY + 1;
const int32 BlockSizeZ = MaxZ - MinZ + 1; const int32 BlockSizeZ = MaxZ - MinZ + 1;
const int32 BlockCount = BlockSizeX * BlockSizeY * BlockSizeZ; const int32 BlockCount = BlockSizeX * BlockSizeY * BlockSizeZ;
VoxelDensityProfile::FScopedTimer OperatorBlockTimer(
VoxelDensityProfile::EBucket::OperatorBlock);
BlockScratch.SetNum(BlockCount); BlockScratch.SetNum(BlockCount);
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::OpBlockScratchBytes,
static_cast<uint64>(BlockCount) * sizeof(FVoxelOpSample));
}
for (FVoxelOpSample& Sample : BlockScratch) for (FVoxelOpSample& Sample : BlockScratch)
{ {
Sample = FVoxelOpSample(); Sample = FVoxelOpSample();
@@ -371,6 +379,13 @@ struct FVoxelDensityBlockSession
} }
} }
} }
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::OpBlockCopyBytes,
static_cast<uint64>(BlockCount)
* (sizeof(FVoxelOpSample) + sizeof(uint8)));
}
if (VoxelDensityProfile::AreCountersEnabled()) if (VoxelDensityProfile::AreCountersEnabled())
{ {
@@ -2053,6 +2068,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// interpreted stack, plus the source's structural hand-off, without allocating // interpreted stack, plus the source's structural hand-off, without allocating
// FVoxelOpSample or traversing the graph one operator at a time for this voxel. // FVoxelOpSample or traversing the graph one operator at a time for this voxel.
bUsedFusedEvaluator = true; bUsedFusedEvaluator = true;
VoxelDensityProfile::FScopedTimer FusedEvaluatorTimer(
VoxelDensityProfile::EBucket::FusedEvaluator);
Result = GetDensityWithParams( Result = GetDensityWithParams(
WorldX, WorldY, WorldZ, CP_Tunnel, CP_TunnelFP, LayoutVersion, WorldX, WorldY, WorldZ, CP_Tunnel, CP_TunnelFP, LayoutVersion,
/*bApplyLegacyStructuralPosts=*/false, /*bApplyLegacyStructuralPosts=*/false,
@@ -2074,6 +2091,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// must not construct a per-sample FVoxelOpSample merely because this compatibility // must not construct a per-sample FVoxelOpSample merely because this compatibility
// path still needs one when the block session is enabled. // path still needs one when the block session is enabled.
FVoxelOpSample BlockCoreSample; FVoxelOpSample BlockCoreSample;
{
VoxelDensityProfile::FScopedTimer OperatorBlockTimer(
VoxelDensityProfile::EBucket::OperatorBlock);
bUsedOpBlockSample = GVoxelDensityBlockSession.FillChunk( bUsedOpBlockSample = GVoxelDensityBlockSession.FillChunk(
*ActiveOpStack, *ActiveOpStack,
ChunkCoord, ChunkCoord,
@@ -2082,6 +2102,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
FMath::RoundToInt(WorldY), FMath::RoundToInt(WorldY),
FMath::RoundToInt(WorldZ)), FMath::RoundToInt(WorldZ)),
BlockCoreSample); BlockCoreSample);
}
if (bUsedOpBlockSample) if (bUsedOpBlockSample)
{ {
BlockDensity = BlockCoreSample.Density; BlockDensity = BlockCoreSample.Density;
@@ -2093,9 +2114,16 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
} }
if (!bUsedFusedEvaluator) if (!bUsedFusedEvaluator)
{ {
Result = bUsedOpBlockSample if (bUsedOpBlockSample)
? -BlockDensity {
: ActiveOpStack->EvalMC(WorldX, WorldY, WorldZ); Result = -BlockDensity;
}
else
{
VoxelDensityProfile::FScopedTimer InterpretedOpStackTimer(
VoxelDensityProfile::EBucket::InterpretedOpStack);
Result = ActiveOpStack->EvalMC(WorldX, WorldY, WorldZ);
}
} }
} }
else switch (CP_GenType) else switch (CP_GenType)
@@ -2221,6 +2249,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// calls use the same landing geometry already evaluated by the legacy and op-stack post. // calls use the same landing geometry already evaluated by the legacy and op-stack post.
VoxelDensityProfile::FScopedTimer DensityStructuralPostsTimer( VoxelDensityProfile::FScopedTimer DensityStructuralPostsTimer(
VoxelDensityProfile::EBucket::DensityStructuralPosts); VoxelDensityProfile::EBucket::DensityStructuralPosts);
VoxelDensityProfile::FScopedTimer StructuralTailTimer(
VoxelDensityProfile::EBucket::StructuralTail);
float LandingBaseDensity = CP_Dist.BaseDensity; float LandingBaseDensity = CP_Dist.BaseDensity;
#if WITH_EDITOR #if WITH_EDITOR
// A composer parent can deliberately use a different structural base than the authored // A composer parent can deliberately use a different structural base than the authored
@@ -2288,17 +2318,29 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
} }
// Origin rooms are structural too. Reassert their air before either support writer so a // Origin rooms are structural too. Reassert their air before either support writer so a
// bridge/ridge cannot plug a landing, while the two floor writers remain last. // bridge/ridge cannot plug a landing, while the two floor writers remain last.
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingAir);
VF_ApplyOriginLandingAirMC( VF_ApplyOriginLandingAirMC(
Result, WorldX, WorldY, WorldZ, Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ, CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius); CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
}
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingFloor);
VF_ApplyOriginLandingFloorMC( VF_ApplyOriginLandingFloorMC(
Result, WorldX, WorldY, WorldZ, Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ, CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius); CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
}
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageStructuralPosts);
StrateManager->ApplyPassageStructuralPostsMC( StrateManager->ApplyPassageStructuralPostsMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity, Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness, bProtectAuthoredTunnelFloor); CP_Dist.BoundarySealThickness, bProtectAuthoredTunnelFloor);
}
// Disturbance features are authored as a generic MC-space post and may add a ridge or // Disturbance features are authored as a generic MC-space post and may add a ridge or
// bridge over a graph tunnel. Reassert the native cached tunnel core here, after every // bridge over a graph tunnel. Reassert the native cached tunnel core here, after every
// solid floor writer but before the global XY seal. This cache is built once per chunk, // solid floor writer but before the global XY seal. This cache is built once per chunk,
@@ -2393,22 +2435,39 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// backstop is allowed to reopen the tunnel, but the landing's proved support floor // backstop is allowed to reopen the tunnel, but the landing's proved support floor
// must own the final floor band; otherwise the graph post can erase the only support // must own the final floor band; otherwise the graph post can erase the only support
// surface at the mouth and leave the player-fit graph with a disconnected pocket. // surface at the mouth and leave the player-fit graph with a disconnected pocket.
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingFloor);
VF_ApplyOriginLandingFloorMC( VF_ApplyOriginLandingFloorMC(
Result, WorldX, WorldY, WorldZ, Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ, CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius); CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
}
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingFloor);
StrateManager->ApplyPassageLandingFloorMC( StrateManager->ApplyPassageLandingFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity); Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
}
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingRoomFloor);
StrateManager->ApplyPassageLandingRoomFloorMC( StrateManager->ApplyPassageLandingRoomFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity); Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
} }
} }
}
// Inter-strate passages own the same final D-floor contract as graph tunnels. This is // Inter-strate passages own the same final D-floor contract as graph tunnels. This is
// composed from the immutable construction-time profile after generic writers and the // composed from the immutable construction-time profile after generic writers and the
// graph overlap, so the legacy passage support slab can be disabled without losing the // graph overlap, so the legacy passage support slab can be disabled without losing the
// floor to a disturbance refill. // floor to a disturbance refill.
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingFloor);
StrateManager->ApplyPassageNativeFloorMC( StrateManager->ApplyPassageNativeFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity); Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
}
StructuralTailTimer.End();
DensityStructuralPostsTimer.End(); DensityStructuralPostsTimer.End();
} }
else else
@@ -2434,7 +2493,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// below and are deliberately still the user override path. // below and are deliberately still the user override path.
VoxelDensityProfile::FScopedTimer DensityBoundarySealTimer( VoxelDensityProfile::FScopedTimer DensityBoundarySealTimer(
VoxelDensityProfile::EBucket::DensityBoundarySeal); VoxelDensityProfile::EBucket::DensityBoundarySeal);
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::XYEdgeSealOp);
VF_ApplyXYEdgeSealMC(Result, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, 8.0f); VF_ApplyXYEdgeSealMC(Result, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, 8.0f);
}
DensityBoundarySealTimer.End(); DensityBoundarySealTimer.End();
//========================================================================= //=========================================================================
@@ -2879,6 +2942,9 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
: VoxelDensityProfile::ECounter::FusedTunnelDetailSkipped); : VoxelDensityProfile::ECounter::FusedTunnelDetailSkipped);
} }
VoxelDensityProfile::FScopedTimer FusedDetailTimer(
VoxelDensityProfile::EBucket::FusedDetail);
//========================================================================= //=========================================================================
// STEP 4b: SURFACE ROUGHNESS (volumetric, SDF-based) // STEP 4b: SURFACE ROUGHNESS (volumetric, SDF-based)
//========================================================================= //=========================================================================
@@ -3502,6 +3568,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
} }
} // end bNearCaveSurface (terrain ops) } // end bNearCaveSurface (terrain ops)
FusedDetailTimer.End();
// Steps 4e / 4f (pits and chimneys) are now handled by the CaveSDF SmoothMin // Steps 4e / 4f (pits and chimneys) are now handled by the CaveSDF SmoothMin
// block inserted above (between EvaluateSDFCached and the CarveFactor section). // block inserted above (between EvaluateSDFCached and the CarveFactor section).
@@ -3524,6 +3591,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
// already computed by Step 4 — free): full strength at the network, smooth fade to zero // already computed by Step 4 — free): full strength at the network, smooth fade to zero
// at Range. Worms become braids/shortcuts hugging the cave system; no isolated speckle. // at Range. Worms become braids/shortcuts hugging the cave system; no isolated speckle.
// Range = 0 → unmasked legacy behaviour. Bonus: fully-masked voxels skip both Perlins. // Range = 0 → unmasked legacy behaviour. Bonus: fully-masked voxels skip both Perlins.
VoxelDensityProfile::FScopedTimer FusedWormTimer(
VoxelDensityProfile::EBucket::FusedWorm);
if (Params.WormStrength > 0.0f && Params.WormThreshold > 0.0f) if (Params.WormStrength > 0.0f && Params.WormThreshold > 0.0f)
{ {
float NetworkMask = 1.0f; float NetworkMask = 1.0f;
@@ -3570,16 +3639,28 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
} }
} }
FusedWormTimer.End();
//========================================================================= //=========================================================================
// STEP 6: STRATE BOUNDARY SEAL (haut + bas) // STEP 6: STRATE BOUNDARY SEAL (haut + bas)
//========================================================================= //=========================================================================
VoxelDensityProfile::FScopedTimer FusedStructuralTimer(
VoxelDensityProfile::EBucket::FusedStructural);
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::OriginSpineOp);
ApplyOriginSpine(Density, WorldX, WorldY, WorldZ, ApplyOriginSpine(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ, Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
}
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::BoundarySealOp);
ApplyBoundarySeal(Density, WorldZ, ApplyBoundarySeal(Density, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ, Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity); Params.BoundarySealThickness, Params.BaseDensity);
}
//========================================================================= //=========================================================================
// STEP 7: INTER-STRATE PASSAGES (perce le seal) // STEP 7: INTER-STRATE PASSAGES (perce le seal)
@@ -3588,6 +3669,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
{ {
if (bApplyLegacyStructuralPosts) if (bApplyLegacyStructuralPosts)
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageModifier);
StrateManager->ApplyPassageModifier( StrateManager->ApplyPassageModifier(
Density, WorldX, WorldY, WorldZ, Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness); Params.BaseDensity, Params.BoundarySealThickness);
@@ -3597,6 +3680,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
// GetDensityAt owns one common MC-space structural tail after either the legacy or // GetDensityAt owns one common MC-space structural tail after either the legacy or
// operator-stack branch. Keep the actual tube carve here, but do not repeat its // operator-stack branch. Keep the actual tube carve here, but do not repeat its
// landing air/floor/tunnel-air posts before the shared disturbance/post sequence. // landing air/floor/tunnel-air posts before the shared disturbance/post sequence.
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageCarveOp);
StrateManager->ApplyPassageCarvingOnly( StrateManager->ApplyPassageCarvingOnly(
Density, WorldX, WorldY, WorldZ, Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness); Params.BaseDensity, Params.BoundarySealThickness);
@@ -3646,6 +3731,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity); WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
} }
FusedStructuralTimer.End();
// Convention MC: négatif = solide, positif = air. // Convention MC: négatif = solide, positif = air.
// La logique interne utilise positif = solide (plus lisible), donc on négate. // La logique interne utilise positif = solide (plus lisible), donc on négate.
return -Density; return -Density;
@@ -300,8 +300,96 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
VoxelDensityProfile::AddCounter( VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherBlockTests); VoxelDensityProfile::ECounter::MesherBlockTests);
} }
const bool bAttributeClassifier =
VoxelDensityProfile::GetMode()
== VoxelDensityProfile::EMode::Attribution;
const bool bSampleClassifier = bAttributeClassifier
&& VoxelDensityProfile::ShouldSample(
VoxelDensityProfile::EBucket::ClassifierTotal);
const uint64 ClassifierStartCycles = bSampleClassifier
? FPlatformTime::Cycles64() : 0;
FVoxelTileClassificationStats ClassifierStats;
if (bAttributeClassifier)
{
VoxelDensityProfile::RecordClassifierCall(bSampleClassifier);
}
const EVoxelTileClass Verdict = Generator->ClassifyTile( const EVoxelTileClass Verdict = Generator->ClassifyTile(
BlockOrigin, Step, BlockCells); BlockOrigin, Step, BlockCells,
bSampleClassifier ? &ClassifierStats : nullptr);
if (bSampleClassifier)
{
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::ClassifierTotal,
FPlatformTime::Cycles64() - ClassifierStartCycles);
VoxelDensityProfile::FSnapshot::FClassifierStats ProfileStats;
ProfileStats.RefineNodes = ClassifierStats.RefineNodes;
ProfileStats.StackBoxCalls = ClassifierStats.StackBoxCalls;
ProfileStats.WholeMixedNodes = ClassifierStats.WholeMixedNodes;
ProfileStats.WholeSolidNodes = ClassifierStats.WholeSolidNodes;
ProfileStats.WholeAirNodes = ClassifierStats.WholeAirNodes;
ProfileStats.NeedsFinalFieldNodes = ClassifierStats.NeedsFinalFieldNodes;
ProfileStats.SplitNodes = ClassifierStats.SplitNodes;
ProfileStats.MaxRefinementDepth = ClassifierStats.MaxRefinementDepth;
ProfileStats.ExactCoreSamples = ClassifierStats.ExactCoreSamples;
ProfileStats.ExactFinalSamples = ClassifierStats.ExactFinalSamples;
ProfileStats.ExactCoreCacheHits = ClassifierStats.ExactCoreCacheHits;
ProfileStats.ExactFinalCacheHits = ClassifierStats.ExactFinalCacheHits;
ProfileStats.ExactCoreLeaves = ClassifierStats.ExactCoreLeaves;
ProfileStats.ExactFinalLeaves = ClassifierStats.ExactFinalLeaves;
ProfileStats.StackBoxCycles = ClassifierStats.StackBoxCycles;
ProfileStats.ExactCoreCycles = ClassifierStats.ExactCoreCycles;
ProfileStats.ExactFinalCycles = ClassifierStats.ExactFinalCycles;
ProfileStats.RoomTailQueries = ClassifierStats.RoomTailQueries;
ProfileStats.RoomTailEvaluated = ClassifierStats.RoomTailEvaluated;
ProfileStats.RoomTailCycles = ClassifierStats.RoomTailCycles;
ProfileStats.RoomPropagateCycles = ClassifierStats.RoomPropagateCycles;
ProfileStats.RoomExactPrimitiveCycles = ClassifierStats.RoomExactPrimitiveCycles;
ProfileStats.RoomCacheWindowCycles = ClassifierStats.RoomCacheWindowCycles;
ProfileStats.RoomNumRooms = FMath::Max(ClassifierStats.RoomNumRooms, 0);
ProfileStats.RoomNumTunnels = FMath::Max(ClassifierStats.RoomNumTunnels, 0);
ProfileStats.RoomNumRoomFloorJoins = FMath::Max(
ClassifierStats.RoomNumRoomFloorJoins, 0);
ProfileStats.RoomNumPits = FMath::Max(ClassifierStats.RoomNumPits, 0);
ProfileStats.RoomNumChimneys = FMath::Max(ClassifierStats.RoomNumChimneys, 0);
VoxelDensityProfile::AddClassifierStats(ProfileStats);
if (ClassifierStats.StackBoxCalls > 0)
{
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::ClassifierIntervalProof,
0, ClassifierStats.StackBoxCalls);
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::ClassifierIntervalProof,
ClassifierStats.StackBoxCycles);
}
if (ClassifierStats.ExactCoreLeaves > 0)
{
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::ClassifierExactCore,
0, ClassifierStats.ExactCoreLeaves);
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::ClassifierExactCore,
ClassifierStats.ExactCoreCycles);
}
if (ClassifierStats.ExactFinalLeaves > 0)
{
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::ClassifierExactFinal,
0, ClassifierStats.ExactFinalLeaves);
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::ClassifierExactFinal,
ClassifierStats.ExactFinalCycles);
}
if (ClassifierStats.RoomTailQueries > 0)
{
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::ClassifierRoomTail,
0, ClassifierStats.RoomTailQueries);
VoxelDensityProfile::AddSampledMeasurement(
VoxelDensityProfile::EBucket::ClassifierRoomTail,
ClassifierStats.RoomTailCycles);
}
}
if (Verdict == EVoxelTileClass::Mixed) if (Verdict == EVoxelTileClass::Mixed)
{ {
MixedBlockIndices.Add( MixedBlockIndices.Add(
@@ -490,6 +578,21 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
} }
} }
} }
if (VoxelDensityProfile::AreCountersEnabled())
{
const uint64 GridSampleCount = static_cast<uint64>(GzHi - GzLo + 1)
* static_cast<uint64>(MDim)
* static_cast<uint64>(MDim);
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherDensityGridBytes,
GridSampleCount * sizeof(float));
if (bUseSharedDensityGrid)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherSharedGridReadBytes,
GridSampleCount * sizeof(float));
}
}
if (bUseOperatorBlock) if (bUseOperatorBlock)
{ {
Generator->EndDensityBlock(); Generator->EndDensityBlock();
@@ -835,6 +938,26 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
MeshData.Triangles.Append(CapTris); MeshData.Triangles.Append(CapTris);
MeshData.NumCeilingTriangles = CapTris.Num() / 3; MeshData.NumCeilingTriangles = CapTris.Num() / 3;
if (VoxelDensityProfile::AreCountersEnabled())
{
const uint64 OutputArrayBytes = static_cast<uint64>(MeshData.Vertices.Num()) * sizeof(FVector)
+ static_cast<uint64>(MeshData.Normals.Num()) * sizeof(FVector)
+ static_cast<uint64>(MeshData.UVs.Num()) * sizeof(FVector2D)
+ static_cast<uint64>(MeshData.Colors.Num()) * sizeof(FColor)
+ static_cast<uint64>(MeshData.Triangles.Num()) * sizeof(int32);
const uint64 OutputAllocatedBytes = static_cast<uint64>(MeshData.Vertices.GetAllocatedSize())
+ static_cast<uint64>(MeshData.Normals.GetAllocatedSize())
+ static_cast<uint64>(MeshData.UVs.GetAllocatedSize())
+ static_cast<uint64>(MeshData.Colors.GetAllocatedSize())
+ static_cast<uint64>(MeshData.Triangles.GetAllocatedSize());
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherOutputArrayBytes,
OutputArrayBytes);
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherOutputAllocatedBytes,
OutputAllocatedBytes);
}
return MeshData; return MeshData;
} }
+27 -3
View File
@@ -68,6 +68,11 @@ namespace
TEXT("voxel.ProfileDensityFull"), TEXT("voxel.ProfileDensityFull"),
GVoxelForgeProfileDensityFull, GVoxelForgeProfileDensityFull,
TEXT("Collect full per-operator profiling for the game path.")); TEXT("Collect full per-operator profiling for the game path."));
int32 GVoxelForgePerfAttribution = 0;
FAutoConsoleVariableRef CVarVoxelForgePerfAttribution(
TEXT("voxel.PerfAttribution"),
GVoxelForgePerfAttribution,
TEXT("Collect low-overhead component attribution for the game path."));
// Startup command-line cvars can arrive before the game world has spawned AVoxelWorld (or // Startup command-line cvars can arrive before the game world has spawned AVoxelWorld (or
// before its first tiles have become visible). Keep the diagnostic edit pending until Tick has // before its first tiles have become visible). Keep the diagnostic edit pending until Tick has
@@ -1038,6 +1043,7 @@ void AVoxelWorld::BeginPlay()
// the default mode because of startup ordering. // the default mode because of startup ordering.
int32 CommandLineProfile = GVoxelForgeProfileDensity; int32 CommandLineProfile = GVoxelForgeProfileDensity;
int32 CommandLineProfileFull = GVoxelForgeProfileDensityFull; int32 CommandLineProfileFull = GVoxelForgeProfileDensityFull;
int32 CommandLinePerfAttribution = GVoxelForgePerfAttribution;
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ProfileDensity="), CommandLineProfile)) if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ProfileDensity="), CommandLineProfile))
{ {
GVoxelForgeProfileDensity = CommandLineProfile; GVoxelForgeProfileDensity = CommandLineProfile;
@@ -1046,8 +1052,21 @@ void AVoxelWorld::BeginPlay()
{ {
GVoxelForgeProfileDensityFull = CommandLineProfileFull; GVoxelForgeProfileDensityFull = CommandLineProfileFull;
} }
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.PerfAttribution="), CommandLinePerfAttribution))
{
GVoxelForgePerfAttribution = CommandLinePerfAttribution;
}
if (GVoxelForgeProfileDensityFull != 0) if (GVoxelForgePerfAttribution != 0)
{
VoxelDensityProfile::SetMode(
VoxelDensityProfile::EMode::Attribution,
VoxelDensityProfile::DefaultSampleInterval);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeDensityProfile] game_mode=attribution sample_interval=%u"),
VoxelDensityProfile::DefaultSampleInterval);
}
else if (GVoxelForgeProfileDensityFull != 0)
{ {
VoxelDensityProfile::SetMode( VoxelDensityProfile::SetMode(
VoxelDensityProfile::EMode::Full, VoxelDensityProfile::EMode::Full,
@@ -2084,7 +2103,8 @@ void AVoxelWorld::LogStreamingLatencySummary() const
const double TravelP95Metres = RequestP95 const double TravelP95Metres = RequestP95
* static_cast<double>(PeakObservedPawnSpeedCmPerSecond) / 100.0; * static_cast<double>(PeakObservedPawnSpeedCmPerSecond) / 100.0;
const bool bDiagnosticsEnabled = GVoxelForgeProfileTileGeneration != 0 const bool bDiagnosticsEnabled = GVoxelForgeProfileTileGeneration != 0
|| VoxelForgeStartupTrace::IsActive(); || VoxelForgeStartupTrace::IsActive()
|| VoxelDensityProfile::GetMode() != VoxelDensityProfile::EMode::Disabled;
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStreamingLatency] mode=%s diagnostics=%s lod0_ready_samples=%d " TEXT("[VoxelForgeStreamingLatency] mode=%s diagnostics=%s lod0_ready_samples=%d "
"request_to_ready_s[p50=%.6f p95=%.6f max=%.6f] " "request_to_ready_s[p50=%.6f p95=%.6f max=%.6f] "
@@ -3651,7 +3671,9 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
const std::atomic<bool>* ObsoleteFlag) const std::atomic<bool>* ObsoleteFlag)
{ {
const bool bLogTileProfile = GVoxelForgeProfileTileGeneration != 0; const bool bLogTileProfile = GVoxelForgeProfileTileGeneration != 0;
const bool bMeasureTile = bLogTileProfile || VoxelForgeStartupTrace::IsActive(); const bool bMeasureTile = bLogTileProfile
|| VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Attribution
|| VoxelForgeStartupTrace::IsActive();
const double TileStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0; const double TileStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0;
double ClassifySeconds = 0.0; double ClassifySeconds = 0.0;
double MeshSeconds = 0.0; double MeshSeconds = 0.0;
@@ -3919,6 +3941,8 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_BuildStreams); TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_BuildStreams);
const double StreamStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0; const double StreamStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0;
Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>(); Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>();
VoxelDensityProfile::FScopedTimer RuntimeStreamTimer(
VoxelDensityProfile::EBucket::RuntimeStreamBuilding);
BuildTileStreamSet(*Result.Streams, MeshData); BuildTileStreamSet(*Result.Streams, MeshData);
StreamSeconds = bMeasureTile ? FPlatformTime::Seconds() - StreamStartSeconds : 0.0; StreamSeconds = bMeasureTile ? FPlatformTime::Seconds() - StreamStartSeconds : 0.0;
if (ShouldAbortWork(ObsoleteFlag)) if (ShouldAbortWork(ObsoleteFlag))
+125 -5
View File
@@ -15,12 +15,15 @@ namespace VoxelDensityProfile
{ {
Disabled, Disabled,
Sampled, Sampled,
// Low-overhead attribution: keeps the parent and stage ledger trustworthy, samples
// hot component scopes, and collects classifier detail without the old full-scope storm.
Attribution,
Full, Full,
}; };
// Count-based sampling is deterministic with respect to the profiler's own counters, but the // 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. // report is intentionally diagnostic only. It never participates in density or mesh output.
constexpr uint32 DefaultSampleInterval = 64; constexpr uint32 DefaultSampleInterval = 128;
enum class EBucket : uint8 enum class EBucket : uint8
{ {
@@ -56,6 +59,23 @@ namespace VoxelDensityProfile
PassageCarveOp, PassageCarveOp,
XYEdgeSealOp, XYEdgeSealOp,
OtherOp, OtherOp,
// Attribution components. These are nested diagnostics; the non-overlapping phase
// buckets below remain the density accounting ledger.
FusedEvaluator,
InterpretedOpStack,
OperatorBlock,
RoomGraphBuild,
RoomGraphSdf,
TunnelCoreWorld,
FusedDetail,
FusedWorm,
FusedStructural,
ClassifierTotal,
ClassifierIntervalProof,
ClassifierExactCore,
ClassifierExactFinal,
ClassifierRoomTail,
RuntimeStreamBuilding,
// Non-overlapping top-level phases. The named operation buckets above are intentionally // Non-overlapping top-level phases. The named operation buckets above are intentionally
// nested diagnostics; these phase buckets are the accounting ledger that must add up to // nested diagnostics; these phase buckets are the accounting ledger that must add up to
// GetDensityAt without double-counting a stack op inside its enclosing phase. // GetDensityAt without double-counting a stack op inside its enclosing phase.
@@ -128,11 +148,24 @@ namespace VoxelDensityProfile
OpBlockOperators, OpBlockOperators,
OpBlockActiveOperators, OpBlockActiveOperators,
OpBlockPrunedOperators, OpBlockPrunedOperators,
MesherDensityGridBytes,
MesherSharedGridReadBytes,
MesherOutputArrayBytes,
MesherOutputAllocatedBytes,
OpBlockScratchBytes,
OpBlockCopyBytes,
Count Count
}; };
constexpr int32 CounterCount = static_cast<int32>(ECounter::Count); constexpr int32 CounterCount = static_cast<int32>(ECounter::Count);
// Export the one fast-path bit so callers in the editor module can skip instrumentation
// without crossing the DLL boundary for every scoped timer or counter. It is diagnostic
// state only; it never participates in density or mesh decisions.
VOXELFORGE_API extern std::atomic<bool> GEnabled;
VOXELFORGE_API extern std::atomic<EMode> GMode;
inline thread_local bool GSampledScopeActive = false;
// Resident allocator-backed bytes for one worker's generation cache family. The profiler // 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 // 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 // cache, not the inline size of the enclosing C++ object. SlotStorageBytes is the one
@@ -193,6 +226,44 @@ namespace VoxelDensityProfile
uint32 ActiveWorkers[BucketCount]{}; uint32 ActiveWorkers[BucketCount]{};
uint64 Counters[CounterCount]{}; uint64 Counters[CounterCount]{};
// Classifier detail is sampled separately from the timer buckets. Calls is the exact
// number of classifier invocations; the remaining fields are worker-local samples
// expanded at Snapshot() using Calls / SampledCalls.
struct FClassifierStats
{
uint64 Calls = 0;
uint64 SampledCalls = 0;
uint64 RefineNodes = 0;
uint64 StackBoxCalls = 0;
uint64 WholeMixedNodes = 0;
uint64 WholeSolidNodes = 0;
uint64 WholeAirNodes = 0;
uint64 NeedsFinalFieldNodes = 0;
uint64 SplitNodes = 0;
uint64 MaxRefinementDepth = 0;
uint64 ExactCoreSamples = 0;
uint64 ExactFinalSamples = 0;
uint64 ExactCoreCacheHits = 0;
uint64 ExactFinalCacheHits = 0;
uint64 ExactCoreLeaves = 0;
uint64 ExactFinalLeaves = 0;
uint64 StackBoxCycles = 0;
uint64 ExactCoreCycles = 0;
uint64 ExactFinalCycles = 0;
uint64 RoomTailQueries = 0;
uint64 RoomTailEvaluated = 0;
uint64 RoomTailCycles = 0;
uint64 RoomPropagateCycles = 0;
uint64 RoomExactPrimitiveCycles = 0;
uint64 RoomCacheWindowCycles = 0;
uint64 RoomNumRooms = 0;
uint64 RoomNumTunnels = 0;
uint64 RoomNumRoomFloorJoins = 0;
uint64 RoomNumPits = 0;
uint64 RoomNumChimneys = 0;
};
FClassifierStats Classifier;
uint64 TimerPairCycles = 0; uint64 TimerPairCycles = 0;
uint32 SampleInterval = DefaultSampleInterval; uint32 SampleInterval = DefaultSampleInterval;
EMode Mode = EMode::Disabled; EMode Mode = EMode::Disabled;
@@ -225,9 +296,47 @@ namespace VoxelDensityProfile
VOXELFORGE_API void SetEnabled(bool bEnabled); VOXELFORGE_API void SetEnabled(bool bEnabled);
VOXELFORGE_API void SetMode(EMode Mode, uint32 SampleInterval = DefaultSampleInterval); VOXELFORGE_API void SetMode(EMode Mode, uint32 SampleInterval = DefaultSampleInterval);
// Coarse counters are intentionally always collected, even when cycle timing is disabled. // Counters are collected only while a diagnostic mode is enabled. In the ordinary clean
// They are the low-cost diagnostic path; only cycle timing is opt-in. // path this is a single relaxed load, so the measurement hooks stay dormant.
FORCEINLINE bool AreCountersEnabled() { return true; } FORCEINLINE bool IsEnabledFast()
{
return GEnabled.load(std::memory_order_relaxed);
}
FORCEINLINE bool AreCountersEnabled() { return IsEnabledFast(); }
FORCEINLINE bool ShouldProbeScopeFast(EBucket Bucket)
{
const EMode Mode = GMode.load(std::memory_order_relaxed);
if (Mode == EMode::Full)
{
return true;
}
if (Mode == EMode::Sampled)
{
return Bucket == EBucket::GetDensityAt
|| Bucket == EBucket::MesherGenerateMesh
|| Bucket == EBucket::MesherDensityGrid
|| Bucket == EBucket::MesherOther;
}
if (Mode == EMode::Attribution)
{
switch (Bucket)
{
case EBucket::GetDensityAt:
case EBucket::MesherGenerateMesh:
case EBucket::MesherDensityGrid:
case EBucket::MesherCellClassification:
case EBucket::MesherGradientNormals:
case EBucket::MesherVertexInterpolation:
case EBucket::MesherStreamBuilding:
case EBucket::MesherOther:
case EBucket::RoomGraphBuild:
return true;
default:
return GSampledScopeActive;
}
}
return false;
}
VOXELFORGE_API bool IsEnabled(); VOXELFORGE_API bool IsEnabled();
VOXELFORGE_API bool IsCycleTimingEnabled(); VOXELFORGE_API bool IsCycleTimingEnabled();
VOXELFORGE_API EMode GetMode(); VOXELFORGE_API EMode GetMode();
@@ -242,6 +351,8 @@ namespace VoxelDensityProfile
VOXELFORGE_API void AddCounter(ECounter Counter, uint64 Amount = 1); VOXELFORGE_API void AddCounter(ECounter Counter, uint64 Amount = 1);
VOXELFORGE_API void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls = 1); VOXELFORGE_API void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls = 1);
VOXELFORGE_API void RecordCall(EBucket Bucket); VOXELFORGE_API void RecordCall(EBucket Bucket);
VOXELFORGE_API void RecordClassifierCall(bool bSampled);
VOXELFORGE_API void AddClassifierStats(const FSnapshot::FClassifierStats& Stats);
VOXELFORGE_API bool ShouldSample(EBucket Bucket); VOXELFORGE_API bool ShouldSample(EBucket Bucket);
VOXELFORGE_API void AddSampledMeasurement(EBucket Bucket, uint64 RawCycles); VOXELFORGE_API void AddSampledMeasurement(EBucket Bucket, uint64 RawCycles);
@@ -279,16 +390,25 @@ namespace VoxelDensityProfile
public: public:
explicit FScopedTimer(EBucket InBucket) explicit FScopedTimer(EBucket InBucket)
: Bucket(InBucket) : Bucket(InBucket)
, Token(BeginScope(InBucket))
{ {
if (IsEnabledFast() && ShouldProbeScopeFast(InBucket))
{
Token = BeginScope(InBucket);
}
} }
explicit FScopedTimer(const TCHAR* InName) explicit FScopedTimer(const TCHAR* InName)
: Bucket(EBucket::OtherOp) : Bucket(EBucket::OtherOp)
{
if (IsEnabledFast())
{ {
Bucket = BucketFromName(InName); Bucket = BucketFromName(InName);
if (ShouldProbeScopeFast(Bucket))
{
Token = BeginScope(Bucket); Token = BeginScope(Bucket);
} }
}
}
~FScopedTimer(); ~FScopedTimer();
@@ -266,6 +266,7 @@ struct FExploreArguments
bool bUseOperatorStack = true; bool bUseOperatorStack = true;
bool bProfileDensity = false; bool bProfileDensity = false;
bool bProfileDensityFull = false; bool bProfileDensityFull = false;
bool bPerfAttribution = false;
bool bProfileLod = false; bool bProfileLod = false;
bool bOpBounds = false; bool bOpBounds = false;
bool bFailureFocusRender = false; bool bFailureFocusRender = false;
@@ -313,6 +314,11 @@ struct FExploreArguments
bool bRenderCameraOverride = false; bool bRenderCameraOverride = false;
FVector RenderCamera = FVector::ZeroVector; FVector RenderCamera = FVector::ZeroVector;
bool IsDensityProfilingEnabled() const
{
return bProfileDensity || bPerfAttribution;
}
FString CanonicalModes() const FString CanonicalModes() const
{ {
FString Result; FString Result;
@@ -358,6 +364,7 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
OutArguments.bProfileDensity = FParse::Param(*Params, TEXT("profiledensity")); OutArguments.bProfileDensity = FParse::Param(*Params, TEXT("profiledensity"));
OutArguments.bProfileDensityFull = FParse::Param(*Params, TEXT("profiledensityfull")); OutArguments.bProfileDensityFull = FParse::Param(*Params, TEXT("profiledensityfull"));
OutArguments.bProfileDensity |= OutArguments.bProfileDensityFull; OutArguments.bProfileDensity |= OutArguments.bProfileDensityFull;
OutArguments.bPerfAttribution = FParse::Param(*Params, TEXT("perfattribution"));
OutArguments.bProfileLod = FParse::Param(*Params, TEXT("profilelod")); OutArguments.bProfileLod = FParse::Param(*Params, TEXT("profilelod"));
OutArguments.bOpBounds = FParse::Param(*Params, TEXT("opbounds")); OutArguments.bOpBounds = FParse::Param(*Params, TEXT("opbounds"));
FParse::Value(*Params, TEXT("out="), OutText); FParse::Value(*Params, TEXT("out="), OutText);
@@ -741,6 +748,10 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
VoxelDensityProfile::EMode ExploreProfileMode(const FExploreArguments& Arguments) VoxelDensityProfile::EMode ExploreProfileMode(const FExploreArguments& Arguments)
{ {
if (Arguments.bPerfAttribution)
{
return VoxelDensityProfile::EMode::Attribution;
}
return Arguments.bProfileDensityFull return Arguments.bProfileDensityFull
? VoxelDensityProfile::EMode::Full ? VoxelDensityProfile::EMode::Full
: VoxelDensityProfile::EMode::Sampled; : VoxelDensityProfile::EMode::Sampled;
@@ -850,6 +861,14 @@ struct FExploreWorld
int32 ExploreMeshTileCount = 0; int32 ExploreMeshTileCount = 0;
int32 ExploreMeshTilesCompleted = 0; int32 ExploreMeshTilesCompleted = 0;
double ExploreMeshSeconds = 0.0; double ExploreMeshSeconds = 0.0;
// Exact wall-clock partition of ExploreMeshSeconds. The first three fields end at the
// parallel tile launch; merge closes the mesh clock. Remainder is intentionally retained
// instead of rounding the ledger to 100%.
double ExploreMeshPreparationSeconds = 0.0;
double ExploreMeshPreTileSetupSeconds = 0.0;
double ExploreMeshMergeSeconds = 0.0;
double ExploreMeshAccountingRemainderSeconds = 0.0;
double ExploreMeshHashSeconds = 0.0;
double ExploreDensityGridSeconds = 0.0; double ExploreDensityGridSeconds = 0.0;
int64 ExploreDensityGridTotalSamples = 0; int64 ExploreDensityGridTotalSamples = 0;
int64 ExploreDensityGridUniqueSamples = 0; int64 ExploreDensityGridUniqueSamples = 0;
@@ -1235,6 +1254,11 @@ struct FExploreExportOutput
FExploreMeshMetrics SurfaceMetrics; FExploreMeshMetrics SurfaceMetrics;
double MeshSeconds = 0.0; double MeshSeconds = 0.0;
double MeshUsPerVoxel = 0.0; double MeshUsPerVoxel = 0.0;
double MeasureSeconds = 0.0;
double ObjWriteSeconds = 0.0;
double ManifestSeconds = 0.0;
double ExportWallSeconds = 0.0;
double ExportAccountingRemainderSeconds = 0.0;
FString GeometryHash; FString GeometryHash;
bool bTruncated = false; bool bTruncated = false;
}; };
@@ -1634,6 +1658,11 @@ bool EnsureExploreMesh(
FMath::FloorToInt(0.5f * static_cast<float>( FMath::FloorToInt(0.5f * static_cast<float>(
World.TargetBottomWorldZ + World.TargetTopWorldZ))); World.TargetBottomWorldZ + World.TargetTopWorldZ)));
World.ExploreMeshSeconds = 0.0; World.ExploreMeshSeconds = 0.0;
World.ExploreMeshPreparationSeconds = 0.0;
World.ExploreMeshPreTileSetupSeconds = 0.0;
World.ExploreMeshMergeSeconds = 0.0;
World.ExploreMeshAccountingRemainderSeconds = 0.0;
World.ExploreMeshHashSeconds = 0.0;
World.ExploreDensityGridSeconds = 0.0; World.ExploreDensityGridSeconds = 0.0;
World.ExploreDensityGridTotalSamples = 0; World.ExploreDensityGridTotalSamples = 0;
World.ExploreDensityGridUniqueSamples = 0; World.ExploreDensityGridUniqueSamples = 0;
@@ -1694,6 +1723,7 @@ bool EnsureExploreMesh(
World.ExploreDensityGridDuplicateSamplesAfter = World.ExploreDensityGridDuplicateSamplesAfter =
Arguments.bReuseDensityGrid ? 0 : World.ExploreDensityGridDuplicateSamplesBefore; Arguments.bReuseDensityGrid ? 0 : World.ExploreDensityGridDuplicateSamplesBefore;
const double DensityStageStartSeconds = FPlatformTime::Seconds();
if (Arguments.bReuseDensityGrid) if (Arguments.bReuseDensityGrid)
{ {
const double DensityGridStartSeconds = FPlatformTime::Seconds(); const double DensityGridStartSeconds = FPlatformTime::Seconds();
@@ -1780,6 +1810,8 @@ bool EnsureExploreMesh(
World.Mesher->SetSharedDensityGrid(&World.ExploreSharedDensityGrid); World.Mesher->SetSharedDensityGrid(&World.ExploreSharedDensityGrid);
World.bExploreDensityGridReused = true; World.bExploreDensityGridReused = true;
} }
const double DensityStageEndSeconds = FPlatformTime::Seconds();
World.ExploreMeshPreparationSeconds = DensityStageStartSeconds - MeshStartSeconds;
// Generate each tile through the canonical mesher on worker threads, then append strictly in // Generate each tile through the canonical mesher on worker threads, then append strictly in
// Z/Y/X order. The mesher's scratch buffers and the generator's hot caches are thread-local; // Z/Y/X order. The mesher's scratch buffers and the generator's hot caches are thread-local;
@@ -1802,6 +1834,7 @@ bool EnsureExploreMesh(
std::atomic<bool> bTileWorkCancelled(false); std::atomic<bool> bTileWorkCancelled(false);
const double TileWorkDeadline = Budget.StartSeconds + Budget.LimitSeconds - 0.25; const double TileWorkDeadline = Budget.StartSeconds + Budget.LimitSeconds - 0.25;
const double ParallelStartSeconds = FPlatformTime::Seconds(); const double ParallelStartSeconds = FPlatformTime::Seconds();
World.ExploreMeshPreTileSetupSeconds = ParallelStartSeconds - DensityStageEndSeconds;
ParallelFor( ParallelFor(
TEXT("VoxelForgeExploreMeshTiles"), TEXT("VoxelForgeExploreMeshTiles"),
World.ExploreMeshTileCount, World.ExploreMeshTileCount,
@@ -1925,9 +1958,20 @@ bool EnsureExploreMesh(
return false; return false;
} }
World.ExploreMeshSeconds = FPlatformTime::Seconds() - MeshStartSeconds; const double MeshEndSeconds = FPlatformTime::Seconds();
World.ExploreMeshSeconds = MeshEndSeconds - MeshStartSeconds;
World.ExploreMeshMergeSeconds = MeshEndSeconds - ParallelEndSeconds;
const double MeshStageSumSeconds = World.ExploreMeshPreparationSeconds
+ World.ExploreDensityGridSeconds
+ World.ExploreMeshPreTileSetupSeconds
+ World.ExploreMeshTaskWallSeconds
+ World.ExploreMeshMergeSeconds;
World.ExploreMeshAccountingRemainderSeconds =
World.ExploreMeshSeconds - MeshStageSumSeconds;
World.bExploreMeshComplete = true; World.bExploreMeshComplete = true;
const double GeometryHashStartSeconds = FPlatformTime::Seconds();
World.ExploreGeometryHash = ComputeGeometryHash(World.ExploreMesh); World.ExploreGeometryHash = ComputeGeometryHash(World.ExploreMesh);
World.ExploreMeshHashSeconds = FPlatformTime::Seconds() - GeometryHashStartSeconds;
OutError.Reset(); OutError.Reset();
return true; return true;
} }
@@ -4021,7 +4065,7 @@ FString BuildManifestJson(
TSharedRef<FExploreJsonWriter> Writer = TSharedRef<FExploreJsonWriter> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json); TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
Writer->WriteObjectStart(); Writer->WriteObjectStart();
Writer->WriteValue(TEXT("schema_version"), 6); Writer->WriteValue(TEXT("schema_version"), 7);
Writer->WriteValue(TEXT("format"), TEXT("OBJ")); Writer->WriteValue(TEXT("format"), TEXT("OBJ"));
Writer->WriteValue(TEXT("mesh_file"), Export.MeshFileName); Writer->WriteValue(TEXT("mesh_file"), Export.MeshFileName);
Writer->WriteValue(TEXT("mesh_positions_units"), TEXT("metres")); Writer->WriteValue(TEXT("mesh_positions_units"), TEXT("metres"));
@@ -4126,6 +4170,7 @@ bool RunExport(
FExploreBudget& Budget) FExploreBudget& Budget)
{ {
OutOutput = FExploreExportOutput(); OutOutput = FExploreExportOutput();
const double ExportStartSeconds = FPlatformTime::Seconds();
OutOutput.RegionSize = Arguments.ExportSize; OutOutput.RegionSize = Arguments.ExportSize;
OutOutput.EstimatedWorkingBytes = EstimateExportWorkingBytes(Arguments.ExportSize); OutOutput.EstimatedWorkingBytes = EstimateExportWorkingBytes(Arguments.ExportSize);
if (OutOutput.EstimatedWorkingBytes > OutOutput.WorkingMemoryCapBytes) if (OutOutput.EstimatedWorkingBytes > OutOutput.WorkingMemoryCapBytes)
@@ -4163,6 +4208,7 @@ bool RunExport(
OutOutput.RefusalReason = FString::Printf(TEXT("Could not create OBJ '%s'."), *MeshPath); OutOutput.RefusalReason = FString::Printf(TEXT("Could not create OBJ '%s'."), *MeshPath);
return false; return false;
} }
const double ObjWriteStartSeconds = FPlatformTime::Seconds();
if (!WriteObjHeader(*Archive)) if (!WriteObjHeader(*Archive))
{ {
OutOutput.Status = TEXT("error"); OutOutput.Status = TEXT("error");
@@ -4170,12 +4216,14 @@ bool RunExport(
return false; return false;
} }
const double MeasureStartSeconds = FPlatformTime::Seconds();
if (!MeasureExploreMesh(World.ExploreMesh, OutOutput.SurfaceMetrics, Error)) if (!MeasureExploreMesh(World.ExploreMesh, OutOutput.SurfaceMetrics, Error))
{ {
OutOutput.Status = TEXT("error"); OutOutput.Status = TEXT("error");
OutOutput.RefusalReason = Error; OutOutput.RefusalReason = Error;
return false; return false;
} }
OutOutput.MeasureSeconds = FPlatformTime::Seconds() - MeasureStartSeconds;
OutOutput.MeshArrayBytes = static_cast<int64>(World.ExploreMesh.Vertices.Num()) * sizeof(FVector) OutOutput.MeshArrayBytes = static_cast<int64>(World.ExploreMesh.Vertices.Num()) * sizeof(FVector)
+ static_cast<int64>(World.ExploreMesh.Normals.Num()) * sizeof(FVector) + static_cast<int64>(World.ExploreMesh.Normals.Num()) * sizeof(FVector)
+ static_cast<int64>(World.ExploreMesh.UVs.Num()) * sizeof(FVector2D) + static_cast<int64>(World.ExploreMesh.UVs.Num()) * sizeof(FVector2D)
@@ -4221,6 +4269,7 @@ bool RunExport(
OutOutput.RefusalReason = TEXT("OBJ was written but its file size could not be read."); OutOutput.RefusalReason = TEXT("OBJ was written but its file size could not be read.");
return false; return false;
} }
OutOutput.ObjWriteSeconds = FPlatformTime::Seconds() - ObjWriteStartSeconds;
OutOutput.bTruncated = Budget.bTruncated OutOutput.bTruncated = Budget.bTruncated
|| !World.bExploreMeshComplete || !World.bExploreMeshComplete
@@ -4235,6 +4284,7 @@ bool RunExport(
: TEXT("The wall-clock budget stopped export after writing a partial mesh."); : TEXT("The wall-clock budget stopped export after writing a partial mesh.");
} }
const double ManifestStartSeconds = FPlatformTime::Seconds();
const FString ManifestJson = BuildManifestJson(Arguments, OutOutput); const FString ManifestJson = BuildManifestJson(Arguments, OutOutput);
const FString ManifestJsonRepeat = BuildManifestJson(Arguments, OutOutput); const FString ManifestJsonRepeat = BuildManifestJson(Arguments, OutOutput);
if (ManifestJson.IsEmpty() || ManifestJsonRepeat.IsEmpty()) if (ManifestJson.IsEmpty() || ManifestJsonRepeat.IsEmpty())
@@ -4258,6 +4308,13 @@ bool RunExport(
OutOutput.RefusalReason = TEXT("Could not write export manifest.json."); OutOutput.RefusalReason = TEXT("Could not write export manifest.json.");
return false; return false;
} }
OutOutput.ManifestSeconds = FPlatformTime::Seconds() - ManifestStartSeconds;
OutOutput.ExportWallSeconds = FPlatformTime::Seconds() - ExportStartSeconds;
OutOutput.ExportAccountingRemainderSeconds = OutOutput.ExportWallSeconds
- (World.ExploreMeshSeconds
+ World.ExploreMeshHashSeconds
+ OutOutput.ObjWriteSeconds
+ OutOutput.ManifestSeconds);
if (!OutOutput.bTruncated) if (!OutOutput.bTruncated)
{ {
OutOutput.Status = TEXT("ok"); OutOutput.Status = TEXT("ok");
@@ -4348,6 +4405,7 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("mesh_min_batch_size"), Arguments.MeshMinBatchSize); Writer->WriteValue(TEXT("mesh_min_batch_size"), Arguments.MeshMinBatchSize);
Writer->WriteValue(TEXT("profile_density"), Arguments.bProfileDensity); Writer->WriteValue(TEXT("profile_density"), Arguments.bProfileDensity);
Writer->WriteValue(TEXT("profile_density_full"), Arguments.bProfileDensityFull); Writer->WriteValue(TEXT("profile_density_full"), Arguments.bProfileDensityFull);
Writer->WriteValue(TEXT("perf_attribution"), Arguments.bPerfAttribution);
Writer->WriteValue(TEXT("profile_lod"), Arguments.bProfileLod); Writer->WriteValue(TEXT("profile_lod"), Arguments.bProfileLod);
Writer->WriteValue(TEXT("op_bounds"), Arguments.bOpBounds); Writer->WriteValue(TEXT("op_bounds"), Arguments.bOpBounds);
Writer->WriteValue(TEXT("budget_minutes"), static_cast<double>(Arguments.BudgetMinutes)); Writer->WriteValue(TEXT("budget_minutes"), static_cast<double>(Arguments.BudgetMinutes));
@@ -4386,6 +4444,27 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("us_per_voxel"), SummaryUsPerVoxel); Writer->WriteValue(TEXT("us_per_voxel"), SummaryUsPerVoxel);
Writer->WriteValue(TEXT("triangle_count"), SummaryTriangleCount); Writer->WriteValue(TEXT("triangle_count"), SummaryTriangleCount);
Writer->WriteValue(TEXT("geometry_hash"), SummaryGeometryHash); Writer->WriteValue(TEXT("geometry_hash"), SummaryGeometryHash);
Writer->WriteObjectStart(TEXT("mesh_wall_ledger"));
Writer->WriteValue(TEXT("mesh_seconds"), World.ExploreMeshSeconds);
Writer->WriteValue(TEXT("preparation_seconds"), World.ExploreMeshPreparationSeconds);
Writer->WriteValue(TEXT("density_grid_seconds"), World.ExploreDensityGridSeconds);
Writer->WriteValue(TEXT("pre_tile_setup_seconds"), World.ExploreMeshPreTileSetupSeconds);
Writer->WriteValue(TEXT("tile_parallel_wall_seconds"), World.ExploreMeshTaskWallSeconds);
Writer->WriteValue(TEXT("tile_merge_seconds"), World.ExploreMeshMergeSeconds);
Writer->WriteValue(TEXT("accounting_remainder_seconds"),
World.ExploreMeshAccountingRemainderSeconds);
Writer->WriteValue(TEXT("stage_sum_seconds"),
World.ExploreMeshPreparationSeconds
+ World.ExploreDensityGridSeconds
+ World.ExploreMeshPreTileSetupSeconds
+ World.ExploreMeshTaskWallSeconds
+ World.ExploreMeshMergeSeconds);
Writer->WriteValue(TEXT("sum_matches_mesh"),
FMath::Abs(World.ExploreMeshAccountingRemainderSeconds) <= 0.000001);
Writer->WriteValue(TEXT("geometry_hash_seconds"), World.ExploreMeshHashSeconds);
Writer->WriteValue(TEXT("note"), TEXT(
"The stage rows are one additive wall-clock ledger. Profiler buckets below are nested worker diagnostics and are not added to this ledger."));
Writer->WriteObjectEnd();
Writer->WriteObjectStart(TEXT("density_grid_reuse")); Writer->WriteObjectStart(TEXT("density_grid_reuse"));
Writer->WriteValue(TEXT("enabled"), World.bExploreDensityGridReused); Writer->WriteValue(TEXT("enabled"), World.bExploreDensityGridReused);
Writer->WriteValue(TEXT("total_tile_grid_samples"), World.ExploreDensityGridTotalSamples); Writer->WriteValue(TEXT("total_tile_grid_samples"), World.ExploreDensityGridTotalSamples);
@@ -4752,6 +4831,13 @@ FString BuildExploreJson(
WriteSurfaceReliefMetrics(*Writer, Output.Export.SurfaceMetrics); WriteSurfaceReliefMetrics(*Writer, Output.Export.SurfaceMetrics);
Writer->WriteValue(TEXT("mesh_seconds"), Output.Export.MeshSeconds); Writer->WriteValue(TEXT("mesh_seconds"), Output.Export.MeshSeconds);
Writer->WriteValue(TEXT("mesh_us_per_voxel"), Output.Export.MeshUsPerVoxel); Writer->WriteValue(TEXT("mesh_us_per_voxel"), Output.Export.MeshUsPerVoxel);
Writer->WriteValue(TEXT("geometry_hash_seconds"), World.ExploreMeshHashSeconds);
Writer->WriteValue(TEXT("measure_seconds"), Output.Export.MeasureSeconds);
Writer->WriteValue(TEXT("obj_write_seconds"), Output.Export.ObjWriteSeconds);
Writer->WriteValue(TEXT("manifest_seconds"), Output.Export.ManifestSeconds);
Writer->WriteValue(TEXT("export_wall_seconds"), Output.Export.ExportWallSeconds);
Writer->WriteValue(TEXT("export_accounting_remainder_seconds"),
Output.Export.ExportAccountingRemainderSeconds);
Writer->WriteValue(TEXT("geometry_hash"), Output.Export.GeometryHash); Writer->WriteValue(TEXT("geometry_hash"), Output.Export.GeometryHash);
Writer->WriteValue(TEXT("density_grid_reuse"), World.bExploreDensityGridReused); Writer->WriteValue(TEXT("density_grid_reuse"), World.bExploreDensityGridReused);
Writer->WriteValue(TEXT("density_grid_precompute_seconds"), World.ExploreDensityGridSeconds); Writer->WriteValue(TEXT("density_grid_precompute_seconds"), World.ExploreDensityGridSeconds);
@@ -4777,7 +4863,9 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("mode"), Writer->WriteValue(TEXT("mode"),
Output.bHasProfile && Output.Profile.Mode != VoxelDensityProfile::EMode::Disabled Output.bHasProfile && Output.Profile.Mode != VoxelDensityProfile::EMode::Disabled
? (Output.Profile.Mode == VoxelDensityProfile::EMode::Full ? (Output.Profile.Mode == VoxelDensityProfile::EMode::Full
? TEXT("full") : TEXT("sampled")) ? TEXT("full")
: (Output.Profile.Mode == VoxelDensityProfile::EMode::Attribution
? TEXT("attribution") : TEXT("sampled")))
: TEXT("disabled")); : TEXT("disabled"));
Writer->WriteValue(TEXT("sample_interval"), static_cast<int32>(Output.Profile.SampleInterval)); Writer->WriteValue(TEXT("sample_interval"), static_cast<int32>(Output.Profile.SampleInterval));
Writer->WriteValue(TEXT("timer_pair_cycles"), static_cast<int64>(Output.Profile.TimerPairCycles)); Writer->WriteValue(TEXT("timer_pair_cycles"), static_cast<int64>(Output.Profile.TimerPairCycles));
@@ -4788,9 +4876,12 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("generation_can_depend_on_profiler"), false); Writer->WriteValue(TEXT("generation_can_depend_on_profiler"), false);
Writer->WriteValue(TEXT("fine_scope_timing"), Writer->WriteValue(TEXT("fine_scope_timing"),
Output.Profile.Mode == VoxelDensityProfile::EMode::Full); Output.Profile.Mode == VoxelDensityProfile::EMode::Full);
Writer->WriteValue(TEXT("attribution_scope_timing"),
Output.Profile.Mode == VoxelDensityProfile::EMode::Attribution);
Writer->WriteValue(TEXT("sampled_scope_policy"), TEXT( Writer->WriteValue(TEXT("sampled_scope_policy"), TEXT(
"Sampled mode times GetDensityAt and mesh container parents; fine operation scopes are " "Attribution mode keeps the parent and additive mesh ledger wall clocks, samples hot "
"opt-in via -profiledensityfull or UE Insights.")); "component scopes, and records classifier detail; full mode remains the invasive per-scope "
"diagnostic."));
Writer->WriteObjectStart(TEXT("wall_clock_self_check")); Writer->WriteObjectStart(TEXT("wall_clock_self_check"));
Writer->WriteValue(TEXT("scope"), Output.ProfilerComparison.Scope); Writer->WriteValue(TEXT("scope"), Output.ProfilerComparison.Scope);
Writer->WriteValue(TEXT("profiled_total_seconds"), Output.ProfilerComparison.ProfiledSeconds); Writer->WriteValue(TEXT("profiled_total_seconds"), Output.ProfilerComparison.ProfiledSeconds);
@@ -4803,6 +4894,7 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("geometry_equal"), Output.ProfilerComparison.bGeometryEqual); Writer->WriteValue(TEXT("geometry_equal"), Output.ProfilerComparison.bGeometryEqual);
Writer->WriteValue(TEXT("self_check"), Output.ProfilerComparison.SelfCheck); Writer->WriteValue(TEXT("self_check"), Output.ProfilerComparison.SelfCheck);
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();
const double CycleToMicroseconds = FPlatformTime::GetSecondsPerCycle64() * 1.0e6;
Writer->WriteObjectStart(TEXT("counters")); Writer->WriteObjectStart(TEXT("counters"));
for (int32 Index = 0; Index < VoxelDensityProfile::CounterCount; ++Index) for (int32 Index = 0; Index < VoxelDensityProfile::CounterCount; ++Index)
{ {
@@ -4811,6 +4903,42 @@ FString BuildExploreJson(
static_cast<int64>(Output.Profile.Counters[Index])); static_cast<int64>(Output.Profile.Counters[Index]));
} }
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();
const VoxelDensityProfile::FSnapshot::FClassifierStats& Classifier = Output.Profile.Classifier;
Writer->WriteObjectStart(TEXT("classifier"));
Writer->WriteValue(TEXT("calls"), static_cast<int64>(Classifier.Calls));
Writer->WriteValue(TEXT("sampled_calls"), static_cast<int64>(Classifier.SampledCalls));
Writer->WriteValue(TEXT("sample_fraction"), Classifier.Calls > 0
? static_cast<double>(Classifier.SampledCalls) / static_cast<double>(Classifier.Calls)
: 0.0);
Writer->WriteValue(TEXT("refine_nodes"), static_cast<int64>(Classifier.RefineNodes));
Writer->WriteValue(TEXT("interval_stack_box_calls"), static_cast<int64>(Classifier.StackBoxCalls));
Writer->WriteValue(TEXT("whole_mixed_nodes"), static_cast<int64>(Classifier.WholeMixedNodes));
Writer->WriteValue(TEXT("whole_solid_nodes"), static_cast<int64>(Classifier.WholeSolidNodes));
Writer->WriteValue(TEXT("whole_air_nodes"), static_cast<int64>(Classifier.WholeAirNodes));
Writer->WriteValue(TEXT("needs_final_field_nodes"), static_cast<int64>(Classifier.NeedsFinalFieldNodes));
Writer->WriteValue(TEXT("split_nodes"), static_cast<int64>(Classifier.SplitNodes));
Writer->WriteValue(TEXT("max_refinement_depth"), static_cast<int64>(Classifier.MaxRefinementDepth));
Writer->WriteValue(TEXT("exact_core_samples"), static_cast<int64>(Classifier.ExactCoreSamples));
Writer->WriteValue(TEXT("exact_final_samples"), static_cast<int64>(Classifier.ExactFinalSamples));
Writer->WriteValue(TEXT("exact_core_cache_hits"), static_cast<int64>(Classifier.ExactCoreCacheHits));
Writer->WriteValue(TEXT("exact_final_cache_hits"), static_cast<int64>(Classifier.ExactFinalCacheHits));
Writer->WriteValue(TEXT("exact_core_leaves"), static_cast<int64>(Classifier.ExactCoreLeaves));
Writer->WriteValue(TEXT("exact_final_leaves"), static_cast<int64>(Classifier.ExactFinalLeaves));
Writer->WriteValue(TEXT("interval_stack_box_us"), static_cast<double>(Classifier.StackBoxCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("exact_core_us"), static_cast<double>(Classifier.ExactCoreCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("exact_final_us"), static_cast<double>(Classifier.ExactFinalCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_tail_queries"), static_cast<int64>(Classifier.RoomTailQueries));
Writer->WriteValue(TEXT("room_tail_evaluated"), static_cast<int64>(Classifier.RoomTailEvaluated));
Writer->WriteValue(TEXT("room_tail_us"), static_cast<double>(Classifier.RoomTailCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_propagate_us"), static_cast<double>(Classifier.RoomPropagateCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_exact_primitive_us"), static_cast<double>(Classifier.RoomExactPrimitiveCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_cache_window_us"), static_cast<double>(Classifier.RoomCacheWindowCycles) * CycleToMicroseconds);
Writer->WriteValue(TEXT("room_count"), static_cast<int64>(Classifier.RoomNumRooms));
Writer->WriteValue(TEXT("tunnel_count"), static_cast<int64>(Classifier.RoomNumTunnels));
Writer->WriteValue(TEXT("room_floor_join_count"), static_cast<int64>(Classifier.RoomNumRoomFloorJoins));
Writer->WriteValue(TEXT("pit_count"), static_cast<int64>(Classifier.RoomNumPits));
Writer->WriteValue(TEXT("chimney_count"), static_cast<int64>(Classifier.RoomNumChimneys));
Writer->WriteObjectEnd();
auto WriteCacheBreakdown = [&Writer]( auto WriteCacheBreakdown = [&Writer](
const TCHAR* Name, const TCHAR* Name,
const VoxelDensityProfile::FCacheMemoryBreakdown& Breakdown) const VoxelDensityProfile::FCacheMemoryBreakdown& Breakdown)
@@ -4836,7 +4964,6 @@ FString BuildExploreJson(
WriteCacheBreakdown(TEXT("room_graph"), Output.Profile.RoomGraphCacheBreakdown); WriteCacheBreakdown(TEXT("room_graph"), Output.Profile.RoomGraphCacheBreakdown);
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();
Writer->WriteObjectStart(TEXT("buckets")); Writer->WriteObjectStart(TEXT("buckets"));
const double CycleToMicroseconds = FPlatformTime::GetSecondsPerCycle64() * 1.0e6;
for (int32 Index = 0; Index < VoxelDensityProfile::BucketCount; ++Index) for (int32 Index = 0; Index < VoxelDensityProfile::BucketCount; ++Index)
{ {
Writer->WriteObjectStart(VoxelDensityProfile::BucketName( Writer->WriteObjectStart(VoxelDensityProfile::BucketName(
@@ -4884,7 +5011,9 @@ FString BuildExploreJson(
Output.Profile.Mode == VoxelDensityProfile::EMode::Full); Output.Profile.Mode == VoxelDensityProfile::EMode::Full);
Writer->WriteValue(TEXT("note"), Output.Profile.Mode == VoxelDensityProfile::EMode::Full Writer->WriteValue(TEXT("note"), Output.Profile.Mode == VoxelDensityProfile::EMode::Full
? TEXT("Phase buckets are the non-overlapping ledger. Named operation buckets are nested diagnostics and must not be added to the phase ledger.") ? TEXT("Phase buckets are the non-overlapping ledger. Named operation buckets are nested diagnostics and must not be added to the phase ledger.")
: TEXT("Sampled mode intentionally disables fine phase and operation timers so the parent survives contact with reality; use -profiledensityfull or UE Insights for fine scope timing.")); : (Output.Profile.Mode == VoxelDensityProfile::EMode::Attribution
? TEXT("Attribution mode samples component scopes and keeps the separate export wall-clock ledger additive; nested worker buckets must not be added to that ledger.")
: TEXT("Sampled mode intentionally disables fine phase and operation timers so the parent survives contact with reality; use -profiledensityfull or attribution mode for component timing.")));
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();
@@ -5437,7 +5566,7 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
// serving the next case's generation. // serving the next case's generation.
VoxelDensityProfile::Reset(); VoxelDensityProfile::Reset();
VoxelDensityProfile::SetMode( VoxelDensityProfile::SetMode(
Arguments.bProfileDensity Arguments.IsDensityProfilingEnabled()
? ExploreProfileMode(Arguments) ? ExploreProfileMode(Arguments)
: VoxelDensityProfile::EMode::Disabled, : VoxelDensityProfile::EMode::Disabled,
VoxelDensityProfile::DefaultSampleInterval); VoxelDensityProfile::DefaultSampleInterval);
@@ -5535,7 +5664,7 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
Mesh.Vertices.Num(), Mesh.Triangles.Num() / 3, Mesh.Vertices.Num(), Mesh.Triangles.Num() / 3,
Mesh.IsEmpty() ? 1 : 0); Mesh.IsEmpty() ? 1 : 0);
} }
if (Arguments.bProfileDensity) if (Arguments.IsDensityProfilingEnabled())
{ {
VoxelDensityProfile::Reset(); VoxelDensityProfile::Reset();
VoxelDensityProfile::SetMode( VoxelDensityProfile::SetMode(
@@ -5616,7 +5745,7 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
// The reference world is fresh, profiling is disabled for it, and its geometry hash is // The reference world is fresh, profiling is disabled for it, and its geometry hash is
// compared with the profiled world below. This is deliberately a diagnostic second pass; // compared with the profiled world below. This is deliberately a diagnostic second pass;
// it cannot affect the profiled world's generation or its deterministic output. // it cannot affect the profiled world's generation or its deterministic output.
if (Arguments.bProfileDensity if (Arguments.IsDensityProfilingEnabled()
&& (Arguments.bRender || Arguments.bExport) && (Arguments.bRender || Arguments.bExport)
&& (CameraSeedWalk != nullptr || Arguments.bExport) && (CameraSeedWalk != nullptr || Arguments.bExport)
&& !Budget.bTruncated) && !Budget.bTruncated)
@@ -5748,7 +5877,7 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
Output.Profile = VoxelDensityProfile::Snapshot(); Output.Profile = VoxelDensityProfile::Snapshot();
Output.bHasProfile = true; Output.bHasProfile = true;
if (Arguments.bProfileDensity) if (Arguments.IsDensityProfilingEnabled())
{ {
if (Output.ProfilerComparison.bAvailable) if (Output.ProfilerComparison.bAvailable)
{ {
@@ -5870,7 +5999,7 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
*ReportPath, *ReportPath,
(bPayloadRepeatEqual && bFinalRepeatEqual) ? TEXT("yes") : TEXT("no")); (bPayloadRepeatEqual && bFinalRepeatEqual) ? TEXT("yes") : TEXT("no"));
if (Arguments.bProfileDensity) if (Arguments.IsDensityProfilingEnabled())
{ {
const VoxelDensityProfile::FSnapshot Profile = VoxelDensityProfile::Snapshot(); const VoxelDensityProfile::FSnapshot Profile = VoxelDensityProfile::Snapshot();
const double CycleToMicroseconds = FPlatformTime::GetSecondsPerCycle64() * 1.0e6; const double CycleToMicroseconds = FPlatformTime::GetSecondsPerCycle64() * 1.0e6;
@@ -6168,6 +6297,8 @@ bool BuildBatchCaseParams(
bool bProfileDensityFull = false; bool bProfileDensityFull = false;
JsonBool(Case, TEXT("profile_density_full"), bProfileDensityFull); JsonBool(Case, TEXT("profile_density_full"), bProfileDensityFull);
bProfileDensity |= bProfileDensityFull; bProfileDensity |= bProfileDensityFull;
bool bPerfAttribution = false;
JsonBool(Case, TEXT("perf_attribution"), bPerfAttribution);
bool bProfileLod = false; bool bProfileLod = false;
JsonBool(Case, TEXT("profile_lod"), bProfileLod); JsonBool(Case, TEXT("profile_lod"), bProfileLod);
bool bOpBounds = false; bool bOpBounds = false;
@@ -6208,6 +6339,10 @@ bool BuildBatchCaseParams(
? TEXT(" -profiledensityfull") ? TEXT(" -profiledensityfull")
: TEXT(" -profiledensity"); : TEXT(" -profiledensity");
} }
if (bPerfAttribution)
{
OutParams += TEXT(" -perfattribution");
}
if (bProfileLod) if (bProfileLod)
{ {
OutParams += TEXT(" -profilelod"); OutParams += TEXT(" -profilelod");