measure(step2b): per-brick setup rebuilds the chunk caches -- 10x slower; live cost attributed

Editor-only brick API (<= 8^3 points per call, TunnelNetwork/Underwater op-stack routes, 1.16%
scalar fallback). Exact: 0 Q16/sign disagreements on the full 259^3 ring and the five-seed probe
set, same hash on 1 and 6 workers, reach shrink still caught (18,107 Q16 / 735 sign).
Speed: 69.19 s wall, 17.6 us/sample vs live 7.04 s, 1.65 us/sample. Stack sampler (inclusive):
BuildChunkCache 93.5%, PropagateSdfOverBox 92.5%, BuildActiveOpList 92.6% -- each brick rebuilds
the cache windows and active-op lists; EvalBlock itself is 3.6%.
Live path attribution (inclusive, 68,934 stacks): op-stack RoomGraph EvalBlock 54.4%, SDF and
spatial candidate scans ~28%, tunnel core 18.4%, passage floor segment 15.0%, structural posts
14.6%, noise octaves 2.6%, disturbances 0.6%, diff layer 0.3%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-09-30 03:10:22 +02:00
1 parent 41a9265ebd
commit fd9f809c1c
7 files changed
+746 -94

No files matched your search

+4 -3
View File
@@ -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.uplugin` | Plugin manifest. Runtime module `VoxelForge` plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. |
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. | | `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`, and `-SharedDDC`; 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` runs the isolated step-2 lattice comparison and candidate-bin ablation. | `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.
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). | | `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 against live `GetDensityAt`. | | `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.*` | Six-partition lattice driver for the commandlet-only step-2 experiment: cold 259³ timing, bitwise repeat hashes, fixed 16-bit quantized comparisons, and exact candidate-visit attribution. It calls the live final-field API in both modes; the prepared mode selects editor-only 8³ SDF candidate bins. | | `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. |
| `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/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). | | `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,6 +363,7 @@ This is **where terrain shape lives.**
| `ApplyBoundarySeal` (static) | 996 | Solidifies strate top/bottom shells. | | `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. | | `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)`. | | **`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. |
| **`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. | | **`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. | | `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. | | `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. |
+307 -66
View File
@@ -641,6 +641,8 @@ thread_local int32 VoxelGenLOD::OctaveBias = 0;
struct FVoxelDensityBlockSession struct FVoxelDensityBlockSession
{ {
bool bActive = false; bool bActive = false;
bool bBatchRequested = false;
bool bBatchSucceeded = false;
FIntVector Origin = FIntVector::ZeroValue; FIntVector Origin = FIntVector::ZeroValue;
int32 Step = 1; int32 Step = 1;
int32 SizeX = 0; int32 SizeX = 0;
@@ -650,6 +652,7 @@ struct FVoxelDensityBlockSession
TArray<uint8> Valid; TArray<uint8> Valid;
TArray<FVoxelOpSample> BlockScratch; TArray<FVoxelOpSample> BlockScratch;
TArray<int32> ActiveOps; TArray<int32> ActiveOps;
TArray<float> BatchResults;
void Begin(FIntVector InOrigin, int32 InStep, int32 InSizeX, int32 InSizeY, int32 InSizeZ) void Begin(FIntVector InOrigin, int32 InStep, int32 InSizeX, int32 InSizeY, int32 InSizeZ)
{ {
@@ -665,6 +668,9 @@ struct FVoxelDensityBlockSession
Valid.Reset(); Valid.Reset();
BlockScratch.Reset(); BlockScratch.Reset();
ActiveOps.Reset(); ActiveOps.Reset();
BatchResults.Reset();
bBatchRequested = false;
bBatchSucceeded = false;
return; return;
} }
@@ -672,6 +678,9 @@ struct FVoxelDensityBlockSession
// (gaps, out-of-layout, degenerate strates) use the ordinary scalar path and should not // (gaps, out-of-layout, degenerate strates) use the ordinary scalar path and should not
// pay for a block scratch lattice merely because the mesher opened the scoped hand-off. // pay for a block scratch lattice merely because the mesher opened the scoped hand-off.
BlockScratch.Reset(); BlockScratch.Reset();
BatchResults.Reset();
bBatchRequested = false;
bBatchSucceeded = false;
} }
void End() void End()
@@ -683,6 +692,9 @@ struct FVoxelDensityBlockSession
Valid.Reset(); Valid.Reset();
BlockScratch.Reset(); BlockScratch.Reset();
ActiveOps.Reset(); ActiveOps.Reset();
BatchResults.Reset();
bBatchRequested = false;
bBatchSucceeded = false;
} }
int32 Index(int32 X, int32 Y, int32 Z) const int32 Index(int32 X, int32 Y, int32 Z) const
@@ -2709,6 +2721,77 @@ void UVoxelGenerator::EndDensityBlock() const
GVoxelDensityBlockSession.End(); GVoxelDensityBlockSession.End();
} }
#if WITH_EDITOR
bool UVoxelGenerator::EvaluateDensityBrick(
FIntVector OriginVoxels, int32 SizeX, int32 SizeY, int32 SizeZ,
TArray<float>& OutDensities, int32& OutFallbackSamples) const
{
OutDensities.Reset();
OutFallbackSamples = 0;
const int64 SampleCount64 = static_cast<int64>(SizeX) * SizeY * SizeZ;
if (SizeX <= 0 || SizeY <= 0 || SizeZ <= 0
|| SizeX > 8 || SizeY > 8 || SizeZ > 8 || SampleCount64 > 512)
{
return false;
}
const FIntVector Last = OriginVoxels + FIntVector(SizeX - 1, SizeY - 1, SizeZ - 1);
const auto SameCell = [](int32 A, int32 B, int32 CellSize)
{
return FMath::FloorToInt(static_cast<float>(A) / static_cast<float>(CellSize))
== FMath::FloorToInt(static_cast<float>(B) / static_cast<float>(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)
|| !SameCell(OriginVoxels.Y, Last.Y, CHUNK_SIZE)
|| !SameCell(OriginVoxels.Z, Last.Z, CHUNK_SIZE))
{
return false;
}
const int32 SampleCount = static_cast<int32>(SampleCount64);
BeginDensityBlock(OriginVoxels, 1, SizeX, SizeY, SizeZ);
GVoxelDensityBlockSession.bBatchRequested = true;
GVoxelDensityBlockSession.bBatchSucceeded = false;
GVoxelDensityBlockSession.BatchResults.SetNumUninitialized(SampleCount);
const float FirstDensity = GetDensityAt(
static_cast<float>(OriginVoxels.X),
static_cast<float>(OriginVoxels.Y),
static_cast<float>(OriginVoxels.Z));
if (GVoxelDensityBlockSession.bBatchSucceeded)
{
OutDensities.SetNumUninitialized(SampleCount);
FMemory::Memcpy(
OutDensities.GetData(), GVoxelDensityBlockSession.BatchResults.GetData(),
static_cast<SIZE_T>(SampleCount) * sizeof(float));
EndDensityBlock();
return OutDensities.Num() == SampleCount;
}
// Recipes without the editor brick port 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);
OutDensities[0] = FirstDensity;
for (int32 Index = 1; Index < SampleCount; ++Index)
{
const int32 X = Index % SizeX;
const int32 Y = (Index / SizeX) % SizeY;
const int32 Z = Index / (SizeX * SizeY);
OutDensities[Index] = GetDensityAt(
static_cast<float>(OriginVoxels.X + X),
static_cast<float>(OriginVoxels.Y + Y),
static_cast<float>(OriginVoxels.Z + Z));
}
OutFallbackSamples = SampleCount;
EndDensityBlock();
return true;
}
#endif
float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ, float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
float* OutPreEditDensity) const float* OutPreEditDensity) const
{ {
@@ -2789,6 +2872,12 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
bIsGapChunk = StrateManager->IsGapChunk(ChunkCoord); bIsGapChunk = StrateManager->IsGapChunk(ChunkCoord);
} }
#if WITH_EDITOR
bool bEvaluateDensityBatch = false;
int32 BatchSampleCount = 0;
int32 DensityBatchCount = 1;
bool bBrickHasDiffMods = false;
#endif
if (StrateManager && bIsGapChunk) if (StrateManager && bIsGapChunk)
{ {
// SOLID BEDROCK gap between two strates. The auto-carved passages still tunnel // SOLID BEDROCK gap between two strates. The auto-carved passages still tunnel
@@ -3361,6 +3450,160 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
} }
DensityCoreTimer.End(); DensityCoreTimer.End();
#if WITH_EDITOR
// The editor region program asks this one routed sample to fill every point in the
// already-evaluated operator block. Its bricks are contained by one 8-voxel research
// bin and one 32-voxel chunk, so the stack, params, route, and cache key above are shared.
BatchSampleCount = GVoxelDensityBlockSession.SizeX
* GVoxelDensityBlockSession.SizeY * GVoxelDensityBlockSession.SizeZ;
bEvaluateDensityBatch = GVoxelDensityBlockSession.bBatchRequested
&& bUsedOpBlockSample && !bUsedFusedEvaluator && CP_UseOpStack
&& !bIsGapChunk && StrateManager != nullptr
&& (CP_GenType == ECaveGeneratorType::TunnelNetwork
|| CP_GenType == ECaveGeneratorType::Underwater)
&& GVoxelDensityBlockSession.Samples.Num() == BatchSampleCount
&& GVoxelDensityBlockSession.BatchResults.Num() == BatchSampleCount;
DensityBatchCount = bEvaluateDensityBatch ? BatchSampleCount : 1;
bool bBrickHasDisturbanceCandidate = true;
bool bBrickHasPassagePostCandidate = true;
if (bEvaluateDensityBatch)
{
const FIntVector BrickLast = GVoxelDensityBlockSession.Origin + FIntVector(
(GVoxelDensityBlockSession.SizeX - 1) * GVoxelDensityBlockSession.Step,
(GVoxelDensityBlockSession.SizeY - 1) * GVoxelDensityBlockSession.Step,
(GVoxelDensityBlockSession.SizeZ - 1) * GVoxelDensityBlockSession.Step);
const FBox BrickBox(
FVector(GVoxelDensityBlockSession.Origin.X,
GVoxelDensityBlockSession.Origin.Y,
GVoxelDensityBlockSession.Origin.Z),
FVector(BrickLast.X, BrickLast.Y, BrickLast.Z));
bBrickHasDisturbanceCandidate = !VoxelDensityAblation::IsDisturbancesOff()
&& VF_AnyDisturbanceCanTouchBox(
BrickBox, CP_Dist, static_cast<uint32>(Seed));
const bool bAllPassagePostsOff =
VoxelDensityAblation::IsPassageStructuralPostsOff()
&& VoxelDensityAblation::IsNativeFloorOff()
&& VoxelDensityAblation::IsLandingPostsOff();
if (bAllPassagePostsOff)
{
bBrickHasPassagePostCandidate = false;
}
else if (!VoxelDensityAblation::IsPassageCarvingOff())
{
bBrickHasPassagePostCandidate = StrateManager->AnyPassageNearBox(
BrickBox.Min, BrickBox.Max);
}
bBrickHasDiffMods = DiffLayer != nullptr && DiffLayer->HasAnyMods();
}
const bool bApplyBrickDisturbances =
!bEvaluateDensityBatch || bBrickHasDisturbanceCandidate;
const bool bApplyBrickPassagePosts =
!bEvaluateDensityBatch || bBrickHasPassagePostCandidate;
#else
const bool bApplyBrickDisturbances = true;
const bool bApplyBrickPassagePosts = true;
#endif
// Chunk parameters and composer/custom recipe structure are fixed by the route above.
// Resolve the landing writer's base once for the whole brick, including edge bricks.
float BrickLandingBaseDensity = CP_Dist.BaseDensity;
#if WITH_EDITOR
if (CP_UseComposerRegions && CP_ComposerRegions.bHasGlobalStructuralParams)
{
BrickLandingBaseDensity = CP_ComposerRegions.BaseDensity;
}
else
#endif
if (CP_UseCustomRecipe)
{
switch (CP_CustomRecipe.StructuralParamBlock)
{
case EVoxelStrateParamBlock::TunnelNetwork:
BrickLandingBaseDensity = CP_CustomParams.TunnelNetworkParams.BaseDensity;
break;
case EVoxelStrateParamBlock::Slab:
BrickLandingBaseDensity = CP_CustomParams.SlabParams.BaseDensity;
break;
case EVoxelStrateParamBlock::Maze:
BrickLandingBaseDensity = CP_CustomParams.MazeParams.BaseDensity;
break;
case EVoxelStrateParamBlock::Surface:
BrickLandingBaseDensity = CP_CustomParams.SurfaceParams.BaseDensity;
break;
case EVoxelStrateParamBlock::VerticalShaft:
BrickLandingBaseDensity = CP_CustomParams.VerticalShaftParams.BaseDensity;
break;
case EVoxelStrateParamBlock::FloatingIsland:
BrickLandingBaseDensity = CP_CustomParams.FloatingIslandParams.BaseDensity;
break;
default:
break;
}
}
else
{
switch (CP_GenType)
{
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
BrickLandingBaseDensity = CP_Slab.BaseDensity;
break;
case ECaveGeneratorType::Maze:
BrickLandingBaseDensity = CP_Maze.BaseDensity;
break;
case ECaveGeneratorType::SurfaceWorld:
BrickLandingBaseDensity = CP_Surface.BaseDensity;
break;
case ECaveGeneratorType::VerticalShafts:
BrickLandingBaseDensity = CP_Vert.BaseDensity;
break;
case ECaveGeneratorType::FloatingIslands:
BrickLandingBaseDensity = CP_Float.BaseDensity;
break;
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
default:
BrickLandingBaseDensity = CP_Tunnel.BaseDensity;
break;
}
}
for (int32 DensityBatchIndex = 0;
#if WITH_EDITOR
DensityBatchIndex < DensityBatchCount;
#else
DensityBatchIndex < 1;
#endif
++DensityBatchIndex)
{
#if WITH_EDITOR
if (bEvaluateDensityBatch)
{
const int32 BatchX = DensityBatchIndex % GVoxelDensityBlockSession.SizeX;
const int32 BatchY = (DensityBatchIndex / GVoxelDensityBlockSession.SizeX)
% GVoxelDensityBlockSession.SizeY;
const int32 BatchZ = DensityBatchIndex
/ (GVoxelDensityBlockSession.SizeX * GVoxelDensityBlockSession.SizeY);
WorldX = static_cast<float>(GVoxelDensityBlockSession.Origin.X
+ BatchX * GVoxelDensityBlockSession.Step);
WorldY = static_cast<float>(GVoxelDensityBlockSession.Origin.Y
+ BatchY * GVoxelDensityBlockSession.Step);
WorldZ = static_cast<float>(GVoxelDensityBlockSession.Origin.Z
+ BatchZ * GVoxelDensityBlockSession.Step);
const FVoxelOpSample& BatchSample =
GVoxelDensityBlockSession.Samples[DensityBatchIndex];
Result = -BatchSample.Density;
bUsedOpBlockSample = true;
bBlockHasTunnelCore = BatchSample.bHasTunnelCoreWorldEvaluation;
BlockTunnelCoreSDF = BatchSample.TunnelCoreWorldSDF;
bBlockTunnelCoreRoomFloor = BatchSample.bTunnelCoreRoomFloor;
bBlockHasTunnelCoreSweptFloor = BatchSample.bHasTunnelCoreSweptFloor;
BlockTunnelCoreSweptFloorZ = BatchSample.TunnelCoreSweptFloorZ;
BlockTunnelCoreSweptFloorRadius = BatchSample.TunnelCoreSweptFloorRadius;
bUsedFusedEvaluator = false;
bRoomOwnsBottomValid = false;
}
#endif
// Resolve the graph's authored floor before the disturbance pass. The operator-stack // Resolve the graph's authored floor before the disturbance pass. The operator-stack
// source has already computed this exact result while publishing its sample; native // source has already computed this exact result while publishing its sample; native
// tunnel generation uses the prepared immutable cache. Keeping the result here means the // tunnel generation uses the prepared immutable cache. Keeping the result here means the
@@ -3420,8 +3663,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
VoxelDensityProfile::EBucket::DensityDisturbances); VoxelDensityProfile::EBucket::DensityDisturbances);
const bool bProtectVerticalShaftAir = const bool bProtectVerticalShaftAir =
VoxelPassageGeometry::VerticalShaftConnectorAirMarker(); VoxelPassageGeometry::VerticalShaftConnectorAirMarker();
if (bApplyBrickDisturbances)
{
ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed, ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed,
bProtectVerticalShaftAir); bProtectVerticalShaftAir);
}
DensityDisturbancesTimer.End(); DensityDisturbancesTimer.End();
// A disturbance is allowed to add visual detail, but it must not refill the landing's // A disturbance is allowed to add visual detail, but it must not refill the landing's
@@ -3432,71 +3678,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
VoxelDensityProfile::EBucket::DensityStructuralPosts); VoxelDensityProfile::EBucket::DensityStructuralPosts);
VoxelDensityProfile::FScopedTimer StructuralTailTimer( VoxelDensityProfile::FScopedTimer StructuralTailTimer(
VoxelDensityProfile::EBucket::StructuralTail); VoxelDensityProfile::EBucket::StructuralTail);
float LandingBaseDensity = CP_Dist.BaseDensity; const float LandingBaseDensity = BrickLandingBaseDensity;
#if WITH_EDITOR
// A composer parent can deliberately use a different structural base than the authored
// definition. The landing floor was already applied by PassageCarveOp with that parent
// base; use the same value for this post-disturbance MC backstop or live-vs-direct stack
// evaluation would differ exactly on floor voxels.
if (CP_UseComposerRegions && CP_ComposerRegions.bHasGlobalStructuralParams)
{
LandingBaseDensity = CP_ComposerRegions.BaseDensity;
}
else
#endif
if (CP_UseCustomRecipe)
{
switch (CP_CustomRecipe.StructuralParamBlock)
{
case EVoxelStrateParamBlock::TunnelNetwork:
LandingBaseDensity = CP_CustomParams.TunnelNetworkParams.BaseDensity;
break;
case EVoxelStrateParamBlock::Slab:
LandingBaseDensity = CP_CustomParams.SlabParams.BaseDensity;
break;
case EVoxelStrateParamBlock::Maze:
LandingBaseDensity = CP_CustomParams.MazeParams.BaseDensity;
break;
case EVoxelStrateParamBlock::Surface:
LandingBaseDensity = CP_CustomParams.SurfaceParams.BaseDensity;
break;
case EVoxelStrateParamBlock::VerticalShaft:
LandingBaseDensity = CP_CustomParams.VerticalShaftParams.BaseDensity;
break;
case EVoxelStrateParamBlock::FloatingIsland:
LandingBaseDensity = CP_CustomParams.FloatingIslandParams.BaseDensity;
break;
default:
break;
}
}
else
{
switch (CP_GenType)
{
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
LandingBaseDensity = CP_Slab.BaseDensity;
break;
case ECaveGeneratorType::Maze:
LandingBaseDensity = CP_Maze.BaseDensity;
break;
case ECaveGeneratorType::SurfaceWorld:
LandingBaseDensity = CP_Surface.BaseDensity;
break;
case ECaveGeneratorType::VerticalShafts:
LandingBaseDensity = CP_Vert.BaseDensity;
break;
case ECaveGeneratorType::FloatingIslands:
LandingBaseDensity = CP_Float.BaseDensity;
break;
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
default:
LandingBaseDensity = CP_Tunnel.BaseDensity;
break;
}
}
auto ApplyTunnelCoreShape = [&](const FTunnelCoreWorldEvaluation& Core) auto ApplyTunnelCoreShape = [&](const FTunnelCoreWorldEvaluation& Core)
{ {
if (!VoxelMath::IsFinite(Core.SDF)) if (!VoxelMath::IsFinite(Core.SDF))
@@ -3538,6 +3720,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ, CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius); CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
} }
if (bApplyBrickPassagePosts)
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageStructuralPosts); VoxelDensityProfile::EBucket::PassageStructuralPosts);
@@ -3567,12 +3750,14 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ, CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius); CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
} }
if (bApplyBrickPassagePosts)
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingFloor); VoxelDensityProfile::EBucket::PassageLandingFloor);
StrateManager->ApplyPassageLandingFloorMC( StrateManager->ApplyPassageLandingFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity); Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
} }
if (bApplyBrickPassagePosts)
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingRoomFloor); VoxelDensityProfile::EBucket::PassageLandingRoomFloor);
@@ -3647,6 +3832,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
// composed from the immutable construction-time profile after generic writers and the // composed from the immutable construction-time profile after generic writers and the
// graph overlap, so the legacy passage support slab can be disabled without losing the // graph overlap, so the legacy passage support slab can be disabled without losing the
// floor to a disturbance refill. // floor to a disturbance refill.
if (bApplyBrickPassagePosts)
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingFloor); VoxelDensityProfile::EBucket::PassageLandingFloor);
@@ -3655,6 +3841,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
} }
StructuralTailTimer.End(); StructuralTailTimer.End();
DensityStructuralPostsTimer.End(); DensityStructuralPostsTimer.End();
#if WITH_EDITOR
if (bEvaluateDensityBatch)
{
GVoxelDensityBlockSession.BatchResults[DensityBatchIndex] = Result;
}
#endif
}
} }
else else
{ {
@@ -3673,6 +3866,33 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
DensityCoreTimer.End(); DensityCoreTimer.End();
} }
#if WITH_EDITOR
const int32 FinalDensityBatchCount = bEvaluateDensityBatch ? BatchSampleCount : 1;
#else
const int32 FinalDensityBatchCount = 1;
#endif
for (int32 FinalDensityBatchIndex = 0;
FinalDensityBatchIndex < FinalDensityBatchCount;
++FinalDensityBatchIndex)
{
#if WITH_EDITOR
if (bEvaluateDensityBatch)
{
const int32 BatchX = FinalDensityBatchIndex % GVoxelDensityBlockSession.SizeX;
const int32 BatchY = (FinalDensityBatchIndex / GVoxelDensityBlockSession.SizeX)
% GVoxelDensityBlockSession.SizeY;
const int32 BatchZ = FinalDensityBatchIndex
/ (GVoxelDensityBlockSession.SizeX * GVoxelDensityBlockSession.SizeY);
WorldX = static_cast<float>(GVoxelDensityBlockSession.Origin.X
+ BatchX * GVoxelDensityBlockSession.Step);
WorldY = static_cast<float>(GVoxelDensityBlockSession.Origin.Y
+ BatchY * GVoxelDensityBlockSession.Step);
WorldZ = static_cast<float>(GVoxelDensityBlockSession.Origin.Z
+ BatchZ * GVoxelDensityBlockSession.Step);
Result = GVoxelDensityBlockSession.BatchResults[FinalDensityBatchIndex];
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
}
#endif
// The edge is the final generated structural invariant. Passage carving is ordered before it // The edge is the final generated structural invariant. Passage carving is ordered before it
// in every archetype/stack, and this MC-facing pass also covers out-of-layout air plus any // in every archetype/stack, and this MC-facing pass also covers out-of-layout air plus any
// disturbance that might otherwise carve back into the shell. Player edits remain separate // disturbance that might otherwise carve back into the shell. Player edits remain separate
@@ -3704,7 +3924,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
{ {
*OutPreEditDensity = Result; *OutPreEditDensity = Result;
} }
if (DiffLayer && DiffLayer->HasAnyMods()) const bool bDiffLayerHasModsForSample = DiffLayer != nullptr &&
#if WITH_EDITOR
(bEvaluateDensityBatch ? bBrickHasDiffMods : DiffLayer->HasAnyMods());
#else
DiffLayer->HasAnyMods();
#endif
if (DiffLayer && bDiffLayerHasModsForSample)
{ {
// ── PER-CHUNK MOD SNAPSHOT ── // ── PER-CHUNK MOD SNAPSHOT ──
// The old HasModifications + GetDensityOffset pair took the diff layer's RWLock + a TMap find // The old HasModifications + GetDensityOffset pair took the diff layer's RWLock + a TMap find
@@ -3740,6 +3966,21 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
} }
DensityDiffLayerTimer.End(); DensityDiffLayerTimer.End();
#if WITH_EDITOR
if (bEvaluateDensityBatch)
{
GVoxelDensityBlockSession.BatchResults[FinalDensityBatchIndex] = Result;
}
#endif
}
#if WITH_EDITOR
if (bEvaluateDensityBatch)
{
GVoxelDensityBlockSession.bBatchSucceeded = true;
}
#endif
VoxelDensityProfile::FScopedTimer DensityTailTimer( VoxelDensityProfile::FScopedTimer DensityTailTimer(
VoxelDensityProfile::EBucket::DensityTail); VoxelDensityProfile::EBucket::DensityTail);
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
@@ -180,6 +180,13 @@ public:
int32 SizeX, int32 SizeY, int32 SizeZ) const; int32 SizeX, int32 SizeY, int32 SizeZ) const;
void EndDensityBlock() const; 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<float>& OutDensities,
int32& OutFallbackSamples) const;
#endif
/** /**
* Prove that the mesher's core lattice is entirely solid from the cheap, structural rules * Prove that the mesher's core lattice is entirely solid from the cheap, structural rules
* that own inter-strate gaps and boundary seals. This never samples density. It is intended * that own inter-strate gaps and boundary seals. This never samples density. It is intended
@@ -1475,6 +1475,40 @@ struct FRegionEvalPass
FString EndedUtc; FString EndedUtc;
}; };
struct FRegionEvalStackSamplerSession
{
bool bStarted = false;
~FRegionEvalStackSamplerSession()
{
if (bStarted)
{
FVoxelStackSampler::Get().StopAndWrite();
}
}
bool Start(uint32 IntervalUs, const FString& OutputDirectory,
const TCHAR* Scenario = TEXT("region_eval_live_ring"))
{
bStarted = FVoxelStackSampler::Get().Start(
IntervalUs, OutputDirectory, Scenario);
return bStarted;
}
bool Stop(FVoxelStackSampler::FSummary& OutSummary)
{
if (!bStarted)
{
return false;
}
OutSummary = FVoxelStackSampler::Get().StopAndWrite();
bStarted = false;
return OutSummary.bOutputWritten
&& OutSummary.RegisteredThreadSamples > 0
&& OutSummary.RetainedSamples > 0;
}
};
int32 FindRoomGraphRecipeIndex(int32 Seed) int32 FindRoomGraphRecipeIndex(int32 Seed)
{ {
for (int32 Index = 0; Index < 64; ++Index) for (int32 Index = 0; Index < 64; ++Index)
@@ -1724,6 +1758,7 @@ bool RunRegionEvalPass(
const FExploreArguments& BaseArguments, const FExploreArguments& BaseArguments,
int32 Seed, int32 Seed,
bool bPrepared, bool bPrepared,
bool bBrickProgram,
float ReachScale, float ReachScale,
bool bFullRing, bool bFullRing,
const TArray<int64>* RequestedIndices, const TArray<int64>* RequestedIndices,
@@ -1731,7 +1766,8 @@ bool RunRegionEvalPass(
bool bCountCandidates, bool bCountCandidates,
bool bKeepDensities, bool bKeepDensities,
FRegionEvalPass& OutPass, FRegionEvalPass& OutPass,
FString& OutError) FString& OutError,
int32 RequestedWorkerCount = VoxelForgeRegionEval::WorkerCount)
{ {
OutPass = FRegionEvalPass(); OutPass = FRegionEvalPass();
FExploreWorld World; FExploreWorld World;
@@ -1773,7 +1809,7 @@ bool RunRegionEvalPass(
bFullRing ? nullptr : &OutPass.SampleIndices, bFullRing ? nullptr : &OutPass.SampleIndices,
bFullRing, bPrepared, ReachScale, bFullRing, bPrepared, ReachScale,
bKeepDensities ? &OutPass.Densities : nullptr, bKeepDensities ? &OutPass.Densities : nullptr,
OutPass.Timing); OutPass.Timing, bBrickProgram, RequestedWorkerCount);
OutPass.EndedUtc = FDateTime::UtcNow().ToIso8601(); OutPass.EndedUtc = FDateTime::UtcNow().ToIso8601();
if (!bEvaluated) if (!bEvaluated)
{ {
@@ -1799,19 +1835,27 @@ bool RunRegionEvalPass(
TSharedRef<FJsonObject> RegionEvalTimingJson(const FRegionEvalPass& Pass, bool bPrepared) TSharedRef<FJsonObject> RegionEvalTimingJson(const FRegionEvalPass& Pass, bool bPrepared)
{ {
TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>();
Json->SetStringField(TEXT("path"), bPrepared ? TEXT("prepared_8cube_bins") : TEXT("live_GetDensityAt")); Json->SetStringField(TEXT("path"), Pass.Timing.bBrickProgram
? TEXT("editor_region_brick_program")
: (bPrepared ? TEXT("prepared_8cube_bins") : TEXT("live_GetDensityAt")));
Json->SetStringField(TEXT("started_utc"), Pass.StartedUtc); Json->SetStringField(TEXT("started_utc"), Pass.StartedUtc);
Json->SetStringField(TEXT("ended_utc"), Pass.EndedUtc); Json->SetStringField(TEXT("ended_utc"), Pass.EndedUtc);
Json->SetNumberField(TEXT("worker_count"), Pass.Timing.Workers); Json->SetNumberField(TEXT("worker_count"), Pass.Timing.Workers);
Json->SetNumberField(TEXT("observed_worker_threads"), Pass.Timing.ObservedWorkerThreads); Json->SetNumberField(TEXT("observed_worker_threads"), Pass.Timing.ObservedWorkerThreads);
Json->SetNumberField(TEXT("samples"), static_cast<double>(Pass.Timing.SampleCount)); Json->SetNumberField(TEXT("samples"), static_cast<double>(Pass.Timing.SampleCount));
Json->SetNumberField(TEXT("evaluated_brick_points"), static_cast<double>(Pass.Timing.EvaluatedSamples));
Json->SetNumberField(TEXT("brick_count"), static_cast<double>(Pass.Timing.BrickCount));
Json->SetNumberField(TEXT("fallback_points"), static_cast<double>(Pass.Timing.FallbackSamples));
Json->SetNumberField(TEXT("fallback_share"), Pass.Timing.FallbackShare);
Json->SetNumberField(TEXT("world_setup_seconds_before_density_ring"), Pass.WorldPrepareSeconds); Json->SetNumberField(TEXT("world_setup_seconds_before_density_ring"), Pass.WorldPrepareSeconds);
Json->SetNumberField(TEXT("wall_seconds"), Pass.Timing.WallSeconds); Json->SetNumberField(TEXT("wall_seconds"), Pass.Timing.WallSeconds);
Json->SetNumberField(TEXT("worker_cpu_seconds"), Pass.Timing.WorkerCpuSeconds); Json->SetNumberField(TEXT("worker_cpu_seconds"), Pass.Timing.WorkerCpuSeconds);
Json->SetNumberField(TEXT("worker_cpu_ns_per_sample"), Pass.Timing.NanosecondsPerSample); Json->SetNumberField(TEXT("worker_cpu_ns_per_sample"), Pass.Timing.NanosecondsPerSample);
Json->SetStringField(TEXT("worker_cpu_source"), Pass.Timing.bWorkerCpuFromOsThreadTimes const FString WorkerCpuSource = Pass.Timing.bWorkerCpuFromOsThreadTimes
? TEXT("summed Windows GetThreadTimes user+kernel CPU across six fixed work partitions") ? FString::Printf(TEXT("summed Windows GetThreadTimes user+kernel CPU across %d fixed work partitions"),
: TEXT("summed worker elapsed fallback; OS thread CPU counters unavailable")); Pass.Timing.Workers)
: TEXT("summed worker elapsed fallback; OS thread CPU counters unavailable");
Json->SetStringField(TEXT("worker_cpu_source"), WorkerCpuSource);
Json->SetStringField(TEXT("quantized_hash_fnv1a64"), Json->SetStringField(TEXT("quantized_hash_fnv1a64"),
FString::Printf(TEXT("%016llx"), static_cast<unsigned long long>(Pass.Timing.QuantizedHash))); FString::Printf(TEXT("%016llx"), static_cast<unsigned long long>(Pass.Timing.QuantizedHash)));
Json->SetStringField(TEXT("raw_float_hash_fnv1a64"), Json->SetStringField(TEXT("raw_float_hash_fnv1a64"),
@@ -1910,8 +1954,112 @@ int32 RunRegionEval(const FString& Params)
int32 RequestedSamples = 262144; int32 RequestedSamples = 262144;
FParse::Value(*Params, TEXT("regionsamples="), RequestedSamples); FParse::Value(*Params, TEXT("regionsamples="), RequestedSamples);
RequestedSamples = FMath::Clamp(RequestedSamples, 32768, 1000000); RequestedSamples = FMath::Clamp(RequestedSamples, 32768, 1000000);
int32 RequestedWorkerCount = VoxelForgeRegionEval::WorkerCount;
FParse::Value(*Params, TEXT("regionworkers="), RequestedWorkerCount);
RequestedWorkerCount = FMath::Clamp(RequestedWorkerCount, 1, 32);
IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true); IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true);
if (FParse::Param(*Params, TEXT("regionhashprobe")))
{
FRegionEvalStackSamplerSession WorkerHashStackSampler;
if (Arguments.bSampleStacks
&& !WorkerHashStackSampler.Start(
Arguments.SampleStacksIntervalUs, Arguments.OutDirectory,
TEXT("region_eval_brick_worker_hash")))
{
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeRegionEval] worker hash stack sampler could not be started."));
return 2;
}
FRegionEvalPass WorkerHashPass;
const bool bHashPassSucceeded = RunRegionEvalPass(
Arguments, 1337, true, true, 1.0f, true, nullptr,
RequestedSamples, false, true, WorkerHashPass, Error,
RequestedWorkerCount);
bool bStackCaptureSucceeded = true;
if (Arguments.bSampleStacks)
{
FVoxelStackSampler::FSummary StackSummary;
bStackCaptureSucceeded = WorkerHashStackSampler.Stop(StackSummary);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeRegionEval] worker-hash stack capture registered=%llu retained=%llu "
"raw=%s summary=%s"),
static_cast<unsigned long long>(StackSummary.RegisteredThreadSamples),
static_cast<unsigned long long>(StackSummary.RetainedSamples),
*StackSummary.RawSamplesPath, *StackSummary.SummaryPath);
}
if (!bHashPassSucceeded || !bStackCaptureSucceeded)
{
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeRegionEval] worker hash probe failed: %s"),
Error.IsEmpty() ? TEXT("stack capture retained no worker samples") : *Error);
return 2;
}
FString ReferenceRawHash;
FParse::Value(*Params, TEXT("regionreferencehash="), ReferenceRawHash);
const FString ActualRawHash = FString::Printf(TEXT("%016llx"),
static_cast<unsigned long long>(WorkerHashPass.Timing.RawFloatHash));
const bool bHasReferenceHash = !ReferenceRawHash.IsEmpty();
const bool bMatchesReference = bHasReferenceHash
&& ActualRawHash.Equals(ReferenceRawHash, ESearchCase::IgnoreCase);
const bool bProbePassed = bMatchesReference
&& WorkerHashPass.Timing.SampleCount
== static_cast<int64>(VoxelForgeRegionEval::RingDimension)
* VoxelForgeRegionEval::RingDimension * VoxelForgeRegionEval::RingDimension
&& WorkerHashPass.Timing.SaturatedSamples == 0
&& WorkerHashPass.Timing.NonFiniteSamples == 0;
TSharedRef<FJsonObject> ProbeReport = MakeShared<FJsonObject>();
ProbeReport->SetBoolField(TEXT("pass"), bProbePassed);
ProbeReport->SetBoolField(TEXT("worker_count_hash_probe"), true);
ProbeReport->SetNumberField(TEXT("worker_count"), WorkerHashPass.Timing.Workers);
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("fallback_points"), WorkerHashPass.Timing.FallbackSamples);
ProbeReport->SetNumberField(TEXT("fallback_share"), WorkerHashPass.Timing.FallbackShare);
ProbeReport->SetNumberField(TEXT("wall_seconds"), WorkerHashPass.Timing.WallSeconds);
ProbeReport->SetNumberField(TEXT("worker_cpu_seconds"), WorkerHashPass.Timing.WorkerCpuSeconds);
ProbeReport->SetStringField(TEXT("reference_raw_float_hash_fnv1a64"), ReferenceRawHash);
ProbeReport->SetStringField(TEXT("raw_float_hash_fnv1a64"), ActualRawHash);
ProbeReport->SetBoolField(TEXT("matches_reference"), bMatchesReference);
FString ProbeJsonText;
TSharedRef<TJsonWriter<>> ProbeWriter = TJsonWriterFactory<>::Create(&ProbeJsonText);
if (!FJsonSerializer::Serialize(ProbeReport, ProbeWriter)
|| !FFileHelper::SaveStringToFile(
ProbeJsonText, *FPaths::Combine(Arguments.OutDirectory, TEXT("region_eval.json")),
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeRegionEval] could not write worker hash probe report."));
return 2;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeRegionEval] worker hash probe workers=%d observed=%d samples=%lld "
"raw=%s reference_match=%s report=%s"),
WorkerHashPass.Timing.Workers, WorkerHashPass.Timing.ObservedWorkerThreads,
static_cast<long long>(WorkerHashPass.Timing.SampleCount), *ActualRawHash,
bMatchesReference ? TEXT("yes") : TEXT("no"),
*FPaths::Combine(Arguments.OutDirectory, TEXT("region_eval.json")));
return bProbePassed ? 0 : 2;
}
// Region-eval returns before the normal Explore sampler setup. Capture only its two cold live
// ring trials so prepared-path and multi-seed work cannot pollute the baseline attribution.
FRegionEvalStackSamplerSession StackSamplerSession;
if (Arguments.bSampleStacks
&& !StackSamplerSession.Start(
Arguments.SampleStacksIntervalUs, Arguments.OutDirectory))
{
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeRegionEval] requested stack sampler could not be started."));
return 2;
}
const int32 Seeds[] = { 1337, 7331, 19427, 30241, 41011 }; const int32 Seeds[] = { 1337, 7331, 19427, 30241, 41011 };
TArray<TSharedPtr<FJsonValue>> SeedReports; TArray<TSharedPtr<FJsonValue>> SeedReports;
TArray<TSharedPtr<FJsonValue>> OldTimingReports; TArray<TSharedPtr<FJsonValue>> OldTimingReports;
@@ -1938,8 +2086,8 @@ int32 RunRegionEval(const FString& Params)
{ {
FRegionEvalPass Pass; FRegionEvalPass Pass;
const bool bOk = RunRegionEvalPass( const bool bOk = RunRegionEvalPass(
Arguments, Seeds[0], false, 1.0f, true, nullptr, Arguments, Seeds[0], false, false, 1.0f, true, nullptr,
RequestedSamples, Trial == 0, true, Pass, Error); RequestedSamples, Trial == 0 && !Arguments.bSampleStacks, true, Pass, Error);
if (!bOk) if (!bOk)
{ {
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] live full-ring trial failed: %s"), *Error); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] live full-ring trial failed: %s"), *Error);
@@ -1964,6 +2112,22 @@ int32 RunRegionEval(const FString& Params)
} }
OldTimingReports.Add(MakeShared<FJsonValueObject>(RegionEvalTimingJson(Pass, false))); OldTimingReports.Add(MakeShared<FJsonValueObject>(RegionEvalTimingJson(Pass, false)));
} }
if (Arguments.bSampleStacks)
{
FVoxelStackSampler::FSummary StackSummary;
if (!StackSamplerSession.Stop(StackSummary))
{
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeRegionEval] stack capture retained no live-ring worker samples."));
return 2;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeRegionEval] live-ring stack capture registered=%llu retained=%llu "
"raw=%s summary=%s"),
static_cast<unsigned long long>(StackSummary.RegisteredThreadSamples),
static_cast<unsigned long long>(StackSummary.RetainedSamples),
*StackSummary.RawSamplesPath, *StackSummary.SummaryPath);
}
if (PrimarySampleIndices.IsEmpty()) if (PrimarySampleIndices.IsEmpty())
{ {
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] primary statistical sample set is empty.")); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] primary statistical sample set is empty."));
@@ -1974,7 +2138,7 @@ int32 RunRegionEval(const FString& Params)
{ {
FRegionEvalPass Pass; FRegionEvalPass Pass;
const bool bOk = RunRegionEvalPass( const bool bOk = RunRegionEvalPass(
Arguments, Seeds[0], true, 1.0f, true, nullptr, Arguments, Seeds[0], true, true, 1.0f, true, nullptr,
RequestedSamples, Trial == 0, true, Pass, Error); RequestedSamples, Trial == 0, true, Pass, Error);
if (!bOk) if (!bOk)
{ {
@@ -2003,11 +2167,46 @@ int32 RunRegionEval(const FString& Params)
// Statistical fixed-lattice comparisons across four additional independent seeds. The sample // 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, // set is a deterministic affine permutation of the entire 259^3 ring plus dense strata,
// passage-floor, landing, carve, and fill probes. // passage-floor, landing, carve, and fill probes.
if (Arguments.bSampleStacks)
{
FRegionEvalStackSamplerSession BrickStackSamplerSession;
if (!BrickStackSamplerSession.Start(
Arguments.SampleStacksIntervalUs, Arguments.OutDirectory,
TEXT("region_eval_brick_ring")))
{
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeRegionEval] brick stack sampler could not be started."));
return 2;
}
FRegionEvalPass ProfiledBrickPass;
if (!RunRegionEvalPass(
Arguments, Seeds[0], true, true, 1.0f, true, nullptr,
RequestedSamples, false, false, ProfiledBrickPass, Error))
{
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeRegionEval] brick attribution run failed: %s"), *Error);
return 2;
}
FVoxelStackSampler::FSummary BrickStackSummary;
if (!BrickStackSamplerSession.Stop(BrickStackSummary))
{
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeRegionEval] brick stack capture retained no worker samples."));
return 2;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeRegionEval] brick stack capture registered=%llu retained=%llu "
"raw=%s summary=%s"),
static_cast<unsigned long long>(BrickStackSummary.RegisteredThreadSamples),
static_cast<unsigned long long>(BrickStackSummary.RetainedSamples),
*BrickStackSummary.RawSamplesPath, *BrickStackSummary.SummaryPath);
}
for (int32 SeedIndex = 1; SeedIndex < UE_ARRAY_COUNT(Seeds); ++SeedIndex) for (int32 SeedIndex = 1; SeedIndex < UE_ARRAY_COUNT(Seeds); ++SeedIndex)
{ {
FRegionEvalPass OldPass; FRegionEvalPass OldPass;
if (!RunRegionEvalPass( if (!RunRegionEvalPass(
Arguments, Seeds[SeedIndex], false, 1.0f, false, nullptr, Arguments, Seeds[SeedIndex], false, false, 1.0f, false, nullptr,
RequestedSamples, true, true, OldPass, Error)) RequestedSamples, true, true, OldPass, Error))
{ {
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] seed %d live sample failed: %s"), UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] seed %d live sample failed: %s"),
@@ -2017,7 +2216,7 @@ int32 RunRegionEval(const FString& Params)
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
FRegionEvalPass NewPass; FRegionEvalPass NewPass;
if (!RunRegionEvalPass( if (!RunRegionEvalPass(
Arguments, Seeds[SeedIndex], true, 1.0f, false, &OldPass.SampleIndices, Arguments, Seeds[SeedIndex], true, true, 1.0f, false, &OldPass.SampleIndices,
RequestedSamples, true, true, NewPass, Error)) RequestedSamples, true, true, NewPass, Error))
{ {
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] seed %d prepared sample failed: %s"), UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] seed %d prepared sample failed: %s"),
@@ -2051,7 +2250,7 @@ int32 RunRegionEval(const FString& Params)
// commandlet pass and every worker resets it to 1.0 afterward. // commandlet pass and every worker resets it to 1.0 afterward.
FRegionEvalPass ShrunkPass; FRegionEvalPass ShrunkPass;
if (!RunRegionEvalPass( if (!RunRegionEvalPass(
Arguments, Seeds[0], true, 0.50f, false, &PrimarySampleIndices, Arguments, Seeds[0], true, true, 0.50f, false, &PrimarySampleIndices,
RequestedSamples, false, true, ShrunkPass, Error)) RequestedSamples, false, true, ShrunkPass, Error))
{ {
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] deliberate reach-shrink run failed: %s"), *Error); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] deliberate reach-shrink run failed: %s"), *Error);
@@ -2110,7 +2309,7 @@ int32 RunRegionEval(const FString& Params)
TSharedRef<FJsonObject> Report = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> Report = MakeShared<FJsonObject>();
Report->SetNumberField(TEXT("schema_version"), 1); Report->SetNumberField(TEXT("schema_version"), 1);
Report->SetStringField(TEXT("route"), TEXT("editor-only explore commandlet; required Unreal composer, manager, and generator types")); Report->SetStringField(TEXT("route"), TEXT("editor-only explore commandlet; required Unreal composer, manager, and generator types"));
Report->SetStringField(TEXT("evaluator"), TEXT("live GetDensityAt final field with editor-only 8^3 SDF room/join/tunnel candidate bins")); 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("comparison_quantization"), TEXT("signed int16; round ties away from zero; multiply density by 16; saturate outside [-2048, 2047.9375] density voxels")); 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("worker_count"), VoxelForgeRegionEval::WorkerCount);
Report->SetNumberField(TEXT("ring_dimension"), VoxelForgeRegionEval::RingDimension); Report->SetNumberField(TEXT("ring_dimension"), VoxelForgeRegionEval::RingDimension);
@@ -5,6 +5,7 @@
#include "VoxelCaveMorphology.h" #include "VoxelCaveMorphology.h"
#include "VoxelDensityProfile.h" #include "VoxelDensityProfile.h"
#include "VoxelGenerator.h" #include "VoxelGenerator.h"
#include "VoxelStackSampler.h"
#include "HAL/PlatformTLS.h" #include "HAL/PlatformTLS.h"
#if PLATFORM_WINDOWS #if PLATFORM_WINDOWS
@@ -73,6 +74,24 @@ namespace VoxelForgeRegionEval
{ {
return static_cast<int64>(Dimension) * Dimension * Dimension; return static_cast<int64>(Dimension) * Dimension * Dimension;
} }
struct FRegionBrickSample
{
int32 LocalIndex = 0;
int32 OutputIndex = 0;
};
struct FRegionBrick
{
FIntVector Origin = FIntVector::ZeroValue;
int32 SizeX = 0;
int32 SizeY = 0;
int32 SizeZ = 0;
int32 LatticeX = 0;
int32 LatticeY = 0;
int32 LatticeZ = 0;
TArray<FRegionBrickSample> RequestedSamples;
};
} }
int64 LinearIndex(int32 X, int32 Y, int32 Z, int32 Dimension) int64 LinearIndex(int32 X, int32 Y, int32 Z, int32 Dimension)
@@ -128,9 +147,13 @@ namespace VoxelForgeRegionEval
bool bPrepared, bool bPrepared,
float ReachScale, float ReachScale,
TArray<float>* OutDensities, TArray<float>* OutDensities,
FTiming& OutTiming) FTiming& OutTiming,
bool bBrickProgram,
int32 RequestedWorkerCount)
{ {
OutTiming = FTiming(); OutTiming = FTiming();
const int32 NumWorkers = FMath::Clamp(RequestedWorkerCount, 1, 32);
OutTiming.Workers = NumWorkers;
if (Dimension <= 0 || Dimension > 4096 if (Dimension <= 0 || Dimension > 4096
|| (bFullRing && SampleIndices != nullptr) || (bFullRing && SampleIndices != nullptr)
|| (!bFullRing && SampleIndices == nullptr)) || (!bFullRing && SampleIndices == nullptr))
@@ -145,6 +168,94 @@ namespace VoxelForgeRegionEval
return false; return false;
} }
const double StartSeconds = FPlatformTime::Seconds(); const double StartSeconds = FPlatformTime::Seconds();
TArray<FRegionBrick> Bricks;
TArray<int32> WorkBricks;
if (bBrickProgram)
{
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<float>(OriginVoxels.X) / 8.0f);
const int32 FirstCellY = FMath::FloorToInt(static_cast<float>(OriginVoxels.Y) / 8.0f);
const int32 FirstCellZ = FMath::FloorToInt(static_cast<float>(OriginVoxels.Z) / 8.0f);
const int32 LastCellX = FMath::FloorToInt(static_cast<float>(LastX) / 8.0f);
const int32 LastCellY = FMath::FloorToInt(static_cast<float>(LastY) / 8.0f);
const int32 LastCellZ = FMath::FloorToInt(static_cast<float>(LastZ) / 8.0f);
const int32 CellsX = LastCellX - FirstCellX + 1;
const int32 CellsY = LastCellY - FirstCellY + 1;
const int32 CellsZ = LastCellZ - FirstCellZ + 1;
Bricks.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(
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;
}
if (bFullRing)
{
WorkBricks.Reserve(Bricks.Num());
for (int32 BrickIndex = 0; BrickIndex < Bricks.Num(); ++BrickIndex)
{
WorkBricks.Add(BrickIndex);
}
}
else
{
WorkBricks.Reserve(Bricks.Num());
for (int32 OutputIndex = 0; OutputIndex < SampleIndices->Num(); ++OutputIndex)
{
const int64 LatticeIndex = (*SampleIndices)[OutputIndex];
if (LatticeIndex < 0 || LatticeIndex >= TotalLatticeSamples)
{
return false;
}
const int32 X = static_cast<int32>(LatticeIndex % Dimension);
const int32 Y = static_cast<int32>((LatticeIndex / Dimension) % Dimension);
const int32 Z = static_cast<int32>(LatticeIndex
/ (static_cast<int64>(Dimension) * Dimension));
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<float>(WorldX) / 8.0f);
const int32 CellY = FMath::FloorToInt(static_cast<float>(WorldY) / 8.0f);
const int32 CellZ = FMath::FloorToInt(static_cast<float>(WorldZ) / 8.0f);
const int32 BrickIndex = ((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;
}
for (int32 BrickIndex = 0; BrickIndex < Bricks.Num(); ++BrickIndex)
{
if (!Bricks[BrickIndex].RequestedSamples.IsEmpty())
{
WorkBricks.Add(BrickIndex);
}
}
}
}
if (OutDensities != nullptr) if (OutDensities != nullptr)
{ {
if (SampleCount > MAX_int32) if (SampleCount > MAX_int32)
@@ -157,24 +268,78 @@ namespace VoxelForgeRegionEval
const int32 OriginX = OriginVoxels.X; const int32 OriginX = OriginVoxels.X;
const int32 OriginY = OriginVoxels.Y; const int32 OriginY = OriginVoxels.Y;
const int32 OriginZ = OriginVoxels.Z; const int32 OriginZ = OriginVoxels.Z;
double WorkerSeconds[WorkerCount] = {}; TArray<double> WorkerSeconds;
double WorkerCpuSeconds[WorkerCount] = {}; TArray<double> WorkerCpuSeconds;
bool WorkerCpuFromOsThreadTimes[WorkerCount] = {}; TArray<uint8> WorkerCpuFromOsThreadTimes;
TArray<uint32> WorkerThreadIds;
TArray<uint64> WorkerNonFinite;
TArray<uint64> WorkerEvaluatedSamples;
TArray<uint64> WorkerFallbackSamples;
TArray<uint64> WorkerEvaluationFailures;
WorkerSeconds.SetNumZeroed(NumWorkers);
WorkerCpuSeconds.SetNumZeroed(NumWorkers);
WorkerCpuFromOsThreadTimes.SetNumZeroed(NumWorkers);
WorkerThreadIds.SetNumZeroed(NumWorkers);
WorkerNonFinite.SetNumZeroed(NumWorkers);
WorkerEvaluatedSamples.SetNumZeroed(NumWorkers);
WorkerFallbackSamples.SetNumZeroed(NumWorkers);
WorkerEvaluationFailures.SetNumZeroed(NumWorkers);
bool bAllWorkerCpuFromOsThreadTimes = true; bool bAllWorkerCpuFromOsThreadTimes = true;
uint32 WorkerThreadIds[WorkerCount] = {}; ParallelFor(NumWorkers, [&](int32 WorkerIndex)
uint64 WorkerNonFinite[WorkerCount] = {};
ParallelFor(WorkerCount, [&](int32 WorkerIndex)
{ {
// Region-eval workers are not launched through AVoxelWorld::LoadTile, so they do not
// 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;
VoxelCaveMorphology::SetResearchBrickBinningEnabled(bPrepared); VoxelCaveMorphology::SetResearchBrickBinningEnabled(bPrepared);
VoxelCaveMorphology::SetResearchBrickRoomReachScale(ReachScale); VoxelCaveMorphology::SetResearchBrickRoomReachScale(ReachScale);
WorkerThreadIds[WorkerIndex] = FPlatformTLS::GetCurrentThreadId(); WorkerThreadIds[WorkerIndex] = FPlatformTLS::GetCurrentThreadId();
const int64 Begin = (SampleCount * WorkerIndex) / WorkerCount; const int64 WorkCount = bBrickProgram ? WorkBricks.Num() : SampleCount;
const int64 End = (SampleCount * (WorkerIndex + 1)) / WorkerCount; const int64 Begin = (WorkCount * WorkerIndex) / NumWorkers;
const int64 End = (WorkCount * (WorkerIndex + 1)) / NumWorkers;
const double WorkerStart = FPlatformTime::Seconds(); const double WorkerStart = FPlatformTime::Seconds();
double WorkerCpuStart = 0.0; double WorkerCpuStart = 0.0;
const bool bHaveWorkerCpuStart = GetCurrentThreadCpuSeconds(WorkerCpuStart); const bool bHaveWorkerCpuStart = GetCurrentThreadCpuSeconds(WorkerCpuStart);
TArray<float> BrickDensities;
for (int64 OutputIndex = Begin; OutputIndex < End; ++OutputIndex) for (int64 OutputIndex = Begin; OutputIndex < End; ++OutputIndex)
{ {
if (bBrickProgram)
{
const FRegionBrick& Brick = Bricks[WorkBricks[static_cast<int32>(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)
{
++WorkerEvaluationFailures[WorkerIndex];
continue;
}
const int32 BrickSampleCount = BrickDensities.Num();
WorkerEvaluatedSamples[WorkerIndex] += static_cast<uint64>(BrickSampleCount);
WorkerFallbackSamples[WorkerIndex] += static_cast<uint64>(BrickFallbackSamples);
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)
{
const int32 LocalIndex = (Bz * Brick.SizeY + By) * Brick.SizeX + Bx;
const int64 LatticeIndex = LinearIndex(
Brick.LatticeX + Bx, Brick.LatticeY + By,
Brick.LatticeZ + Bz, Dimension);
(*OutDensities)[static_cast<int32>(LatticeIndex)] = BrickDensities[LocalIndex];
}
}
else if (OutDensities != nullptr)
{
for (const FRegionBrickSample& Sample : Brick.RequestedSamples)
{
(*OutDensities)[Sample.OutputIndex] = BrickDensities[Sample.LocalIndex];
}
}
continue;
}
const int64 LatticeIndex = bFullRing const int64 LatticeIndex = bFullRing
? OutputIndex : (*SampleIndices)[static_cast<int32>(OutputIndex)]; ? OutputIndex : (*SampleIndices)[static_cast<int32>(OutputIndex)];
if (LatticeIndex < 0 || LatticeIndex >= TotalLatticeSamples) if (LatticeIndex < 0 || LatticeIndex >= TotalLatticeSamples)
@@ -212,11 +377,20 @@ namespace VoxelForgeRegionEval
OutTiming.WallSeconds = FPlatformTime::Seconds() - StartSeconds; OutTiming.WallSeconds = FPlatformTime::Seconds() - StartSeconds;
OutTiming.SampleCount = SampleCount; OutTiming.SampleCount = SampleCount;
for (int32 WorkerIndex = 0; WorkerIndex < WorkerCount; ++WorkerIndex) OutTiming.bBrickProgram = bBrickProgram;
OutTiming.BrickCount = bBrickProgram ? WorkBricks.Num() : 0;
for (int32 WorkerIndex = 0; WorkerIndex < NumWorkers; ++WorkerIndex)
{ {
OutTiming.WorkerCpuSeconds += WorkerCpuSeconds[WorkerIndex]; OutTiming.WorkerCpuSeconds += WorkerCpuSeconds[WorkerIndex];
bAllWorkerCpuFromOsThreadTimes &= WorkerCpuFromOsThreadTimes[WorkerIndex]; bAllWorkerCpuFromOsThreadTimes &= WorkerCpuFromOsThreadTimes[WorkerIndex] != 0;
OutTiming.NonFiniteSamples += WorkerNonFinite[WorkerIndex]; OutTiming.NonFiniteSamples += WorkerNonFinite[WorkerIndex];
OutTiming.EvaluatedSamples += bBrickProgram
? static_cast<int64>(WorkerEvaluatedSamples[WorkerIndex]) : 0;
OutTiming.FallbackSamples += static_cast<int64>(WorkerFallbackSamples[WorkerIndex]);
if (WorkerEvaluationFailures[WorkerIndex] != 0)
{
return false;
}
bool bAlreadyCounted = false; bool bAlreadyCounted = false;
for (int32 PriorWorker = 0; PriorWorker < WorkerIndex; ++PriorWorker) for (int32 PriorWorker = 0; PriorWorker < WorkerIndex; ++PriorWorker)
{ {
@@ -225,6 +399,11 @@ namespace VoxelForgeRegionEval
OutTiming.ObservedWorkerThreads += bAlreadyCounted ? 0 : 1; OutTiming.ObservedWorkerThreads += bAlreadyCounted ? 0 : 1;
} }
OutTiming.bWorkerCpuFromOsThreadTimes = bAllWorkerCpuFromOsThreadTimes; OutTiming.bWorkerCpuFromOsThreadTimes = bAllWorkerCpuFromOsThreadTimes;
if (OutTiming.EvaluatedSamples > 0)
{
OutTiming.FallbackShare = static_cast<double>(OutTiming.FallbackSamples)
/ static_cast<double>(OutTiming.EvaluatedSamples);
}
OutTiming.NanosecondsPerSample = OutTiming.SampleCount > 0 OutTiming.NanosecondsPerSample = OutTiming.SampleCount > 0
? OutTiming.WorkerCpuSeconds * 1.0e9 / static_cast<double>(OutTiming.SampleCount) ? OutTiming.WorkerCpuSeconds * 1.0e9 / static_cast<double>(OutTiming.SampleCount)
: 0.0; : 0.0;
@@ -17,10 +17,15 @@ namespace VoxelForgeRegionEval
int32 Workers = WorkerCount; int32 Workers = WorkerCount;
int32 ObservedWorkerThreads = 0; int32 ObservedWorkerThreads = 0;
int64 SampleCount = 0; int64 SampleCount = 0;
int64 EvaluatedSamples = 0;
int64 BrickCount = 0;
int64 FallbackSamples = 0;
double FallbackShare = 0.0;
double WallSeconds = 0.0; double WallSeconds = 0.0;
double WorkerCpuSeconds = 0.0; double WorkerCpuSeconds = 0.0;
double NanosecondsPerSample = 0.0; double NanosecondsPerSample = 0.0;
bool bWorkerCpuFromOsThreadTimes = false; bool bWorkerCpuFromOsThreadTimes = false;
bool bBrickProgram = false;
uint64 QuantizedHash = 0; uint64 QuantizedHash = 0;
uint64 RawFloatHash = 0; uint64 RawFloatHash = 0;
uint64 SaturatedSamples = 0; uint64 SaturatedSamples = 0;
@@ -67,7 +72,9 @@ namespace VoxelForgeRegionEval
bool bPrepared, bool bPrepared,
float ReachScale, float ReachScale,
TArray<float>* OutDensities, TArray<float>* OutDensities,
FTiming& OutTiming); FTiming& OutTiming,
bool bBrickProgram = false,
int32 RequestedWorkerCount = WorkerCount);
/** Count exact room/tunnel/core/passage candidate visits with instrumentation enabled. */ /** Count exact room/tunnel/core/passage candidate visits with instrumentation enabled. */
FCandidateVisits CountCandidates( FCandidateVisits CountCandidates(
+18
View File
@@ -4,6 +4,13 @@ param(
[string]$Scenario = 'canonical', [string]$Scenario = 'canonical',
[switch]$Build, [switch]$Build,
[switch]$EnableRender, [switch]$EnableRender,
[switch]$SampleDensityStacks,
[ValidateRange(1, 1000000)]
[int]$StackSampleIntervalUs = 1000,
[switch]$RegionHashProbe,
[ValidateRange(1, 32)]
[int]$RegionWorkers = 6,
[string]$RegionReferenceHash = '',
[ValidateSet('owner', 'default')] [ValidateSet('owner', 'default')]
[string]$Assets = 'owner', [string]$Assets = 'owner',
[hashtable]$Cvars = @{}, [hashtable]$Cvars = @{},
@@ -1430,6 +1437,17 @@ try {
$args = New-CommonArguments 'commandlet' $regionLog (Merge-Cvars $baseCvars) $args = New-CommonArguments 'commandlet' $regionLog (Merge-Cvars $baseCvars)
$args += New-CommandletArguments $commandletOut 'walk,export' ($Assets -eq 'owner') $false 128 $args += New-CommandletArguments $commandletOut 'walk,export' ($Assets -eq 'owner') $false 128
$args += @('-regioneval', '-regionsamples=262144') $args += @('-regioneval', '-regionsamples=262144')
if ($SampleDensityStacks) { $args += "-samplestacks=$StackSampleIntervalUs" }
if ($RegionHashProbe) {
if ([string]::IsNullOrWhiteSpace($RegionReferenceHash)) {
throw '-RegionHashProbe requires -RegionReferenceHash from a reference full-ring run.'
}
$args += @(
'-regionhashprobe',
"-regionworkers=$RegionWorkers",
"-regionreferencehash=$RegionReferenceHash"
)
}
$commandletLedger = Invoke-UnrealLaunch 'region-eval-commandlet' $args ` $commandletLedger = Invoke-UnrealLaunch 'region-eval-commandlet' $args `
$regionLog (Join-Path $RunRoot 'Logs\region-eval.stdout.log') ` $regionLog (Join-Path $RunRoot 'Logs\region-eval.stdout.log') `
(Join-Path $RunRoot 'Logs\region-eval.stderr.log') (Join-Path $RunRoot 'Logs\region-eval.stderr.log')