measure(step2a): candidate bins alone are slower -- the per-sample GetDensityAt call must go
Editor-only ablation (explore commandlet, -regioneval, default off): 8^3 bins for rooms, room-floor joins and tunnel chains, but every point still goes through GetDensityAt. 259^3 ring, seed 1337, six workers: live : 6.776 s wall, 29.16 s worker CPU, 1.679 us/sample, 21.2 candidate visits/sample binned: 19.54 s wall, 50.02 s worker CPU, 2.879 us/sample, 16.4 candidate visits/sample Correctness: zero Q16 (x16, ties away from zero) disagreements on the full ring and ~284k probes on 4 more seeds (composer recipes, warped rooms, passages, landings, strate edges, edits); shrinking room reach to 0.5 -> 18,107 Q16 / 735 sign differences (the check bites). Not the planned region program: the step-2 question is still open. Baseline for the next attempt: 1.68 us CPU per sample on the live path. 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
3b4d6855fe
commit
41a9265ebd
8 files changed
+1693
-31
No files matched your search
@@ -46,6 +46,11 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
thread_local bool GVoxelResearchBrickBinningEnabled = false;
|
||||
thread_local float GVoxelResearchBrickRoomReachScale = 1.0f;
|
||||
#endif
|
||||
|
||||
using FVFPlayerDensitySampler = TFunctionRef<float(float, float, float)>;
|
||||
|
||||
// Density coordinates and cached cull radii are float-valued. FVector is double-valued in
|
||||
@@ -1282,6 +1287,68 @@ bool FChunkSDFSpatialIndex::GetRange(
|
||||
return OutBegin >= 0 && OutEnd >= OutBegin && OutEnd <= ItemIndices.Num();
|
||||
}
|
||||
|
||||
void FChunkSDFBrickIndex::Reset()
|
||||
{
|
||||
MinBrickX = MinBrickY = MinBrickZ = 0;
|
||||
NumBricksX = NumBricksY = NumBricksZ = 0;
|
||||
bValid = false;
|
||||
CellOffsets.Reset();
|
||||
ItemIndices.Reset();
|
||||
}
|
||||
|
||||
void FChunkSDFBrickIndex::Release()
|
||||
{
|
||||
MinBrickX = MinBrickY = MinBrickZ = 0;
|
||||
NumBricksX = NumBricksY = NumBricksZ = 0;
|
||||
bValid = false;
|
||||
CellOffsets.Empty();
|
||||
ItemIndices.Empty();
|
||||
}
|
||||
|
||||
SIZE_T FChunkSDFBrickIndex::GetAllocatedSize() const
|
||||
{
|
||||
return static_cast<SIZE_T>(CellOffsets.GetAllocatedSize())
|
||||
+ static_cast<SIZE_T>(ItemIndices.GetAllocatedSize());
|
||||
}
|
||||
|
||||
bool FChunkSDFBrickIndex::GetRange(
|
||||
float WorldX, float WorldY, float WorldZ,
|
||||
int32& OutBegin, int32& OutEnd) const
|
||||
{
|
||||
OutBegin = OutEnd = 0;
|
||||
constexpr double BrickSize = 8.0;
|
||||
if (!bValid || NumBricksX <= 0 || NumBricksY <= 0 || NumBricksZ <= 0
|
||||
|| !VoxelMath::IsFinite(WorldX) || !VoxelMath::IsFinite(WorldY)
|
||||
|| !VoxelMath::IsFinite(WorldZ))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const double BX = FMath::FloorToDouble(static_cast<double>(WorldX) / BrickSize);
|
||||
const double BY = FMath::FloorToDouble(static_cast<double>(WorldY) / BrickSize);
|
||||
const double BZ = FMath::FloorToDouble(static_cast<double>(WorldZ) / BrickSize);
|
||||
if (BX < MIN_int32 || BX > MAX_int32 || BY < MIN_int32 || BY > MAX_int32
|
||||
|| BZ < MIN_int32 || BZ > MAX_int32)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int64 X = static_cast<int64>(BX) - MinBrickX;
|
||||
const int64 Y = static_cast<int64>(BY) - MinBrickY;
|
||||
const int64 Z = static_cast<int64>(BZ) - MinBrickZ;
|
||||
if (X < 0 || X >= NumBricksX || Y < 0 || Y >= NumBricksY
|
||||
|| Z < 0 || Z >= NumBricksZ)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int64 Slot = (Z * NumBricksY + Y) * NumBricksX + X;
|
||||
if (Slot < 0 || Slot + 1 >= CellOffsets.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutBegin = CellOffsets[static_cast<int32>(Slot)];
|
||||
OutEnd = CellOffsets[static_cast<int32>(Slot + 1)];
|
||||
return OutBegin >= 0 && OutEnd >= OutBegin && OutEnd <= ItemIndices.Num();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
struct FVFSpatialBounds
|
||||
@@ -1513,6 +1580,42 @@ namespace
|
||||
return CandidateCount;
|
||||
}
|
||||
|
||||
template <typename FVisit>
|
||||
static FORCEINLINE int32 VF_ForEachSDFCandidate(
|
||||
const FChunkSDFSpatialIndex& SpatialIndex,
|
||||
#if WITH_EDITOR
|
||||
const FChunkSDFBrickIndex& BrickIndex,
|
||||
#endif
|
||||
int32 ItemCount, float WorldX, float WorldY, float WorldZ,
|
||||
FVisit&& Visit, bool bUseSpatialIndex = true)
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
if (VoxelCaveMorphology::IsResearchBrickBinningEnabled())
|
||||
{
|
||||
int32 Begin = 0;
|
||||
int32 End = 0;
|
||||
if (BrickIndex.GetRange(WorldX, WorldY, WorldZ, Begin, End))
|
||||
{
|
||||
int32 CandidateCount = 0;
|
||||
const int32* Indices = BrickIndex.ItemIndices.GetData();
|
||||
for (int32 Cursor = Begin; Cursor < End; ++Cursor)
|
||||
{
|
||||
const int32 ItemIndex = Indices[Cursor];
|
||||
if (ItemIndex >= 0 && ItemIndex < ItemCount)
|
||||
{
|
||||
++CandidateCount;
|
||||
Visit(ItemIndex);
|
||||
}
|
||||
}
|
||||
return CandidateCount;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return VF_ForEachSpatialCandidate(
|
||||
SpatialIndex, ItemCount, WorldX, WorldY,
|
||||
Forward<FVisit>(Visit), bUseSpatialIndex);
|
||||
}
|
||||
|
||||
static bool VF_GetRoomSpatialBounds(int32, const FCachedRoom& Room, FVFSpatialBounds& OutBounds)
|
||||
{
|
||||
return VF_SetSpatialBounds(
|
||||
@@ -1559,9 +1662,13 @@ namespace
|
||||
}
|
||||
bInside = VF_CachedRoomShapeSDF(Room, Position) < 0.0f;
|
||||
};
|
||||
VF_ForEachSpatialCandidate(
|
||||
Cache.RoomSpatialIndex, Cache.Rooms.Num(),
|
||||
static_cast<float>(Position.X), static_cast<float>(Position.Y),
|
||||
VF_ForEachSDFCandidate(
|
||||
Cache.RoomSpatialIndex,
|
||||
#if WITH_EDITOR
|
||||
Cache.RoomBrickIndex,
|
||||
#endif
|
||||
Cache.Rooms.Num(), static_cast<float>(Position.X),
|
||||
static_cast<float>(Position.Y), static_cast<float>(Position.Z),
|
||||
VisitRoom, bUseSpatialIndex);
|
||||
return bInside;
|
||||
}
|
||||
@@ -1643,6 +1750,261 @@ namespace
|
||||
Column.CenterX, Column.CenterY,
|
||||
FMath::Sqrt(FMath::Max(Column.BoundXYRadiusSq, 0.0f)), OutBounds);
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
struct FVFBrickBounds
|
||||
{
|
||||
FVector Min = FVector::ZeroVector;
|
||||
FVector Max = FVector::ZeroVector;
|
||||
};
|
||||
|
||||
static bool VF_IncludeBrickBounds(
|
||||
FVFBrickBounds& InOut, const FVector& Min, const FVector& Max, bool& bHasBounds)
|
||||
{
|
||||
if (!VoxelMath::IsFinite(Min.X) || !VoxelMath::IsFinite(Min.Y)
|
||||
|| !VoxelMath::IsFinite(Min.Z) || !VoxelMath::IsFinite(Max.X)
|
||||
|| !VoxelMath::IsFinite(Max.Y) || !VoxelMath::IsFinite(Max.Z)
|
||||
|| Min.X > Max.X || Min.Y > Max.Y || Min.Z > Max.Z)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!bHasBounds)
|
||||
{
|
||||
InOut.Min = Min;
|
||||
InOut.Max = Max;
|
||||
bHasBounds = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
InOut.Min.X = FMath::Min(InOut.Min.X, Min.X);
|
||||
InOut.Min.Y = FMath::Min(InOut.Min.Y, Min.Y);
|
||||
InOut.Min.Z = FMath::Min(InOut.Min.Z, Min.Z);
|
||||
InOut.Max.X = FMath::Max(InOut.Max.X, Max.X);
|
||||
InOut.Max.Y = FMath::Max(InOut.Max.Y, Max.Y);
|
||||
InOut.Max.Z = FMath::Max(InOut.Max.Z, Max.Z);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool VF_IncludeBrickSphere(
|
||||
FVFBrickBounds& InOut, const FVector& Center, float Radius, bool& bHasBounds)
|
||||
{
|
||||
if (!VoxelMath::IsFinite(Radius) || Radius < 0.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const FVector FloatCenter(
|
||||
static_cast<float>(Center.X), static_cast<float>(Center.Y), static_cast<float>(Center.Z));
|
||||
const float CoordinateScale = FMath::Max3(
|
||||
1.0f, FMath::Abs(static_cast<float>(Center.X)),
|
||||
FMath::Max3(FMath::Abs(static_cast<float>(Center.Y)),
|
||||
FMath::Abs(static_cast<float>(Center.Z)), FMath::Abs(Radius)));
|
||||
// Leave a float-operation margin around the exact live cull. It covers rounding in the
|
||||
// float center conversion, distance square/sum, and radius-square comparison; it is a
|
||||
// numerical bound, not an authored feature-radius guess.
|
||||
const float ConservativeRadius = Radius + CoordinateScale * (32.0f * FLT_EPSILON);
|
||||
const FVector R(ConservativeRadius, ConservativeRadius, ConservativeRadius);
|
||||
return VF_IncludeBrickBounds(InOut, FloatCenter - R, FloatCenter + R, bHasBounds);
|
||||
}
|
||||
|
||||
template <typename FGetBounds>
|
||||
static void VF_BuildBrickIndex(
|
||||
FChunkSDFBrickIndex& OutIndex, int32 ItemCount, FGetBounds&& GetBounds)
|
||||
{
|
||||
OutIndex.Reset();
|
||||
if (ItemCount <= 0)
|
||||
{
|
||||
OutIndex.bValid = true;
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr double BrickSize = 8.0;
|
||||
constexpr int64 MaxBuckets = 1024 * 1024;
|
||||
constexpr int64 MaxReferences = 4 * 1024 * 1024;
|
||||
TArray<FIntVector> ItemMin;
|
||||
TArray<FIntVector> ItemMax;
|
||||
ItemMin.SetNumUninitialized(ItemCount);
|
||||
ItemMax.SetNumUninitialized(ItemCount);
|
||||
int32 GlobalMinX = MAX_int32, GlobalMinY = MAX_int32, GlobalMinZ = MAX_int32;
|
||||
int32 GlobalMaxX = MIN_int32, GlobalMaxY = MIN_int32, GlobalMaxZ = MIN_int32;
|
||||
int64 TotalReferences = 0;
|
||||
for (int32 Item = 0; Item < ItemCount; ++Item)
|
||||
{
|
||||
FVFBrickBounds Bounds;
|
||||
if (!GetBounds(Item, Bounds)
|
||||
|| !VoxelMath::IsFinite(Bounds.Min.X) || !VoxelMath::IsFinite(Bounds.Min.Y)
|
||||
|| !VoxelMath::IsFinite(Bounds.Min.Z) || !VoxelMath::IsFinite(Bounds.Max.X)
|
||||
|| !VoxelMath::IsFinite(Bounds.Max.Y) || !VoxelMath::IsFinite(Bounds.Max.Z))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const double MinX = FMath::FloorToDouble(Bounds.Min.X / BrickSize);
|
||||
const double MinY = FMath::FloorToDouble(Bounds.Min.Y / BrickSize);
|
||||
const double MinZ = FMath::FloorToDouble(Bounds.Min.Z / BrickSize);
|
||||
const double MaxX = FMath::FloorToDouble(Bounds.Max.X / BrickSize);
|
||||
const double MaxY = FMath::FloorToDouble(Bounds.Max.Y / BrickSize);
|
||||
const double MaxZ = FMath::FloorToDouble(Bounds.Max.Z / BrickSize);
|
||||
if (MinX < MIN_int32 || MinX > MAX_int32 || MinY < MIN_int32 || MinY > MAX_int32
|
||||
|| MinZ < MIN_int32 || MinZ > MAX_int32 || MaxX < MIN_int32 || MaxX > MAX_int32
|
||||
|| MaxY < MIN_int32 || MaxY > MAX_int32 || MaxZ < MIN_int32 || MaxZ > MAX_int32)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const FIntVector Lower(
|
||||
static_cast<int32>(MinX), static_cast<int32>(MinY), static_cast<int32>(MinZ));
|
||||
const FIntVector Upper(
|
||||
static_cast<int32>(MaxX), static_cast<int32>(MaxY), static_cast<int32>(MaxZ));
|
||||
if (Lower.X > Upper.X || Lower.Y > Upper.Y || Lower.Z > Upper.Z)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ItemMin[Item] = Lower;
|
||||
ItemMax[Item] = Upper;
|
||||
GlobalMinX = FMath::Min(GlobalMinX, Lower.X);
|
||||
GlobalMinY = FMath::Min(GlobalMinY, Lower.Y);
|
||||
GlobalMinZ = FMath::Min(GlobalMinZ, Lower.Z);
|
||||
GlobalMaxX = FMath::Max(GlobalMaxX, Upper.X);
|
||||
GlobalMaxY = FMath::Max(GlobalMaxY, Upper.Y);
|
||||
GlobalMaxZ = FMath::Max(GlobalMaxZ, Upper.Z);
|
||||
const int64 NX = static_cast<int64>(Upper.X) - Lower.X + 1;
|
||||
const int64 NY = static_cast<int64>(Upper.Y) - Lower.Y + 1;
|
||||
const int64 NZ = static_cast<int64>(Upper.Z) - Lower.Z + 1;
|
||||
if (NX <= 0 || NY <= 0 || NZ <= 0 || NX > MaxReferences / NY
|
||||
|| NX * NY > MaxReferences / NZ)
|
||||
{
|
||||
return;
|
||||
}
|
||||
TotalReferences += NX * NY * NZ;
|
||||
if (TotalReferences > MaxReferences)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const int64 NumX = static_cast<int64>(GlobalMaxX) - GlobalMinX + 1;
|
||||
const int64 NumY = static_cast<int64>(GlobalMaxY) - GlobalMinY + 1;
|
||||
const int64 NumZ = static_cast<int64>(GlobalMaxZ) - GlobalMinZ + 1;
|
||||
if (NumX <= 0 || NumY <= 0 || NumZ <= 0 || NumX > MaxBuckets / NumY
|
||||
|| NumX * NumY > MaxBuckets / NumZ)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const int64 BucketCount = NumX * NumY * NumZ;
|
||||
OutIndex.MinBrickX = GlobalMinX;
|
||||
OutIndex.MinBrickY = GlobalMinY;
|
||||
OutIndex.MinBrickZ = GlobalMinZ;
|
||||
OutIndex.NumBricksX = static_cast<int32>(NumX);
|
||||
OutIndex.NumBricksY = static_cast<int32>(NumY);
|
||||
OutIndex.NumBricksZ = static_cast<int32>(NumZ);
|
||||
OutIndex.CellOffsets.SetNumZeroed(static_cast<int32>(BucketCount + 1));
|
||||
for (int32 Item = 0; Item < ItemCount; ++Item)
|
||||
{
|
||||
for (int32 Z = ItemMin[Item].Z; Z <= ItemMax[Item].Z; ++Z)
|
||||
{
|
||||
for (int32 Y = ItemMin[Item].Y; Y <= ItemMax[Item].Y; ++Y)
|
||||
{
|
||||
const int64 Row = (static_cast<int64>(Z - OutIndex.MinBrickZ)
|
||||
* OutIndex.NumBricksY + (Y - OutIndex.MinBrickY))
|
||||
* OutIndex.NumBricksX;
|
||||
for (int32 X = ItemMin[Item].X; X <= ItemMax[Item].X; ++X)
|
||||
{
|
||||
const int64 Slot = Row + (X - OutIndex.MinBrickX);
|
||||
++OutIndex.CellOffsets[static_cast<int32>(Slot + 1)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int32 Slot = 0; Slot < static_cast<int32>(BucketCount); ++Slot)
|
||||
{
|
||||
OutIndex.CellOffsets[Slot + 1] += OutIndex.CellOffsets[Slot];
|
||||
}
|
||||
OutIndex.ItemIndices.SetNumUninitialized(static_cast<int32>(TotalReferences));
|
||||
TArray<int32> WriteOffsets = OutIndex.CellOffsets;
|
||||
for (int32 Item = 0; Item < ItemCount; ++Item)
|
||||
{
|
||||
for (int32 Z = ItemMin[Item].Z; Z <= ItemMax[Item].Z; ++Z)
|
||||
{
|
||||
for (int32 Y = ItemMin[Item].Y; Y <= ItemMax[Item].Y; ++Y)
|
||||
{
|
||||
const int64 Row = (static_cast<int64>(Z - OutIndex.MinBrickZ)
|
||||
* OutIndex.NumBricksY + (Y - OutIndex.MinBrickY))
|
||||
* OutIndex.NumBricksX;
|
||||
for (int32 X = ItemMin[Item].X; X <= ItemMax[Item].X; ++X)
|
||||
{
|
||||
const int64 Slot = Row + (X - OutIndex.MinBrickX);
|
||||
OutIndex.ItemIndices[WriteOffsets[static_cast<int32>(Slot)]++] = Item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
OutIndex.bValid = true;
|
||||
}
|
||||
|
||||
static bool VF_GetRoomBrickBoundsSafe(
|
||||
int32, const FCachedRoom& Room, FVFBrickBounds& OutBounds)
|
||||
{
|
||||
// EvaluateSDFCached receives the already-warped room query coordinate. Bin in that same
|
||||
// space, so we preserve the live query's exact warp without estimating an inverse-warp
|
||||
// displacement. CullRadiusSq is the baked superset of room shape, relief, blend and op reach.
|
||||
const float Radius = FMath::Sqrt(FMath::Max(Room.CullRadiusSq, 0.0f))
|
||||
* FMath::Clamp(GVoxelResearchBrickRoomReachScale, 0.0f, 1.0f);
|
||||
const FVector R(Radius, Radius, Radius);
|
||||
bool bHasBounds = false;
|
||||
return VF_IncludeBrickBounds(OutBounds, Room.Center - R, Room.Center + R, bHasBounds);
|
||||
}
|
||||
|
||||
static bool VF_GetJoinBrickBounds(
|
||||
int32, const FCachedRoomFloorJoin& Join, FVFBrickBounds& OutBounds)
|
||||
{
|
||||
const float Radius = FMath::Sqrt(FMath::Max(Join.BoundRadiusSq, 0.0f));
|
||||
const FVector R(Radius, Radius, Radius);
|
||||
bool bHasBounds = false;
|
||||
return VF_IncludeBrickBounds(OutBounds, Join.BoundCenter - R, Join.BoundCenter + R, bHasBounds);
|
||||
}
|
||||
|
||||
static bool VF_GetTunnelBrickBounds(
|
||||
int32, const FCachedTunnel& Tunnel, FVFBrickBounds& OutBounds)
|
||||
{
|
||||
// The final field evaluates both the warped SDF chain and the authored world chain. Keep
|
||||
// both bound sets, including their finalized blend/detail/floor influence, in every bin.
|
||||
bool bHasBounds = false;
|
||||
const auto IncludeSphere = [&](const FVector& Center, float Radius)
|
||||
{
|
||||
return VF_IncludeBrickSphere(OutBounds, Center, Radius, bHasBounds);
|
||||
};
|
||||
const auto IncludeAabb = [&](const FVector& Min, const FVector& Max, float Radius)
|
||||
{
|
||||
const FVector FloatMin(
|
||||
static_cast<float>(Min.X), static_cast<float>(Min.Y), static_cast<float>(Min.Z));
|
||||
const FVector FloatMax(
|
||||
static_cast<float>(Max.X), static_cast<float>(Max.Y), static_cast<float>(Max.Z));
|
||||
const float CoordinateScale = FMath::Max3(
|
||||
1.0f,
|
||||
FMath::Max3(FMath::Abs(static_cast<float>(Min.X)), FMath::Abs(static_cast<float>(Max.X)), FMath::Abs(Radius)),
|
||||
FMath::Max3(FMath::Abs(static_cast<float>(Min.Y)), FMath::Abs(static_cast<float>(Max.Y)),
|
||||
FMath::Max(FMath::Abs(static_cast<float>(Min.Z)), FMath::Abs(static_cast<float>(Max.Z)))));
|
||||
const float ConservativeRadius = FMath::Max(Radius, 0.0f)
|
||||
+ CoordinateScale * (32.0f * FLT_EPSILON);
|
||||
const FVector R(ConservativeRadius, ConservativeRadius, ConservativeRadius);
|
||||
return VF_IncludeBrickBounds(OutBounds, FloatMin - R, FloatMax + R, bHasBounds);
|
||||
};
|
||||
return IncludeSphere(Tunnel.BoundCenter, FMath::Sqrt(FMath::Max(Tunnel.BoundRadiusSq, 0.0f)))
|
||||
&& IncludeSphere(Tunnel.WorldBoundCenter, FMath::Sqrt(FMath::Max(Tunnel.WorldBoundRadiusSq, 0.0f)))
|
||||
&& IncludeAabb(Tunnel.SDFCenterlineMin, Tunnel.SDFCenterlineMax, Tunnel.SDFInfluenceRadius)
|
||||
&& IncludeAabb(Tunnel.WorldCenterlineMin, Tunnel.WorldCenterlineMax, Tunnel.WorldInfluenceRadius)
|
||||
&& bHasBounds;
|
||||
}
|
||||
|
||||
static void VF_BuildResearchBrickIndices(FChunkSDFCache& Cache)
|
||||
{
|
||||
VF_BuildBrickIndex(Cache.RoomBrickIndex, Cache.Rooms.Num(),
|
||||
[&Cache](int32 I, FVFBrickBounds& B) { return VF_GetRoomBrickBoundsSafe(I, Cache.Rooms[I], B); });
|
||||
VF_BuildBrickIndex(Cache.RoomFloorJoinBrickIndex, Cache.RoomFloorJoins.Num(),
|
||||
[&Cache](int32 I, FVFBrickBounds& B) { return VF_GetJoinBrickBounds(I, Cache.RoomFloorJoins[I], B); });
|
||||
VF_BuildBrickIndex(Cache.TunnelBrickIndex, Cache.Tunnels.Num(),
|
||||
[&Cache](int32 I, FVFBrickBounds& B) { return VF_GetTunnelBrickBounds(I, Cache.Tunnels[I], B); });
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void FChunkSDFCache::Reset()
|
||||
@@ -1667,6 +2029,11 @@ void FChunkSDFCache::Reset()
|
||||
PitSpatialIndex.Reset();
|
||||
ChimneySpatialIndex.Reset();
|
||||
ColumnSpatialIndex.Reset();
|
||||
#if WITH_EDITOR
|
||||
RoomBrickIndex.Reset();
|
||||
RoomFloorJoinBrickIndex.Reset();
|
||||
TunnelBrickIndex.Reset();
|
||||
#endif
|
||||
SDFBlendRadius = 0.0f;
|
||||
}
|
||||
|
||||
@@ -1684,6 +2051,11 @@ void FChunkSDFCache::Release()
|
||||
PitSpatialIndex.Release();
|
||||
ChimneySpatialIndex.Release();
|
||||
ColumnSpatialIndex.Release();
|
||||
#if WITH_EDITOR
|
||||
RoomBrickIndex.Release();
|
||||
RoomFloorJoinBrickIndex.Release();
|
||||
TunnelBrickIndex.Release();
|
||||
#endif
|
||||
}
|
||||
|
||||
SIZE_T FChunkSDFCache::GetAllocatedSize() const
|
||||
@@ -1712,6 +2084,12 @@ VoxelDensityProfile::FCacheMemoryBreakdown FChunkSDFCache::GetAllocatedSizeBreak
|
||||
+ PitSpatialIndex.GetAllocatedSize()
|
||||
+ ChimneySpatialIndex.GetAllocatedSize()
|
||||
+ ColumnSpatialIndex.GetAllocatedSize());
|
||||
#if WITH_EDITOR
|
||||
Breakdown.SpatialIndexBytes += static_cast<uint64>(
|
||||
RoomBrickIndex.GetAllocatedSize()
|
||||
+ RoomFloorJoinBrickIndex.GetAllocatedSize()
|
||||
+ TunnelBrickIndex.GetAllocatedSize());
|
||||
#endif
|
||||
for (const FCachedTunnel& Tunnel : Tunnels)
|
||||
{
|
||||
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.ControlPoints);
|
||||
@@ -2753,6 +3131,12 @@ static void BakeRoomFeature(
|
||||
{
|
||||
return VF_GetColumnSpatialBounds(Index, Cache.Columns[Index], Bounds);
|
||||
});
|
||||
#if WITH_EDITOR
|
||||
if (VoxelCaveMorphology::IsResearchBrickBinningEnabled())
|
||||
{
|
||||
VF_BuildResearchBrickIndices(Cache);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
namespace
|
||||
{
|
||||
@@ -6278,6 +6662,24 @@ namespace
|
||||
VoxelPassageGeometry::SealSafetyMarginVoxels;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void VoxelCaveMorphology::SetResearchBrickBinningEnabled(bool bEnabled)
|
||||
{
|
||||
GVoxelResearchBrickBinningEnabled = bEnabled;
|
||||
}
|
||||
|
||||
bool VoxelCaveMorphology::IsResearchBrickBinningEnabled()
|
||||
{
|
||||
return GVoxelResearchBrickBinningEnabled;
|
||||
}
|
||||
|
||||
void VoxelCaveMorphology::SetResearchBrickRoomReachScale(float Scale)
|
||||
{
|
||||
GVoxelResearchBrickRoomReachScale = VoxelMath::IsFinite(Scale)
|
||||
? FMath::Clamp(Scale, 0.0f, 1.0f) : 1.0f;
|
||||
}
|
||||
#endif
|
||||
|
||||
FVector VoxelCaveMorphology::ApplyCaveWarp(
|
||||
const FVector& WorldPoint,
|
||||
const FStrateGenerationParams& Params,
|
||||
@@ -8702,9 +9104,12 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
||||
int32 RoomCandidateCount = 0;
|
||||
if (!VoxelDensityAblation::IsRoomSDFOff())
|
||||
{
|
||||
RoomCandidateCount = VF_ForEachSpatialCandidate(
|
||||
Cache.RoomSpatialIndex, Cache.Rooms.Num(), WorldX, WorldY, EvaluateRoom,
|
||||
bUseSpatialIndex);
|
||||
RoomCandidateCount = VF_ForEachSDFCandidate(
|
||||
Cache.RoomSpatialIndex,
|
||||
#if WITH_EDITOR
|
||||
Cache.RoomBrickIndex,
|
||||
#endif
|
||||
Cache.Rooms.Num(), WorldX, WorldY, WorldZ, EvaluateRoom, bUseSpatialIndex);
|
||||
}
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
@@ -8742,9 +9147,13 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
||||
};
|
||||
if (!VoxelDensityAblation::IsRoomSDFOff())
|
||||
{
|
||||
VF_ForEachSpatialCandidate(
|
||||
Cache.RoomFloorJoinSpatialIndex, Cache.RoomFloorJoins.Num(),
|
||||
WorldX, WorldY, EvaluateJoin, bUseSpatialIndex);
|
||||
VF_ForEachSDFCandidate(
|
||||
Cache.RoomFloorJoinSpatialIndex,
|
||||
#if WITH_EDITOR
|
||||
Cache.RoomFloorJoinBrickIndex,
|
||||
#endif
|
||||
Cache.RoomFloorJoins.Num(), WorldX, WorldY, WorldZ,
|
||||
EvaluateJoin, bUseSpatialIndex);
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
@@ -8839,10 +9248,14 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
||||
int32 TunnelCandidateCount = 0;
|
||||
if (!VoxelDensityAblation::IsTunnelSDFOff())
|
||||
{
|
||||
TunnelCandidateCount = VF_ForEachSpatialCandidate(
|
||||
Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), TunnelPosition.X, TunnelPosition.Y,
|
||||
EvaluateTunnel,
|
||||
bUseSpatialIndex);
|
||||
TunnelCandidateCount = VF_ForEachSDFCandidate(
|
||||
Cache.TunnelSpatialIndex,
|
||||
#if WITH_EDITOR
|
||||
Cache.TunnelBrickIndex,
|
||||
#endif
|
||||
Cache.Tunnels.Num(), static_cast<float>(TunnelPosition.X),
|
||||
static_cast<float>(TunnelPosition.Y), static_cast<float>(TunnelPosition.Z),
|
||||
EvaluateTunnel, bUseSpatialIndex);
|
||||
}
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
@@ -9002,9 +9415,13 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
||||
}
|
||||
};
|
||||
|
||||
const int32 TunnelCandidateCount = VF_ForEachSpatialCandidate(
|
||||
Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel,
|
||||
bUseSpatialIndex);
|
||||
const int32 TunnelCandidateCount = VF_ForEachSDFCandidate(
|
||||
Cache.TunnelSpatialIndex,
|
||||
#if WITH_EDITOR
|
||||
Cache.TunnelBrickIndex,
|
||||
#endif
|
||||
Cache.Tunnels.Num(), WorldX, WorldY, WorldZ,
|
||||
EvaluateTunnel, bUseSpatialIndex);
|
||||
if (VoxelDensityProfile::AreCountersEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
|
||||
@@ -858,6 +858,28 @@ struct FChunkSDFSpatialIndex
|
||||
bool GetRange(float WorldX, float WorldY, int32& OutBegin, int32& OutEnd) const;
|
||||
};
|
||||
|
||||
// Research-only 3D broad phase used by the step-2 region evaluator. It uses 8-voxel bins and
|
||||
// keeps source indices in ascending order so SmoothMin / ownership reductions retain the live
|
||||
// evaluator's order. Invalid or oversized grids fall back to the existing XY index.
|
||||
struct FChunkSDFBrickIndex
|
||||
{
|
||||
int32 MinBrickX = 0;
|
||||
int32 MinBrickY = 0;
|
||||
int32 MinBrickZ = 0;
|
||||
int32 NumBricksX = 0;
|
||||
int32 NumBricksY = 0;
|
||||
int32 NumBricksZ = 0;
|
||||
bool bValid = false;
|
||||
TArray<int32> CellOffsets;
|
||||
TArray<int32> ItemIndices;
|
||||
|
||||
void Reset();
|
||||
void Release();
|
||||
SIZE_T GetAllocatedSize() const;
|
||||
bool GetRange(float WorldX, float WorldY, float WorldZ,
|
||||
int32& OutBegin, int32& OutEnd) const;
|
||||
};
|
||||
|
||||
template <typename FVisit>
|
||||
FORCEINLINE int32 VF_ForEachChunkSDFSpatialCandidate(
|
||||
const FChunkSDFSpatialIndex& Index, int32 ItemCount,
|
||||
@@ -910,6 +932,12 @@ struct FChunkSDFCache
|
||||
FChunkSDFSpatialIndex ChimneySpatialIndex;
|
||||
FChunkSDFSpatialIndex ColumnSpatialIndex;
|
||||
|
||||
#if WITH_EDITOR
|
||||
FChunkSDFBrickIndex RoomBrickIndex;
|
||||
FChunkSDFBrickIndex RoomFloorJoinBrickIndex;
|
||||
FChunkSDFBrickIndex TunnelBrickIndex;
|
||||
#endif
|
||||
|
||||
// Kept with the cache because the world-space structural evaluator has no params argument.
|
||||
// A zero value is a valid fallback for legacy/empty caches.
|
||||
float SDFBlendRadius = 0.0f;
|
||||
@@ -951,6 +979,15 @@ struct FTunnelCoreWorldEvaluation
|
||||
|
||||
namespace VoxelCaveMorphology
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Enable the isolated step-2 8^3-cell SDF binning path on the current worker thread. The
|
||||
* default is off and gameplay callers never construct or select these indices.
|
||||
*/
|
||||
VOXELFORGE_API void SetResearchBrickBinningEnabled(bool bEnabled);
|
||||
VOXELFORGE_API bool IsResearchBrickBinningEnabled();
|
||||
VOXELFORGE_API void SetResearchBrickRoomReachScale(float Scale);
|
||||
#endif
|
||||
// The player-fit memo is process-wide and uses a fixed-size table. These controls are kept
|
||||
// beside the cave source so the command-line value is applied before generation workers start,
|
||||
// while the CVar remains available for an in-process A/B switch.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "VoxelForgeExploreCommandlet.h"
|
||||
#include "VoxelStackOfflineSymbolizer.h"
|
||||
#include "VoxelForgeRegionEvalExperiment.h"
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Async/ParallelFor.h"
|
||||
@@ -40,6 +41,7 @@
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelStrateManager.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
#include "VoxelStrateComposer.h"
|
||||
#include "VoxelTypes.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -1456,6 +1458,737 @@ struct FExploreWorld
|
||||
}
|
||||
};
|
||||
|
||||
struct FRegionEvalPass
|
||||
{
|
||||
VoxelForgeRegionEval::FTiming Timing;
|
||||
VoxelForgeRegionEval::FCandidateVisits Visits;
|
||||
TArray<float> Densities;
|
||||
TArray<int64> SampleIndices;
|
||||
FIntVector Origin = FIntVector::ZeroValue;
|
||||
int32 RecipeIndex = INDEX_NONE;
|
||||
uint32 RecipeHash = 0;
|
||||
int32 PassageCount = 0;
|
||||
int32 EditChunkCount = 0;
|
||||
int32 TargetedSampleCount = 0;
|
||||
double WorldPrepareSeconds = 0.0;
|
||||
FString StartedUtc;
|
||||
FString EndedUtc;
|
||||
};
|
||||
|
||||
int32 FindRoomGraphRecipeIndex(int32 Seed)
|
||||
{
|
||||
for (int32 Index = 0; Index < 64; ++Index)
|
||||
{
|
||||
const FVoxelOpStackRecipe Recipe = VF_RollStrateStructure(Seed, Index);
|
||||
if (Recipe.ShapeSource.OpClass == EVoxelStrateOpClass::RoomGraphSource)
|
||||
{
|
||||
return Index;
|
||||
}
|
||||
}
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
bool PrepareRegionEvalWorld(
|
||||
FExploreWorld& World,
|
||||
const FExploreArguments& BaseArguments,
|
||||
int32 Seed,
|
||||
int32& OutRecipeIndex,
|
||||
uint32& OutRecipeHash,
|
||||
int32& OutEditChunkCount,
|
||||
FString& OutError)
|
||||
{
|
||||
FExploreArguments Arguments = BaseArguments;
|
||||
Arguments.Seed = Seed;
|
||||
Arguments.bSeedOverride = true;
|
||||
Arguments.bRender = false;
|
||||
Arguments.bWalk = false;
|
||||
Arguments.bExport = false;
|
||||
Arguments.bBlockEarlyOut = false;
|
||||
if (!World.Build(Arguments, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!World.Definitions.IsValidIndex(Arguments.Slot))
|
||||
{
|
||||
OutError = TEXT("Region evaluator target definition is missing.");
|
||||
return false;
|
||||
}
|
||||
UVoxelStrateDefinition* Definition = World.Definitions[Arguments.Slot].Get();
|
||||
if (Definition == nullptr || Definition->GeneratorType != ECaveGeneratorType::TunnelNetwork)
|
||||
{
|
||||
OutError = TEXT("Region evaluator requires the owner TunnelNetwork target slot.");
|
||||
return false;
|
||||
}
|
||||
|
||||
OutRecipeIndex = FindRoomGraphRecipeIndex(Seed);
|
||||
if (OutRecipeIndex == INDEX_NONE)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("No RoomGraphSource recipe found in the 64-entry structure roll for seed %d."), Seed);
|
||||
return false;
|
||||
}
|
||||
const FVoxelOpStackRecipe Recipe = VF_RollStrateStructure(Seed, OutRecipeIndex);
|
||||
OutRecipeHash = VF_HashStrateStructureRecipe(Recipe);
|
||||
|
||||
// Make the selected RoomGraph recipe exercise the authored warp path. The definition is a
|
||||
// transient duplicate of DA_Strate3, and both the live and prepared passes receive the same
|
||||
// operator parameters and recipe.
|
||||
FVoxelStrateArchetypeParams NativeParams;
|
||||
NativeParams.TunnelNetworkParams = Definition->GenerationParams;
|
||||
NativeParams.TunnelNetworkParams.TunnelWarpStrength = FMath::Max(
|
||||
NativeParams.TunnelNetworkParams.TunnelWarpStrength, 1.25f);
|
||||
NativeParams.SlabParams = Definition->SlabParams;
|
||||
NativeParams.MazeParams = Definition->MazeParams;
|
||||
NativeParams.SurfaceParams = Definition->SurfaceParams;
|
||||
NativeParams.VerticalShaftParams = Definition->VerticalShaftParams;
|
||||
NativeParams.FloatingIslandParams = Definition->FloatingIslandParams;
|
||||
FString ComposerError;
|
||||
if (!World.Manager->SetComposerOverrideForStrate(
|
||||
Arguments.Slot, Seed, Definition->GeneratorType,
|
||||
NativeParams, true, &Recipe, ComposerError))
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Could not install rolled composer recipe for seed %d: %s"),
|
||||
Seed, *ComposerError);
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 CenterZ = (World.TargetTopWorldZ + World.TargetBottomWorldZ) / 2;
|
||||
FVoxelModification Carve;
|
||||
Carve.Center = FVector(8.0f, 4.0f, static_cast<float>(CenterZ));
|
||||
Carve.Radius = 6.0f;
|
||||
Carve.Strength = -12.0f;
|
||||
Carve.Shape = EVoxelBrushShape::Sphere;
|
||||
const TArray<FIntVector> CarvedChunks = World.DiffLayer->ApplyModification(Carve);
|
||||
FVoxelModification Fill;
|
||||
Fill.Center = FVector(-12.0f, 6.0f, static_cast<float>(CenterZ - 10));
|
||||
Fill.Radius = 4.0f;
|
||||
Fill.Strength = 12.0f;
|
||||
Fill.Shape = EVoxelBrushShape::Sphere;
|
||||
const TArray<FIntVector> FilledChunks = World.DiffLayer->ApplyModification(Fill);
|
||||
if (CarvedChunks.IsEmpty() || FilledChunks.IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Transient player carve/fill edits did not enter the diff layer.");
|
||||
return false;
|
||||
}
|
||||
OutEditChunkCount = CarvedChunks.Num() + FilledChunks.Num();
|
||||
|
||||
int32 RecipeSeed = 0;
|
||||
ECaveGeneratorType RecipeArchetype = ECaveGeneratorType::TunnelNetwork;
|
||||
FVoxelStrateArchetypeParams ResolvedParams;
|
||||
FVoxelOpStackRecipe ResolvedRecipe;
|
||||
const int32 ChunkZ = FMath::FloorToInt(static_cast<float>(CenterZ) / CHUNK_SIZE);
|
||||
if (!World.Manager->GetRecipeForChunk(
|
||||
FIntVector(0, 0, ChunkZ), RecipeSeed, RecipeArchetype,
|
||||
ResolvedParams, ResolvedRecipe)
|
||||
|| !VF_AreStrateStructureRecipesIdentical(Recipe, ResolvedRecipe))
|
||||
{
|
||||
OutError = TEXT("Composer recipe was not active on the target density lattice.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AddRegionSampleIndex(
|
||||
TSet<int64>& Unique,
|
||||
int32 X, int32 Y, int32 Z,
|
||||
FIntVector Origin, int32 Dimension)
|
||||
{
|
||||
const int32 LX = X - Origin.X;
|
||||
const int32 LY = Y - Origin.Y;
|
||||
const int32 LZ = Z - Origin.Z;
|
||||
if (LX >= 0 && LX < Dimension && LY >= 0 && LY < Dimension
|
||||
&& LZ >= 0 && LZ < Dimension)
|
||||
{
|
||||
Unique.Add(VoxelForgeRegionEval::LinearIndex(LX, LY, LZ, Dimension));
|
||||
}
|
||||
}
|
||||
|
||||
TArray<int64> BuildRegionEvalSampleIndices(
|
||||
const FExploreWorld& World,
|
||||
FIntVector Origin,
|
||||
int32 Dimension,
|
||||
int32 RequestedRandomSamples,
|
||||
int32 Seed,
|
||||
int32& OutTargetedSamples)
|
||||
{
|
||||
const int64 Total = static_cast<int64>(Dimension) * Dimension * Dimension;
|
||||
const int32 RandomCount = FMath::Clamp(RequestedRandomSamples, 1, static_cast<int32>(Total));
|
||||
TArray<int64> Indices;
|
||||
Indices.Reserve(RandomCount + 50000);
|
||||
|
||||
auto Gcd = [](int64 A, int64 B)
|
||||
{
|
||||
while (B != 0)
|
||||
{
|
||||
const int64 R = A % B;
|
||||
A = B;
|
||||
B = R;
|
||||
}
|
||||
return A;
|
||||
};
|
||||
int64 Multiplier = 104729;
|
||||
while (Gcd(Multiplier, Total) != 1)
|
||||
{
|
||||
Multiplier += 2;
|
||||
}
|
||||
const int64 Offset = (static_cast<int64>(Seed) * 48271ll) % Total;
|
||||
for (int32 Sample = 0; Sample < RandomCount; ++Sample)
|
||||
{
|
||||
Indices.Add((static_cast<int64>(Sample) * Multiplier + Offset) % Total);
|
||||
}
|
||||
|
||||
TSet<int64> Unique;
|
||||
Unique.Reserve(50000);
|
||||
for (const int64 Index : Indices)
|
||||
{
|
||||
Unique.Add(Index);
|
||||
}
|
||||
const int32 RandomUniqueCount = Unique.Num();
|
||||
|
||||
// Dense probes around both edited volumes.
|
||||
const int32 CenterZ = (World.TargetTopWorldZ + World.TargetBottomWorldZ) / 2;
|
||||
const FVector EditCenters[] = {
|
||||
FVector(8.0f, 4.0f, static_cast<float>(CenterZ)),
|
||||
FVector(-12.0f, 6.0f, static_cast<float>(CenterZ - 10)),
|
||||
};
|
||||
for (const FVector& Center : EditCenters)
|
||||
{
|
||||
const int32 CX = FMath::FloorToInt(Center.X);
|
||||
const int32 CY = FMath::FloorToInt(Center.Y);
|
||||
const int32 CZ = FMath::FloorToInt(Center.Z);
|
||||
for (int32 Z = CZ - 8; Z <= CZ + 8; ++Z)
|
||||
{
|
||||
for (int32 Y = CY - 8; Y <= CY + 8; ++Y)
|
||||
{
|
||||
for (int32 X = CX - 8; X <= CX + 8; ++X)
|
||||
{
|
||||
AddRegionSampleIndex(Unique, X, Y, Z, Origin, Dimension);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Include lattice sites around each authored route tube, landing room, and floor anchor.
|
||||
for (const FVoxelPassage& Passage : World.Manager->GetPassages())
|
||||
{
|
||||
TArray<FVector> Anchors;
|
||||
Anchors.Add(Passage.UpperPoint);
|
||||
Anchors.Add(Passage.LowerPoint);
|
||||
Anchors.Add(Passage.UpperLanding.StandingPoint);
|
||||
Anchors.Add(Passage.UpperLanding.DoorPoint);
|
||||
Anchors.Add(Passage.LowerLanding.StandingPoint);
|
||||
Anchors.Add(Passage.LowerLanding.DoorPoint);
|
||||
Anchors.Append(Passage.ControlPoints);
|
||||
for (const FVector& Anchor : Anchors)
|
||||
{
|
||||
const int32 AX = FMath::FloorToInt(Anchor.X);
|
||||
const int32 AY = FMath::FloorToInt(Anchor.Y);
|
||||
const int32 AZ = FMath::FloorToInt(Anchor.Z);
|
||||
for (int32 Z = AZ - 3; Z <= AZ + 3; ++Z)
|
||||
{
|
||||
for (int32 Y = AY - 3; Y <= AY + 3; ++Y)
|
||||
{
|
||||
for (int32 X = AX - 3; X <= AX + 3; ++X)
|
||||
{
|
||||
AddRegionSampleIndex(Unique, X, Y, Z, Origin, Dimension);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Both strate boundaries are swept across the horizontal region, including ±3 lattice rows.
|
||||
const int32 Boundaries[] = { World.TargetTopWorldZ, World.TargetBottomWorldZ };
|
||||
for (const int32 BoundaryZ : Boundaries)
|
||||
{
|
||||
for (int32 Dz = -3; Dz <= 3; ++Dz)
|
||||
{
|
||||
for (int32 Y = -128; Y <= 128; Y += 8)
|
||||
{
|
||||
for (int32 X = -128; X <= 128; X += 8)
|
||||
{
|
||||
AddRegionSampleIndex(Unique, X, Y, BoundaryZ + Dz, Origin, Dimension);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Indices = Unique.Array();
|
||||
Indices.Sort();
|
||||
OutTargetedSamples = FMath::Max(0, Indices.Num() - RandomUniqueCount);
|
||||
return Indices;
|
||||
}
|
||||
|
||||
bool RunRegionEvalPass(
|
||||
const FExploreArguments& BaseArguments,
|
||||
int32 Seed,
|
||||
bool bPrepared,
|
||||
float ReachScale,
|
||||
bool bFullRing,
|
||||
const TArray<int64>* RequestedIndices,
|
||||
int32 RequestedSamples,
|
||||
bool bCountCandidates,
|
||||
bool bKeepDensities,
|
||||
FRegionEvalPass& OutPass,
|
||||
FString& OutError)
|
||||
{
|
||||
OutPass = FRegionEvalPass();
|
||||
FExploreWorld World;
|
||||
const double PrepareStartSeconds = FPlatformTime::Seconds();
|
||||
if (!PrepareRegionEvalWorld(
|
||||
World, BaseArguments, Seed, OutPass.RecipeIndex,
|
||||
OutPass.RecipeHash, OutPass.EditChunkCount, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutPass.WorldPrepareSeconds = FPlatformTime::Seconds() - PrepareStartSeconds;
|
||||
OutPass.PassageCount = World.Manager->GetPassages().Num();
|
||||
const int32 CenterZ = (World.TargetTopWorldZ + World.TargetBottomWorldZ) / 2;
|
||||
OutPass.Origin = FIntVector(-129, -129, CenterZ - 129);
|
||||
if (OutPass.PassageCount <= 0)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Seed %d generated no inter-strate route passages."), Seed);
|
||||
return false;
|
||||
}
|
||||
if (RequestedIndices != nullptr)
|
||||
{
|
||||
OutPass.SampleIndices = *RequestedIndices;
|
||||
}
|
||||
else
|
||||
{
|
||||
OutPass.SampleIndices = BuildRegionEvalSampleIndices(
|
||||
World, OutPass.Origin, VoxelForgeRegionEval::RingDimension,
|
||||
RequestedSamples, Seed, OutPass.TargetedSampleCount);
|
||||
}
|
||||
if (OutPass.SampleIndices.IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Region evaluator produced an empty correctness sample set.");
|
||||
return false;
|
||||
}
|
||||
|
||||
OutPass.StartedUtc = FDateTime::UtcNow().ToIso8601();
|
||||
const bool bEvaluated = VoxelForgeRegionEval::Evaluate(
|
||||
*World.Generator, OutPass.Origin, VoxelForgeRegionEval::RingDimension,
|
||||
bFullRing ? nullptr : &OutPass.SampleIndices,
|
||||
bFullRing, bPrepared, ReachScale,
|
||||
bKeepDensities ? &OutPass.Densities : nullptr,
|
||||
OutPass.Timing);
|
||||
OutPass.EndedUtc = FDateTime::UtcNow().ToIso8601();
|
||||
if (!bEvaluated)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Density evaluation failed or emitted a non-finite value (seed %d)."), Seed);
|
||||
return false;
|
||||
}
|
||||
if (bCountCandidates)
|
||||
{
|
||||
OutPass.Visits = VoxelForgeRegionEval::CountCandidates(
|
||||
*World.Generator, OutPass.Origin,
|
||||
VoxelForgeRegionEval::RingDimension,
|
||||
OutPass.SampleIndices, bPrepared, ReachScale);
|
||||
if (OutPass.Visits.SampleCount != OutPass.SampleIndices.Num())
|
||||
{
|
||||
OutError = TEXT("Candidate counter sweep did not consume the full fixed sample set.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> RegionEvalTimingJson(const FRegionEvalPass& Pass, bool bPrepared)
|
||||
{
|
||||
TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>();
|
||||
Json->SetStringField(TEXT("path"), bPrepared ? TEXT("prepared_8cube_bins") : TEXT("live_GetDensityAt"));
|
||||
Json->SetStringField(TEXT("started_utc"), Pass.StartedUtc);
|
||||
Json->SetStringField(TEXT("ended_utc"), Pass.EndedUtc);
|
||||
Json->SetNumberField(TEXT("worker_count"), Pass.Timing.Workers);
|
||||
Json->SetNumberField(TEXT("observed_worker_threads"), Pass.Timing.ObservedWorkerThreads);
|
||||
Json->SetNumberField(TEXT("samples"), static_cast<double>(Pass.Timing.SampleCount));
|
||||
Json->SetNumberField(TEXT("world_setup_seconds_before_density_ring"), Pass.WorldPrepareSeconds);
|
||||
Json->SetNumberField(TEXT("wall_seconds"), Pass.Timing.WallSeconds);
|
||||
Json->SetNumberField(TEXT("worker_cpu_seconds"), Pass.Timing.WorkerCpuSeconds);
|
||||
Json->SetNumberField(TEXT("worker_cpu_ns_per_sample"), Pass.Timing.NanosecondsPerSample);
|
||||
Json->SetStringField(TEXT("worker_cpu_source"), Pass.Timing.bWorkerCpuFromOsThreadTimes
|
||||
? TEXT("summed Windows GetThreadTimes user+kernel CPU across six fixed work partitions")
|
||||
: TEXT("summed worker elapsed fallback; OS thread CPU counters unavailable"));
|
||||
Json->SetStringField(TEXT("quantized_hash_fnv1a64"),
|
||||
FString::Printf(TEXT("%016llx"), static_cast<unsigned long long>(Pass.Timing.QuantizedHash)));
|
||||
Json->SetStringField(TEXT("raw_float_hash_fnv1a64"),
|
||||
FString::Printf(TEXT("%016llx"), static_cast<unsigned long long>(Pass.Timing.RawFloatHash)));
|
||||
Json->SetNumberField(TEXT("saturated_samples"), static_cast<double>(Pass.Timing.SaturatedSamples));
|
||||
Json->SetNumberField(TEXT("non_finite_samples"), static_cast<double>(Pass.Timing.NonFiniteSamples));
|
||||
if (Pass.Visits.SampleCount > 0)
|
||||
{
|
||||
Json->SetNumberField(TEXT("candidate_visits_per_sample"), Pass.Visits.PerSample);
|
||||
Json->SetNumberField(TEXT("room_candidate_visits"), static_cast<double>(Pass.Visits.Room));
|
||||
Json->SetNumberField(TEXT("tunnel_candidate_visits"), static_cast<double>(Pass.Visits.Tunnel));
|
||||
Json->SetNumberField(TEXT("tunnel_core_candidate_visits"), static_cast<double>(Pass.Visits.TunnelCore));
|
||||
Json->SetNumberField(TEXT("passage_candidate_visits"), static_cast<double>(Pass.Visits.Passage));
|
||||
Json->SetNumberField(TEXT("tunnel_cache_lookups"), static_cast<double>(Pass.Visits.TunnelCacheLookups));
|
||||
Json->SetNumberField(TEXT("tunnel_cache_hits"), static_cast<double>(Pass.Visits.TunnelCacheHits));
|
||||
Json->SetNumberField(TEXT("tunnel_cache_misses"), static_cast<double>(Pass.Visits.TunnelCacheMisses));
|
||||
Json->SetNumberField(TEXT("sdf_cache_builds"), static_cast<double>(Pass.Visits.SdfCacheBuilds));
|
||||
Json->SetNumberField(TEXT("candidate_counter_samples"), static_cast<double>(Pass.Visits.SampleCount));
|
||||
Json->SetNumberField(TEXT("attribution_sample_interval"), Pass.Visits.AttributionSampleInterval);
|
||||
Json->SetStringField(TEXT("attribution_basis"), TEXT("sampled VoxelDensityProfile phases; diagnostic only, excluded from clean timing"));
|
||||
Json->SetNumberField(TEXT("get_density_at_attributed_ns_per_sample"), Pass.Visits.GetDensityAtAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_prologue_attributed_ns_per_sample"), Pass.Visits.DensityPrologueAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_core_attributed_ns_per_sample"), Pass.Visits.DensityCoreAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_disturbances_attributed_ns_per_sample"), Pass.Visits.DensityDisturbancesAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_structural_posts_attributed_ns_per_sample"), Pass.Visits.DensityStructuralPostsAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_boundary_seal_attributed_ns_per_sample"), Pass.Visits.DensityBoundarySealAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_diff_layer_attributed_ns_per_sample"), Pass.Visits.DensityDiffLayerAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_tail_attributed_ns_per_sample"), Pass.Visits.DensityTailAttributedNsPerSample);
|
||||
}
|
||||
return Json;
|
||||
}
|
||||
|
||||
struct FQuantizedComparison
|
||||
{
|
||||
int64 Disagreements = 0;
|
||||
int64 SignDisagreements = 0;
|
||||
double MaxAbsoluteFloatDelta = 0.0;
|
||||
int64 FirstIndex = INDEX_NONE;
|
||||
};
|
||||
|
||||
FQuantizedComparison CompareQuantizedArrays(
|
||||
const TArray<float>& A, const TArray<float>& B,
|
||||
const TArray<int64>* LatticeIndices = nullptr)
|
||||
{
|
||||
FQuantizedComparison Result;
|
||||
if (A.Num() != B.Num())
|
||||
{
|
||||
Result.Disagreements = FMath::Max(A.Num(), B.Num());
|
||||
return Result;
|
||||
}
|
||||
for (int32 Index = 0; Index < A.Num(); ++Index)
|
||||
{
|
||||
bool bSaturatedA = false, bFiniteA = false;
|
||||
bool bSaturatedB = false, bFiniteB = false;
|
||||
const int16 QA = VoxelForgeRegionEval::QuantizeDensity(A[Index], bSaturatedA, bFiniteA);
|
||||
const int16 QB = VoxelForgeRegionEval::QuantizeDensity(B[Index], bSaturatedB, bFiniteB);
|
||||
Result.MaxAbsoluteFloatDelta = FMath::Max(
|
||||
Result.MaxAbsoluteFloatDelta,
|
||||
FMath::Abs(static_cast<double>(A[Index]) - static_cast<double>(B[Index])));
|
||||
if (!bFiniteA || !bFiniteB || bSaturatedA || bSaturatedB || QA != QB)
|
||||
{
|
||||
++Result.Disagreements;
|
||||
if (Result.FirstIndex == INDEX_NONE)
|
||||
{
|
||||
Result.FirstIndex = LatticeIndices != nullptr
|
||||
? (*LatticeIndices)[Index] : static_cast<int64>(Index);
|
||||
}
|
||||
}
|
||||
if ((QA < 0) != (QB < 0))
|
||||
{
|
||||
++Result.SignDisagreements;
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> RegionEvalComparisonJson(const FQuantizedComparison& Comparison)
|
||||
{
|
||||
TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>();
|
||||
Json->SetNumberField(TEXT("quantized_disagreements"), static_cast<double>(Comparison.Disagreements));
|
||||
Json->SetNumberField(TEXT("quantized_sign_disagreements"), static_cast<double>(Comparison.SignDisagreements));
|
||||
Json->SetNumberField(TEXT("max_abs_float_delta"), Comparison.MaxAbsoluteFloatDelta);
|
||||
Json->SetNumberField(TEXT("first_disagreement_linear_index"), static_cast<double>(Comparison.FirstIndex));
|
||||
return Json;
|
||||
}
|
||||
|
||||
int32 RunRegionEval(const FString& Params)
|
||||
{
|
||||
FExploreArguments Arguments;
|
||||
FString Error;
|
||||
if (!ParseArguments(Params, Arguments, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] argument error: %s"), *Error);
|
||||
return 2;
|
||||
}
|
||||
int32 RequestedSamples = 262144;
|
||||
FParse::Value(*Params, TEXT("regionsamples="), RequestedSamples);
|
||||
RequestedSamples = FMath::Clamp(RequestedSamples, 32768, 1000000);
|
||||
IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true);
|
||||
|
||||
const int32 Seeds[] = { 1337, 7331, 19427, 30241, 41011 };
|
||||
TArray<TSharedPtr<FJsonValue>> SeedReports;
|
||||
TArray<TSharedPtr<FJsonValue>> OldTimingReports;
|
||||
TArray<TSharedPtr<FJsonValue>> PreparedTimingReports;
|
||||
TArray<float> OldFullDensities;
|
||||
TArray<float> PreparedFullDensities;
|
||||
TArray<int64> PrimarySampleIndices;
|
||||
FQuantizedComparison FullComparison;
|
||||
uint64 OldFullRawHash = 0;
|
||||
uint64 PreparedFullRawHash = 0;
|
||||
uint64 OldFullSaturatedSamples = 0;
|
||||
uint64 PreparedFullSaturatedSamples = 0;
|
||||
bool bAllSeedComparisonsPass = true;
|
||||
bool bFullDeterminismPass = true;
|
||||
int32 PrimaryTargetedSampleCount = 0;
|
||||
int32 PrimaryPassageCount = 0;
|
||||
int32 PrimaryEditChunkCount = 0;
|
||||
int32 PrimaryRecipeIndex = INDEX_NONE;
|
||||
uint32 PrimaryRecipeHash = 0;
|
||||
|
||||
// Cold full-ring timings use independent transient worlds for every pass. Caches cannot be
|
||||
// warmed by the comparison leg, and the seed/layout/composer/edit payload remains identical.
|
||||
for (int32 Trial = 0; Trial < 2; ++Trial)
|
||||
{
|
||||
FRegionEvalPass Pass;
|
||||
const bool bOk = RunRegionEvalPass(
|
||||
Arguments, Seeds[0], false, 1.0f, true, nullptr,
|
||||
RequestedSamples, Trial == 0, true, Pass, Error);
|
||||
if (!bOk)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] live full-ring trial failed: %s"), *Error);
|
||||
return 2;
|
||||
}
|
||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||
if (Trial == 0)
|
||||
{
|
||||
OldFullDensities = MoveTemp(Pass.Densities);
|
||||
OldFullRawHash = Pass.Timing.RawFloatHash;
|
||||
OldFullSaturatedSamples = Pass.Timing.SaturatedSamples;
|
||||
PrimarySampleIndices = Pass.SampleIndices;
|
||||
PrimaryTargetedSampleCount = Pass.TargetedSampleCount;
|
||||
PrimaryPassageCount = Pass.PassageCount;
|
||||
PrimaryEditChunkCount = Pass.EditChunkCount;
|
||||
PrimaryRecipeIndex = Pass.RecipeIndex;
|
||||
PrimaryRecipeHash = Pass.RecipeHash;
|
||||
}
|
||||
else
|
||||
{
|
||||
bFullDeterminismPass &= Pass.Timing.RawFloatHash == OldFullRawHash;
|
||||
}
|
||||
OldTimingReports.Add(MakeShared<FJsonValueObject>(RegionEvalTimingJson(Pass, false)));
|
||||
}
|
||||
if (PrimarySampleIndices.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] primary statistical sample set is empty."));
|
||||
return 2;
|
||||
}
|
||||
|
||||
for (int32 Trial = 0; Trial < 2; ++Trial)
|
||||
{
|
||||
FRegionEvalPass Pass;
|
||||
const bool bOk = RunRegionEvalPass(
|
||||
Arguments, Seeds[0], true, 1.0f, true, nullptr,
|
||||
RequestedSamples, Trial == 0, true, Pass, Error);
|
||||
if (!bOk)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] prepared full-ring trial failed: %s"), *Error);
|
||||
return 2;
|
||||
}
|
||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||
if (Trial == 0)
|
||||
{
|
||||
PreparedFullDensities = MoveTemp(Pass.Densities);
|
||||
PreparedFullRawHash = Pass.Timing.RawFloatHash;
|
||||
PreparedFullSaturatedSamples = Pass.Timing.SaturatedSamples;
|
||||
FullComparison = CompareQuantizedArrays(OldFullDensities, PreparedFullDensities);
|
||||
}
|
||||
else
|
||||
{
|
||||
bFullDeterminismPass &= Pass.Timing.RawFloatHash == PreparedFullRawHash;
|
||||
}
|
||||
PreparedTimingReports.Add(MakeShared<FJsonValueObject>(RegionEvalTimingJson(Pass, true)));
|
||||
}
|
||||
bAllSeedComparisonsPass &= FullComparison.Disagreements == 0
|
||||
&& FullComparison.SignDisagreements == 0
|
||||
&& OldFullSaturatedSamples == 0
|
||||
&& PreparedFullSaturatedSamples == 0;
|
||||
|
||||
// Statistical fixed-lattice comparisons across four additional independent seeds. The sample
|
||||
// set is a deterministic affine permutation of the entire 259^3 ring plus dense strata,
|
||||
// passage-floor, landing, carve, and fill probes.
|
||||
for (int32 SeedIndex = 1; SeedIndex < UE_ARRAY_COUNT(Seeds); ++SeedIndex)
|
||||
{
|
||||
FRegionEvalPass OldPass;
|
||||
if (!RunRegionEvalPass(
|
||||
Arguments, Seeds[SeedIndex], false, 1.0f, false, nullptr,
|
||||
RequestedSamples, true, true, OldPass, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] seed %d live sample failed: %s"),
|
||||
Seeds[SeedIndex], *Error);
|
||||
return 2;
|
||||
}
|
||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||
FRegionEvalPass NewPass;
|
||||
if (!RunRegionEvalPass(
|
||||
Arguments, Seeds[SeedIndex], true, 1.0f, false, &OldPass.SampleIndices,
|
||||
RequestedSamples, true, true, NewPass, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] seed %d prepared sample failed: %s"),
|
||||
Seeds[SeedIndex], *Error);
|
||||
return 2;
|
||||
}
|
||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||
const FQuantizedComparison Comparison = CompareQuantizedArrays(
|
||||
OldPass.Densities, NewPass.Densities, &OldPass.SampleIndices);
|
||||
bAllSeedComparisonsPass &= Comparison.Disagreements == 0
|
||||
&& Comparison.SignDisagreements == 0
|
||||
&& OldPass.Timing.SaturatedSamples == 0
|
||||
&& NewPass.Timing.SaturatedSamples == 0;
|
||||
|
||||
TSharedRef<FJsonObject> SeedJson = MakeShared<FJsonObject>();
|
||||
SeedJson->SetNumberField(TEXT("seed"), Seeds[SeedIndex]);
|
||||
SeedJson->SetNumberField(TEXT("samples"), OldPass.Timing.SampleCount);
|
||||
SeedJson->SetNumberField(TEXT("targeted_route_edit_boundary_samples"), OldPass.TargetedSampleCount);
|
||||
SeedJson->SetNumberField(TEXT("composer_recipe_index"), OldPass.RecipeIndex);
|
||||
SeedJson->SetStringField(TEXT("composer_recipe_hash"), FString::Printf(TEXT("%08x"), OldPass.RecipeHash));
|
||||
SeedJson->SetNumberField(TEXT("passage_count"), OldPass.PassageCount);
|
||||
SeedJson->SetNumberField(TEXT("edit_chunk_count"), OldPass.EditChunkCount);
|
||||
SeedJson->SetObjectField(TEXT("comparison"), RegionEvalComparisonJson(Comparison));
|
||||
SeedJson->SetObjectField(TEXT("live_candidate_visits"), RegionEvalTimingJson(OldPass, false));
|
||||
SeedJson->SetObjectField(TEXT("prepared_candidate_visits"), RegionEvalTimingJson(NewPass, true));
|
||||
SeedReports.Add(MakeShared<FJsonValueObject>(SeedJson));
|
||||
}
|
||||
|
||||
// Deliberately shrink one reach, evaluate the same statistical lattice points, and require
|
||||
// the reference comparison to catch omitted room influence. The scale is scoped to this one
|
||||
// commandlet pass and every worker resets it to 1.0 afterward.
|
||||
FRegionEvalPass ShrunkPass;
|
||||
if (!RunRegionEvalPass(
|
||||
Arguments, Seeds[0], true, 0.50f, false, &PrimarySampleIndices,
|
||||
RequestedSamples, false, true, ShrunkPass, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] deliberate reach-shrink run failed: %s"), *Error);
|
||||
return 2;
|
||||
}
|
||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||
FQuantizedComparison ShrinkComparison;
|
||||
ShrinkComparison.FirstIndex = INDEX_NONE;
|
||||
for (int32 Sample = 0; Sample < ShrunkPass.Densities.Num(); ++Sample)
|
||||
{
|
||||
const int64 LatticeIndex = PrimarySampleIndices[Sample];
|
||||
bool bSaturatedOld = false, bFiniteOld = false;
|
||||
bool bSaturatedShrink = false, bFiniteShrink = false;
|
||||
const int16 QOld = VoxelForgeRegionEval::QuantizeDensity(
|
||||
OldFullDensities[static_cast<int32>(LatticeIndex)], bSaturatedOld, bFiniteOld);
|
||||
const int16 QShrink = VoxelForgeRegionEval::QuantizeDensity(
|
||||
ShrunkPass.Densities[Sample], bSaturatedShrink, bFiniteShrink);
|
||||
ShrinkComparison.MaxAbsoluteFloatDelta = FMath::Max(
|
||||
ShrinkComparison.MaxAbsoluteFloatDelta,
|
||||
FMath::Abs(static_cast<double>(OldFullDensities[static_cast<int32>(LatticeIndex)])
|
||||
- static_cast<double>(ShrunkPass.Densities[Sample])));
|
||||
if (!bFiniteOld || !bFiniteShrink || bSaturatedOld || bSaturatedShrink || QOld != QShrink)
|
||||
{
|
||||
++ShrinkComparison.Disagreements;
|
||||
if (ShrinkComparison.FirstIndex == INDEX_NONE) ShrinkComparison.FirstIndex = LatticeIndex;
|
||||
}
|
||||
if ((QOld < 0) != (QShrink < 0)) ++ShrinkComparison.SignDisagreements;
|
||||
}
|
||||
|
||||
double PreparedWallSum = 0.0;
|
||||
for (const TSharedPtr<FJsonValue>& Value : PreparedTimingReports)
|
||||
{
|
||||
const TSharedPtr<FJsonObject> Json = Value->AsObject();
|
||||
PreparedWallSum += Json->GetNumberField(TEXT("wall_seconds"));
|
||||
}
|
||||
const double PreparedMedianWall = PreparedWallSum / 2.0;
|
||||
double OldWallSum = 0.0;
|
||||
for (const TSharedPtr<FJsonValue>& Value : OldTimingReports)
|
||||
{
|
||||
OldWallSum += Value->AsObject()->GetNumberField(TEXT("wall_seconds"));
|
||||
}
|
||||
const double OldMedianWall = OldWallSum / 2.0;
|
||||
bool bSixWorkersObserved = true;
|
||||
for (const TSharedPtr<FJsonValue>& Value : OldTimingReports)
|
||||
{
|
||||
bSixWorkersObserved &= Value->AsObject()->GetNumberField(TEXT("observed_worker_threads"))
|
||||
>= VoxelForgeRegionEval::WorkerCount;
|
||||
}
|
||||
for (const TSharedPtr<FJsonValue>& Value : PreparedTimingReports)
|
||||
{
|
||||
bSixWorkersObserved &= Value->AsObject()->GetNumberField(TEXT("observed_worker_threads"))
|
||||
>= VoxelForgeRegionEval::WorkerCount;
|
||||
}
|
||||
const bool bSpeedPass = PreparedMedianWall <= 0.600 && bSixWorkersObserved;
|
||||
|
||||
TSharedRef<FJsonObject> Report = MakeShared<FJsonObject>();
|
||||
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("evaluator"), TEXT("live GetDensityAt final field with editor-only 8^3 SDF room/join/tunnel candidate bins"));
|
||||
Report->SetStringField(TEXT("comparison_quantization"), TEXT("signed int16; round ties away from zero; multiply density by 16; saturate outside [-2048, 2047.9375] density voxels"));
|
||||
Report->SetNumberField(TEXT("worker_count"), VoxelForgeRegionEval::WorkerCount);
|
||||
Report->SetNumberField(TEXT("ring_dimension"), VoxelForgeRegionEval::RingDimension);
|
||||
Report->SetNumberField(TEXT("ring_lattice_samples"), static_cast<double>(VoxelForgeRegionEval::RingDimension) * VoxelForgeRegionEval::RingDimension * VoxelForgeRegionEval::RingDimension);
|
||||
Report->SetNumberField(TEXT("ring_interval_span_voxels"), VoxelForgeRegionEval::RingDimension - 1);
|
||||
Report->SetNumberField(TEXT("ring_wall_p50_live_seconds"), OldMedianWall);
|
||||
Report->SetNumberField(TEXT("ring_wall_p50_prepared_seconds"), PreparedMedianWall);
|
||||
Report->SetBoolField(TEXT("speed_pass_600ms"), bSpeedPass);
|
||||
Report->SetBoolField(TEXT("all_full_ring_trials_used_six_threads"), bSixWorkersObserved);
|
||||
Report->SetBoolField(TEXT("five_seed_correctness_pass"), bAllSeedComparisonsPass);
|
||||
Report->SetBoolField(TEXT("two_run_determinism_pass"), bFullDeterminismPass);
|
||||
Report->SetStringField(TEXT("determinism_basis"), TEXT("bitwise float32 lattice hash on two independent cold worlds for live and prepared paths"));
|
||||
Report->SetObjectField(TEXT("full_ring_comparison"), RegionEvalComparisonJson(FullComparison));
|
||||
Report->SetArrayField(TEXT("live_full_ring_trials"), OldTimingReports);
|
||||
Report->SetArrayField(TEXT("prepared_full_ring_trials"), PreparedTimingReports);
|
||||
Report->SetArrayField(TEXT("additional_seed_comparisons"), SeedReports);
|
||||
|
||||
TSharedRef<FJsonObject> Coverage = MakeShared<FJsonObject>();
|
||||
Coverage->SetStringField(TEXT("owner_settings"), Arguments.SettingsReference);
|
||||
Coverage->SetStringField(TEXT("owner_strate"), Arguments.StrateReference);
|
||||
Coverage->SetNumberField(TEXT("primary_seed"), Seeds[0]);
|
||||
Coverage->SetNumberField(TEXT("primary_targeted_route_edit_boundary_samples"), PrimaryTargetedSampleCount);
|
||||
Coverage->SetNumberField(TEXT("target_passages"), PrimaryPassageCount);
|
||||
Coverage->SetNumberField(TEXT("transient_edit_chunks"), PrimaryEditChunkCount);
|
||||
Coverage->SetNumberField(TEXT("composer_recipe_index"), PrimaryRecipeIndex);
|
||||
Coverage->SetStringField(TEXT("composer_recipe_hash"), FString::Printf(TEXT("%08x"), PrimaryRecipeHash));
|
||||
Coverage->SetStringField(TEXT("composer_recipe_family"), TEXT("RoomGraphSource with TunnelNetwork params; rolled by VF_RollStrateStructure"));
|
||||
Coverage->SetNumberField(TEXT("room_warp_strength_lower_bound"), 1.25);
|
||||
Coverage->SetStringField(TEXT("room_bin_reach"), TEXT("cached CullRadiusSq: shape extent + floor-relief envelope + 3x SDF blend + conservative terrain-op reach; mouth-rise reach uses center-to-mouth distance + blend"));
|
||||
Coverage->SetStringField(TEXT("tunnel_bin_reach"), TEXT("union of finalized warped-SDF and world-chain centerline bounds expanded by their influence radii, plus both conservative bounding spheres"));
|
||||
Coverage->SetStringField(TEXT("warp_coordinates"), TEXT("room bins are queried after the exact live cave warp in the same coordinate domain; tunnel bins union warped-SDF and unwarped world-chain reach"));
|
||||
Coverage->SetStringField(TEXT("tunnel_bin_unit"), TEXT("whole cached tunnel chain; per-chain segment loops remain in the exact evaluator"));
|
||||
Coverage->SetStringField(TEXT("unbinned_writers"), TEXT("passage/landing posts, disturbances, and player diff edits stay in canonical GetDensityAt tail; this is the candidate-bin ablation, not the full region compiler"));
|
||||
Coverage->SetBoolField(TEXT("route_landing_floor_probes"), PrimaryPassageCount > 0);
|
||||
Coverage->SetBoolField(TEXT("strata_top_and_bottom_boundary_sweep"), true);
|
||||
Coverage->SetBoolField(TEXT("player_carve_and_fill_edits"), true);
|
||||
Report->SetObjectField(TEXT("content_coverage"), Coverage);
|
||||
|
||||
TSharedRef<FJsonObject> Shrink = MakeShared<FJsonObject>();
|
||||
Shrink->SetNumberField(TEXT("room_reach_scale"), 0.50);
|
||||
Shrink->SetBoolField(TEXT("deliberately_shrunk_bin_was_caught"), ShrinkComparison.Disagreements > 0);
|
||||
Shrink->SetNumberField(TEXT("saturated_samples"), static_cast<double>(ShrunkPass.Timing.SaturatedSamples));
|
||||
Shrink->SetNumberField(TEXT("quantized_disagreements"), static_cast<double>(ShrinkComparison.Disagreements));
|
||||
Shrink->SetNumberField(TEXT("sign_disagreements"), static_cast<double>(ShrinkComparison.SignDisagreements));
|
||||
Shrink->SetNumberField(TEXT("max_abs_float_delta"), ShrinkComparison.MaxAbsoluteFloatDelta);
|
||||
Shrink->SetNumberField(TEXT("first_disagreement_linear_index"), static_cast<double>(ShrinkComparison.FirstIndex));
|
||||
Report->SetObjectField(TEXT("deliberate_reach_shrink"), Shrink);
|
||||
|
||||
const bool bPassed = bAllSeedComparisonsPass && bFullDeterminismPass
|
||||
&& ShrinkComparison.Disagreements > 0
|
||||
&& ShrunkPass.Timing.SaturatedSamples == 0;
|
||||
Report->SetStringField(TEXT("status"), bPassed ? TEXT("correctness_pass_speed_measured") : TEXT("correctness_failed"));
|
||||
Report->SetBoolField(TEXT("pass"), bPassed);
|
||||
Report->SetStringField(TEXT("speed_verdict"), bSpeedPass
|
||||
? TEXT("meets 600 ms") : TEXT("misses 600 ms; candidate bins are an ablation, not a full region compiler"));
|
||||
FString JsonText;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonText);
|
||||
if (!FJsonSerializer::Serialize(Report, Writer)
|
||||
|| !FFileHelper::SaveStringToFile(
|
||||
JsonText, *FPaths::Combine(Arguments.OutDirectory, TEXT("region_eval.json")),
|
||||
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] could not write region_eval.json."));
|
||||
return 2;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeRegionEval] status=%s speed600=%s live_ms=%.3f prepared_ms=%.3f "
|
||||
"full_q16_diff=%lld five_seed=%s deterministic=%s shrink_q16_diff=%lld report=%s"),
|
||||
bPassed ? TEXT("pass") : TEXT("fail"), bSpeedPass ? TEXT("yes") : TEXT("no"),
|
||||
OldMedianWall * 1000.0, PreparedMedianWall * 1000.0,
|
||||
static_cast<long long>(FullComparison.Disagreements),
|
||||
bAllSeedComparisonsPass ? TEXT("pass") : TEXT("fail"),
|
||||
bFullDeterminismPass ? TEXT("pass") : TEXT("fail"),
|
||||
static_cast<long long>(ShrinkComparison.Disagreements),
|
||||
*FPaths::Combine(Arguments.OutDirectory, TEXT("region_eval.json")));
|
||||
return bPassed ? 0 : 2;
|
||||
}
|
||||
|
||||
class FGeneratorDensitySampler final : public IVoxelStrateDensitySampler
|
||||
{
|
||||
public:
|
||||
@@ -8593,6 +9326,14 @@ int32 RunBatch(const FString& Params)
|
||||
|
||||
int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
||||
{
|
||||
const bool bRegionEvalSwitch = FParse::Param(*Params, TEXT("regioneval"));
|
||||
int32 RegionEvalFlag = 0;
|
||||
const bool bRegionEvalValue = FParse::Value(*Params, TEXT("regioneval="), RegionEvalFlag);
|
||||
if (bRegionEvalSwitch || (bRegionEvalValue && RegionEvalFlag != 0))
|
||||
{
|
||||
return RunRegionEval(Params);
|
||||
}
|
||||
|
||||
FString SymbolizeCsvPath;
|
||||
if (FParse::Value(*Params, TEXT("symbolizestacks="), SymbolizeCsvPath))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
#include "VoxelForgeRegionEvalExperiment.h"
|
||||
|
||||
#include "Async/ParallelFor.h"
|
||||
#include "HAL/PlatformTime.h"
|
||||
#include "VoxelCaveMorphology.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
#include "VoxelGenerator.h"
|
||||
#include "HAL/PlatformTLS.h"
|
||||
|
||||
#if PLATFORM_WINDOWS
|
||||
#include "Windows/AllowWindowsPlatformTypes.h"
|
||||
THIRD_PARTY_INCLUDES_START
|
||||
#include <Windows.h>
|
||||
THIRD_PARTY_INCLUDES_END
|
||||
#include "Windows/HideWindowsPlatformTypes.h"
|
||||
#endif
|
||||
|
||||
namespace VoxelForgeRegionEval
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr double WindowsFileTimeSeconds = 1.0e-7;
|
||||
|
||||
uint64 Fnv1aByte(uint64 Hash, uint8 Byte)
|
||||
{
|
||||
Hash ^= Byte;
|
||||
return Hash * 1099511628211ull;
|
||||
}
|
||||
|
||||
uint64 HashRawFloats(const TArray<float>& Densities)
|
||||
{
|
||||
uint64 Hash = 14695981039346656037ull;
|
||||
for (const float Density : Densities)
|
||||
{
|
||||
uint32 Bits = 0;
|
||||
FMemory::Memcpy(&Bits, &Density, sizeof(Bits));
|
||||
Hash = Fnv1aByte(Hash, static_cast<uint8>(Bits & 0xFFu));
|
||||
Hash = Fnv1aByte(Hash, static_cast<uint8>((Bits >> 8) & 0xFFu));
|
||||
Hash = Fnv1aByte(Hash, static_cast<uint8>((Bits >> 16) & 0xFFu));
|
||||
Hash = Fnv1aByte(Hash, static_cast<uint8>((Bits >> 24) & 0xFFu));
|
||||
}
|
||||
return Hash;
|
||||
}
|
||||
|
||||
bool GetCurrentThreadCpuSeconds(double& OutSeconds)
|
||||
{
|
||||
#if PLATFORM_WINDOWS
|
||||
FILETIME CreationTime = {};
|
||||
FILETIME ExitTime = {};
|
||||
FILETIME KernelTime = {};
|
||||
FILETIME UserTime = {};
|
||||
if (GetThreadTimes(
|
||||
GetCurrentThread(), &CreationTime, &ExitTime, &KernelTime, &UserTime) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ULARGE_INTEGER KernelTicks = {};
|
||||
ULARGE_INTEGER UserTicks = {};
|
||||
KernelTicks.LowPart = KernelTime.dwLowDateTime;
|
||||
KernelTicks.HighPart = KernelTime.dwHighDateTime;
|
||||
UserTicks.LowPart = UserTime.dwLowDateTime;
|
||||
UserTicks.HighPart = UserTime.dwHighDateTime;
|
||||
OutSeconds = static_cast<double>(KernelTicks.QuadPart + UserTicks.QuadPart)
|
||||
* WindowsFileTimeSeconds;
|
||||
return true;
|
||||
#else
|
||||
(void)OutSeconds;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
int64 LatticeCount(int32 Dimension)
|
||||
{
|
||||
return static_cast<int64>(Dimension) * Dimension * Dimension;
|
||||
}
|
||||
}
|
||||
|
||||
int64 LinearIndex(int32 X, int32 Y, int32 Z, int32 Dimension)
|
||||
{
|
||||
return (static_cast<int64>(Z) * Dimension + Y) * Dimension + X;
|
||||
}
|
||||
|
||||
int16 QuantizeDensity(float Density, bool& bOutSaturated, bool& bOutFinite)
|
||||
{
|
||||
bOutFinite = FMath::IsFinite(Density);
|
||||
bOutSaturated = false;
|
||||
if (!bOutFinite)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
const double Scaled = static_cast<double>(Density) * QuantizationScale;
|
||||
const double Rounded = Scaled >= 0.0
|
||||
? FMath::FloorToDouble(Scaled + 0.5)
|
||||
: FMath::CeilToDouble(Scaled - 0.5);
|
||||
if (Rounded > static_cast<double>(MAX_int16))
|
||||
{
|
||||
bOutSaturated = true;
|
||||
return MAX_int16;
|
||||
}
|
||||
if (Rounded < static_cast<double>(MIN_int16))
|
||||
{
|
||||
bOutSaturated = true;
|
||||
return MIN_int16;
|
||||
}
|
||||
return static_cast<int16>(Rounded);
|
||||
}
|
||||
|
||||
uint64 HashQuantized(const TArray<float>& Densities)
|
||||
{
|
||||
uint64 Hash = 14695981039346656037ull;
|
||||
for (const float Density : Densities)
|
||||
{
|
||||
bool bSaturated = false;
|
||||
bool bFinite = false;
|
||||
const uint16 Q = static_cast<uint16>(QuantizeDensity(Density, bSaturated, bFinite));
|
||||
Hash = Fnv1aByte(Hash, static_cast<uint8>(Q & 0xFFu));
|
||||
Hash = Fnv1aByte(Hash, static_cast<uint8>(Q >> 8));
|
||||
}
|
||||
return Hash;
|
||||
}
|
||||
|
||||
bool Evaluate(
|
||||
const UVoxelGenerator& Generator,
|
||||
FIntVector OriginVoxels,
|
||||
int32 Dimension,
|
||||
const TArray<int64>* SampleIndices,
|
||||
bool bFullRing,
|
||||
bool bPrepared,
|
||||
float ReachScale,
|
||||
TArray<float>* OutDensities,
|
||||
FTiming& OutTiming)
|
||||
{
|
||||
OutTiming = FTiming();
|
||||
if (Dimension <= 0 || Dimension > 4096
|
||||
|| (bFullRing && SampleIndices != nullptr)
|
||||
|| (!bFullRing && SampleIndices == nullptr))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int64 TotalLatticeSamples = LatticeCount(Dimension);
|
||||
const int64 SampleCount = bFullRing
|
||||
? TotalLatticeSamples : static_cast<int64>(SampleIndices->Num());
|
||||
if (SampleCount <= 0 || SampleCount > TotalLatticeSamples)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const double StartSeconds = FPlatformTime::Seconds();
|
||||
if (OutDensities != nullptr)
|
||||
{
|
||||
if (SampleCount > MAX_int32)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutDensities->SetNumUninitialized(static_cast<int32>(SampleCount));
|
||||
}
|
||||
|
||||
const int32 OriginX = OriginVoxels.X;
|
||||
const int32 OriginY = OriginVoxels.Y;
|
||||
const int32 OriginZ = OriginVoxels.Z;
|
||||
double WorkerSeconds[WorkerCount] = {};
|
||||
double WorkerCpuSeconds[WorkerCount] = {};
|
||||
bool WorkerCpuFromOsThreadTimes[WorkerCount] = {};
|
||||
bool bAllWorkerCpuFromOsThreadTimes = true;
|
||||
uint32 WorkerThreadIds[WorkerCount] = {};
|
||||
uint64 WorkerNonFinite[WorkerCount] = {};
|
||||
ParallelFor(WorkerCount, [&](int32 WorkerIndex)
|
||||
{
|
||||
VoxelCaveMorphology::SetResearchBrickBinningEnabled(bPrepared);
|
||||
VoxelCaveMorphology::SetResearchBrickRoomReachScale(ReachScale);
|
||||
WorkerThreadIds[WorkerIndex] = FPlatformTLS::GetCurrentThreadId();
|
||||
const int64 Begin = (SampleCount * WorkerIndex) / WorkerCount;
|
||||
const int64 End = (SampleCount * (WorkerIndex + 1)) / WorkerCount;
|
||||
const double WorkerStart = FPlatformTime::Seconds();
|
||||
double WorkerCpuStart = 0.0;
|
||||
const bool bHaveWorkerCpuStart = GetCurrentThreadCpuSeconds(WorkerCpuStart);
|
||||
for (int64 OutputIndex = Begin; OutputIndex < End; ++OutputIndex)
|
||||
{
|
||||
const int64 LatticeIndex = bFullRing
|
||||
? OutputIndex : (*SampleIndices)[static_cast<int32>(OutputIndex)];
|
||||
if (LatticeIndex < 0 || LatticeIndex >= TotalLatticeSamples)
|
||||
{
|
||||
++WorkerNonFinite[WorkerIndex];
|
||||
continue;
|
||||
}
|
||||
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 float Density = Generator.GetDensityAt(
|
||||
static_cast<float>(OriginX + X),
|
||||
static_cast<float>(OriginY + Y),
|
||||
static_cast<float>(OriginZ + Z));
|
||||
if (OutDensities != nullptr)
|
||||
{
|
||||
(*OutDensities)[static_cast<int32>(OutputIndex)] = Density;
|
||||
}
|
||||
}
|
||||
WorkerSeconds[WorkerIndex] = FPlatformTime::Seconds() - WorkerStart;
|
||||
double WorkerCpuEnd = 0.0;
|
||||
if (bHaveWorkerCpuStart && GetCurrentThreadCpuSeconds(WorkerCpuEnd))
|
||||
{
|
||||
WorkerCpuSeconds[WorkerIndex] = FMath::Max(0.0, WorkerCpuEnd - WorkerCpuStart);
|
||||
WorkerCpuFromOsThreadTimes[WorkerIndex] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
WorkerCpuSeconds[WorkerIndex] = WorkerSeconds[WorkerIndex];
|
||||
}
|
||||
VoxelCaveMorphology::SetResearchBrickBinningEnabled(false);
|
||||
VoxelCaveMorphology::SetResearchBrickRoomReachScale(1.0f);
|
||||
}, EParallelForFlags::None);
|
||||
|
||||
OutTiming.WallSeconds = FPlatformTime::Seconds() - StartSeconds;
|
||||
OutTiming.SampleCount = SampleCount;
|
||||
for (int32 WorkerIndex = 0; WorkerIndex < WorkerCount; ++WorkerIndex)
|
||||
{
|
||||
OutTiming.WorkerCpuSeconds += WorkerCpuSeconds[WorkerIndex];
|
||||
bAllWorkerCpuFromOsThreadTimes &= WorkerCpuFromOsThreadTimes[WorkerIndex];
|
||||
OutTiming.NonFiniteSamples += WorkerNonFinite[WorkerIndex];
|
||||
bool bAlreadyCounted = false;
|
||||
for (int32 PriorWorker = 0; PriorWorker < WorkerIndex; ++PriorWorker)
|
||||
{
|
||||
bAlreadyCounted |= WorkerThreadIds[PriorWorker] == WorkerThreadIds[WorkerIndex];
|
||||
}
|
||||
OutTiming.ObservedWorkerThreads += bAlreadyCounted ? 0 : 1;
|
||||
}
|
||||
OutTiming.bWorkerCpuFromOsThreadTimes = bAllWorkerCpuFromOsThreadTimes;
|
||||
OutTiming.NanosecondsPerSample = OutTiming.SampleCount > 0
|
||||
? OutTiming.WorkerCpuSeconds * 1.0e9 / static_cast<double>(OutTiming.SampleCount)
|
||||
: 0.0;
|
||||
if (OutDensities != nullptr)
|
||||
{
|
||||
for (const float Density : *OutDensities)
|
||||
{
|
||||
bool bSaturated = false;
|
||||
bool bFinite = false;
|
||||
QuantizeDensity(Density, bSaturated, bFinite);
|
||||
OutTiming.SaturatedSamples += bSaturated ? 1u : 0u;
|
||||
OutTiming.NonFiniteSamples += bFinite ? 0u : 1u;
|
||||
}
|
||||
OutTiming.QuantizedHash = HashQuantized(*OutDensities);
|
||||
OutTiming.RawFloatHash = HashRawFloats(*OutDensities);
|
||||
}
|
||||
return OutTiming.NonFiniteSamples == 0;
|
||||
}
|
||||
|
||||
FCandidateVisits CountCandidates(
|
||||
const UVoxelGenerator& Generator,
|
||||
FIntVector OriginVoxels,
|
||||
int32 Dimension,
|
||||
const TArray<int64>& SampleIndices,
|
||||
bool bPrepared,
|
||||
float ReachScale)
|
||||
{
|
||||
FCandidateVisits Result;
|
||||
Result.SampleCount = SampleIndices.Num();
|
||||
VoxelDensityProfile::Reset();
|
||||
VoxelDensityProfile::SetMode(
|
||||
VoxelDensityProfile::EMode::Attribution,
|
||||
VoxelDensityProfile::DefaultSampleInterval);
|
||||
FTiming IgnoredTiming;
|
||||
const bool bEvaluated = Evaluate(
|
||||
Generator, OriginVoxels, Dimension, &SampleIndices, false,
|
||||
bPrepared, ReachScale, nullptr, IgnoredTiming);
|
||||
const VoxelDensityProfile::FSnapshot Snapshot = VoxelDensityProfile::Snapshot();
|
||||
VoxelDensityProfile::SetMode(
|
||||
VoxelDensityProfile::EMode::Disabled,
|
||||
VoxelDensityProfile::DefaultSampleInterval);
|
||||
VoxelDensityProfile::Reset();
|
||||
if (!bEvaluated)
|
||||
{
|
||||
return Result;
|
||||
}
|
||||
using VoxelDensityProfile::ECounter;
|
||||
Result.Room = Snapshot.Counters[static_cast<int32>(ECounter::CaveRoomCandidates)];
|
||||
Result.Tunnel = Snapshot.Counters[static_cast<int32>(ECounter::CaveTunnelCandidates)];
|
||||
Result.TunnelCore = Snapshot.Counters[static_cast<int32>(ECounter::TunnelCoreCandidates)];
|
||||
Result.Passage = Snapshot.Counters[static_cast<int32>(ECounter::PassageCandidates)];
|
||||
Result.TunnelCacheLookups = Snapshot.Counters[static_cast<int32>(ECounter::TunnelCacheLookup)];
|
||||
Result.TunnelCacheHits = Snapshot.Counters[static_cast<int32>(ECounter::TunnelCacheHit)];
|
||||
Result.TunnelCacheMisses = Snapshot.Counters[static_cast<int32>(ECounter::TunnelCacheMiss)];
|
||||
Result.SdfCacheBuilds = Snapshot.Counters[static_cast<int32>(ECounter::SdfCacheBuild)];
|
||||
Result.Total = Result.Room + Result.Tunnel + Result.TunnelCore + Result.Passage;
|
||||
Result.PerSample = Result.SampleCount > 0
|
||||
? static_cast<double>(Result.Total) / Result.SampleCount : 0.0;
|
||||
Result.AttributionSampleInterval = Snapshot.SampleInterval;
|
||||
const auto AttributedNanosecondsPerSample = [&](VoxelDensityProfile::EBucket Bucket)
|
||||
{
|
||||
return Result.SampleCount > 0
|
||||
? FPlatformTime::ToSeconds64(Snapshot.Cycles[static_cast<int32>(Bucket)])
|
||||
* 1.0e9 / static_cast<double>(Result.SampleCount)
|
||||
: 0.0;
|
||||
};
|
||||
Result.GetDensityAtAttributedNsPerSample = AttributedNanosecondsPerSample(
|
||||
VoxelDensityProfile::EBucket::GetDensityAt);
|
||||
Result.DensityPrologueAttributedNsPerSample = AttributedNanosecondsPerSample(
|
||||
VoxelDensityProfile::EBucket::DensityPrologue);
|
||||
Result.DensityCoreAttributedNsPerSample = AttributedNanosecondsPerSample(
|
||||
VoxelDensityProfile::EBucket::DensityCore);
|
||||
Result.DensityDisturbancesAttributedNsPerSample = AttributedNanosecondsPerSample(
|
||||
VoxelDensityProfile::EBucket::DensityDisturbances);
|
||||
Result.DensityStructuralPostsAttributedNsPerSample = AttributedNanosecondsPerSample(
|
||||
VoxelDensityProfile::EBucket::DensityStructuralPosts);
|
||||
Result.DensityBoundarySealAttributedNsPerSample = AttributedNanosecondsPerSample(
|
||||
VoxelDensityProfile::EBucket::DensityBoundarySeal);
|
||||
Result.DensityDiffLayerAttributedNsPerSample = AttributedNanosecondsPerSample(
|
||||
VoxelDensityProfile::EBucket::DensityDiffLayer);
|
||||
Result.DensityTailAttributedNsPerSample = AttributedNanosecondsPerSample(
|
||||
VoxelDensityProfile::EBucket::DensityTail);
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
class UVoxelGenerator;
|
||||
|
||||
namespace VoxelForgeRegionEval
|
||||
{
|
||||
constexpr int32 WorkerCount = 6;
|
||||
constexpr int32 RingDimension = 259;
|
||||
// 1/16 voxel resolution covers the full prepared test field without clipping ordinary
|
||||
// interior/exterior distances into indistinguishable int16 saturation values.
|
||||
constexpr double QuantizationScale = 16.0;
|
||||
|
||||
struct FTiming
|
||||
{
|
||||
int32 Workers = WorkerCount;
|
||||
int32 ObservedWorkerThreads = 0;
|
||||
int64 SampleCount = 0;
|
||||
double WallSeconds = 0.0;
|
||||
double WorkerCpuSeconds = 0.0;
|
||||
double NanosecondsPerSample = 0.0;
|
||||
bool bWorkerCpuFromOsThreadTimes = false;
|
||||
uint64 QuantizedHash = 0;
|
||||
uint64 RawFloatHash = 0;
|
||||
uint64 SaturatedSamples = 0;
|
||||
uint64 NonFiniteSamples = 0;
|
||||
};
|
||||
|
||||
struct FCandidateVisits
|
||||
{
|
||||
int64 SampleCount = 0;
|
||||
uint64 Room = 0;
|
||||
uint64 Tunnel = 0;
|
||||
uint64 TunnelCore = 0;
|
||||
uint64 Passage = 0;
|
||||
uint64 TunnelCacheLookups = 0;
|
||||
uint64 TunnelCacheHits = 0;
|
||||
uint64 TunnelCacheMisses = 0;
|
||||
uint64 SdfCacheBuilds = 0;
|
||||
uint64 Total = 0;
|
||||
double PerSample = 0.0;
|
||||
uint32 AttributionSampleInterval = 0;
|
||||
double GetDensityAtAttributedNsPerSample = 0.0;
|
||||
double DensityPrologueAttributedNsPerSample = 0.0;
|
||||
double DensityCoreAttributedNsPerSample = 0.0;
|
||||
double DensityDisturbancesAttributedNsPerSample = 0.0;
|
||||
double DensityStructuralPostsAttributedNsPerSample = 0.0;
|
||||
double DensityBoundarySealAttributedNsPerSample = 0.0;
|
||||
double DensityDiffLayerAttributedNsPerSample = 0.0;
|
||||
double DensityTailAttributedNsPerSample = 0.0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Evaluate a deterministic lattice through the live final-field API. The prepared mode only
|
||||
* enables the commandlet-only 8^3 SDF candidate bins; all field writers and their order remain
|
||||
* the live GetDensityAt implementation. FullRing uses brick-grouped output ownership and
|
||||
* writes every lattice density exactly once. SampleIndices are sorted lattice offsets for the
|
||||
* statistical correctness / candidate-counter sweeps.
|
||||
*/
|
||||
bool Evaluate(
|
||||
const UVoxelGenerator& Generator,
|
||||
FIntVector OriginVoxels,
|
||||
int32 Dimension,
|
||||
const TArray<int64>* SampleIndices,
|
||||
bool bFullRing,
|
||||
bool bPrepared,
|
||||
float ReachScale,
|
||||
TArray<float>* OutDensities,
|
||||
FTiming& OutTiming);
|
||||
|
||||
/** Count exact room/tunnel/core/passage candidate visits with instrumentation enabled. */
|
||||
FCandidateVisits CountCandidates(
|
||||
const UVoxelGenerator& Generator,
|
||||
FIntVector OriginVoxels,
|
||||
int32 Dimension,
|
||||
const TArray<int64>& SampleIndices,
|
||||
bool bPrepared,
|
||||
float ReachScale);
|
||||
|
||||
int16 QuantizeDensity(float Density, bool& bOutSaturated, bool& bOutFinite);
|
||||
uint64 HashQuantized(const TArray<float>& Densities);
|
||||
int64 LinearIndex(int32 X, int32 Y, int32 Z, int32 Dimension);
|
||||
}
|
||||
Reference in new issue
Block a user