wip(streaming): LOD rings tile exactly once, cache race, reconcile 104x -- gate stress FAILS 3/4
- Clipmap: finer rings snap to child-pair boundaries, so no two desired tiles of different levels overlap. Owner config 635 overlapping pairs -> 0; LOD0 ring 343 -> 512 tiles. New test sweeps 6,174 cases; fails on7486c29as intended, passes here. - Shared room-graph cache: Entry->Cache read/written only under the mutex (race fix). - ReconcileReadyTransitionVisibility indexed: moving p95 32.0 ms -> 0.31 ms. - Block-vs-scalar density equivalence test: 5.1M samples bit-identical; catches an injected optimistic EffectOverBox (5,612 mismatches). UNVALIDATED for merge: collision-gate stress passed 1 of 4 runs on this code (fails at 4.56 m, support collision unready at start); it passed on7486c29. No control run yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
7486c299d4
commit
85a803b873
6 files changed
+1299
-50
No files matched your search
@@ -0,0 +1,385 @@
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "VoxelClipmapDesiredTiles.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
using VoxelClipmapDesiredTiles::FParameters;
|
||||
|
||||
struct FOverlapMeasure
|
||||
{
|
||||
uint64 PairCount = 0;
|
||||
uint64 PairwiseVolumeChunkCubed = 0;
|
||||
};
|
||||
|
||||
static FOverlapMeasure MeasureOverlaps(const TArray<FVoxelTileKey>& Tiles)
|
||||
{
|
||||
TSet<FVoxelTileKey> TileSet;
|
||||
int32 MaxLevel = 0;
|
||||
for (const FVoxelTileKey& Tile : Tiles)
|
||||
{
|
||||
TileSet.Add(Tile);
|
||||
MaxLevel = FMath::Max(MaxLevel, Tile.Level);
|
||||
}
|
||||
|
||||
FOverlapMeasure Result;
|
||||
for (const FVoxelTileKey& Fine : Tiles)
|
||||
{
|
||||
for (int32 CoarseLevel = Fine.Level + 1; CoarseLevel <= MaxLevel; ++CoarseLevel)
|
||||
{
|
||||
const int32 Divisor = 1 << (CoarseLevel - Fine.Level);
|
||||
const FVoxelTileKey Coarse(
|
||||
VoxelClipmapDesiredTiles::FloorDiv(Fine.Coord, Divisor), CoarseLevel);
|
||||
if (TileSet.Contains(Coarse))
|
||||
{
|
||||
++Result.PairCount;
|
||||
Result.PairwiseVolumeChunkCubed += (uint64(1) << (3 * Fine.Level));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
static bool IsFootprintCovered(const FVoxelTileKey& Tile,
|
||||
const TSet<FVoxelTileKey>& CoveringTiles,
|
||||
int32 MaxLevel, int32 MinChunkZ, int32 MaxChunkZ)
|
||||
{
|
||||
const int32 TileMinZ = Tile.Coord.Z * (1 << Tile.Level);
|
||||
const int32 TileMaxZ = (Tile.Coord.Z + 1) * (1 << Tile.Level) - 1;
|
||||
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CoveringTiles.Contains(Tile))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// A coarser dyadic tile covers this whole footprint when it is an ancestor.
|
||||
for (int32 ParentLevel = Tile.Level + 1; ParentLevel <= MaxLevel; ++ParentLevel)
|
||||
{
|
||||
const int32 Divisor = 1 << (ParentLevel - Tile.Level);
|
||||
const FVoxelTileKey Parent(
|
||||
VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord, Divisor), ParentLevel);
|
||||
if (CoveringTiles.Contains(Parent))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Tile.Level == 0)
|
||||
{
|
||||
return Tile.Coord.Z < MinChunkZ || Tile.Coord.Z > MaxChunkZ;
|
||||
}
|
||||
|
||||
const int32 ChildLevel = Tile.Level - 1;
|
||||
const FIntVector ChildBase = Tile.Coord * 2;
|
||||
for (int32 Z = 0; Z < 2; ++Z)
|
||||
for (int32 Y = 0; Y < 2; ++Y)
|
||||
for (int32 X = 0; X < 2; ++X)
|
||||
{
|
||||
if (!IsFootprintCovered(
|
||||
FVoxelTileKey(ChildBase + FIntVector(X, Y, Z), ChildLevel),
|
||||
CoveringTiles, MaxLevel, MinChunkZ, MaxChunkZ))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool CoversEveryLegacyTile(const TArray<FVoxelTileKey>& Legacy,
|
||||
const TArray<FVoxelTileKey>& Current,
|
||||
int32 MinChunkZ, int32 MaxChunkZ)
|
||||
{
|
||||
TSet<FVoxelTileKey> CurrentSet;
|
||||
int32 MaxLevel = 0;
|
||||
for (const FVoxelTileKey& Tile : Current)
|
||||
{
|
||||
CurrentSet.Add(Tile);
|
||||
MaxLevel = FMath::Max(MaxLevel, Tile.Level);
|
||||
}
|
||||
for (const FVoxelTileKey& Tile : Legacy)
|
||||
{
|
||||
if (!IsFootprintCovered(Tile, CurrentSet, MaxLevel, MinChunkZ, MaxChunkZ))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Frozen copy of the 7486c29 selector. It is the coverage oracle and keeps the regression
|
||||
// test able to prove that an exact tiling still covers every volume the old selector covered.
|
||||
static void Build7486c29(const FIntVector& Center, int32 MinChunkZ, int32 MaxChunkZ,
|
||||
const FParameters& Parameters, TArray<FVoxelTileKey>& OutTiles)
|
||||
{
|
||||
OutTiles.Reset();
|
||||
const int32 Radius = FMath::Max(1, Parameters.ClipRadius);
|
||||
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
|
||||
const int32 OuterRadius = VoxelClipmapDesiredTiles::OuterShellRadius(
|
||||
Parameters, Radius, MaxLevel);
|
||||
int32 SheetLevel = MaxLevel;
|
||||
int32 SheetRadius = OuterRadius;
|
||||
VoxelClipmapDesiredTiles::OuterShell(
|
||||
Parameters, Radius, MaxLevel, SheetLevel, SheetRadius);
|
||||
const bool bSheetRing = SheetLevel > MaxLevel;
|
||||
const int32 MaxMCRingLevel = bSheetRing ? SheetLevel - 1 : MaxLevel;
|
||||
|
||||
for (int32 Level = 0; Level <= MaxMCRingLevel; ++Level)
|
||||
{
|
||||
const int32 Pow = 1 << Level;
|
||||
const FIntVector CenterLevel = VoxelClipmapDesiredTiles::FloorDiv(Center, Pow);
|
||||
const FIntVector CenterFiner = Level > 0
|
||||
? VoxelClipmapDesiredTiles::FloorDiv(Center, Pow >> 1)
|
||||
: FIntVector::ZeroValue;
|
||||
const int32 LevelRadius = (Level == MaxLevel && !bSheetRing)
|
||||
? OuterRadius : Radius;
|
||||
|
||||
int32 DeltaZMin = -LevelRadius;
|
||||
int32 DeltaZMax = LevelRadius;
|
||||
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
||||
{
|
||||
DeltaZMin = FMath::Max(
|
||||
DeltaZMin, VoxelClipmapDesiredTiles::FloorDiv(MinChunkZ, Pow) - CenterLevel.Z);
|
||||
DeltaZMax = FMath::Min(
|
||||
DeltaZMax, VoxelClipmapDesiredTiles::FloorDiv(MaxChunkZ, Pow) - CenterLevel.Z);
|
||||
}
|
||||
|
||||
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
||||
for (int32 DeltaY = -LevelRadius; DeltaY <= LevelRadius; ++DeltaY)
|
||||
for (int32 DeltaX = -LevelRadius; DeltaX <= LevelRadius; ++DeltaX)
|
||||
{
|
||||
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ);
|
||||
const int32 TileMinZ = Tile.Z << Level;
|
||||
const int32 TileMaxZ = ((Tile.Z + 1) << Level) - 1;
|
||||
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Level > 0)
|
||||
{
|
||||
const bool bCovered =
|
||||
(2 * Tile.X >= CenterFiner.X - Radius)
|
||||
&& (2 * Tile.X + 1 <= CenterFiner.X + Radius)
|
||||
&& (2 * Tile.Y >= CenterFiner.Y - Radius)
|
||||
&& (2 * Tile.Y + 1 <= CenterFiner.Y + Radius)
|
||||
&& (2 * Tile.Z >= CenterFiner.Z - Radius)
|
||||
&& (2 * Tile.Z + 1 <= CenterFiner.Z + Radius);
|
||||
if (bCovered)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
OutTiles.Emplace(Tile, Level);
|
||||
}
|
||||
}
|
||||
|
||||
if (bSheetRing)
|
||||
{
|
||||
const int32 SheetPow = 1 << SheetLevel;
|
||||
const FIntVector CenterSheet = VoxelClipmapDesiredTiles::FloorDiv(Center, SheetPow);
|
||||
const FIntVector CenterMC = VoxelClipmapDesiredTiles::FloorDiv(
|
||||
Center, 1 << MaxMCRingLevel);
|
||||
const int32 LevelDelta = SheetLevel - MaxMCRingLevel;
|
||||
|
||||
int32 DeltaZMin = -SheetRadius;
|
||||
int32 DeltaZMax = SheetRadius;
|
||||
if (MinChunkZ != MIN_int32)
|
||||
{
|
||||
DeltaZMin = FMath::Max(DeltaZMin,
|
||||
VoxelClipmapDesiredTiles::FloorDiv(MinChunkZ, SheetPow) - CenterSheet.Z);
|
||||
DeltaZMax = FMath::Min(DeltaZMax,
|
||||
VoxelClipmapDesiredTiles::FloorDiv(MaxChunkZ, SheetPow) - CenterSheet.Z);
|
||||
}
|
||||
|
||||
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
||||
for (int32 DeltaY = -SheetRadius; DeltaY <= SheetRadius; ++DeltaY)
|
||||
for (int32 DeltaX = -SheetRadius; DeltaX <= SheetRadius; ++DeltaX)
|
||||
{
|
||||
const FIntVector Tile = CenterSheet + FIntVector(DeltaX, DeltaY, DeltaZ);
|
||||
const int32 TileMinZ = Tile.Z << SheetLevel;
|
||||
const int32 TileMaxZ = ((Tile.Z + 1) << SheetLevel) - 1;
|
||||
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool bCovered =
|
||||
((Tile.X << LevelDelta) >= CenterMC.X - Radius)
|
||||
&& ((((Tile.X + 1) << LevelDelta) - 1) <= CenterMC.X + Radius)
|
||||
&& ((Tile.Y << LevelDelta) >= CenterMC.Y - Radius)
|
||||
&& ((((Tile.Y + 1) << LevelDelta) - 1) <= CenterMC.Y + Radius)
|
||||
&& ((Tile.Z << LevelDelta) >= CenterMC.Z - Radius)
|
||||
&& ((((Tile.Z + 1) << LevelDelta) - 1) <= CenterMC.Z + Radius);
|
||||
if (!bCovered)
|
||||
{
|
||||
OutTiles.Emplace(Tile, SheetLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static FString DescribeMeasure(const FOverlapMeasure& Measure)
|
||||
{
|
||||
return FString::Printf(TEXT("pairs=%llu pairwise_volume_chunk3=%llu"),
|
||||
static_cast<unsigned long long>(Measure.PairCount),
|
||||
static_cast<unsigned long long>(Measure.PairwiseVolumeChunkCubed));
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeClipmapNoOverlapTest,
|
||||
"VoxelForge.Streaming.ClipmapDesiredTilesNoOverlapCoverage",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
(void)Parameters;
|
||||
|
||||
FParameters Owner;
|
||||
Owner.ClipRadius = 3;
|
||||
Owner.MaxClipLevel = 5;
|
||||
Owner.RenderDistanceChunks = 768;
|
||||
Owner.bFarSheetRing = false;
|
||||
|
||||
FParameters Defaults;
|
||||
|
||||
TArray<FVoxelTileKey> LegacyOwner, CurrentOwner, LegacyDefaults, CurrentDefaults;
|
||||
Build7486c29(FIntVector::ZeroValue, MIN_int32, MAX_int32, Owner, LegacyOwner);
|
||||
VoxelClipmapDesiredTiles::Build(
|
||||
FIntVector::ZeroValue, MIN_int32, MAX_int32, Owner, CurrentOwner);
|
||||
Build7486c29(FIntVector::ZeroValue, MIN_int32, MAX_int32, Defaults, LegacyDefaults);
|
||||
VoxelClipmapDesiredTiles::Build(
|
||||
FIntVector::ZeroValue, MIN_int32, MAX_int32, Defaults, CurrentDefaults);
|
||||
|
||||
const FOverlapMeasure LegacyOwnerOverlap = MeasureOverlaps(LegacyOwner);
|
||||
const FOverlapMeasure CurrentOwnerOverlap = MeasureOverlaps(CurrentOwner);
|
||||
const FOverlapMeasure LegacyDefaultOverlap = MeasureOverlaps(LegacyDefaults);
|
||||
const FOverlapMeasure CurrentDefaultOverlap = MeasureOverlaps(CurrentDefaults);
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("owner center=(0,0,0): before tiles=%d %s; after tiles=%d %s"),
|
||||
LegacyOwner.Num(), *DescribeMeasure(LegacyOwnerOverlap),
|
||||
CurrentOwner.Num(), *DescribeMeasure(CurrentOwnerOverlap)));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("FParameters defaults center=(0,0,0): before tiles=%d %s; after tiles=%d %s"),
|
||||
LegacyDefaults.Num(), *DescribeMeasure(LegacyDefaultOverlap),
|
||||
CurrentDefaults.Num(), *DescribeMeasure(CurrentDefaultOverlap)));
|
||||
|
||||
TestTrue(TEXT("7486c29 owner overlap measurement matches the checked source geometry"),
|
||||
LegacyOwnerOverlap.PairCount == 635
|
||||
&& LegacyOwnerOverlap.PairwiseVolumeChunkCubed == 594487);
|
||||
TestTrue(TEXT("7486c29 FParameters-default overlap measurement matches the checked source geometry"),
|
||||
LegacyDefaultOverlap.PairCount == 508
|
||||
&& LegacyDefaultOverlap.PairwiseVolumeChunkCubed == 74295);
|
||||
TestTrue(TEXT("owner desired set has no overlapping level pairs"),
|
||||
CurrentOwnerOverlap.PairCount == 0 && CurrentOwnerOverlap.PairwiseVolumeChunkCubed == 0);
|
||||
TestTrue(TEXT("default desired set has no overlapping level pairs"),
|
||||
CurrentDefaultOverlap.PairCount == 0 && CurrentDefaultOverlap.PairwiseVolumeChunkCubed == 0);
|
||||
TestTrue(TEXT("owner coverage is identical or larger than 7486c29"),
|
||||
CoversEveryLegacyTile(LegacyOwner, CurrentOwner, MIN_int32, MAX_int32));
|
||||
TestTrue(TEXT("default coverage is identical or larger than 7486c29"),
|
||||
CoversEveryLegacyTile(LegacyDefaults, CurrentDefaults, MIN_int32, MAX_int32));
|
||||
|
||||
struct FSweepSettings
|
||||
{
|
||||
int32 Radius;
|
||||
int32 MaxLevel;
|
||||
int32 RenderDistance;
|
||||
bool bSheet;
|
||||
int32 SheetSpan;
|
||||
};
|
||||
static const FSweepSettings SweepSettings[] = {
|
||||
{1, 0, 0, false, 2},
|
||||
{2, 2, 0, false, 2},
|
||||
{3, 4, 0, false, 2},
|
||||
{2, 3, 49, false, 2},
|
||||
{2, 3, 49, true, 2},
|
||||
{2, 4, 87, true, 3},
|
||||
};
|
||||
|
||||
int32 SweptCases = 0;
|
||||
int32 OverlapCases = 0;
|
||||
int32 CoverageFailures = 0;
|
||||
uint64 TotalOverlapPairs = 0;
|
||||
FString FirstFailure;
|
||||
const int32 CenterMin = -3;
|
||||
const int32 CenterMax = 3;
|
||||
for (int32 Z = CenterMin; Z <= CenterMax; ++Z)
|
||||
for (int32 Y = CenterMin; Y <= CenterMax; ++Y)
|
||||
for (int32 X = CenterMin; X <= CenterMax; ++X)
|
||||
for (const FSweepSettings& Sweep : SweepSettings)
|
||||
{
|
||||
FParameters P;
|
||||
P.ClipRadius = Sweep.Radius;
|
||||
P.MaxClipLevel = Sweep.MaxLevel;
|
||||
P.RenderDistanceChunks = Sweep.RenderDistance;
|
||||
P.bFarSheetRing = Sweep.bSheet;
|
||||
P.FarSheetSpanLevels = Sweep.SheetSpan;
|
||||
|
||||
// Exercise the unbounded view, a tight local clamp, and a clamp crossing a tile boundary.
|
||||
for (int32 ClampCase = 0; ClampCase < 3; ++ClampCase)
|
||||
{
|
||||
int32 MinZ = MIN_int32;
|
||||
int32 MaxZ = MAX_int32;
|
||||
if (ClampCase == 1)
|
||||
{
|
||||
MinZ = Z - 2;
|
||||
MaxZ = Z + 3;
|
||||
}
|
||||
else if (ClampCase == 2)
|
||||
{
|
||||
const int32 Band = VoxelClipmapDesiredTiles::FloorDiv(Z, 1 << Sweep.MaxLevel);
|
||||
MinZ = (Band << Sweep.MaxLevel) - 1;
|
||||
MaxZ = MinZ + 6;
|
||||
}
|
||||
|
||||
const FIntVector Center(X, Y, Z);
|
||||
TArray<FVoxelTileKey> Legacy, Current;
|
||||
Build7486c29(Center, MinZ, MaxZ, P, Legacy);
|
||||
VoxelClipmapDesiredTiles::Build(Center, MinZ, MaxZ, P, Current);
|
||||
++SweptCases;
|
||||
|
||||
const FOverlapMeasure CurrentOverlap = MeasureOverlaps(Current);
|
||||
if (CurrentOverlap.PairCount != 0)
|
||||
{
|
||||
++OverlapCases;
|
||||
TotalOverlapPairs += CurrentOverlap.PairCount;
|
||||
if (FirstFailure.IsEmpty())
|
||||
{
|
||||
FirstFailure = FString::Printf(
|
||||
TEXT("overlap center=(%d,%d,%d) radius=%d level=%d distance=%d sheet=%d clamp=%d: %s"),
|
||||
X, Y, Z, Sweep.Radius, Sweep.MaxLevel, Sweep.RenderDistance,
|
||||
Sweep.bSheet ? 1 : 0, ClampCase, *DescribeMeasure(CurrentOverlap));
|
||||
}
|
||||
}
|
||||
if (!CoversEveryLegacyTile(Legacy, Current, MinZ, MaxZ))
|
||||
{
|
||||
++CoverageFailures;
|
||||
if (FirstFailure.IsEmpty())
|
||||
{
|
||||
FirstFailure = FString::Printf(
|
||||
TEXT("coverage center=(%d,%d,%d) radius=%d level=%d distance=%d sheet=%d clamp=%d"),
|
||||
X, Y, Z, Sweep.Radius, Sweep.MaxLevel, Sweep.RenderDistance,
|
||||
Sweep.bSheet ? 1 : 0, ClampCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("sweep cases=%d centers=343 settings=%d clamps=3 overlap_cases=%d overlap_pairs=%llu coverage_failures=%d first=%s"),
|
||||
SweptCases, UE_ARRAY_COUNT(SweepSettings), OverlapCases,
|
||||
static_cast<unsigned long long>(TotalOverlapPairs), CoverageFailures,
|
||||
FirstFailure.IsEmpty() ? TEXT("none") : *FirstFailure));
|
||||
TestEqual(TEXT("all swept desired sets have zero overlapping pairs"), OverlapCases, 0);
|
||||
TestEqual(TEXT("all swept desired sets cover the legacy footprint"), CoverageFailures, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -0,0 +1,592 @@
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "VoxelClipmapDesiredTiles.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
#include "VoxelForgeTestFixture.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// The marching-cubes grid contains 32 cells, with one sample of halo on each side.
|
||||
constexpr int32 MesherGridSize = 35;
|
||||
constexpr int32 ArchetypeCount = 8;
|
||||
constexpr int32 PassageGridCount = 2;
|
||||
constexpr int32 GridSetCount = ArchetypeCount + PassageGridCount;
|
||||
|
||||
static bool SameFloatBits(float A, float B)
|
||||
{
|
||||
return FMemory::Memcmp(&A, &B, sizeof(float)) == 0;
|
||||
}
|
||||
|
||||
static FIntVector MakeMesherGridOrigin(const FIntVector& TargetChunk, int32 Level)
|
||||
{
|
||||
const int32 TileSpanChunks = 1 << Level;
|
||||
const FIntVector TileCoord = VoxelClipmapDesiredTiles::FloorDiv(
|
||||
TargetChunk, TileSpanChunks);
|
||||
const int32 Step = 1 << Level;
|
||||
return TileCoord * (CHUNK_SIZE << Level) - FIntVector(Step, Step, Step);
|
||||
}
|
||||
|
||||
struct FGridComparison
|
||||
{
|
||||
int64 Samples = 0;
|
||||
int32 Mismatches = 0;
|
||||
FString FirstMismatch;
|
||||
TArray<float> BlockDensities;
|
||||
};
|
||||
|
||||
static bool BuildProductionStackForArchetype(
|
||||
VoxelForgeTest::FTestWorld& World, ECaveGeneratorType Type,
|
||||
const FIntVector& ChunkCoord, FVoxelOpStack& OutStack, FVoxelOpContext& OutContext)
|
||||
{
|
||||
const int32 Seed = World.Settings->GetEffectiveWorldSeed();
|
||||
const float SpineRadius = World.Settings->GetEffectiveOriginSpineRadius();
|
||||
const UVoxelStrateManager* Manager = World.StrateManager.Get();
|
||||
OutContext = FVoxelOpContext();
|
||||
OutContext.ChunkCoord = ChunkCoord;
|
||||
OutContext.Seed = static_cast<uint32>(Seed);
|
||||
OutContext.LayoutVersion = Manager->GetLayoutVersion();
|
||||
OutContext.WorldRadiusVoxels = World.Settings->GetEffectiveWorldRadiusVoxels();
|
||||
OutContext.EdgeSealThickness = World.Settings->EdgeSealThickness;
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
case ECaveGeneratorType::FlatPlain:
|
||||
case ECaveGeneratorType::CrystalChamber:
|
||||
{
|
||||
const FSlabGenerationParams P = Manager->GetSlabParamsForChunk(ChunkCoord);
|
||||
OutContext.StrateTopWorldZ = P.StrateTopWorldZ;
|
||||
OutContext.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||
VoxelDensityOps::BuildSlabStack(OutStack, P, Seed, SpineRadius, Manager);
|
||||
return true;
|
||||
}
|
||||
case ECaveGeneratorType::Maze:
|
||||
{
|
||||
const FMazeGenerationParams P = Manager->GetMazeParamsForChunk(ChunkCoord);
|
||||
OutContext.StrateTopWorldZ = P.StrateTopWorldZ;
|
||||
OutContext.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||
VoxelDensityOps::BuildMazeStack(OutStack, P, Seed, SpineRadius, Manager);
|
||||
return true;
|
||||
}
|
||||
case ECaveGeneratorType::SurfaceWorld:
|
||||
{
|
||||
const FSurfaceGenerationParams P = Manager->GetSurfaceParamsForChunk(ChunkCoord);
|
||||
OutContext.StrateTopWorldZ = P.StrateTopWorldZ;
|
||||
OutContext.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||
// The generator-grid comparison below covers the resolved biome route. This builder
|
||||
// exercises the production stack's base surface branch directly.
|
||||
VoxelDensityOps::BuildSurfaceStack(
|
||||
OutStack, P, Seed, SpineRadius, Manager, TArray<FSurfaceGenerationParams>(), nullptr);
|
||||
return true;
|
||||
}
|
||||
case ECaveGeneratorType::VerticalShafts:
|
||||
{
|
||||
const FVerticalShaftParams P = Manager->GetVerticalShaftParamsForChunk(ChunkCoord);
|
||||
OutContext.StrateTopWorldZ = P.StrateTopWorldZ;
|
||||
OutContext.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||
VoxelDensityOps::BuildVerticalShaftStack(OutStack, P, Seed, SpineRadius, Manager);
|
||||
return true;
|
||||
}
|
||||
case ECaveGeneratorType::FloatingIslands:
|
||||
{
|
||||
const FFloatingIslandParams P = Manager->GetFloatingIslandParamsForChunk(ChunkCoord);
|
||||
OutContext.StrateTopWorldZ = P.StrateTopWorldZ;
|
||||
OutContext.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||
VoxelDensityOps::BuildFloatingIslandStack(OutStack, P, Seed, SpineRadius, Manager);
|
||||
return true;
|
||||
}
|
||||
case ECaveGeneratorType::TunnelNetwork:
|
||||
case ECaveGeneratorType::Underwater:
|
||||
{
|
||||
const FStrateGenerationParams P = Manager->GetGenerationParams(ChunkCoord);
|
||||
OutContext.StrateTopWorldZ = P.StrateTopWorldZ;
|
||||
OutContext.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||
VoxelDensityOps::BuildTunnelNetworkStack(OutStack, P, Seed, SpineRadius, Manager);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool FindChunkSampleRange(int32 AxisOrigin, int32 Step, int32 AxisSize,
|
||||
int32 TargetChunk, int32& OutMin, int32& OutMax)
|
||||
{
|
||||
OutMin = INT32_MAX;
|
||||
OutMax = INT32_MIN;
|
||||
for (int32 Index = 0; Index < AxisSize; ++Index)
|
||||
{
|
||||
const int32 Coordinate = AxisOrigin + Index * Step;
|
||||
if (FMath::FloorToInt(static_cast<float>(Coordinate) / CHUNK_SIZE) == TargetChunk)
|
||||
{
|
||||
OutMin = FMath::Min(OutMin, Index);
|
||||
OutMax = FMath::Max(OutMax, Index);
|
||||
}
|
||||
}
|
||||
return OutMin != INT32_MAX;
|
||||
}
|
||||
|
||||
static FGridComparison CompareStackChunkBlockWithScalar(
|
||||
FVoxelOpStack& Stack, FIntVector GridOrigin, int32 Step,
|
||||
const FIntVector& TargetChunk)
|
||||
{
|
||||
FGridComparison Result;
|
||||
int32 MinX = 0, MaxX = 0, MinY = 0, MaxY = 0, MinZ = 0, MaxZ = 0;
|
||||
if (!FindChunkSampleRange(GridOrigin.X, Step, MesherGridSize, TargetChunk.X, MinX, MaxX)
|
||||
|| !FindChunkSampleRange(GridOrigin.Y, Step, MesherGridSize, TargetChunk.Y, MinY, MaxY)
|
||||
|| !FindChunkSampleRange(GridOrigin.Z, Step, MesherGridSize, TargetChunk.Z, MinZ, MaxZ))
|
||||
{
|
||||
Result.FirstMismatch = TEXT("mesher grid has no samples in the stack's target chunk");
|
||||
return Result;
|
||||
}
|
||||
|
||||
const int32 SizeX = MaxX - MinX + 1;
|
||||
const int32 SizeY = MaxY - MinY + 1;
|
||||
const int32 SizeZ = MaxZ - MinZ + 1;
|
||||
const FIntVector Origin = GridOrigin + FIntVector(MinX * Step, MinY * Step, MinZ * Step);
|
||||
const FIntVector Last = Origin + FIntVector(
|
||||
(SizeX - 1) * Step, (SizeY - 1) * Step, (SizeZ - 1) * Step);
|
||||
const FBox Box(
|
||||
FVector((float)Origin.X, (float)Origin.Y, (float)Origin.Z),
|
||||
FVector((float)Last.X, (float)Last.Y, (float)Last.Z));
|
||||
|
||||
TArray<int32> ActiveOps;
|
||||
const int32 ActiveCount = Stack.BuildActiveOpList(Box, Step, Origin, ActiveOps);
|
||||
TArray<FVoxelOpSample> Samples;
|
||||
Samples.SetNumUninitialized(SizeX * SizeY * SizeZ);
|
||||
for (FVoxelOpSample& Sample : Samples) { Sample = FVoxelOpSample(); }
|
||||
|
||||
FVoxelOpBlock Block;
|
||||
Block.OriginVoxels = Origin;
|
||||
Block.Step = Step;
|
||||
Block.SizeX = SizeX;
|
||||
Block.SizeY = SizeY;
|
||||
Block.SizeZ = SizeZ;
|
||||
Block.Samples = Samples.GetData();
|
||||
Stack.EvalBlock(Block, ActiveOps);
|
||||
|
||||
for (int32 Z = 0; Z < SizeZ; ++Z)
|
||||
for (int32 Y = 0; Y < SizeY; ++Y)
|
||||
for (int32 X = 0; X < SizeX; ++X)
|
||||
{
|
||||
const FIntVector SamplePos = Origin + FIntVector(X * Step, Y * Step, Z * Step);
|
||||
const float BlockDensity = Block.At(X, Y, Z).Density;
|
||||
const float ScalarDensity = Stack.EvalInternal(
|
||||
(float)SamplePos.X, (float)SamplePos.Y, (float)SamplePos.Z);
|
||||
Result.BlockDensities.Add(BlockDensity);
|
||||
++Result.Samples;
|
||||
if (!SameFloatBits(BlockDensity, ScalarDensity))
|
||||
{
|
||||
++Result.Mismatches;
|
||||
if (Result.FirstMismatch.IsEmpty())
|
||||
{
|
||||
Result.FirstMismatch = FString::Printf(
|
||||
TEXT("box=[(%d,%d,%d)..(%d,%d,%d)] active_ops=%d/%d sample=(%d,%d,%d) block=%.9g scalar=%.9g delta=%.9g"),
|
||||
Origin.X, Origin.Y, Origin.Z, Last.X, Last.Y, Last.Z,
|
||||
ActiveCount, Stack.Num(), SamplePos.X, SamplePos.Y, SamplePos.Z,
|
||||
BlockDensity, ScalarDensity, BlockDensity - ScalarDensity);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
static FGridComparison CompareGeneratorBlockWithScalar(
|
||||
UVoxelGenerator& Generator, FIntVector Origin, int32 Step)
|
||||
{
|
||||
FGridComparison Result;
|
||||
const int32 Count = MesherGridSize * MesherGridSize * MesherGridSize;
|
||||
Result.BlockDensities.SetNumUninitialized(Count);
|
||||
Generator.BeginDensityBlock(Origin, Step, MesherGridSize, MesherGridSize, MesherGridSize);
|
||||
int32 Index = 0;
|
||||
for (int32 Z = 0; Z < MesherGridSize; ++Z)
|
||||
for (int32 Y = 0; Y < MesherGridSize; ++Y)
|
||||
for (int32 X = 0; X < MesherGridSize; ++X, ++Index)
|
||||
{
|
||||
const FIntVector SamplePos = Origin + FIntVector(X * Step, Y * Step, Z * Step);
|
||||
Result.BlockDensities[Index] = Generator.GetDensityAt(
|
||||
(float)SamplePos.X, (float)SamplePos.Y, (float)SamplePos.Z);
|
||||
}
|
||||
Generator.EndDensityBlock();
|
||||
|
||||
Index = 0;
|
||||
for (int32 Z = 0; Z < MesherGridSize; ++Z)
|
||||
for (int32 Y = 0; Y < MesherGridSize; ++Y)
|
||||
for (int32 X = 0; X < MesherGridSize; ++X, ++Index)
|
||||
{
|
||||
const FIntVector SamplePos = Origin + FIntVector(X * Step, Y * Step, Z * Step);
|
||||
const float ScalarDensity = Generator.GetDensityAt(
|
||||
(float)SamplePos.X, (float)SamplePos.Y, (float)SamplePos.Z);
|
||||
++Result.Samples;
|
||||
if (!SameFloatBits(Result.BlockDensities[Index], ScalarDensity))
|
||||
{
|
||||
++Result.Mismatches;
|
||||
if (Result.FirstMismatch.IsEmpty())
|
||||
{
|
||||
Result.FirstMismatch = FString::Printf(
|
||||
TEXT("sample=(%d,%d,%d) block=%.9g scalar=%.9g delta=%.9g"),
|
||||
SamplePos.X, SamplePos.Y, SamplePos.Z, Result.BlockDensities[Index],
|
||||
ScalarDensity, Result.BlockDensities[Index] - ScalarDensity);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
static int32 FindGapChunkZ(const VoxelForgeTest::FTestWorld& World)
|
||||
{
|
||||
const int32 Min = World.BottomChunkZ - 8;
|
||||
const int32 Max = World.TopChunkZ + 8;
|
||||
for (int32 ChunkZ = Min; ChunkZ <= Max; ++ChunkZ)
|
||||
{
|
||||
if (World.StrateManager->IsGapChunk(FIntVector(0, 0, ChunkZ)))
|
||||
{
|
||||
return ChunkZ;
|
||||
}
|
||||
}
|
||||
return MIN_int32;
|
||||
}
|
||||
|
||||
static int32 CountGapSamples(const VoxelForgeTest::FTestWorld& World,
|
||||
FIntVector Origin, int32 Step)
|
||||
{
|
||||
TSet<int32> SampleChunkZs;
|
||||
for (int32 Z = 0; Z < MesherGridSize; ++Z)
|
||||
{
|
||||
SampleChunkZs.Add(FMath::FloorToInt(
|
||||
static_cast<float>(Origin.Z + Z * Step) / CHUNK_SIZE));
|
||||
}
|
||||
int32 Count = 0;
|
||||
for (const int32 ChunkZ : SampleChunkZs)
|
||||
{
|
||||
if (World.StrateManager->IsGapChunk(FIntVector(0, 0, ChunkZ)))
|
||||
{
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
return Count;
|
||||
}
|
||||
|
||||
struct FScopedConsoleIntOverride
|
||||
{
|
||||
IConsoleVariable* Variable = nullptr;
|
||||
int32 OriginalValue = 0;
|
||||
|
||||
explicit FScopedConsoleIntOverride(const TCHAR* Name, int32 Value)
|
||||
: Variable(IConsoleManager::Get().FindConsoleVariable(Name))
|
||||
{
|
||||
if (Variable != nullptr)
|
||||
{
|
||||
OriginalValue = Variable->GetInt();
|
||||
Variable->Set(Value, ECVF_SetByCode);
|
||||
}
|
||||
}
|
||||
|
||||
~FScopedConsoleIntOverride()
|
||||
{
|
||||
if (Variable != nullptr)
|
||||
{
|
||||
Variable->Set(OriginalValue, ECVF_SetByCode);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeDensityBlockEquivalenceTest,
|
||||
"VoxelForge.Density.OperatorBlockMatchesScalarGrid",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FVoxelForgeDensityBlockEquivalenceTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
(void)Parameters;
|
||||
|
||||
// The production mesher initializes these once at startup. This headless test does so too,
|
||||
// then temporarily disables the fused TunnelNetwork evaluator so the real per-chunk
|
||||
// BeginDensityBlock -> BuildActiveOpList -> EvalBlock path is compared against scalar output.
|
||||
VoxelGenLOD::InitializeConsoleSwitches();
|
||||
FScopedConsoleIntOverride FusedEvaluatorOverride(TEXT("voxel.UseFusedEvaluator"), 0);
|
||||
if (FusedEvaluatorOverride.Variable == nullptr)
|
||||
{
|
||||
AddError(TEXT("voxel.UseFusedEvaluator cvar is unavailable; block route was not forced"));
|
||||
return false;
|
||||
}
|
||||
|
||||
static const int32 Seeds[] = {1337, 9176, 17015, 24854, 32693, 40532};
|
||||
static const ECaveGeneratorType Archetypes[ArchetypeCount] = {
|
||||
ECaveGeneratorType::TunnelNetwork,
|
||||
ECaveGeneratorType::FlatPlain,
|
||||
ECaveGeneratorType::CrystalChamber,
|
||||
ECaveGeneratorType::Maze,
|
||||
ECaveGeneratorType::SurfaceWorld,
|
||||
ECaveGeneratorType::VerticalShafts,
|
||||
ECaveGeneratorType::FloatingIslands,
|
||||
ECaveGeneratorType::Underwater,
|
||||
};
|
||||
|
||||
int64 GeneratorSamples = 0;
|
||||
int32 GeneratorMismatches = 0;
|
||||
FString FirstGeneratorMismatch;
|
||||
int64 DirectStackSamples = 0;
|
||||
int32 DirectStackMismatches = 0;
|
||||
FString FirstDirectStackMismatch;
|
||||
int32 DirectStackBlocks = 0;
|
||||
int32 GeneratorBlocks = 0;
|
||||
int32 ProductionArchetypeBlocks = 0;
|
||||
int32 BoundaryBlocks = 0;
|
||||
int32 NearOriginBlocks = 0;
|
||||
int32 FarOriginBlocks = 0;
|
||||
int32 GapCoverageBlocks = 0;
|
||||
int32 PassageFocusedBlocks = 0;
|
||||
int32 SpecialGapCoverageBlocks = 0;
|
||||
int32 EditedSamplesChanged = 0;
|
||||
int32 AcceptedEdits = 0;
|
||||
int32 ArchetypeLevelCounts[ArchetypeCount][6] = {};
|
||||
|
||||
for (int32 SeedIndex = 0; SeedIndex < UE_ARRAY_COUNT(Seeds); ++SeedIndex)
|
||||
{
|
||||
VoxelForgeTest::FTestWorld World;
|
||||
World.Build(Seeds[SeedIndex], /*gap chunks=*/2, /*operator stack=*/true);
|
||||
if (!World.IsValid())
|
||||
{
|
||||
AddError(World.WhyInvalid());
|
||||
return false;
|
||||
}
|
||||
// The fixture uses coarse-LOD edits up to 64 voxels wide. Keep the actual diff layer in
|
||||
// the route while allowing those bounded test brushes.
|
||||
World.DiffLayer->SetBudget(/*max mods=*/0, /*max radius=*/256.0f,
|
||||
/*max accumulated volume=*/0.0f);
|
||||
|
||||
const TArray<FStrateSlot>& Slots = World.StrateManager->GetLayout();
|
||||
const TArray<FVoxelPassage>& Passages = World.StrateManager->GetPassages();
|
||||
if (Slots.Num() < ArchetypeCount || Passages.IsEmpty())
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("seed %d is missing an archetype slot or passage fixture"), Seeds[SeedIndex]));
|
||||
return false;
|
||||
}
|
||||
const int32 GapChunkZ = FindGapChunkZ(World);
|
||||
if (GapChunkZ == MIN_int32)
|
||||
{
|
||||
AddError(FString::Printf(TEXT("seed %d produced no gap chunk fixture"), Seeds[SeedIndex]));
|
||||
return false;
|
||||
}
|
||||
|
||||
FIntVector GridOrigins[GridSetCount];
|
||||
int32 GridSteps[GridSetCount] = {};
|
||||
int32 GridArchetypes[GridSetCount] = {};
|
||||
FGridComparison BaseResults[GridSetCount];
|
||||
|
||||
// One complete mesher lattice for each production archetype per seed. Rotating level by
|
||||
// seed gives every archetype all levels 0..5 over the six seeds. The sample grids touch a
|
||||
// strate edge, and cycle through origin, near-origin, and both far-field signs.
|
||||
for (int32 ArchetypeIndex = 0; ArchetypeIndex < ArchetypeCount; ++ArchetypeIndex)
|
||||
{
|
||||
const int32 Level = (SeedIndex + ArchetypeIndex) % 6;
|
||||
const int32 Step = 1 << Level;
|
||||
const FStrateSlot& Slot = Slots[ArchetypeIndex];
|
||||
const int32 TargetChunkZ = (SeedIndex & 1) ? Slot.BottomChunkZ : Slot.TopChunkZ;
|
||||
FIntVector TargetChunk;
|
||||
switch ((SeedIndex + ArchetypeIndex) % 4)
|
||||
{
|
||||
case 0: TargetChunk = FIntVector(0, 0, TargetChunkZ); break;
|
||||
case 1: TargetChunk = FIntVector(128, 96, TargetChunkZ); ++FarOriginBlocks; break;
|
||||
case 2: TargetChunk = FIntVector(8, -8, TargetChunkZ); ++NearOriginBlocks; break;
|
||||
default: TargetChunk = FIntVector(-128, -96, TargetChunkZ); ++FarOriginBlocks; break;
|
||||
}
|
||||
if ((SeedIndex + ArchetypeIndex) % 4 == 0) { ++NearOriginBlocks; }
|
||||
|
||||
const int32 GridIndex = ArchetypeIndex;
|
||||
GridOrigins[GridIndex] = MakeMesherGridOrigin(TargetChunk, Level);
|
||||
GridSteps[GridIndex] = Step;
|
||||
GridArchetypes[GridIndex] = ArchetypeIndex;
|
||||
|
||||
FVoxelOpStack Stack;
|
||||
FVoxelOpContext Context;
|
||||
if (!BuildProductionStackForArchetype(
|
||||
World, Archetypes[ArchetypeIndex], TargetChunk, Stack, Context))
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("seed %d archetype %d production stack could not be built"),
|
||||
Seeds[SeedIndex], ArchetypeIndex));
|
||||
return false;
|
||||
}
|
||||
Context.Step = 1;
|
||||
Stack.PrepareChunk(Context);
|
||||
const FGridComparison DirectComparison = CompareStackChunkBlockWithScalar(
|
||||
Stack, GridOrigins[GridIndex], Step, TargetChunk);
|
||||
DirectStackSamples += DirectComparison.Samples;
|
||||
DirectStackMismatches += DirectComparison.Mismatches;
|
||||
++DirectStackBlocks;
|
||||
if (FirstDirectStackMismatch.IsEmpty() && DirectComparison.Mismatches > 0)
|
||||
{
|
||||
FirstDirectStackMismatch = FString::Printf(
|
||||
TEXT("seed=%d archetype=%d level=%d chunk=%s %s"),
|
||||
Seeds[SeedIndex], ArchetypeIndex, Level, *TargetChunk.ToString(),
|
||||
*DirectComparison.FirstMismatch);
|
||||
}
|
||||
|
||||
++ArchetypeLevelCounts[ArchetypeIndex][Level];
|
||||
++ProductionArchetypeBlocks;
|
||||
++BoundaryBlocks;
|
||||
|
||||
if (!World.StrateManager->UsesOperatorStackForChunk(TargetChunk)
|
||||
|| World.StrateManager->GetGeneratorTypeForChunk(TargetChunk)
|
||||
!= Archetypes[ArchetypeIndex])
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("seed %d archetype %d target chunk did not route to its production operator stack"),
|
||||
Seeds[SeedIndex], ArchetypeIndex));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Passage-mouth lattices centered in a real inter-strate gap exercise the routed
|
||||
// TunnelNetwork/Underwater block implementation through boundaries and bedrock holes.
|
||||
for (int32 PassageGrid = 0; PassageGrid < PassageGridCount; ++PassageGrid)
|
||||
{
|
||||
const int32 GridIndex = ArchetypeCount + PassageGrid;
|
||||
const int32 ArchetypeIndex = PassageGrid == 0 ? 0 : 7;
|
||||
const int32 Level = (SeedIndex + (PassageGrid == 0 ? 0 : 7)) % 6;
|
||||
const int32 Step = 1 << Level;
|
||||
const FVoxelPassage& Passage = Passages[(SeedIndex + PassageGrid) % Passages.Num()];
|
||||
const FVector Focus = PassageGrid == 0 ? Passage.UpperPoint : Passage.LowerPoint;
|
||||
const FIntVector TargetChunk(
|
||||
FMath::FloorToInt(Focus.X / CHUNK_SIZE),
|
||||
FMath::FloorToInt(Focus.Y / CHUNK_SIZE),
|
||||
GapChunkZ);
|
||||
GridOrigins[GridIndex] = MakeMesherGridOrigin(TargetChunk, Level);
|
||||
GridSteps[GridIndex] = Step;
|
||||
GridArchetypes[GridIndex] = ArchetypeIndex;
|
||||
++PassageFocusedBlocks;
|
||||
if (CountGapSamples(World, GridOrigins[GridIndex], Step) > 0)
|
||||
{
|
||||
++GapCoverageBlocks;
|
||||
++SpecialGapCoverageBlocks;
|
||||
}
|
||||
}
|
||||
|
||||
// No-edit grids. In the two canonical network archetypes this enters the actual worker
|
||||
// block session, which partitions each mesher grid by resolved chunk before running the
|
||||
// active operator list. The six legacy density routes still run their production scalar
|
||||
// evaluator, as they do outside the block-safe network branch.
|
||||
for (int32 GridIndex = 0; GridIndex < GridSetCount; ++GridIndex)
|
||||
{
|
||||
BaseResults[GridIndex] = CompareGeneratorBlockWithScalar(
|
||||
*World.Generator.Get(), GridOrigins[GridIndex], GridSteps[GridIndex]);
|
||||
GeneratorSamples += BaseResults[GridIndex].Samples;
|
||||
GeneratorMismatches += BaseResults[GridIndex].Mismatches;
|
||||
++GeneratorBlocks;
|
||||
if (FirstGeneratorMismatch.IsEmpty() && BaseResults[GridIndex].Mismatches > 0)
|
||||
{
|
||||
FirstGeneratorMismatch = FString::Printf(
|
||||
TEXT("seed=%d archetype=%d level=%d edited=0 origin=%s %s"),
|
||||
Seeds[SeedIndex], GridArchetypes[GridIndex],
|
||||
FMath::FloorLog2(GridSteps[GridIndex]), *GridOrigins[GridIndex].ToString(),
|
||||
*BaseResults[GridIndex].FirstMismatch);
|
||||
}
|
||||
}
|
||||
|
||||
// Put one real diff-layer box edit in every mesher grid, then compare the same exact
|
||||
// density lattices again. The scalar pass below runs with these player edits present.
|
||||
for (int32 GridIndex = 0; GridIndex < GridSetCount; ++GridIndex)
|
||||
{
|
||||
const int32 Step = GridSteps[GridIndex];
|
||||
FVoxelModification Edit;
|
||||
Edit.Shape = EVoxelBrushShape::Box;
|
||||
Edit.Center = FVector(GridOrigins[GridIndex]) + FVector(17 * Step);
|
||||
Edit.BoxExtent = FVector(2.0f * Step);
|
||||
Edit.Falloff = 1.0f;
|
||||
Edit.Strength = -13.0f;
|
||||
const TArray<FIntVector> ChangedChunks = World.DiffLayer->ApplyModification(Edit);
|
||||
if (ChangedChunks.IsEmpty())
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("seed %d edit for grid %d archetype %d was rejected"),
|
||||
Seeds[SeedIndex], GridIndex, GridArchetypes[GridIndex]));
|
||||
return false;
|
||||
}
|
||||
++AcceptedEdits;
|
||||
}
|
||||
|
||||
for (int32 GridIndex = 0; GridIndex < GridSetCount; ++GridIndex)
|
||||
{
|
||||
const FGridComparison Edited = CompareGeneratorBlockWithScalar(
|
||||
*World.Generator.Get(), GridOrigins[GridIndex], GridSteps[GridIndex]);
|
||||
GeneratorSamples += Edited.Samples;
|
||||
GeneratorMismatches += Edited.Mismatches;
|
||||
++GeneratorBlocks;
|
||||
if (FirstGeneratorMismatch.IsEmpty() && Edited.Mismatches > 0)
|
||||
{
|
||||
FirstGeneratorMismatch = FString::Printf(
|
||||
TEXT("seed=%d archetype=%d level=%d edited=1 origin=%s %s"),
|
||||
Seeds[SeedIndex], GridArchetypes[GridIndex],
|
||||
FMath::FloorLog2(GridSteps[GridIndex]), *GridOrigins[GridIndex].ToString(),
|
||||
*Edited.FirstMismatch);
|
||||
}
|
||||
|
||||
const int32 SharedCount = FMath::Min(
|
||||
Edited.BlockDensities.Num(), BaseResults[GridIndex].BlockDensities.Num());
|
||||
for (int32 SampleIndex = 0; SampleIndex < SharedCount; ++SampleIndex)
|
||||
{
|
||||
if (!SameFloatBits(Edited.BlockDensities[SampleIndex],
|
||||
BaseResults[GridIndex].BlockDensities[SampleIndex]))
|
||||
{
|
||||
++EditedSamplesChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool bEveryArchetypeCoveredEveryLevel = true;
|
||||
for (int32 ArchetypeIndex = 0; ArchetypeIndex < ArchetypeCount; ++ArchetypeIndex)
|
||||
{
|
||||
for (int32 Level = 0; Level < 6; ++Level)
|
||||
{
|
||||
bEveryArchetypeCoveredEveryLevel &= ArchetypeLevelCounts[ArchetypeIndex][Level] > 0;
|
||||
}
|
||||
}
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("BeginDensityBlock grid vs scalar GetDensityAt: seeds=%d archetype_grids=%d all_archetype_levels=%d boundary_grids=%d near_origin=%d far_origin=%d blocks=%d samples=%lld mismatches=%d gap_grids=%d passage_focused=%d accepted_edits=%d edited_samples_changed=%d first=%s"),
|
||||
UE_ARRAY_COUNT(Seeds), ProductionArchetypeBlocks,
|
||||
bEveryArchetypeCoveredEveryLevel ? 1 : 0, BoundaryBlocks, NearOriginBlocks,
|
||||
FarOriginBlocks, GeneratorBlocks, static_cast<long long>(GeneratorSamples),
|
||||
GeneratorMismatches, GapCoverageBlocks, PassageFocusedBlocks, AcceptedEdits,
|
||||
EditedSamplesChanged,
|
||||
FirstGeneratorMismatch.IsEmpty() ? TEXT("none") : *FirstGeneratorMismatch));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("chunk-local direct EvalBlock vs EvalInternal: blocks=%d mesher_samples=%lld mismatches=%d first=%s"),
|
||||
DirectStackBlocks, static_cast<long long>(DirectStackSamples),
|
||||
DirectStackMismatches,
|
||||
FirstDirectStackMismatch.IsEmpty() ? TEXT("none") : *FirstDirectStackMismatch));
|
||||
|
||||
TestEqual(TEXT("all eight production operator archetypes get six complete mesher grids"),
|
||||
ProductionArchetypeBlocks,
|
||||
static_cast<int32>(UE_ARRAY_COUNT(Seeds)) * ArchetypeCount);
|
||||
TestTrue(TEXT("every production operator archetype covers every meshing level 0..5"),
|
||||
bEveryArchetypeCoveredEveryLevel);
|
||||
TestEqual(TEXT("all production archetype grids touch a strate boundary"),
|
||||
BoundaryBlocks, ProductionArchetypeBlocks);
|
||||
TestTrue(TEXT("test includes near-origin and both-sign far-field grids"),
|
||||
NearOriginBlocks > 0 && FarOriginBlocks > 0);
|
||||
TestEqual(TEXT("each passage-focused grid includes a real gap sample"),
|
||||
SpecialGapCoverageBlocks,
|
||||
static_cast<int32>(UE_ARRAY_COUNT(Seeds)) * PassageGridCount);
|
||||
TestEqual(TEXT("passage-focused grids cover both network archetypes per seed"),
|
||||
PassageFocusedBlocks,
|
||||
static_cast<int32>(UE_ARRAY_COUNT(Seeds)) * PassageGridCount);
|
||||
TestEqual(TEXT("all block-session density grids match scalar output bit-for-bit"),
|
||||
GeneratorMismatches, 0);
|
||||
TestEqual(TEXT("all production stack chunk blocks match scalar bit-for-bit"),
|
||||
DirectStackMismatches, 0);
|
||||
TestEqual(TEXT("one valid player edit was applied to every tested grid"),
|
||||
AcceptedEdits, static_cast<int32>(UE_ARRAY_COUNT(Seeds)) * GridSetCount);
|
||||
TestTrue(TEXT("player edits alter samples consumed by the mesher"),
|
||||
EditedSamplesChanged > 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -24,6 +24,27 @@ namespace VoxelClipmapDesiredTiles
|
||||
FloorDiv(Value.Z, Divisor));
|
||||
}
|
||||
|
||||
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
|
||||
{
|
||||
return -FloorDiv(-Value, Divisor);
|
||||
}
|
||||
|
||||
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
|
||||
FIntVector& OutMin, FIntVector& OutMax)
|
||||
{
|
||||
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
|
||||
int32& OutAxisMin, int32& OutAxisMax)
|
||||
{
|
||||
const int32 BaseMin = CenterAxis - Radius;
|
||||
const int32 BaseEnd = CenterAxis + Radius + 1;
|
||||
OutAxisMin = FloorDiv(BaseMin, Alignment) * Alignment;
|
||||
OutAxisMax = CeilDiv(BaseEnd, Alignment) * Alignment - 1;
|
||||
};
|
||||
AlignAxis(Center.X, OutMin.X, OutMax.X);
|
||||
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
|
||||
AlignAxis(Center.Z, OutMin.Z, OutMax.Z);
|
||||
}
|
||||
|
||||
FORCEINLINE int32 OuterShellRadius(const FParameters& Parameters, int32 Radius, int32 MaxLevel)
|
||||
{
|
||||
if (Parameters.RenderDistanceChunks <= 0)
|
||||
@@ -81,18 +102,35 @@ namespace VoxelClipmapDesiredTiles
|
||||
OuterShell(Parameters, Radius, MaxLevel, SheetLevel, SheetRadius);
|
||||
const bool bSheetRing = SheetLevel > MaxLevel;
|
||||
const int32 MaxMCRingLevel = bSheetRing ? SheetLevel - 1 : MaxLevel;
|
||||
FIntVector PreviousRingMin = FIntVector::ZeroValue;
|
||||
FIntVector PreviousRingMax = FIntVector::ZeroValue;
|
||||
FIntVector MCRingMin = FIntVector::ZeroValue;
|
||||
FIntVector MCRingMax = FIntVector::ZeroValue;
|
||||
|
||||
for (int32 Level = 0; Level <= MaxMCRingLevel; ++Level)
|
||||
{
|
||||
const int32 Pow = 1 << Level;
|
||||
const FIntVector CenterLevel = FloorDiv(Center, Pow);
|
||||
const FIntVector CenterFiner = Level > 0
|
||||
? FloorDiv(Center, Pow >> 1) : FIntVector::ZeroValue;
|
||||
const int32 LevelRadius = (Level == MaxLevel && !bSheetRing)
|
||||
? OuterRadius : Radius;
|
||||
|
||||
int32 DeltaZMin = -LevelRadius;
|
||||
int32 DeltaZMax = LevelRadius;
|
||||
// Every finer shell must end on a complete child-pair boundary so a coarser tile is
|
||||
// either wholly inside it or wholly outside it. The final MC ring uses the sheet's
|
||||
// grouping width when a sheet ring follows it; otherwise the ordinary LOD boundary
|
||||
// only needs pair alignment.
|
||||
const int32 RingAlignment = Level < MaxMCRingLevel
|
||||
? 2
|
||||
: (bSheetRing ? (1 << (SheetLevel - MaxMCRingLevel)) : 1);
|
||||
FIntVector RingMin, RingMax;
|
||||
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
||||
if (Level == MaxMCRingLevel)
|
||||
{
|
||||
MCRingMin = RingMin;
|
||||
MCRingMax = RingMax;
|
||||
}
|
||||
|
||||
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
|
||||
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
|
||||
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
||||
{
|
||||
DeltaZMin = FMath::Max(DeltaZMin, FloorDiv(MinChunkZ, Pow) - CenterLevel.Z);
|
||||
@@ -100,8 +138,10 @@ namespace VoxelClipmapDesiredTiles
|
||||
}
|
||||
|
||||
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
||||
for (int32 DeltaY = -LevelRadius; DeltaY <= LevelRadius; ++DeltaY)
|
||||
for (int32 DeltaX = -LevelRadius; DeltaX <= LevelRadius; ++DeltaX)
|
||||
for (int32 DeltaY = RingMin.Y - CenterLevel.Y;
|
||||
DeltaY <= RingMax.Y - CenterLevel.Y; ++DeltaY)
|
||||
for (int32 DeltaX = RingMin.X - CenterLevel.X;
|
||||
DeltaX <= RingMax.X - CenterLevel.X; ++DeltaX)
|
||||
{
|
||||
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ);
|
||||
const int32 TileMinZ = Tile.Z << Level;
|
||||
@@ -114,12 +154,12 @@ namespace VoxelClipmapDesiredTiles
|
||||
if (Level > 0)
|
||||
{
|
||||
const bool bCovered =
|
||||
(2 * Tile.X >= CenterFiner.X - Radius)
|
||||
&& (2 * Tile.X + 1 <= CenterFiner.X + Radius)
|
||||
&& (2 * Tile.Y >= CenterFiner.Y - Radius)
|
||||
&& (2 * Tile.Y + 1 <= CenterFiner.Y + Radius)
|
||||
&& (2 * Tile.Z >= CenterFiner.Z - Radius)
|
||||
&& (2 * Tile.Z + 1 <= CenterFiner.Z + Radius);
|
||||
(2 * Tile.X >= PreviousRingMin.X)
|
||||
&& (2 * Tile.X + 1 <= PreviousRingMax.X)
|
||||
&& (2 * Tile.Y >= PreviousRingMin.Y)
|
||||
&& (2 * Tile.Y + 1 <= PreviousRingMax.Y)
|
||||
&& (2 * Tile.Z >= PreviousRingMin.Z)
|
||||
&& (2 * Tile.Z + 1 <= PreviousRingMax.Z);
|
||||
if (bCovered)
|
||||
{
|
||||
continue;
|
||||
@@ -128,14 +168,17 @@ namespace VoxelClipmapDesiredTiles
|
||||
|
||||
OutTiles.Emplace(Tile, Level);
|
||||
}
|
||||
|
||||
PreviousRingMin = RingMin;
|
||||
PreviousRingMax = RingMax;
|
||||
}
|
||||
|
||||
if (bSheetRing)
|
||||
{
|
||||
const int32 SheetPow = 1 << SheetLevel;
|
||||
const FIntVector CenterSheet = FloorDiv(Center, SheetPow);
|
||||
const FIntVector CenterMC = FloorDiv(Center, 1 << MaxMCRingLevel);
|
||||
const int32 LevelDelta = SheetLevel - MaxMCRingLevel;
|
||||
const int32 MCPerSheet = 1 << LevelDelta;
|
||||
|
||||
int32 DeltaZMin = -SheetRadius;
|
||||
int32 DeltaZMax = SheetRadius;
|
||||
@@ -160,12 +203,12 @@ namespace VoxelClipmapDesiredTiles
|
||||
}
|
||||
|
||||
const bool bCovered =
|
||||
((Tile.X << LevelDelta) >= CenterMC.X - Radius)
|
||||
&& ((((Tile.X + 1) << LevelDelta) - 1) <= CenterMC.X + Radius)
|
||||
&& ((Tile.Y << LevelDelta) >= CenterMC.Y - Radius)
|
||||
&& ((((Tile.Y + 1) << LevelDelta) - 1) <= CenterMC.Y + Radius)
|
||||
&& ((Tile.Z << LevelDelta) >= CenterMC.Z - Radius)
|
||||
&& ((((Tile.Z + 1) << LevelDelta) - 1) <= CenterMC.Z + Radius);
|
||||
(Tile.X * MCPerSheet >= MCRingMin.X)
|
||||
&& ((Tile.X + 1) * MCPerSheet - 1 <= MCRingMax.X)
|
||||
&& (Tile.Y * MCPerSheet >= MCRingMin.Y)
|
||||
&& ((Tile.Y + 1) * MCPerSheet - 1 <= MCRingMax.Y)
|
||||
&& (Tile.Z * MCPerSheet >= MCRingMin.Z)
|
||||
&& ((Tile.Z + 1) * MCPerSheet - 1 <= MCRingMax.Z);
|
||||
if (!bCovered)
|
||||
{
|
||||
OutTiles.Emplace(Tile, SheetLevel);
|
||||
|
||||
@@ -3407,6 +3407,16 @@ namespace
|
||||
// unknown/evicted proof data therefore falls back to sampling and cannot change the mesh.
|
||||
constexpr uint64 SharedRoomGraphCacheBudgetBytes = 256ull * 1024ull * 1024ull;
|
||||
|
||||
static TSharedPtr<const FChunkSDFCache, ESPMode::ThreadSafe>
|
||||
SnapshotSharedRoomGraphCache(
|
||||
const TSharedPtr<FSharedRoomGraphCacheEntry, ESPMode::ThreadSafe>& Entry)
|
||||
{
|
||||
FScopeLock Lock(&GSharedRoomGraphCacheMutex);
|
||||
return Entry.IsValid()
|
||||
? Entry->Cache
|
||||
: TSharedPtr<const FChunkSDFCache, ESPMode::ThreadSafe>();
|
||||
}
|
||||
|
||||
int32 SharedRoomGraphCacheBuildBank(int32 RegionSize)
|
||||
{
|
||||
return RegionSize <= 2 * CHUNK_SIZE ? 0 : 1;
|
||||
@@ -3593,8 +3603,11 @@ namespace
|
||||
Entry->RegionSize = RegionSize;
|
||||
Entry->LastUse = 0;
|
||||
Entry->AllocatedBytes = AllocatedBytes;
|
||||
{
|
||||
FScopeLock Lock(&GSharedRoomGraphCacheMutex);
|
||||
Entry->Cache = BuiltCache;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FScopeLock Lock(&GSharedRoomGraphCacheMutex);
|
||||
@@ -3647,7 +3660,7 @@ namespace
|
||||
return Entry;
|
||||
}
|
||||
|
||||
if (Entry->Cache.IsValid())
|
||||
if (SnapshotSharedRoomGraphCache(Entry).IsValid())
|
||||
{
|
||||
return Entry;
|
||||
}
|
||||
@@ -4119,9 +4132,13 @@ namespace
|
||||
|
||||
if (SampleStep > 1)
|
||||
{
|
||||
TSharedPtr<const FChunkSDFCache, ESPMode::ThreadSafe> SharedRoomGraphCache =
|
||||
S.SharedCacheEntry.IsValid()
|
||||
? SnapshotSharedRoomGraphCache(S.SharedCacheEntry)
|
||||
: TSharedPtr<const FChunkSDFCache, ESPMode::ThreadSafe>();
|
||||
const bool bSharedCacheMatches =
|
||||
S.SharedCacheEntry.IsValid()
|
||||
&& S.SharedCacheEntry->Cache.IsValid()
|
||||
&& SharedRoomGraphCache.IsValid()
|
||||
&& S.SharedCacheEntry->Seed == SeedU
|
||||
&& S.SharedCacheEntry->StrateIndex == StrateIdx
|
||||
&& S.SharedCacheEntry->ParamsFingerprint == ParamsFingerprint
|
||||
@@ -4143,8 +4160,9 @@ namespace
|
||||
(float)(RegionMinY + RegionSize) + CacheExpansion,
|
||||
P,
|
||||
ERoomGraphBuildSite::OpShared);
|
||||
SharedRoomGraphCache = SnapshotSharedRoomGraphCache(S.SharedCacheEntry);
|
||||
}
|
||||
S.ActiveCache = S.SharedCacheEntry->Cache.Get();
|
||||
S.ActiveCache = SharedRoomGraphCache.Get();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -5109,7 +5127,9 @@ namespace
|
||||
* VOXEL_NOISE_SCALE * VF_PerlinAbsBound + 2.0f),
|
||||
P,
|
||||
ERoomGraphBuildSite::ClassifierShared);
|
||||
B.ActiveCache = SharedCache->Cache.Get();
|
||||
const TSharedPtr<const FChunkSDFCache, ESPMode::ThreadSafe>
|
||||
SharedRoomGraphCache = SnapshotSharedRoomGraphCache(SharedCache);
|
||||
B.ActiveCache = SharedRoomGraphCache.Get();
|
||||
B.CacheWindowSharedEntry = SharedCache;
|
||||
B.CacheWindowSharedRegionMinX = SharedRegionMinX;
|
||||
B.CacheWindowSharedRegionMinY = SharedRegionMinY;
|
||||
@@ -5165,7 +5185,8 @@ namespace
|
||||
}
|
||||
else if (B.bCacheWindowUsesShared)
|
||||
{
|
||||
B.ActiveCache = B.CacheWindowSharedEntry->Cache.Get();
|
||||
B.ActiveCache = SnapshotSharedRoomGraphCache(
|
||||
B.CacheWindowSharedEntry).Get();
|
||||
}
|
||||
// Large root boxes are only a cache warm-up. Their interval is intentionally still
|
||||
// unknown; proof resumes at bounded descendants where all bounds below are finite.
|
||||
|
||||
@@ -592,6 +592,8 @@ void AVoxelWorld::RegenerateAllChunks()
|
||||
bAllChunksLoaded = false;
|
||||
DesiredSorted.Reset();
|
||||
CriticalDesiredTiles.Reset();
|
||||
DesiredTransitionDescendants.Reset();
|
||||
DesiredTransitionMaxLevel = 0;
|
||||
DesiredStamped.Reset();
|
||||
++DesiredEpoch;
|
||||
TransitionHold.Reset();
|
||||
@@ -1421,6 +1423,7 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
VoxelForgeStartupTrace::Finish(TEXT("end_play_before_steady_state"));
|
||||
}
|
||||
|
||||
LogReadyTransitionVisibilityMeasure();
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
@@ -1429,6 +1432,17 @@ void AVoxelWorld::BeginPlay()
|
||||
VoxelForgeStartupTrace::BeginFromCommandLine();
|
||||
VoxelForgeStartupTrace::FStageScope StartupTraceStage(TEXT("BeginPlay"));
|
||||
Super::BeginPlay();
|
||||
int32 MeasureReadyTransitionVisibility = 0;
|
||||
FParse::Value(FCommandLine::Get(),
|
||||
TEXT("voxel.MeasureReadyTransitionVisibility="), MeasureReadyTransitionVisibility);
|
||||
bMeasureReadyTransitionVisibility = MeasureReadyTransitionVisibility != 0;
|
||||
ReadyTransitionVisibilitySamples.Reset();
|
||||
FarFieldSubmitSamples.Reset();
|
||||
if (bMeasureReadyTransitionVisibility)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeReadyVisibilityMeasure] enabled=1 instrumentation=command_line"));
|
||||
}
|
||||
VoxelCaveMorphology::ConfigurePlayerFitMemoFromCommandLine();
|
||||
VoxelCaveMorphology::ResetPlayerFitMemoStats();
|
||||
bShuttingDown.store(false, std::memory_order_relaxed);
|
||||
@@ -3724,6 +3738,90 @@ void AVoxelWorld::LogVisualStalenessAudit()
|
||||
VisualAuditLODOverlapMaxPairs);
|
||||
}
|
||||
|
||||
void AVoxelWorld::LogReadyTransitionVisibilityMeasure()
|
||||
{
|
||||
if (!bMeasureReadyTransitionVisibility)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ReadyTransitionVisibilitySamples.IsEmpty())
|
||||
{
|
||||
TArray<FReadyTransitionVisibilitySample> Sorted = ReadyTransitionVisibilitySamples;
|
||||
Sorted.Sort([](const FReadyTransitionVisibilitySample& A,
|
||||
const FReadyTransitionVisibilitySample& B)
|
||||
{
|
||||
return A.Cycles < B.Cycles;
|
||||
});
|
||||
const int32 P50Index = Sorted.Num() / 2;
|
||||
const int32 P95Index = FMath::Clamp(
|
||||
FMath::CeilToInt(static_cast<float>(Sorted.Num()) * 0.95f) - 1,
|
||||
0, Sorted.Num() - 1);
|
||||
const FReadyTransitionVisibilitySample& P95 = Sorted[P95Index];
|
||||
const FReadyTransitionVisibilitySample& Max = Sorted.Last();
|
||||
|
||||
int32 MinComponents = MAX_int32, MaxComponents = 0;
|
||||
int32 MinDesired = MAX_int32, MaxDesired = 0;
|
||||
int64 TotalComponents = 0, TotalDesired = 0;
|
||||
for (const FReadyTransitionVisibilitySample& Sample : Sorted)
|
||||
{
|
||||
MinComponents = FMath::Min(MinComponents, Sample.TileComponentCount);
|
||||
MaxComponents = FMath::Max(MaxComponents, Sample.TileComponentCount);
|
||||
MinDesired = FMath::Min(MinDesired, Sample.DesiredTileCount);
|
||||
MaxDesired = FMath::Max(MaxDesired, Sample.DesiredTileCount);
|
||||
TotalComponents += Sample.TileComponentCount;
|
||||
TotalDesired += Sample.DesiredTileCount;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeReadyVisibilityMeasure] samples=%d p50_ms=%.6f p95_ms=%.6f max_ms=%.6f "
|
||||
"p95_components=%d p95_desired=%d components_range=%d..%d desired_range=%d..%d "
|
||||
"components_mean=%.1f desired_mean=%.1f"),
|
||||
Sorted.Num(),
|
||||
FPlatformTime::ToSeconds64(Sorted[P50Index].Cycles) * 1000.0,
|
||||
FPlatformTime::ToSeconds64(P95.Cycles) * 1000.0,
|
||||
FPlatformTime::ToSeconds64(Max.Cycles) * 1000.0,
|
||||
P95.TileComponentCount, P95.DesiredTileCount,
|
||||
MinComponents, MaxComponents, MinDesired, MaxDesired,
|
||||
static_cast<double>(TotalComponents) / Sorted.Num(),
|
||||
static_cast<double>(TotalDesired) / Sorted.Num());
|
||||
}
|
||||
|
||||
if (!FarFieldSubmitSamples.IsEmpty())
|
||||
{
|
||||
TArray<FFarFieldSubmitSample> Sorted = FarFieldSubmitSamples;
|
||||
Sorted.Sort([](const FFarFieldSubmitSample& A, const FFarFieldSubmitSample& B)
|
||||
{
|
||||
return A.Cycles < B.Cycles;
|
||||
});
|
||||
const int32 P50Index = Sorted.Num() / 2;
|
||||
const int32 P95Index = FMath::Clamp(
|
||||
FMath::CeilToInt(static_cast<float>(Sorted.Num()) * 0.95f) - 1,
|
||||
0, Sorted.Num() - 1);
|
||||
const FFarFieldSubmitSample& P95 = Sorted[P95Index];
|
||||
const FFarFieldSubmitSample& Max = Sorted.Last();
|
||||
|
||||
int32 MinDesired = MAX_int32, MaxDesired = 0;
|
||||
int32 MinLevel = MAX_int32, MaxLevel = 0;
|
||||
for (const FFarFieldSubmitSample& Sample : Sorted)
|
||||
{
|
||||
MinDesired = FMath::Min(MinDesired, Sample.DesiredTileCount);
|
||||
MaxDesired = FMath::Max(MaxDesired, Sample.DesiredTileCount);
|
||||
MinLevel = FMath::Min(MinLevel, Sample.MaxDesiredLevel);
|
||||
MaxLevel = FMath::Max(MaxLevel, Sample.MaxDesiredLevel);
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeFarFieldSubmitMeasure] samples=%d p50_ms=%.6f p95_ms=%.6f max_ms=%.6f "
|
||||
"p95_desired=%d p95_max_level=%d desired_range=%d..%d levels=%d..%d"),
|
||||
Sorted.Num(),
|
||||
FPlatformTime::ToSeconds64(Sorted[P50Index].Cycles) * 1000.0,
|
||||
FPlatformTime::ToSeconds64(P95.Cycles) * 1000.0,
|
||||
FPlatformTime::ToSeconds64(Max.Cycles) * 1000.0,
|
||||
P95.DesiredTileCount, P95.MaxDesiredLevel,
|
||||
MinDesired, MaxDesired, MinLevel, MaxLevel);
|
||||
}
|
||||
}
|
||||
|
||||
void AVoxelWorld::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
@@ -3932,9 +4030,9 @@ void AVoxelWorld::Tick(float DeltaTime)
|
||||
}
|
||||
|
||||
// Complete the one-shot diagnostic request only after the player's level-0 centre tile is
|
||||
// loaded and idle. Waiting for any coarse tile would take the async-neighbour branch and would
|
||||
// not measure the normal synchronous centre remesh that makes a local edit feel expensive.
|
||||
// No pawn is a distinct state from a pawn at (0,0,0); keep the diagnostic fallback for
|
||||
// loaded and idle. The edit fixture selects an already-visible level-0 geometry tile, so it
|
||||
// measures the normal synchronous centre remesh without depending on a coarse overlay being
|
||||
// visible. No pawn is distinct from a pawn at (0,0,0); retain the actor-location fallback for
|
||||
// headless/editor use without making the world-origin pawn skip streaming.
|
||||
FVector ModificationPosition = bHasPlayer ? PlayerLastPos : GetActorLocation();
|
||||
FVoxelTileKey ModificationCenterTile(
|
||||
@@ -3962,17 +4060,8 @@ void AVoxelWorld::Tick(float DeltaTime)
|
||||
}
|
||||
if (GVoxelForgeTestModificationLoadedGeometryTarget)
|
||||
{
|
||||
bool bHasVisibleCoarseOverlay = false;
|
||||
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& Pair : TileComponents)
|
||||
{
|
||||
if (Pair.Key.Level > 0 && Pair.Value && Pair.Value->IsVisible()
|
||||
&& VF_TileFootprintsOverlap(CandidateTile, Pair.Key))
|
||||
{
|
||||
bHasVisibleCoarseOverlay = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bHasVisibleCoarseOverlay)
|
||||
URealtimeMeshComponent* CandidateComponent = TileComponents.FindRef(CandidateTile);
|
||||
if (!CandidateComponent || !CandidateComponent->IsVisible())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -5575,12 +5664,32 @@ void AVoxelWorld::HideOverlappingDesiredVisuals(const FVoxelTileKey& RetiredTile
|
||||
|
||||
void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
||||
{
|
||||
const bool bMeasure = bMeasureReadyTransitionVisibility;
|
||||
const uint64 MeasureStartCycles = bMeasure ? FPlatformTime::Cycles64() : 0;
|
||||
const int32 ComponentCountAtStart = bMeasure ? TileComponents.Num() : 0;
|
||||
const int32 DesiredCountAtStart = bMeasure ? DesiredSorted.Num() : 0;
|
||||
const auto RecordMeasure = [this, bMeasure, MeasureStartCycles,
|
||||
ComponentCountAtStart, DesiredCountAtStart]()
|
||||
{
|
||||
if (!bMeasure)
|
||||
{
|
||||
return;
|
||||
}
|
||||
FReadyTransitionVisibilitySample& Sample =
|
||||
ReadyTransitionVisibilitySamples.AddDefaulted_GetRef();
|
||||
Sample.Cycles = FPlatformTime::Cycles64() - MeasureStartCycles;
|
||||
Sample.TileComponentCount = ComponentCountAtStart;
|
||||
Sample.DesiredTileCount = DesiredCountAtStart;
|
||||
};
|
||||
|
||||
if (bAllChunksLoaded && TransitionHold.Num() == 0 && PendingUnload.Num() == 0)
|
||||
{
|
||||
RecordMeasure();
|
||||
return;
|
||||
}
|
||||
if (DesiredSorted.Num() == 0 || TileComponents.Num() == 0)
|
||||
{
|
||||
RecordMeasure();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5590,6 +5699,11 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
||||
// queue below.
|
||||
TArray<FVoxelTileKey> ReadyToHide;
|
||||
ReadyToHide.Reserve(TileComponents.Num());
|
||||
auto GetAncestorAtLevel = [](const FVoxelTileKey& Tile, int32 AncestorLevel)
|
||||
{
|
||||
const int32 Divisor = 1 << (AncestorLevel - Tile.Level);
|
||||
return FVoxelTileKey(VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord, Divisor), AncestorLevel);
|
||||
};
|
||||
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& Pair : TileComponents)
|
||||
{
|
||||
const FVoxelTileKey& OldTile = Pair.Key;
|
||||
@@ -5606,9 +5720,12 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
||||
|
||||
bool bHasReplacement = false;
|
||||
bool bAllReplacementsLoaded = true;
|
||||
for (const FVoxelTileKey& NewTile : DesiredSorted)
|
||||
// A coarser desired tile can only overlap this tile by containing it. Test those exact
|
||||
// ancestors directly, then test desired finer tiles from the prebuilt ancestor index.
|
||||
for (int32 Level = OldTile.Level + 1; Level <= DesiredTransitionMaxLevel; ++Level)
|
||||
{
|
||||
if (!VF_TileFootprintsOverlap(OldTile, NewTile))
|
||||
const FVoxelTileKey NewTile = GetAncestorAtLevel(OldTile, Level);
|
||||
if (!IsDesired(NewTile))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -5619,6 +5736,21 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bAllReplacementsLoaded)
|
||||
{
|
||||
if (const TArray<FVoxelTileKey>* NewDescendants = DesiredTransitionDescendants.Find(OldTile))
|
||||
{
|
||||
for (const FVoxelTileKey& NewTile : *NewDescendants)
|
||||
{
|
||||
bHasReplacement = true;
|
||||
if (!LoadedTiles.Contains(NewTile))
|
||||
{
|
||||
bAllReplacementsLoaded = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bHasReplacement && bAllReplacementsLoaded)
|
||||
{
|
||||
ReadyToHide.Add(OldTile);
|
||||
@@ -5634,6 +5766,26 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
||||
// only part of the transition. Reveal them only after every overlapping retired component has
|
||||
// entered PendingUnload (and was therefore hidden above). Collision visibility is independent:
|
||||
// the component remains queryable until the budgeted UnloadTile call.
|
||||
TSet<FVoxelTileKey> VisibleRetiredTiles;
|
||||
TSet<FVoxelTileKey> VisibleRetiredAncestors;
|
||||
int32 MaxVisibleRetiredLevel = DesiredTransitionMaxLevel;
|
||||
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& OldPair : TileComponents)
|
||||
{
|
||||
const FVoxelTileKey& OldTile = OldPair.Key;
|
||||
URealtimeMeshComponent* OldComp = OldPair.Value;
|
||||
if (IsDesired(OldTile) || PendingUnload.Contains(OldTile)
|
||||
|| !OldComp || !OldComp->IsVisible())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
VisibleRetiredTiles.Add(OldTile);
|
||||
MaxVisibleRetiredLevel = FMath::Max(MaxVisibleRetiredLevel, OldTile.Level);
|
||||
for (int32 Level = OldTile.Level + 1; Level <= DesiredTransitionMaxLevel; ++Level)
|
||||
{
|
||||
VisibleRetiredAncestors.Add(GetAncestorAtLevel(OldTile, Level));
|
||||
}
|
||||
}
|
||||
|
||||
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& NewPair : TileComponents)
|
||||
{
|
||||
const FVoxelTileKey& NewTile = NewPair.Key;
|
||||
@@ -5643,19 +5795,13 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
||||
continue;
|
||||
}
|
||||
|
||||
bool bBlockedByVisibleRetiredTile = false;
|
||||
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& OldPair : TileComponents)
|
||||
bool bBlockedByVisibleRetiredTile = VisibleRetiredAncestors.Contains(NewTile);
|
||||
for (int32 Level = NewTile.Level + 1;
|
||||
!bBlockedByVisibleRetiredTile && Level <= MaxVisibleRetiredLevel; ++Level)
|
||||
{
|
||||
const FVoxelTileKey& OldTile = OldPair.Key;
|
||||
if (IsDesired(OldTile) || PendingUnload.Contains(OldTile)
|
||||
|| !OldPair.Value || !OldPair.Value->IsVisible())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (VF_TileFootprintsOverlap(NewTile, OldTile))
|
||||
if (VisibleRetiredTiles.Contains(GetAncestorAtLevel(NewTile, Level)))
|
||||
{
|
||||
bBlockedByVisibleRetiredTile = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bBlockedByVisibleRetiredTile)
|
||||
@@ -5663,6 +5809,8 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
||||
NewComp->SetVisibility(true);
|
||||
}
|
||||
}
|
||||
|
||||
RecordMeasure();
|
||||
}
|
||||
|
||||
// Integer floor-division (correct for negatives), scalar + vector.
|
||||
@@ -5855,6 +6003,36 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
|
||||
}
|
||||
}
|
||||
|
||||
// Build a dyadic ancestor index once per desired-set epoch. Different-level tile overlap is
|
||||
// containment because every tile footprint is power-of-two aligned. This lets the per-frame
|
||||
// transition pass inspect only actual overlapping candidates.
|
||||
DesiredTransitionDescendants.Reset();
|
||||
DesiredTransitionMaxLevel = 0;
|
||||
for (const FVoxelTileKey& Key : DesiredSorted)
|
||||
{
|
||||
DesiredTransitionMaxLevel = FMath::Max(DesiredTransitionMaxLevel, Key.Level);
|
||||
}
|
||||
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& Pair : TileComponents)
|
||||
{
|
||||
DesiredTransitionMaxLevel = FMath::Max(DesiredTransitionMaxLevel, Pair.Key.Level);
|
||||
}
|
||||
// A worker from the previous desired epoch can finish late with a component at a level no
|
||||
// longer present in TileComponents when this index is built. Index through the supported
|
||||
// maximum sheet level (MaxClipLevel 8 + FarSheetSpanLevels 4) so that such a retired tile still
|
||||
// finds all of its desired descendants when the result is reconciled.
|
||||
constexpr int32 MaxSupportedTileLevel = 12;
|
||||
const int32 DescendantIndexMaxLevel = FMath::Max(
|
||||
DesiredTransitionMaxLevel, MaxSupportedTileLevel);
|
||||
for (const FVoxelTileKey& Key : DesiredSorted)
|
||||
{
|
||||
for (int32 Level = Key.Level + 1; Level <= DescendantIndexMaxLevel; ++Level)
|
||||
{
|
||||
const int32 Divisor = 1 << (Level - Key.Level);
|
||||
const FVoxelTileKey Ancestor(VF_FloorDiv(Key.Coord, Divisor), Level);
|
||||
DesiredTransitionDescendants.FindOrAdd(Ancestor).Add(Key);
|
||||
}
|
||||
}
|
||||
|
||||
if (VoxelForgeStartupTrace::IsActive())
|
||||
{
|
||||
int32 LOD0Count = 0;
|
||||
@@ -6433,6 +6611,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
// FAR FIELD — preserve the clipmap's level ordering explicitly. DesiredSorted is
|
||||
// distance-sorted for the critical/near work, but a level pass prevents a level-5 horizon
|
||||
// tile from being admitted ahead of an unfinished level-1/2 bridge tile.
|
||||
const uint64 FarFieldMeasureStartCycles = bMeasureReadyTransitionVisibility
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
int32 MaxDesiredLevel = 0;
|
||||
for (const FVoxelTileKey& T : DesiredSorted)
|
||||
{
|
||||
@@ -6451,6 +6631,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
}
|
||||
if (PendingTiles.Num() >= CoarseTaskCap) break;
|
||||
}
|
||||
if (bMeasureReadyTransitionVisibility)
|
||||
{
|
||||
FFarFieldSubmitSample& Sample = FarFieldSubmitSamples.AddDefaulted_GetRef();
|
||||
Sample.Cycles = FPlatformTime::Cycles64() - FarFieldMeasureStartCycles;
|
||||
Sample.DesiredTileCount = DesiredSorted.Num();
|
||||
Sample.MaxDesiredLevel = MaxDesiredLevel;
|
||||
}
|
||||
|
||||
// Bande de strate changée : re-gen budgétée des tuiles grossières concernées (le vieux
|
||||
// mesh reste visible jusqu'au résultat — même schéma que RemeshDirtyChunks).
|
||||
|
||||
@@ -560,6 +560,7 @@ private:
|
||||
void MaybeFinishHeadlessStreamingTest();
|
||||
void UpdateVisualStalenessAudit();
|
||||
void LogVisualStalenessAudit();
|
||||
void LogReadyTransitionVisibilityMeasure();
|
||||
void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
||||
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
|
||||
bool bSheetTile, const FVoxelMeshData& MeshData);
|
||||
@@ -989,10 +990,30 @@ public:
|
||||
bool bSampleStacksStarted = false;
|
||||
TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
|
||||
TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile
|
||||
// Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this
|
||||
// index to test transition coverage without rescanning the full desired set per old tile.
|
||||
TMap<FVoxelTileKey, TArray<FVoxelTileKey>> DesiredTransitionDescendants;
|
||||
int32 DesiredTransitionMaxLevel = 0;
|
||||
uint32 DesiredEpoch = 0; // increments whenever DesiredStamped is rebuilt
|
||||
int32 ObsoleteTileAbortCount = 0; // cumulative per-world moving-session cancellation count
|
||||
float PeakObservedPawnSpeedCmPerSecond = 0.0f;
|
||||
|
||||
struct FReadyTransitionVisibilitySample
|
||||
{
|
||||
uint64 Cycles = 0;
|
||||
int32 TileComponentCount = 0;
|
||||
int32 DesiredTileCount = 0;
|
||||
};
|
||||
struct FFarFieldSubmitSample
|
||||
{
|
||||
uint64 Cycles = 0;
|
||||
int32 DesiredTileCount = 0;
|
||||
int32 MaxDesiredLevel = 0;
|
||||
};
|
||||
bool bMeasureReadyTransitionVisibility = false;
|
||||
TArray<FReadyTransitionVisibilitySample> ReadyTransitionVisibilitySamples;
|
||||
TArray<FFarFieldSubmitSample> FarFieldSubmitSamples;
|
||||
|
||||
// Clean performance accounting. LOD 0..8 covers every marching-cubes clip level; sheet tiles
|
||||
// above the configured MC range never enter the outer classifier.
|
||||
static constexpr int32 TrackedClassifierLODCount = 9;
|
||||
|
||||
Reference in new issue
Block a user