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