diff --git a/CODEMAP.md b/CODEMAP.md index 4c66bdf..9b048a4 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -117,10 +117,10 @@ remains `0`, and lateral regions remain gated off. |------|------| | `../../VoxelForge.uplugin` | Plugin manifest. Runtime module `VoxelForge` plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. | | `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. | -| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor brick program on the 259³ lattice and five-seed probe set; `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval. +| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set, reporting whole-ring setup/candidate counters; `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval. | `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). | -| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. Optional `-exporttilex/y/z` sets an exact L0 tile origin for controlled export/game parity; omitting all three preserves the existing center-derived origin. `-regioneval=1` runs the default-off step-2 research harness: full-ring and five-seed Q16 comparisons, repeat hashes, shrunk-reach falsification, cold timings, counted live fallbacks, and separate before/after stack captures when `-SampleDensityStacks` is set. `-RegionHashProbe` runs one full-ring brick evaluation and compares its raw float hash with a reference from another worker count. | -| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Lattice driver with six workers by default for the commandlet-only region experiment. The live path calls `GetDensityAt` per point; the brick path groups the lattice into aligned 8³ regions and calls the editor-only `EvaluateDensityBrick` once per region, counts scalar fallback points, and compares the exact final field. The operator block uses prepared SDF candidate bins and retains canonical per-point post-stack writer order. The full-ring hash probe supports 1–32 fixed partitions to check determinism across worker counts. | +| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. Optional `-exporttilex/y/z` sets an exact L0 tile origin for controlled export/game parity; omitting all three preserves the existing center-derived origin. `-regioneval=1` runs the default-off step-2 research harness: full-ring and five-seed Q16 comparisons, repeat hashes, shrunk-reach falsification, cold timings, full-ring setup/candidate counters, counted live fallbacks, and separate before/after stack captures when `-SampleDensityStacks` is set. `-RegionHashProbe` runs one full-ring chunk evaluation and compares its raw float hash with a reference from another worker count. | +| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Lattice driver with six workers by default for the commandlet-only region experiment. The live path calls `GetDensityAt` per point; the prepared path assigns each 32³ chunk once and calls editor-only `EvaluateDensityChunk`, which seeds one block evaluation and runs canonical final-field writers across that chunk. The prepared room/SDF cache window is aligned to that chunk; 8-voxel SDF candidate bins stay editor-only and default off. Unsupported routes use counted scalar fallbacks. Full-ring output reports room/SDF cache, active-op-list, and broad-phase/index builds; general-path entries and fallbacks; candidate visits/evaluations per point; and fresh sampled attribution. The full-ring hash probe supports 1–32 fixed partitions to check determinism across worker counts. | | `Public/VoxelGeometryHash.h` | Header-only geometry digest shared by commandlet export and runtime tile-hash dump; hashes mesh counts and the vertex/normal/UV/color/triangle arrays with the historical CRC order. | | `Public/VoxelStats.h` / `Private/VoxelStats.cpp` | `stat VoxelForge` DWORD counters for tile classification, skipping, meshing, operator-stack verdicts, and cave-bail diagnosis. Each `Cave Bail *` counter names one guard; `Cave Bail No Stack` counts slots with no buildable stack (no definition, or a degenerate strate). | @@ -363,7 +363,7 @@ This is **where terrain shape lives.** | `ApplyBoundarySeal` (static) | 996 | Solidifies strate top/bottom shells. | | `InitializeSettings` | 2691 | Copies the effective seed, spine radius, and global XY world-bound settings from `UVoxelSettings` (the season owns all three when assigned); radius 0 preserves the unbounded field. | | **`GetDensityAt`** | 2712 | **Entry point.** Picks strate + generator type and, on the per-chunk refetch, builds the chunk's operator stack (editor composer → cooked-season recipe → native archetype stack); evaluates it (TunnelNetwork/Underwater through the fused `GetDensityWithParams`), then adds disturbances and structural posts. Optional `OutPreEditDensity` receives the exact final procedural field immediately before the player DiffLayer tail, allowing the near-player dig cache to replay edits without regenerating the operator stack. Its ordinary one-argument behavior is unchanged. A degenerate strate has no stack and is air; `CP_*` state remains keyed by `(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`. | -| **`EvaluateDensityBrick`** | 2728 | **Editor-only, default-off research API.** Accepts a lattice region contained by one 8-voxel bin and one 32-voxel chunk. Routes once, reuses `FVoxelDensityBlockSession::FillChunk` / `EvalBlock` for supported TunnelNetwork and Underwater operator stacks, and applies the existing post-stack writers across all brick points before returning. Unsupported routes use scalar `GetDensityAt` and return their fallback count. The API is absent from non-editor builds; streaming and gameplay continue through the original scalar API. | +| **`EvaluateDensityChunk`** | 2728 | **Editor-only, default-off research API.** Accepts a lattice region contained by one 32-voxel chunk. Routes once, reuses `FVoxelDensityBlockSession::FillChunk` / `EvalBlock` for supported TunnelNetwork and Underwater operator stacks, and applies the existing post-stack writers across all chunk points before returning. Unsupported routes use scalar `GetDensityAt` and return both fallback and general-path entry counts. The region evaluator prepares bounded cache/index state once per region or chunk. The API is absent from non-editor builds; streaming and gameplay continue through the original scalar API. | | **`GetDensityWithParams`** | 3749 | TunnelNetwork pipeline (~1000 lines) — the fused evaluator of the TunnelNetwork/Underwater stack in `GetDensityAt`. See §4. ⚠️ Takes **required** `ParamsFingerprint` + `LayoutVersion` since the AUDIT §C2 fix (2026-07-28) — they go into the SDF cache key so a chunk can no longer be evaluated against a neighbour's rooms. Callers compute the CRC **once per chunk** (`CP_TunnelFP`), never per voxel. The optional `voxel.WormBlockSkip` path (default 0) uses `VF_TryGetWormBlockSkip` + a worker-local 4³ lattice-block cache to prove `|N1| >= WormThreshold` and remove both worm noise calls only; it never changes `NetworkMask` or the fallback field path. | | `SampleSurfaceStructuralZ` | — | **F20:** the RAW SurfaceWorld heightfield (continents+mountains+detail), BEFORE any terrain op; returns terrain Z + relief M. Cliff re-samples it at an XY offset for a cheap analytic slope. | | `ComputeSurfaceTerrainZ` / `ComputeSurfaceColumn` | — | SurfaceWorld heightfield → terrain Z (+ sky-cap ceiling and overhang gate per column); biome **output-blend** lerps dominant/neighbour heights (`ParamsD`/`ParamsN`/weight). **F20 surface ops** (`FSurfaceGenerationParams`, biome-selected + slope/relief-conditioned, all default off): Cliff (slope-gated STEEPENING — push height from local mean where steep ⇒ sheer walls; 4 structural resamples only when on), Terrace (relief-gated + `TerraceHardness`), LayerLines (sedimentary shelves) — pure per-column height REMAPS applied here so the single height oracle stays consistent (MC/ClassifyTile/deco/BP bridge). **Phase 2 OVERHANG** (volumetric — real jutting shelves): in the stack's `FOverhangShelfMod`, for AIR voxels in a window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height (tiny low ⇒ air over the void, full high ⇒ borrows the far cliff rock) and unioned in ⇒ a shelf attached to the cliff, tapering out over the void with air beneath (the sketch). Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill `(DirX,DirY)` resolved once in `ComputeSurfaceColumn` (gradient sampled at the REACH scale so a spot over the void can see the cliff), cached on `FSurfaceColumn`. Genuine 3D (per-voxel structural re-eval, gated to steep overhang columns). Off ⇒ byte-identical. The field-preserving LOD0 pass removes only needless temporaries/default construction; every output remains bit-identical. §8.14. | diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index 59f5937..91d8421 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -1416,6 +1416,11 @@ namespace static void VF_BuildSpatialIndex( FChunkSDFSpatialIndex& OutIndex, int32 ItemCount, FGetBounds&& GetBounds) { + if (VoxelDensityProfile::AreCountersEnabled()) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::SdfSpatialIndexBuild); + } OutIndex.Reset(); if (ItemCount <= 0) { @@ -1811,6 +1816,11 @@ namespace static void VF_BuildBrickIndex( FChunkSDFBrickIndex& OutIndex, int32 ItemCount, FGetBounds&& GetBounds) { + if (VoxelDensityProfile::AreCountersEnabled()) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::SdfBrickIndexBuild); + } OutIndex.Reset(); if (ItemCount <= 0) { diff --git a/Source/VoxelForge/Private/VoxelDensityProfile.cpp b/Source/VoxelForge/Private/VoxelDensityProfile.cpp index 107285d..fd68763 100644 --- a/Source/VoxelForge/Private/VoxelDensityProfile.cpp +++ b/Source/VoxelForge/Private/VoxelDensityProfile.cpp @@ -640,6 +640,8 @@ namespace VoxelDensityProfile case ECounter::TunnelCacheHit: return TEXT("TunnelCacheHit"); case ECounter::TunnelCacheMiss: return TEXT("TunnelCacheMiss"); case ECounter::SdfCacheBuild: return TEXT("SdfCacheBuild"); + case ECounter::SdfSpatialIndexBuild: return TEXT("SdfSpatialIndexBuild"); + case ECounter::SdfBrickIndexBuild: return TEXT("SdfBrickIndexBuild"); case ECounter::RoomGraphBuildGeneratorTile: return TEXT("RoomGraphBuildGeneratorTile"); case ECounter::RoomGraphBuildGeneratorTunnelCore: return TEXT("RoomGraphBuildGeneratorTunnelCore"); case ECounter::RoomGraphBuildOpShared: return TEXT("RoomGraphBuildOpShared"); diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 4ea1b35..5c25ea6 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -38,6 +38,9 @@ namespace VoxelMath namespace { +#if WITH_EDITOR + thread_local bool GVoxelResearchRegionCacheWindowEnabled = false; +#endif // The default-on branch uses VoxelMath::IsFiniteFast, while 0 retains the original // FMath::IsFinite call for a real within-round A/B and sampler comparison. FAutoConsoleVariableRef CVarVoxelForgeFastIsFinite( @@ -884,6 +887,12 @@ bool VoxelGenLOD::IsTileCacheWindowEnabled(bool bFusedPath) { return false; } +#if WITH_EDITOR + if (GVoxelResearchRegionCacheWindowEnabled) + { + return true; + } +#endif const int32 RequiredMinLOD = bFusedPath ? GVoxelForgeTileCacheWindowMinLOD : GVoxelForgeTileCacheWindowOpStackMinLOD; @@ -891,6 +900,13 @@ bool VoxelGenLOD::IsTileCacheWindowEnabled(bool bFusedPath) return FMath::Max(SampleStep, 1) >= RequiredStep; } +#if WITH_EDITOR +void VoxelGenLOD::SetResearchRegionCacheWindowEnabled(bool bEnabled) +{ + GVoxelResearchRegionCacheWindowEnabled = bEnabled; +} +#endif + bool VoxelGenLOD::GetThreadTileCacheWindow( FIntVector& OutOriginVoxels, int32& OutStep, int32& OutCellsPerAxis) { @@ -2722,15 +2738,19 @@ void UVoxelGenerator::EndDensityBlock() const } #if WITH_EDITOR -bool UVoxelGenerator::EvaluateDensityBrick( +bool UVoxelGenerator::EvaluateDensityChunk( FIntVector OriginVoxels, int32 SizeX, int32 SizeY, int32 SizeZ, - TArray& OutDensities, int32& OutFallbackSamples) const + TArray& OutDensities, int32& OutFallbackSamples, + uint64& OutGeneralPathEntries, uint64& OutSupportedBlockSetupEntries) const { OutDensities.Reset(); OutFallbackSamples = 0; + OutGeneralPathEntries = 0; + OutSupportedBlockSetupEntries = 0; const int64 SampleCount64 = static_cast(SizeX) * SizeY * SizeZ; if (SizeX <= 0 || SizeY <= 0 || SizeZ <= 0 - || SizeX > 8 || SizeY > 8 || SizeZ > 8 || SampleCount64 > 512) + || SizeX > CHUNK_SIZE || SizeY > CHUNK_SIZE || SizeZ > CHUNK_SIZE + || SampleCount64 > static_cast(CHUNK_SIZE) * CHUNK_SIZE * CHUNK_SIZE) { return false; } @@ -2741,10 +2761,7 @@ bool UVoxelGenerator::EvaluateDensityBrick( return FMath::FloorToInt(static_cast(A) / static_cast(CellSize)) == FMath::FloorToInt(static_cast(B) / static_cast(CellSize)); }; - if (!SameCell(OriginVoxels.X, Last.X, 8) - || !SameCell(OriginVoxels.Y, Last.Y, 8) - || !SameCell(OriginVoxels.Z, Last.Z, 8) - || !SameCell(OriginVoxels.X, Last.X, CHUNK_SIZE) + if (!SameCell(OriginVoxels.X, Last.X, CHUNK_SIZE) || !SameCell(OriginVoxels.Y, Last.Y, CHUNK_SIZE) || !SameCell(OriginVoxels.Z, Last.Z, CHUNK_SIZE)) { @@ -2761,17 +2778,19 @@ bool UVoxelGenerator::EvaluateDensityBrick( static_cast(OriginVoxels.X), static_cast(OriginVoxels.Y), static_cast(OriginVoxels.Z)); + OutGeneralPathEntries = 1; if (GVoxelDensityBlockSession.bBatchSucceeded) { OutDensities.SetNumUninitialized(SampleCount); FMemory::Memcpy( OutDensities.GetData(), GVoxelDensityBlockSession.BatchResults.GetData(), static_cast(SampleCount) * sizeof(float)); + OutSupportedBlockSetupEntries = 1; EndDensityBlock(); return OutDensities.Num() == SampleCount; } - // Recipes without the editor brick port retain the canonical point evaluator. The first + // Routes without the editor block program retain the canonical point evaluator. The first // call already produced point zero; the remaining samples are explicit counted fallbacks. GVoxelDensityBlockSession.bBatchRequested = false; OutDensities.SetNumUninitialized(SampleCount); @@ -2787,6 +2806,7 @@ bool UVoxelGenerator::EvaluateDensityBrick( static_cast(OriginVoxels.Z + Z)); } OutFallbackSamples = SampleCount; + OutGeneralPathEntries = static_cast(SampleCount); EndDensityBlock(); return true; } diff --git a/Source/VoxelForge/Public/VoxelDensityProfile.h b/Source/VoxelForge/Public/VoxelDensityProfile.h index 6e9abeb..810e208 100644 --- a/Source/VoxelForge/Public/VoxelDensityProfile.h +++ b/Source/VoxelForge/Public/VoxelDensityProfile.h @@ -106,6 +106,8 @@ namespace VoxelDensityProfile TunnelCacheHit, TunnelCacheMiss, SdfCacheBuild, + SdfSpatialIndexBuild, + SdfBrickIndexBuild, RoomGraphBuildGeneratorTile, RoomGraphBuildGeneratorTunnelCore, RoomGraphBuildOpShared, diff --git a/Source/VoxelForge/Public/VoxelGenerator.h b/Source/VoxelForge/Public/VoxelGenerator.h index 56fdb98..abd7b8a 100644 --- a/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Source/VoxelForge/Public/VoxelGenerator.h @@ -77,6 +77,10 @@ namespace VoxelGenLOD // Called by the module's game-thread startup before any worker can enter GetDensityAt. VOXELFORGE_API void InitializeConsoleSwitches(); VOXELFORGE_API bool IsTileCacheWindowEnabled(bool bFusedPath = true); +#if WITH_EDITOR + /** Research-only override for the bounded editor region evaluator; default false per thread. */ + VOXELFORGE_API void SetResearchRegionCacheWindowEnabled(bool bEnabled); +#endif VOXELFORGE_API bool GetThreadTileCacheWindow( FIntVector& OutOriginVoxels, int32& OutStep, int32& OutCellsPerAxis); VOXELFORGE_API void SetThreadTileContext( @@ -181,10 +185,11 @@ public: void EndDensityBlock() const; #if WITH_EDITOR - /** Editor-only region-program entry point; unsupported routes report live point fallbacks. */ - bool EvaluateDensityBrick(FIntVector OriginVoxels, int32 SizeX, int32 SizeY, int32 SizeZ, - TArray& OutDensities, - int32& OutFallbackSamples) const; + /** Editor-only chunk program; supported routes enter GetDensityAt once to seed a whole block. */ + bool EvaluateDensityChunk(FIntVector OriginVoxels, int32 SizeX, int32 SizeY, int32 SizeZ, + TArray& OutDensities, int32& OutFallbackSamples, + uint64& OutGeneralPathEntries, + uint64& OutSupportedBlockSetupEntries) const; #endif /** diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index 984c5d0..6c79e4b 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -1758,7 +1758,7 @@ bool RunRegionEvalPass( const FExploreArguments& BaseArguments, int32 Seed, bool bPrepared, - bool bBrickProgram, + bool bRegionProgram, float ReachScale, bool bFullRing, const TArray* RequestedIndices, @@ -1767,7 +1767,8 @@ bool RunRegionEvalPass( bool bKeepDensities, FRegionEvalPass& OutPass, FString& OutError, - int32 RequestedWorkerCount = VoxelForgeRegionEval::WorkerCount) + int32 RequestedWorkerCount = VoxelForgeRegionEval::WorkerCount, + bool bCollectWholeRunCounters = false) { OutPass = FRegionEvalPass(); FExploreWorld World; @@ -1809,7 +1810,7 @@ bool RunRegionEvalPass( bFullRing ? nullptr : &OutPass.SampleIndices, bFullRing, bPrepared, ReachScale, bKeepDensities ? &OutPass.Densities : nullptr, - OutPass.Timing, bBrickProgram, RequestedWorkerCount); + OutPass.Timing, bRegionProgram, RequestedWorkerCount, bCollectWholeRunCounters); OutPass.EndedUtc = FDateTime::UtcNow().ToIso8601(); if (!bEvaluated) { @@ -1835,18 +1836,102 @@ bool RunRegionEvalPass( TSharedRef RegionEvalTimingJson(const FRegionEvalPass& Pass, bool bPrepared) { TSharedRef Json = MakeShared(); - Json->SetStringField(TEXT("path"), Pass.Timing.bBrickProgram - ? TEXT("editor_region_brick_program") + Json->SetStringField(TEXT("path"), Pass.Timing.bRegionProgram + ? TEXT("editor_region_chunk_program") : (bPrepared ? TEXT("prepared_8cube_bins") : TEXT("live_GetDensityAt"))); Json->SetStringField(TEXT("started_utc"), Pass.StartedUtc); Json->SetStringField(TEXT("ended_utc"), Pass.EndedUtc); Json->SetNumberField(TEXT("worker_count"), Pass.Timing.Workers); Json->SetNumberField(TEXT("observed_worker_threads"), Pass.Timing.ObservedWorkerThreads); Json->SetNumberField(TEXT("samples"), static_cast(Pass.Timing.SampleCount)); - Json->SetNumberField(TEXT("evaluated_brick_points"), static_cast(Pass.Timing.EvaluatedSamples)); - Json->SetNumberField(TEXT("brick_count"), static_cast(Pass.Timing.BrickCount)); + Json->SetNumberField(TEXT("evaluated_points"), static_cast(Pass.Timing.EvaluatedSamples)); + Json->SetNumberField(TEXT("region_count"), static_cast(Pass.Timing.RegionCount)); Json->SetNumberField(TEXT("fallback_points"), static_cast(Pass.Timing.FallbackSamples)); Json->SetNumberField(TEXT("fallback_share"), Pass.Timing.FallbackShare); + Json->SetNumberField(TEXT("general_path_entries"), static_cast(Pass.Timing.GeneralPathEntries)); + Json->SetNumberField(TEXT("general_path_entries_per_evaluated_sample"), + Pass.Timing.GeneralPathEntriesPerSample); + Json->SetNumberField(TEXT("supported_block_setup_entries"), + static_cast(Pass.Timing.SupportedBlockSetupEntries)); + Json->SetBoolField(TEXT("whole_run_counters_present"), Pass.Timing.bHasWholeRunCounters); + if (Pass.Timing.bHasWholeRunCounters) + { + Json->SetNumberField(TEXT("room_graph_sdf_cache_builds"), + static_cast(Pass.Timing.SdfCacheBuilds)); + Json->SetNumberField(TEXT("active_op_list_builds"), + static_cast(Pass.Timing.ActiveOpListBuilds)); + Json->SetNumberField(TEXT("broad_phase_index_builds"), + static_cast(Pass.Timing.BroadPhaseIndexBuilds)); + Json->SetNumberField(TEXT("sdf_spatial_index_builds"), + static_cast(Pass.Timing.SdfSpatialIndexBuilds)); + Json->SetNumberField(TEXT("sdf_3d_brick_index_builds"), + static_cast(Pass.Timing.SdfBrickIndexBuilds)); + Json->SetNumberField(TEXT("candidate_visits_total"), + static_cast(Pass.Timing.TotalCandidateVisits)); + Json->SetNumberField(TEXT("candidate_visits_per_sample"), + Pass.Timing.CandidateVisitsPerSample); + Json->SetNumberField(TEXT("candidate_evaluations_total"), + static_cast(Pass.Timing.TotalCandidateEvaluations)); + Json->SetNumberField(TEXT("candidate_evaluations_per_sample"), + Pass.Timing.CandidateEvaluationsPerSample); + Json->SetNumberField(TEXT("room_candidate_visits"), + static_cast(Pass.Timing.RoomCandidateVisits)); + Json->SetNumberField(TEXT("room_evaluations"), + static_cast(Pass.Timing.RoomEvaluations)); + Json->SetNumberField(TEXT("tunnel_candidate_visits"), + static_cast(Pass.Timing.TunnelCandidateVisits)); + Json->SetNumberField(TEXT("tunnel_evaluations"), + static_cast(Pass.Timing.TunnelEvaluations)); + Json->SetNumberField(TEXT("tunnel_core_candidate_visits"), + static_cast(Pass.Timing.TunnelCoreCandidateVisits)); + Json->SetNumberField(TEXT("tunnel_core_evaluations"), + static_cast(Pass.Timing.TunnelCoreEvaluations)); + Json->SetNumberField(TEXT("passage_candidate_visits"), + static_cast(Pass.Timing.PassageCandidateVisits)); + Json->SetNumberField(TEXT("passage_evaluations"), + static_cast(Pass.Timing.PassageEvaluations)); + + TSharedRef Attribution = MakeShared(); + Attribution->SetNumberField(TEXT("get_density_at_ns_per_sample"), + Pass.Timing.GetDensityAtNsPerSample); + Attribution->SetNumberField(TEXT("operator_block_ns_per_sample"), + Pass.Timing.OperatorBlockNsPerSample); + Attribution->SetNumberField(TEXT("room_graph_source_ns_per_sample"), + Pass.Timing.RoomGraphSourceNsPerSample); + Attribution->SetNumberField(TEXT("room_graph_build_ns_per_sample"), + Pass.Timing.RoomGraphBuildNsPerSample); + Attribution->SetNumberField(TEXT("room_graph_sdf_ns_per_sample"), + Pass.Timing.RoomGraphSdfNsPerSample); + Attribution->SetNumberField(TEXT("fused_evaluator_ns_per_sample"), + Pass.Timing.FusedEvaluatorNsPerSample); + Attribution->SetNumberField(TEXT("tunnel_core_sdf_ns_per_sample"), + Pass.Timing.TunnelCoreSdfNsPerSample); + Attribution->SetNumberField(TEXT("tunnel_core_world_ns_per_sample"), + Pass.Timing.TunnelCoreWorldNsPerSample); + Attribution->SetNumberField(TEXT("tunnel_core_posts_ns_per_sample"), + Pass.Timing.TunnelCorePostsNsPerSample); + Attribution->SetNumberField(TEXT("passage_modifier_ns_per_sample"), + Pass.Timing.PassageModifierNsPerSample); + Attribution->SetNumberField(TEXT("passage_structural_posts_ns_per_sample"), + Pass.Timing.PassageStructuralPostsNsPerSample); + Attribution->SetNumberField(TEXT("passage_floor_ns_per_sample"), + Pass.Timing.PassageFloorNsPerSample); + Attribution->SetNumberField(TEXT("structural_posts_ns_per_sample"), + Pass.Timing.StructuralPostsNsPerSample); + Attribution->SetNumberField(TEXT("density_prologue_ns_per_sample"), + Pass.Timing.DensityPrologueNsPerSample); + Attribution->SetNumberField(TEXT("density_core_ns_per_sample"), + Pass.Timing.DensityCoreNsPerSample); + Attribution->SetNumberField(TEXT("disturbances_ns_per_sample"), + Pass.Timing.DisturbancesNsPerSample); + Attribution->SetNumberField(TEXT("boundary_seal_ns_per_sample"), + Pass.Timing.DensityBoundarySealNsPerSample); + Attribution->SetNumberField(TEXT("diff_layer_ns_per_sample"), + Pass.Timing.DiffLayerNsPerSample); + Attribution->SetNumberField(TEXT("density_tail_ns_per_sample"), + Pass.Timing.DensityTailNsPerSample); + Json->SetObjectField(TEXT("fresh_attribution"), Attribution); + } Json->SetNumberField(TEXT("world_setup_seconds_before_density_ring"), Pass.WorldPrepareSeconds); Json->SetNumberField(TEXT("wall_seconds"), Pass.Timing.WallSeconds); Json->SetNumberField(TEXT("worker_cpu_seconds"), Pass.Timing.WorkerCpuSeconds); @@ -1864,26 +1949,31 @@ TSharedRef RegionEvalTimingJson(const FRegionEvalPass& Pass, bool b Json->SetNumberField(TEXT("non_finite_samples"), static_cast(Pass.Timing.NonFiniteSamples)); if (Pass.Visits.SampleCount > 0) { - Json->SetNumberField(TEXT("candidate_visits_per_sample"), Pass.Visits.PerSample); - Json->SetNumberField(TEXT("room_candidate_visits"), static_cast(Pass.Visits.Room)); - Json->SetNumberField(TEXT("tunnel_candidate_visits"), static_cast(Pass.Visits.Tunnel)); - Json->SetNumberField(TEXT("tunnel_core_candidate_visits"), static_cast(Pass.Visits.TunnelCore)); - Json->SetNumberField(TEXT("passage_candidate_visits"), static_cast(Pass.Visits.Passage)); - Json->SetNumberField(TEXT("tunnel_cache_lookups"), static_cast(Pass.Visits.TunnelCacheLookups)); - Json->SetNumberField(TEXT("tunnel_cache_hits"), static_cast(Pass.Visits.TunnelCacheHits)); - Json->SetNumberField(TEXT("tunnel_cache_misses"), static_cast(Pass.Visits.TunnelCacheMisses)); - Json->SetNumberField(TEXT("sdf_cache_builds"), static_cast(Pass.Visits.SdfCacheBuilds)); - Json->SetNumberField(TEXT("candidate_counter_samples"), static_cast(Pass.Visits.SampleCount)); - Json->SetNumberField(TEXT("attribution_sample_interval"), Pass.Visits.AttributionSampleInterval); - Json->SetStringField(TEXT("attribution_basis"), TEXT("sampled VoxelDensityProfile phases; diagnostic only, excluded from clean timing")); - Json->SetNumberField(TEXT("get_density_at_attributed_ns_per_sample"), Pass.Visits.GetDensityAtAttributedNsPerSample); - Json->SetNumberField(TEXT("density_prologue_attributed_ns_per_sample"), Pass.Visits.DensityPrologueAttributedNsPerSample); - Json->SetNumberField(TEXT("density_core_attributed_ns_per_sample"), Pass.Visits.DensityCoreAttributedNsPerSample); - Json->SetNumberField(TEXT("density_disturbances_attributed_ns_per_sample"), Pass.Visits.DensityDisturbancesAttributedNsPerSample); - Json->SetNumberField(TEXT("density_structural_posts_attributed_ns_per_sample"), Pass.Visits.DensityStructuralPostsAttributedNsPerSample); - Json->SetNumberField(TEXT("density_boundary_seal_attributed_ns_per_sample"), Pass.Visits.DensityBoundarySealAttributedNsPerSample); - Json->SetNumberField(TEXT("density_diff_layer_attributed_ns_per_sample"), Pass.Visits.DensityDiffLayerAttributedNsPerSample); - Json->SetNumberField(TEXT("density_tail_attributed_ns_per_sample"), Pass.Visits.DensityTailAttributedNsPerSample); + Json->SetNumberField(TEXT("probe_candidate_visits_per_sample"), Pass.Visits.PerSample); + Json->SetNumberField(TEXT("probe_candidate_evaluations_per_sample"), Pass.Visits.EvaluatedPerSample); + Json->SetNumberField(TEXT("probe_room_candidate_visits"), static_cast(Pass.Visits.Room)); + Json->SetNumberField(TEXT("probe_room_candidate_evaluations"), static_cast(Pass.Visits.RoomEvaluated)); + Json->SetNumberField(TEXT("probe_tunnel_candidate_visits"), static_cast(Pass.Visits.Tunnel)); + Json->SetNumberField(TEXT("probe_tunnel_candidate_evaluations"), static_cast(Pass.Visits.TunnelEvaluated)); + Json->SetNumberField(TEXT("probe_tunnel_core_candidate_visits"), static_cast(Pass.Visits.TunnelCore)); + Json->SetNumberField(TEXT("probe_tunnel_core_candidate_evaluations"), static_cast(Pass.Visits.TunnelCoreEvaluated)); + Json->SetNumberField(TEXT("probe_passage_candidate_visits"), static_cast(Pass.Visits.Passage)); + Json->SetNumberField(TEXT("probe_passage_candidate_evaluations"), static_cast(Pass.Visits.PassageEvaluated)); + Json->SetNumberField(TEXT("probe_tunnel_cache_lookups"), static_cast(Pass.Visits.TunnelCacheLookups)); + Json->SetNumberField(TEXT("probe_tunnel_cache_hits"), static_cast(Pass.Visits.TunnelCacheHits)); + Json->SetNumberField(TEXT("probe_tunnel_cache_misses"), static_cast(Pass.Visits.TunnelCacheMisses)); + Json->SetNumberField(TEXT("probe_sdf_cache_builds"), static_cast(Pass.Visits.SdfCacheBuilds)); + Json->SetNumberField(TEXT("probe_candidate_counter_samples"), static_cast(Pass.Visits.SampleCount)); + Json->SetNumberField(TEXT("probe_attribution_sample_interval"), Pass.Visits.AttributionSampleInterval); + Json->SetStringField(TEXT("probe_attribution_basis"), TEXT("sampled VoxelDensityProfile phases; diagnostic only, excluded from clean timing")); + Json->SetNumberField(TEXT("probe_get_density_at_attributed_ns_per_sample"), Pass.Visits.GetDensityAtAttributedNsPerSample); + Json->SetNumberField(TEXT("probe_density_prologue_attributed_ns_per_sample"), Pass.Visits.DensityPrologueAttributedNsPerSample); + Json->SetNumberField(TEXT("probe_density_core_attributed_ns_per_sample"), Pass.Visits.DensityCoreAttributedNsPerSample); + Json->SetNumberField(TEXT("probe_density_disturbances_attributed_ns_per_sample"), Pass.Visits.DensityDisturbancesAttributedNsPerSample); + Json->SetNumberField(TEXT("probe_density_structural_posts_attributed_ns_per_sample"), Pass.Visits.DensityStructuralPostsAttributedNsPerSample); + Json->SetNumberField(TEXT("probe_density_boundary_seal_attributed_ns_per_sample"), Pass.Visits.DensityBoundarySealAttributedNsPerSample); + Json->SetNumberField(TEXT("probe_density_diff_layer_attributed_ns_per_sample"), Pass.Visits.DensityDiffLayerAttributedNsPerSample); + Json->SetNumberField(TEXT("probe_density_tail_attributed_ns_per_sample"), Pass.Visits.DensityTailAttributedNsPerSample); } return Json; } @@ -2018,7 +2108,7 @@ int32 RunRegionEval(const FString& Params) ProbeReport->SetNumberField(TEXT("observed_worker_threads"), WorkerHashPass.Timing.ObservedWorkerThreads); ProbeReport->SetNumberField(TEXT("samples"), WorkerHashPass.Timing.SampleCount); - ProbeReport->SetNumberField(TEXT("brick_count"), WorkerHashPass.Timing.BrickCount); + ProbeReport->SetNumberField(TEXT("region_count"), WorkerHashPass.Timing.RegionCount); ProbeReport->SetNumberField(TEXT("fallback_points"), WorkerHashPass.Timing.FallbackSamples); ProbeReport->SetNumberField(TEXT("fallback_share"), WorkerHashPass.Timing.FallbackShare); ProbeReport->SetNumberField(TEXT("wall_seconds"), WorkerHashPass.Timing.WallSeconds); @@ -2064,6 +2154,8 @@ int32 RunRegionEval(const FString& Params) TArray> SeedReports; TArray> OldTimingReports; TArray> PreparedTimingReports; + TSharedPtr LiveFullRingDiagnostics; + TSharedPtr PreparedFullRingDiagnostics; TArray OldFullDensities; TArray PreparedFullDensities; TArray PrimarySampleIndices; @@ -2164,42 +2256,69 @@ int32 RunRegionEval(const FString& Params) && OldFullSaturatedSamples == 0 && PreparedFullSaturatedSamples == 0; + // Exact whole-ring counters and sampled phase attribution are collected in separate cold + // passes. Their diagnostic hooks stay out of the two wall-time trials used for the 600 ms bar. + FRegionEvalPass LiveDiagnosticPass; + if (!RunRegionEvalPass( + Arguments, Seeds[0], false, false, 1.0f, true, nullptr, + RequestedSamples, false, false, LiveDiagnosticPass, Error, + VoxelForgeRegionEval::WorkerCount, true)) + { + UE_LOG(LogTemp, Error, + TEXT("[VoxelForgeRegionEval] live full-ring diagnostic pass failed: %s"), *Error); + return 2; + } + LiveFullRingDiagnostics = RegionEvalTimingJson(LiveDiagnosticPass, false); + CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); + FRegionEvalPass PreparedDiagnosticPass; + if (!RunRegionEvalPass( + Arguments, Seeds[0], true, true, 1.0f, true, nullptr, + RequestedSamples, false, false, PreparedDiagnosticPass, Error, + VoxelForgeRegionEval::WorkerCount, true)) + { + UE_LOG(LogTemp, Error, + TEXT("[VoxelForgeRegionEval] prepared full-ring diagnostic pass failed: %s"), *Error); + return 2; + } + PreparedFullRingDiagnostics = RegionEvalTimingJson(PreparedDiagnosticPass, true); + CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); + // Statistical fixed-lattice comparisons across four additional independent seeds. The sample // set is a deterministic affine permutation of the entire 259^3 ring plus dense strata, // passage-floor, landing, carve, and fill probes. if (Arguments.bSampleStacks) { - FRegionEvalStackSamplerSession BrickStackSamplerSession; - if (!BrickStackSamplerSession.Start( + FRegionEvalStackSamplerSession RegionStackSamplerSession; + if (!RegionStackSamplerSession.Start( Arguments.SampleStacksIntervalUs, Arguments.OutDirectory, - TEXT("region_eval_brick_ring"))) + TEXT("region_eval_chunk_ring"))) { UE_LOG(LogTemp, Error, - TEXT("[VoxelForgeRegionEval] brick stack sampler could not be started.")); + TEXT("[VoxelForgeRegionEval] chunk stack sampler could not be started.")); return 2; } - FRegionEvalPass ProfiledBrickPass; + FRegionEvalPass ProfiledRegionPass; if (!RunRegionEvalPass( Arguments, Seeds[0], true, true, 1.0f, true, nullptr, - RequestedSamples, false, false, ProfiledBrickPass, Error)) + RequestedSamples, false, false, ProfiledRegionPass, Error)) { UE_LOG(LogTemp, Error, - TEXT("[VoxelForgeRegionEval] brick attribution run failed: %s"), *Error); + TEXT("[VoxelForgeRegionEval] chunk attribution run failed: %s"), *Error); return 2; } - FVoxelStackSampler::FSummary BrickStackSummary; - if (!BrickStackSamplerSession.Stop(BrickStackSummary)) + FVoxelStackSampler::FSummary RegionStackSummary; + if (!RegionStackSamplerSession.Stop(RegionStackSummary)) { UE_LOG(LogTemp, Error, - TEXT("[VoxelForgeRegionEval] brick stack capture retained no worker samples.")); + TEXT("[VoxelForgeRegionEval] chunk stack capture retained no worker samples.")); return 2; } UE_LOG(LogTemp, Display, - TEXT("[VoxelForgeRegionEval] brick stack capture registered=%llu retained=%llu " + TEXT("[VoxelForgeRegionEval] chunk stack capture registered=%llu retained=%llu " "raw=%s summary=%s"), - static_cast(BrickStackSummary.RegisteredThreadSamples), - static_cast(BrickStackSummary.RetainedSamples), - *BrickStackSummary.RawSamplesPath, *BrickStackSummary.SummaryPath); + static_cast(RegionStackSummary.RegisteredThreadSamples), + static_cast(RegionStackSummary.RetainedSamples), + *RegionStackSummary.RawSamplesPath, *RegionStackSummary.SummaryPath); } for (int32 SeedIndex = 1; SeedIndex < UE_ARRAY_COUNT(Seeds); ++SeedIndex) @@ -2307,9 +2426,11 @@ int32 RunRegionEval(const FString& Params) const bool bSpeedPass = PreparedMedianWall <= 0.600 && bSixWorkersObserved; TSharedRef Report = MakeShared(); - Report->SetNumberField(TEXT("schema_version"), 1); + Report->SetNumberField(TEXT("schema_version"), 2); Report->SetStringField(TEXT("route"), TEXT("editor-only explore commandlet; required Unreal composer, manager, and generator types")); - Report->SetStringField(TEXT("evaluator"), TEXT("editor-only 8^3 region brick program; block-evaluated composer operator core and canonical per-point final-field writers; unsupported routes use counted live fallbacks")); + Report->SetStringField(TEXT("evaluator"), TEXT("editor-only chunk program; region-scoped room/SDF caches, one operator block per chunk, canonical post-stack writers, and counted live point fallbacks")); + Report->SetStringField(TEXT("timing_basis"), TEXT("two cold wall-time trials per path with counters disabled; exact whole-ring counters and sampled phase attribution are separate diagnostic passes")); + Report->SetBoolField(TEXT("diagnostic_passes_excluded_from_speed_verdict"), true); Report->SetStringField(TEXT("comparison_quantization"), TEXT("signed int16; round ties away from zero; multiply density by 16; saturate outside [-2048, 2047.9375] density voxels")); Report->SetNumberField(TEXT("worker_count"), VoxelForgeRegionEval::WorkerCount); Report->SetNumberField(TEXT("ring_dimension"), VoxelForgeRegionEval::RingDimension); @@ -2325,6 +2446,8 @@ int32 RunRegionEval(const FString& Params) Report->SetObjectField(TEXT("full_ring_comparison"), RegionEvalComparisonJson(FullComparison)); Report->SetArrayField(TEXT("live_full_ring_trials"), OldTimingReports); Report->SetArrayField(TEXT("prepared_full_ring_trials"), PreparedTimingReports); + Report->SetObjectField(TEXT("live_full_ring_diagnostics"), LiveFullRingDiagnostics.ToSharedRef()); + Report->SetObjectField(TEXT("prepared_full_ring_diagnostics"), PreparedFullRingDiagnostics.ToSharedRef()); Report->SetArrayField(TEXT("additional_seed_comparisons"), SeedReports); TSharedRef Coverage = MakeShared(); @@ -2342,7 +2465,7 @@ int32 RunRegionEval(const FString& Params) Coverage->SetStringField(TEXT("tunnel_bin_reach"), TEXT("union of finalized warped-SDF and world-chain centerline bounds expanded by their influence radii, plus both conservative bounding spheres")); Coverage->SetStringField(TEXT("warp_coordinates"), TEXT("room bins are queried after the exact live cave warp in the same coordinate domain; tunnel bins union warped-SDF and unwarped world-chain reach")); Coverage->SetStringField(TEXT("tunnel_bin_unit"), TEXT("whole cached tunnel chain; per-chain segment loops remain in the exact evaluator")); - Coverage->SetStringField(TEXT("unbinned_writers"), TEXT("passage/landing posts, disturbances, and player diff edits stay in canonical GetDensityAt tail; this is the candidate-bin ablation, not the full region compiler")); + Coverage->SetStringField(TEXT("unbinned_writers"), TEXT("passage/landing posts, disturbances, and player diff edits retain their canonical per-sample order inside the chunk block writer pass")); Coverage->SetBoolField(TEXT("route_landing_floor_probes"), PrimaryPassageCount > 0); Coverage->SetBoolField(TEXT("strata_top_and_bottom_boundary_sweep"), true); Coverage->SetBoolField(TEXT("player_carve_and_fill_edits"), true); diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.cpp index d02634c..5116389 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.cpp @@ -75,13 +75,13 @@ namespace VoxelForgeRegionEval return static_cast(Dimension) * Dimension * Dimension; } - struct FRegionBrickSample + struct FRegionChunkSample { int32 LocalIndex = 0; int32 OutputIndex = 0; }; - struct FRegionBrick + struct FRegionChunk { FIntVector Origin = FIntVector::ZeroValue; int32 SizeX = 0; @@ -90,7 +90,7 @@ namespace VoxelForgeRegionEval int32 LatticeX = 0; int32 LatticeY = 0; int32 LatticeZ = 0; - TArray RequestedSamples; + TArray RequestedSamples; }; } @@ -148,8 +148,9 @@ namespace VoxelForgeRegionEval float ReachScale, TArray* OutDensities, FTiming& OutTiming, - bool bBrickProgram, - int32 RequestedWorkerCount) + bool bRegionProgram, + int32 RequestedWorkerCount, + bool bRequestWholeRunCounters) { OutTiming = FTiming(); const int32 NumWorkers = FMath::Clamp(RequestedWorkerCount, 1, 32); @@ -167,58 +168,70 @@ namespace VoxelForgeRegionEval { return false; } + const VoxelDensityProfile::EMode PreviousProfileMode = VoxelDensityProfile::GetMode(); + const uint32 PreviousProfileInterval = VoxelDensityProfile::GetSampleInterval(); + if (OutDensities != nullptr && SampleCount > MAX_int32) + { + return false; + } + const bool bCollectWholeRunCounters = bRequestWholeRunCounters && bFullRing; + if (bCollectWholeRunCounters) + { + VoxelDensityProfile::Reset(); + VoxelDensityProfile::SetMode(VoxelDensityProfile::EMode::Attribution, 16384); + } const double StartSeconds = FPlatformTime::Seconds(); - TArray Bricks; - TArray WorkBricks; - if (bBrickProgram) + TArray Regions; + TArray WorkRegions; + if (bRegionProgram) { const int32 LastX = OriginVoxels.X + Dimension - 1; const int32 LastY = OriginVoxels.Y + Dimension - 1; const int32 LastZ = OriginVoxels.Z + Dimension - 1; - const int32 FirstCellX = FMath::FloorToInt(static_cast(OriginVoxels.X) / 8.0f); - const int32 FirstCellY = FMath::FloorToInt(static_cast(OriginVoxels.Y) / 8.0f); - const int32 FirstCellZ = FMath::FloorToInt(static_cast(OriginVoxels.Z) / 8.0f); - const int32 LastCellX = FMath::FloorToInt(static_cast(LastX) / 8.0f); - const int32 LastCellY = FMath::FloorToInt(static_cast(LastY) / 8.0f); - const int32 LastCellZ = FMath::FloorToInt(static_cast(LastZ) / 8.0f); + const int32 FirstCellX = FMath::FloorToInt(static_cast(OriginVoxels.X) / CHUNK_SIZE); + const int32 FirstCellY = FMath::FloorToInt(static_cast(OriginVoxels.Y) / CHUNK_SIZE); + const int32 FirstCellZ = FMath::FloorToInt(static_cast(OriginVoxels.Z) / CHUNK_SIZE); + const int32 LastCellX = FMath::FloorToInt(static_cast(LastX) / CHUNK_SIZE); + const int32 LastCellY = FMath::FloorToInt(static_cast(LastY) / CHUNK_SIZE); + const int32 LastCellZ = FMath::FloorToInt(static_cast(LastZ) / CHUNK_SIZE); const int32 CellsX = LastCellX - FirstCellX + 1; const int32 CellsY = LastCellY - FirstCellY + 1; const int32 CellsZ = LastCellZ - FirstCellZ + 1; - Bricks.Reserve(CellsX * CellsY * CellsZ); + Regions.Reserve(CellsX * CellsY * CellsZ); for (int32 CellZ = FirstCellZ; CellZ <= LastCellZ; ++CellZ) for (int32 CellY = FirstCellY; CellY <= LastCellY; ++CellY) for (int32 CellX = FirstCellX; CellX <= LastCellX; ++CellX) { - FRegionBrick& Brick = Bricks.AddDefaulted_GetRef(); - const int32 CellMinX = CellX * 8; - const int32 CellMinY = CellY * 8; - const int32 CellMinZ = CellZ * 8; - Brick.Origin = FIntVector( + FRegionChunk& Region = Regions.AddDefaulted_GetRef(); + const int32 CellMinX = CellX * CHUNK_SIZE; + const int32 CellMinY = CellY * CHUNK_SIZE; + const int32 CellMinZ = CellZ * CHUNK_SIZE; + Region.Origin = FIntVector( FMath::Max(CellMinX, OriginVoxels.X), FMath::Max(CellMinY, OriginVoxels.Y), FMath::Max(CellMinZ, OriginVoxels.Z)); - const int32 EndX = FMath::Min(CellMinX + 8, LastX + 1); - const int32 EndY = FMath::Min(CellMinY + 8, LastY + 1); - const int32 EndZ = FMath::Min(CellMinZ + 8, LastZ + 1); - Brick.SizeX = EndX - Brick.Origin.X; - Brick.SizeY = EndY - Brick.Origin.Y; - Brick.SizeZ = EndZ - Brick.Origin.Z; - Brick.LatticeX = Brick.Origin.X - OriginVoxels.X; - Brick.LatticeY = Brick.Origin.Y - OriginVoxels.Y; - Brick.LatticeZ = Brick.Origin.Z - OriginVoxels.Z; + const int32 EndX = FMath::Min(CellMinX + CHUNK_SIZE, LastX + 1); + const int32 EndY = FMath::Min(CellMinY + CHUNK_SIZE, LastY + 1); + const int32 EndZ = FMath::Min(CellMinZ + CHUNK_SIZE, LastZ + 1); + Region.SizeX = EndX - Region.Origin.X; + Region.SizeY = EndY - Region.Origin.Y; + Region.SizeZ = EndZ - Region.Origin.Z; + Region.LatticeX = Region.Origin.X - OriginVoxels.X; + Region.LatticeY = Region.Origin.Y - OriginVoxels.Y; + Region.LatticeZ = Region.Origin.Z - OriginVoxels.Z; } if (bFullRing) { - WorkBricks.Reserve(Bricks.Num()); - for (int32 BrickIndex = 0; BrickIndex < Bricks.Num(); ++BrickIndex) + WorkRegions.Reserve(Regions.Num()); + for (int32 RegionIndex = 0; RegionIndex < Regions.Num(); ++RegionIndex) { - WorkBricks.Add(BrickIndex); + WorkRegions.Add(RegionIndex); } } else { - WorkBricks.Reserve(Bricks.Num()); + WorkRegions.Reserve(Regions.Num()); for (int32 OutputIndex = 0; OutputIndex < SampleIndices->Num(); ++OutputIndex) { const int64 LatticeIndex = (*SampleIndices)[OutputIndex]; @@ -233,25 +246,25 @@ namespace VoxelForgeRegionEval const int32 WorldX = OriginVoxels.X + X; const int32 WorldY = OriginVoxels.Y + Y; const int32 WorldZ = OriginVoxels.Z + Z; - const int32 CellX = FMath::FloorToInt(static_cast(WorldX) / 8.0f); - const int32 CellY = FMath::FloorToInt(static_cast(WorldY) / 8.0f); - const int32 CellZ = FMath::FloorToInt(static_cast(WorldZ) / 8.0f); - const int32 BrickIndex = ((CellZ - FirstCellZ) * CellsY + const int32 CellX = FMath::FloorToInt(static_cast(WorldX) / CHUNK_SIZE); + const int32 CellY = FMath::FloorToInt(static_cast(WorldY) / CHUNK_SIZE); + const int32 CellZ = FMath::FloorToInt(static_cast(WorldZ) / CHUNK_SIZE); + const int32 RegionIndex = ((CellZ - FirstCellZ) * CellsY + (CellY - FirstCellY)) * CellsX + (CellX - FirstCellX); - FRegionBrick& Brick = Bricks[BrickIndex]; - const int32 LocalX = WorldX - Brick.Origin.X; - const int32 LocalY = WorldY - Brick.Origin.Y; - const int32 LocalZ = WorldZ - Brick.Origin.Z; - FRegionBrickSample& BrickSample = Brick.RequestedSamples.AddDefaulted_GetRef(); - BrickSample.LocalIndex = - (LocalZ * Brick.SizeY + LocalY) * Brick.SizeX + LocalX; - BrickSample.OutputIndex = OutputIndex; + FRegionChunk& Region = Regions[RegionIndex]; + const int32 LocalX = WorldX - Region.Origin.X; + const int32 LocalY = WorldY - Region.Origin.Y; + const int32 LocalZ = WorldZ - Region.Origin.Z; + FRegionChunkSample& ChunkSample = Region.RequestedSamples.AddDefaulted_GetRef(); + ChunkSample.LocalIndex = + (LocalZ * Region.SizeY + LocalY) * Region.SizeX + LocalX; + ChunkSample.OutputIndex = OutputIndex; } - for (int32 BrickIndex = 0; BrickIndex < Bricks.Num(); ++BrickIndex) + for (int32 RegionIndex = 0; RegionIndex < Regions.Num(); ++RegionIndex) { - if (!Bricks[BrickIndex].RequestedSamples.IsEmpty()) + if (!Regions[RegionIndex].RequestedSamples.IsEmpty()) { - WorkBricks.Add(BrickIndex); + WorkRegions.Add(RegionIndex); } } } @@ -275,6 +288,8 @@ namespace VoxelForgeRegionEval TArray WorkerNonFinite; TArray WorkerEvaluatedSamples; TArray WorkerFallbackSamples; + TArray WorkerGeneralPathEntries; + TArray WorkerSupportedBlockSetupEntries; TArray WorkerEvaluationFailures; WorkerSeconds.SetNumZeroed(NumWorkers); WorkerCpuSeconds.SetNumZeroed(NumWorkers); @@ -283,6 +298,8 @@ namespace VoxelForgeRegionEval WorkerNonFinite.SetNumZeroed(NumWorkers); WorkerEvaluatedSamples.SetNumZeroed(NumWorkers); WorkerFallbackSamples.SetNumZeroed(NumWorkers); + WorkerGeneralPathEntries.SetNumZeroed(NumWorkers); + WorkerSupportedBlockSetupEntries.SetNumZeroed(NumWorkers); WorkerEvaluationFailures.SetNumZeroed(NumWorkers); bool bAllWorkerCpuFromOsThreadTimes = true; ParallelFor(NumWorkers, [&](int32 WorkerIndex) @@ -291,51 +308,78 @@ namespace VoxelForgeRegionEval // inherit its task-level sampler registration. Keep this registration scoped to the // research worker partition; with sampling disabled it is one relaxed flag load. FScopedVoxelStackRegistration StackRegistration; + FIntVector PreviousTileOrigin = FIntVector::ZeroValue; + int32 PreviousTileStep = 1; + int32 PreviousTileCells = 0; + VoxelGenLOD::GetThreadTileCacheWindow( + PreviousTileOrigin, PreviousTileStep, PreviousTileCells); + const int32 PreviousOctaveBias = VoxelGenLOD::GetThreadOctaveBias(); VoxelCaveMorphology::SetResearchBrickBinningEnabled(bPrepared); VoxelCaveMorphology::SetResearchBrickRoomReachScale(ReachScale); + VoxelGenLOD::SetThreadSampleStep(1); + VoxelGenLOD::SetThreadOctaveBias(0); + VoxelGenLOD::SetThreadTileContext(OriginVoxels, Dimension); + VoxelGenLOD::SetResearchRegionCacheWindowEnabled(bRegionProgram && bPrepared); WorkerThreadIds[WorkerIndex] = FPlatformTLS::GetCurrentThreadId(); - const int64 WorkCount = bBrickProgram ? WorkBricks.Num() : SampleCount; + const int64 WorkCount = bRegionProgram ? WorkRegions.Num() : SampleCount; const int64 Begin = (WorkCount * WorkerIndex) / NumWorkers; const int64 End = (WorkCount * (WorkerIndex + 1)) / NumWorkers; const double WorkerStart = FPlatformTime::Seconds(); double WorkerCpuStart = 0.0; const bool bHaveWorkerCpuStart = GetCurrentThreadCpuSeconds(WorkerCpuStart); - TArray BrickDensities; + TArray RegionDensities; for (int64 OutputIndex = Begin; OutputIndex < End; ++OutputIndex) { - if (bBrickProgram) + if (bRegionProgram) { - const FRegionBrick& Brick = Bricks[WorkBricks[static_cast(OutputIndex)]]; - int32 BrickFallbackSamples = 0; - if (!Generator.EvaluateDensityBrick( - Brick.Origin, Brick.SizeX, Brick.SizeY, Brick.SizeZ, - BrickDensities, BrickFallbackSamples) - || BrickDensities.Num() != Brick.SizeX * Brick.SizeY * Brick.SizeZ) + const FRegionChunk& Region = Regions[WorkRegions[static_cast(OutputIndex)]]; + if (bPrepared) + { + // Build/cache one bounded graph window for the current CHUNK_SIZE region. + // A ring-wide context made each worker retain a duplicate ring-sized graph + // even though its density block only consumes this one chunk. + const FIntVector CacheWindowOrigin( + FMath::FloorToInt(static_cast(Region.Origin.X) / CHUNK_SIZE) * CHUNK_SIZE, + FMath::FloorToInt(static_cast(Region.Origin.Y) / CHUNK_SIZE) * CHUNK_SIZE, + FMath::FloorToInt(static_cast(Region.Origin.Z) / CHUNK_SIZE) * CHUNK_SIZE); + VoxelGenLOD::SetThreadTileContext( + CacheWindowOrigin, CHUNK_SIZE); + } + int32 RegionFallbackSamples = 0; + uint64 RegionGeneralPathEntries = 0; + uint64 RegionSupportedSetupEntries = 0; + if (!Generator.EvaluateDensityChunk( + Region.Origin, Region.SizeX, Region.SizeY, Region.SizeZ, + RegionDensities, RegionFallbackSamples, + RegionGeneralPathEntries, RegionSupportedSetupEntries) + || RegionDensities.Num() != Region.SizeX * Region.SizeY * Region.SizeZ) { ++WorkerEvaluationFailures[WorkerIndex]; continue; } - const int32 BrickSampleCount = BrickDensities.Num(); - WorkerEvaluatedSamples[WorkerIndex] += static_cast(BrickSampleCount); - WorkerFallbackSamples[WorkerIndex] += static_cast(BrickFallbackSamples); + const int32 RegionSampleCount = RegionDensities.Num(); + WorkerEvaluatedSamples[WorkerIndex] += static_cast(RegionSampleCount); + WorkerFallbackSamples[WorkerIndex] += static_cast(RegionFallbackSamples); + WorkerGeneralPathEntries[WorkerIndex] += RegionGeneralPathEntries; + WorkerSupportedBlockSetupEntries[WorkerIndex] += RegionSupportedSetupEntries; if (bFullRing && OutDensities != nullptr) { - for (int32 Bz = 0; Bz < Brick.SizeZ; ++Bz) - for (int32 By = 0; By < Brick.SizeY; ++By) - for (int32 Bx = 0; Bx < Brick.SizeX; ++Bx) + for (int32 Bz = 0; Bz < Region.SizeZ; ++Bz) + for (int32 By = 0; By < Region.SizeY; ++By) + for (int32 Bx = 0; Bx < Region.SizeX; ++Bx) { - const int32 LocalIndex = (Bz * Brick.SizeY + By) * Brick.SizeX + Bx; + const int32 LocalIndex = (Bz * Region.SizeY + By) * Region.SizeX + Bx; const int64 LatticeIndex = LinearIndex( - Brick.LatticeX + Bx, Brick.LatticeY + By, - Brick.LatticeZ + Bz, Dimension); - (*OutDensities)[static_cast(LatticeIndex)] = BrickDensities[LocalIndex]; + Region.LatticeX + Bx, Region.LatticeY + By, + Region.LatticeZ + Bz, Dimension); + (*OutDensities)[static_cast(LatticeIndex)] = RegionDensities[LocalIndex]; } } else if (OutDensities != nullptr) { - for (const FRegionBrickSample& Sample : Brick.RequestedSamples) + for (const FRegionChunkSample& Sample : Region.RequestedSamples) { - (*OutDensities)[Sample.OutputIndex] = BrickDensities[Sample.LocalIndex]; + (*OutDensities)[Sample.OutputIndex] = RegionDensities[Sample.LocalIndex]; } } continue; @@ -355,6 +399,8 @@ namespace VoxelForgeRegionEval static_cast(OriginX + X), static_cast(OriginY + Y), static_cast(OriginZ + Z)); + ++WorkerEvaluatedSamples[WorkerIndex]; + ++WorkerGeneralPathEntries[WorkerIndex]; if (OutDensities != nullptr) { (*OutDensities)[static_cast(OutputIndex)] = Density; @@ -373,20 +419,34 @@ namespace VoxelForgeRegionEval } VoxelCaveMorphology::SetResearchBrickBinningEnabled(false); VoxelCaveMorphology::SetResearchBrickRoomReachScale(1.0f); + VoxelGenLOD::SetResearchRegionCacheWindowEnabled(false); + VoxelGenLOD::SetThreadTileContext(PreviousTileOrigin, PreviousTileCells); + VoxelGenLOD::SetThreadSampleStep(PreviousTileStep); + VoxelGenLOD::SetThreadOctaveBias(PreviousOctaveBias); }, EParallelForFlags::None); + VoxelDensityProfile::FSnapshot ProfileSnapshot; + if (bCollectWholeRunCounters) + { + ProfileSnapshot = VoxelDensityProfile::Snapshot(); + VoxelDensityProfile::SetMode(PreviousProfileMode, PreviousProfileInterval); + VoxelDensityProfile::Reset(); + } + OutTiming.WallSeconds = FPlatformTime::Seconds() - StartSeconds; OutTiming.SampleCount = SampleCount; - OutTiming.bBrickProgram = bBrickProgram; - OutTiming.BrickCount = bBrickProgram ? WorkBricks.Num() : 0; + OutTiming.bRegionProgram = bRegionProgram; + OutTiming.bHasWholeRunCounters = bCollectWholeRunCounters; + OutTiming.RegionCount = bRegionProgram ? WorkRegions.Num() : 0; for (int32 WorkerIndex = 0; WorkerIndex < NumWorkers; ++WorkerIndex) { OutTiming.WorkerCpuSeconds += WorkerCpuSeconds[WorkerIndex]; bAllWorkerCpuFromOsThreadTimes &= WorkerCpuFromOsThreadTimes[WorkerIndex] != 0; OutTiming.NonFiniteSamples += WorkerNonFinite[WorkerIndex]; - OutTiming.EvaluatedSamples += bBrickProgram - ? static_cast(WorkerEvaluatedSamples[WorkerIndex]) : 0; + OutTiming.EvaluatedSamples += static_cast(WorkerEvaluatedSamples[WorkerIndex]); OutTiming.FallbackSamples += static_cast(WorkerFallbackSamples[WorkerIndex]); + OutTiming.GeneralPathEntries += WorkerGeneralPathEntries[WorkerIndex]; + OutTiming.SupportedBlockSetupEntries += WorkerSupportedBlockSetupEntries[WorkerIndex]; if (WorkerEvaluationFailures[WorkerIndex] != 0) { return false; @@ -403,10 +463,75 @@ namespace VoxelForgeRegionEval { OutTiming.FallbackShare = static_cast(OutTiming.FallbackSamples) / static_cast(OutTiming.EvaluatedSamples); + OutTiming.GeneralPathEntriesPerSample = static_cast(OutTiming.GeneralPathEntries) + / static_cast(OutTiming.EvaluatedSamples); } OutTiming.NanosecondsPerSample = OutTiming.SampleCount > 0 ? OutTiming.WorkerCpuSeconds * 1.0e9 / static_cast(OutTiming.SampleCount) : 0.0; + + if (bCollectWholeRunCounters) + { + using VoxelDensityProfile::ECounter; + using VoxelDensityProfile::EBucket; + const auto Counter = [&ProfileSnapshot](ECounter Id) + { + return ProfileSnapshot.Counters[static_cast(Id)]; + }; + OutTiming.SdfCacheBuilds = Counter(ECounter::SdfCacheBuild); + OutTiming.ActiveOpListBuilds = Counter(ECounter::OpBlockBuilds); + OutTiming.SdfSpatialIndexBuilds = Counter(ECounter::SdfSpatialIndexBuild); + OutTiming.SdfBrickIndexBuilds = Counter(ECounter::SdfBrickIndexBuild); + OutTiming.BroadPhaseIndexBuilds = OutTiming.SdfSpatialIndexBuilds + + OutTiming.SdfBrickIndexBuilds; + OutTiming.RoomCandidateVisits = Counter(ECounter::CaveRoomCandidates); + OutTiming.RoomEvaluations = Counter(ECounter::CaveRoomEvaluated); + OutTiming.TunnelCandidateVisits = Counter(ECounter::CaveTunnelCandidates); + OutTiming.TunnelEvaluations = Counter(ECounter::CaveTunnelEvaluated); + OutTiming.TunnelCoreCandidateVisits = Counter(ECounter::TunnelCoreCandidates); + OutTiming.TunnelCoreEvaluations = Counter(ECounter::TunnelCoreEvaluated); + OutTiming.PassageCandidateVisits = Counter(ECounter::PassageCandidates); + OutTiming.PassageEvaluations = Counter(ECounter::PassageEvaluated); + OutTiming.TotalCandidateVisits = OutTiming.RoomCandidateVisits + + OutTiming.TunnelCandidateVisits + OutTiming.TunnelCoreCandidateVisits + + OutTiming.PassageCandidateVisits; + OutTiming.TotalCandidateEvaluations = OutTiming.RoomEvaluations + + OutTiming.TunnelEvaluations + OutTiming.TunnelCoreEvaluations + + OutTiming.PassageEvaluations; + if (OutTiming.SampleCount > 0) + { + OutTiming.CandidateVisitsPerSample = static_cast(OutTiming.TotalCandidateVisits) + / static_cast(OutTiming.SampleCount); + OutTiming.CandidateEvaluationsPerSample = static_cast(OutTiming.TotalCandidateEvaluations) + / static_cast(OutTiming.SampleCount); + } + const auto AttributedNsPerSample = [&ProfileSnapshot, SampleCount](EBucket Bucket) + { + return SampleCount > 0 + ? FPlatformTime::ToSeconds64(ProfileSnapshot.Cycles[static_cast(Bucket)]) + * 1.0e9 / static_cast(SampleCount) + : 0.0; + }; + OutTiming.GetDensityAtNsPerSample = AttributedNsPerSample(EBucket::GetDensityAt); + OutTiming.OperatorBlockNsPerSample = AttributedNsPerSample(EBucket::OperatorBlock); + OutTiming.RoomGraphSourceNsPerSample = AttributedNsPerSample(EBucket::RoomGraphSource); + OutTiming.RoomGraphBuildNsPerSample = AttributedNsPerSample(EBucket::RoomGraphBuild); + OutTiming.RoomGraphSdfNsPerSample = AttributedNsPerSample(EBucket::RoomGraphSdf); + OutTiming.FusedEvaluatorNsPerSample = AttributedNsPerSample(EBucket::FusedEvaluator); + OutTiming.TunnelCoreSdfNsPerSample = AttributedNsPerSample(EBucket::TunnelCoreSdf); + OutTiming.TunnelCoreWorldNsPerSample = AttributedNsPerSample(EBucket::TunnelCoreWorld); + OutTiming.TunnelCorePostsNsPerSample = AttributedNsPerSample(EBucket::TunnelCorePosts); + OutTiming.PassageModifierNsPerSample = AttributedNsPerSample(EBucket::PassageModifier); + OutTiming.PassageStructuralPostsNsPerSample = AttributedNsPerSample(EBucket::PassageStructuralPosts); + OutTiming.PassageFloorNsPerSample = AttributedNsPerSample(EBucket::PassageLandingFloor); + OutTiming.StructuralPostsNsPerSample = AttributedNsPerSample(EBucket::DensityStructuralPosts); + OutTiming.DensityPrologueNsPerSample = AttributedNsPerSample(EBucket::DensityPrologue); + OutTiming.DensityCoreNsPerSample = AttributedNsPerSample(EBucket::DensityCore); + OutTiming.DisturbancesNsPerSample = AttributedNsPerSample(EBucket::DensityDisturbances); + OutTiming.DensityBoundarySealNsPerSample = AttributedNsPerSample(EBucket::DensityBoundarySeal); + OutTiming.DiffLayerNsPerSample = AttributedNsPerSample(EBucket::DensityDiffLayer); + OutTiming.DensityTailNsPerSample = AttributedNsPerSample(EBucket::DensityTail); + } if (OutDensities != nullptr) { for (const float Density : *OutDensities) @@ -452,16 +577,24 @@ namespace VoxelForgeRegionEval } using VoxelDensityProfile::ECounter; Result.Room = Snapshot.Counters[static_cast(ECounter::CaveRoomCandidates)]; + Result.RoomEvaluated = Snapshot.Counters[static_cast(ECounter::CaveRoomEvaluated)]; Result.Tunnel = Snapshot.Counters[static_cast(ECounter::CaveTunnelCandidates)]; + Result.TunnelEvaluated = Snapshot.Counters[static_cast(ECounter::CaveTunnelEvaluated)]; Result.TunnelCore = Snapshot.Counters[static_cast(ECounter::TunnelCoreCandidates)]; + Result.TunnelCoreEvaluated = Snapshot.Counters[static_cast(ECounter::TunnelCoreEvaluated)]; Result.Passage = Snapshot.Counters[static_cast(ECounter::PassageCandidates)]; + Result.PassageEvaluated = Snapshot.Counters[static_cast(ECounter::PassageEvaluated)]; Result.TunnelCacheLookups = Snapshot.Counters[static_cast(ECounter::TunnelCacheLookup)]; Result.TunnelCacheHits = Snapshot.Counters[static_cast(ECounter::TunnelCacheHit)]; Result.TunnelCacheMisses = Snapshot.Counters[static_cast(ECounter::TunnelCacheMiss)]; Result.SdfCacheBuilds = Snapshot.Counters[static_cast(ECounter::SdfCacheBuild)]; Result.Total = Result.Room + Result.Tunnel + Result.TunnelCore + Result.Passage; + Result.TotalEvaluated = Result.RoomEvaluated + Result.TunnelEvaluated + + Result.TunnelCoreEvaluated + Result.PassageEvaluated; Result.PerSample = Result.SampleCount > 0 ? static_cast(Result.Total) / Result.SampleCount : 0.0; + Result.EvaluatedPerSample = Result.SampleCount > 0 + ? static_cast(Result.TotalEvaluated) / Result.SampleCount : 0.0; Result.AttributionSampleInterval = Snapshot.SampleInterval; const auto AttributedNanosecondsPerSample = [&](VoxelDensityProfile::EBucket Bucket) { diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.h b/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.h index 6fd5cab..5b76f62 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.h +++ b/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.h @@ -18,33 +18,79 @@ namespace VoxelForgeRegionEval int32 ObservedWorkerThreads = 0; int64 SampleCount = 0; int64 EvaluatedSamples = 0; - int64 BrickCount = 0; + int64 RegionCount = 0; int64 FallbackSamples = 0; + uint64 GeneralPathEntries = 0; + uint64 SupportedBlockSetupEntries = 0; double FallbackShare = 0.0; + double GeneralPathEntriesPerSample = 0.0; double WallSeconds = 0.0; double WorkerCpuSeconds = 0.0; double NanosecondsPerSample = 0.0; bool bWorkerCpuFromOsThreadTimes = false; - bool bBrickProgram = false; + bool bRegionProgram = false; + bool bHasWholeRunCounters = false; uint64 QuantizedHash = 0; uint64 RawFloatHash = 0; uint64 SaturatedSamples = 0; uint64 NonFiniteSamples = 0; + uint64 SdfCacheBuilds = 0; + uint64 ActiveOpListBuilds = 0; + uint64 BroadPhaseIndexBuilds = 0; + uint64 SdfSpatialIndexBuilds = 0; + uint64 SdfBrickIndexBuilds = 0; + uint64 RoomCandidateVisits = 0; + uint64 RoomEvaluations = 0; + uint64 TunnelCandidateVisits = 0; + uint64 TunnelEvaluations = 0; + uint64 TunnelCoreCandidateVisits = 0; + uint64 TunnelCoreEvaluations = 0; + uint64 PassageCandidateVisits = 0; + uint64 PassageEvaluations = 0; + uint64 TotalCandidateVisits = 0; + uint64 TotalCandidateEvaluations = 0; + double CandidateVisitsPerSample = 0.0; + double CandidateEvaluationsPerSample = 0.0; + double GetDensityAtNsPerSample = 0.0; + double OperatorBlockNsPerSample = 0.0; + double RoomGraphSourceNsPerSample = 0.0; + double RoomGraphBuildNsPerSample = 0.0; + double RoomGraphSdfNsPerSample = 0.0; + double FusedEvaluatorNsPerSample = 0.0; + double TunnelCoreSdfNsPerSample = 0.0; + double TunnelCoreWorldNsPerSample = 0.0; + double TunnelCorePostsNsPerSample = 0.0; + double PassageModifierNsPerSample = 0.0; + double PassageStructuralPostsNsPerSample = 0.0; + double PassageFloorNsPerSample = 0.0; + double StructuralPostsNsPerSample = 0.0; + double DensityPrologueNsPerSample = 0.0; + double DensityCoreNsPerSample = 0.0; + double DisturbancesNsPerSample = 0.0; + double DensityBoundarySealNsPerSample = 0.0; + double DiffLayerNsPerSample = 0.0; + double DensityTailNsPerSample = 0.0; }; struct FCandidateVisits { int64 SampleCount = 0; uint64 Room = 0; + uint64 RoomEvaluated = 0; uint64 Tunnel = 0; + uint64 TunnelEvaluated = 0; uint64 TunnelCore = 0; + uint64 TunnelCoreEvaluated = 0; uint64 Passage = 0; + uint64 PassageEvaluated = 0; uint64 TunnelCacheLookups = 0; uint64 TunnelCacheHits = 0; uint64 TunnelCacheMisses = 0; uint64 SdfCacheBuilds = 0; uint64 Total = 0; + uint64 TotalEvaluated = 0; double PerSample = 0.0; + double EvaluatedPerSample = 0.0; uint32 AttributionSampleInterval = 0; double GetDensityAtAttributedNsPerSample = 0.0; double DensityPrologueAttributedNsPerSample = 0.0; @@ -57,11 +103,10 @@ namespace VoxelForgeRegionEval }; /** - * Evaluate a deterministic lattice through the live final-field API. The prepared mode only - * enables the commandlet-only 8^3 SDF candidate bins; all field writers and their order remain - * the live GetDensityAt implementation. FullRing uses brick-grouped output ownership and - * writes every lattice density exactly once. SampleIndices are sorted lattice offsets for the - * statistical correctness / candidate-counter sweeps. + * Evaluate a deterministic lattice through the live final-field API. The prepared mode uses + * the commandlet-only 8^3 SDF candidate bins and chunk-sized operator blocks. FullRing assigns + * each chunk to one worker and writes every lattice density exactly once. SampleIndices are + * sorted lattice offsets for the statistical correctness / candidate-counter sweeps. */ bool Evaluate( const UVoxelGenerator& Generator, @@ -73,8 +118,9 @@ namespace VoxelForgeRegionEval float ReachScale, TArray* OutDensities, FTiming& OutTiming, - bool bBrickProgram = false, - int32 RequestedWorkerCount = WorkerCount); + bool bRegionProgram = false, + int32 RequestedWorkerCount = WorkerCount, + bool bCollectWholeRunCounters = false); /** Count exact room/tunnel/core/passage candidate visits with instrumentation enabled. */ FCandidateVisits CountCandidates(