perf(gen): the game path is NOTHING like the export path -- and empty tiles cost 5.6s

Overnight task 3. The headline is not a speedup: it is that we have been measuring
the wrong thing all along.

⭐ IN-GAME MEASUREMENT, FINALLY (headless -game -nullrhi, TunnelNetwork PIE map)

  46,078 profiled tile calls
  46,025 EMPTY  (99.885%)
      53 actually meshed
  meshed    p50 0.6085 s   p95 4.9075 s   max 5.0199 s   mean 1.3515 s
  LOD1-3 empty medians ~2-3 us
  LOD4 empty OUTLIERS up to 5.618 s

The pawn fell through successive strata during the run, reproducing the owner's
"I can outrun the generation and fall" exactly.

Two facts to sit with:
1. **Almost nothing is meshed. 99.9% of tile requests produce nothing** -- and some
   of those empty tiles take FIVE AND A HALF SECONDS. A tile that generates no
   geometry costing seconds is the bug, and it is classification cost, not meshing.
2. The export path we have optimised for three rounds does 64 tiles in ~1.5 s
   total. The game does 46,078 calls with multi-second tile latency. **They share
   the density function and nothing else about their cost profile.**

⇒ Every optimisation so far was aimed at the path the owner does not play.

WHY NO BLOCK CAN BE SKIPPED -- answered, and it is structural
Per-op bound-vs-brute-force table produced (op_bounds.json). Most ops bound fine
once given proven suprema, and those were tightened:
  FBM envelope 1.5, cellular 6.0, SdfConvertOp exact 2*|BaseDensity| = 16,
  Lerp radius envelope max(Min,Max), plus state-aware room/column/floor-bias/worm/
  terrace/cliff/arch/dome/pinch/roughness bounds. Structural ops now return exact
  zero outside their geometric bands.

But OriginSpineOp, PassageCarveOp and the active seals **cannot** take a finite
generic density bound: they Lerp/Min/Max against the CURRENT density, so with
arbitrary incoming density their supremum is mathematically unbounded. This is not
a missing EffectOverBox, it is the operator semantics.
⇒ Hierarchical evaluation requires propagating a density INTERVAL through the
stack, or changing those operators. That is the real prerequisite, now named.

Early-out with tightened bounds still skips 0 of 4,096 blocks and costs 96.6%
(3.305 -> 6.497 s). Left opt-in and DISABLED. ClassifyTile's TerrainZ/overhang
guard untouched.

SPATIAL INDEX REMOVED (ee89c4d's, which never earned its keep)
  wall clock   3.1407 s on -> 3.1894 s off   = 1.5%, below the 17% floor
  cache bytes  557,626,564 -> 246,049,920    = -297 MiB
  candidates   19.5M -> 81.6M scanned (4.17x), evaluated +25.6%
Memory the owner can feel beats counters he cannot. Per-primitive broadphase stays.

VERIFIED END-TO-END on a correctly built binary: 1.515 / 1.554 / 1.569 s, geometry
hash 98eaad61..d3c4f unchanged from 2f33870. Task 3 is memory, not speed.

⚠️ PROCESS, second time: the agent's reported DLL timestamp was from a staging copy
it built under the plugin's own Saved/, not Plugins/VoxelForge/Binaries. My first
three runs measured the 05:58 binary again. Verify the binary you are about to
measure, not the one someone says they built.

Determinism: all export hashes ED0F1F31 within the run; classifier vs generator
verdicts zero mismatches. Nothing outside Plugins/VoxelForge; RMC untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-09 09:00:41 +02:00
1 parent 2f33870a84
commit 0e9df9e5a7
9 files changed
+1302 -713

No files matched your search

@@ -69,279 +69,6 @@ namespace
return DX * DX + DY * DY + DZ * DZ;
}
FORCEINLINE bool VF_IndexCell(float Coordinate, int32& OutCell)
{
const float Scaled = Coordinate / static_cast<float>(FCaveSpatialIndex::BinSizeVoxels);
if (!FMath::IsFinite(Scaled)
|| Scaled < static_cast<float>(MIN_int32) + 1.0f
|| Scaled > static_cast<float>(MAX_int32) - 1.0f)
{
return false;
}
OutCell = FMath::FloorToInt(Scaled);
return true;
}
FORCEINLINE bool VF_IndexEntryRange(
const FCaveSpatialIndexEntry& Entry,
int32& OutMinX, int32& OutMinY, int32& OutMinZ,
int32& OutMaxX, int32& OutMaxY, int32& OutMaxZ)
{
const float MinX = static_cast<float>(FMath::Min(Entry.Min.X, Entry.Max.X));
const float MinY = static_cast<float>(FMath::Min(Entry.Min.Y, Entry.Max.Y));
const float MinZ = static_cast<float>(FMath::Min(Entry.Min.Z, Entry.Max.Z));
const float MaxX = static_cast<float>(FMath::Max(Entry.Min.X, Entry.Max.X));
const float MaxY = static_cast<float>(FMath::Max(Entry.Min.Y, Entry.Max.Y));
const float MaxZ = static_cast<float>(FMath::Max(Entry.Min.Z, Entry.Max.Z));
return VF_IndexCell(MinX, OutMinX)
&& VF_IndexCell(MinY, OutMinY)
&& VF_IndexCell(MinZ, OutMinZ)
&& VF_IndexCell(MaxX, OutMaxX)
&& VF_IndexCell(MaxY, OutMaxY)
&& VF_IndexCell(MaxZ, OutMaxZ)
&& OutMinX <= OutMaxX
&& OutMinY <= OutMaxY
&& OutMinZ <= OutMaxZ;
}
}
void FCaveSpatialIndex::Reset()
{
bValid = false;
MinBinX = 0;
MinBinY = 0;
MinBinZ = 0;
CellsX = 0;
CellsY = 0;
CellsZ = 0;
Bins.Reset();
CandidateIndices.Reset();
}
void FCaveSpatialIndex::Build(const TArray<FCaveSpatialIndexEntry>& Entries)
{
Reset();
// An empty cache is a valid empty index. This keeps the query path branch-light while still
// making malformed/overflowing non-empty indexes fall back to the exact full scan.
if (Entries.Num() == 0)
{
bValid = true;
return;
}
constexpr int64 MaxBins = 1ll << 20;
constexpr int64 MaxReferences = 1ll << 23;
int32 GlobalMinX = MAX_int32;
int32 GlobalMinY = MAX_int32;
int32 GlobalMinZ = MAX_int32;
int32 GlobalMaxX = MIN_int32;
int32 GlobalMaxY = MIN_int32;
int32 GlobalMaxZ = MIN_int32;
TArray<FIntVector, TInlineAllocator<64>> MinCells;
TArray<FIntVector, TInlineAllocator<64>> MaxCells;
MinCells.SetNumUninitialized(Entries.Num());
MaxCells.SetNumUninitialized(Entries.Num());
int64 TotalReferences = 0;
for (int32 EntryIndex = 0; EntryIndex < Entries.Num(); ++EntryIndex)
{
const FCaveSpatialIndexEntry& Entry = Entries[EntryIndex];
if (Entry.CandidateIndex < 0)
{
return;
}
int32 MinX, MinY, MinZ, MaxX, MaxY, MaxZ;
if (!VF_IndexEntryRange(Entry, MinX, MinY, MinZ, MaxX, MaxY, MaxZ))
{
return;
}
const int64 SpanX = static_cast<int64>(MaxX) - MinX + 1;
const int64 SpanY = static_cast<int64>(MaxY) - MinY + 1;
const int64 SpanZ = static_cast<int64>(MaxZ) - MinZ + 1;
const int64 References = SpanX * SpanY * SpanZ;
if (References <= 0 || References > MaxReferences
|| TotalReferences > MaxReferences - References)
{
return;
}
MinCells[EntryIndex] = FIntVector(MinX, MinY, MinZ);
MaxCells[EntryIndex] = FIntVector(MaxX, MaxY, MaxZ);
GlobalMinX = FMath::Min(GlobalMinX, MinX);
GlobalMinY = FMath::Min(GlobalMinY, MinY);
GlobalMinZ = FMath::Min(GlobalMinZ, MinZ);
GlobalMaxX = FMath::Max(GlobalMaxX, MaxX);
GlobalMaxY = FMath::Max(GlobalMaxY, MaxY);
GlobalMaxZ = FMath::Max(GlobalMaxZ, MaxZ);
TotalReferences += References;
}
const int64 NumBins64 =
(static_cast<int64>(GlobalMaxX) - GlobalMinX + 1)
* (static_cast<int64>(GlobalMaxY) - GlobalMinY + 1)
* (static_cast<int64>(GlobalMaxZ) - GlobalMinZ + 1);
if (NumBins64 <= 0 || NumBins64 > MaxBins || TotalReferences > MAX_int32)
{
return;
}
MinBinX = GlobalMinX;
MinBinY = GlobalMinY;
MinBinZ = GlobalMinZ;
CellsX = GlobalMaxX - GlobalMinX + 1;
CellsY = GlobalMaxY - GlobalMinY + 1;
CellsZ = GlobalMaxZ - GlobalMinZ + 1;
const int32 NumBins = static_cast<int32>(NumBins64);
Bins.SetNumZeroed(NumBins);
TArray<int32> Counts;
Counts.Init(0, NumBins);
auto BinNumber = [this](int32 CellX, int32 CellY, int32 CellZ) -> int32
{
const int64 X = static_cast<int64>(CellX) - MinBinX;
const int64 Y = static_cast<int64>(CellY) - MinBinY;
const int64 Z = static_cast<int64>(CellZ) - MinBinZ;
return static_cast<int32>((Z * CellsY + Y) * CellsX + X);
};
for (int32 EntryIndex = 0; EntryIndex < Entries.Num(); ++EntryIndex)
{
const FIntVector MinCell = MinCells[EntryIndex];
const FIntVector MaxCell = MaxCells[EntryIndex];
for (int32 Z = MinCell.Z; Z <= MaxCell.Z; ++Z)
for (int32 Y = MinCell.Y; Y <= MaxCell.Y; ++Y)
for (int32 X = MinCell.X; X <= MaxCell.X; ++X)
{
++Counts[BinNumber(X, Y, Z)];
}
}
int32 Running = 0;
for (int32 BinIndex = 0; BinIndex < NumBins; ++BinIndex)
{
Bins[BinIndex].First = Running;
Bins[BinIndex].Count = Counts[BinIndex];
Running += Counts[BinIndex];
}
CandidateIndices.SetNumUninitialized(Running);
TArray<int32> WriteOffsets;
WriteOffsets.SetNumUninitialized(NumBins);
for (int32 BinIndex = 0; BinIndex < NumBins; ++BinIndex)
{
WriteOffsets[BinIndex] = Bins[BinIndex].First;
}
// Entries are generated outermost by candidate ID and innermost by segment index. Therefore
// every bin receives nondecreasing IDs here. Duplicates are removed at query time with one
// LastCandidate comparison; preserving this order also preserves the old deterministic SDF
// reduction order.
for (int32 EntryIndex = 0; EntryIndex < Entries.Num(); ++EntryIndex)
{
const FCaveSpatialIndexEntry& Entry = Entries[EntryIndex];
const FIntVector MinCell = MinCells[EntryIndex];
const FIntVector MaxCell = MaxCells[EntryIndex];
for (int32 Z = MinCell.Z; Z <= MaxCell.Z; ++Z)
for (int32 Y = MinCell.Y; Y <= MaxCell.Y; ++Y)
for (int32 X = MinCell.X; X <= MaxCell.X; ++X)
{
const int32 BinIndex = BinNumber(X, Y, Z);
CandidateIndices[WriteOffsets[BinIndex]++] = Entry.CandidateIndex;
}
}
bValid = true;
}
bool FCaveSpatialIndex::GetBinAt(
int32 CellX, int32 CellY, int32 CellZ,
int32& OutFirst, int32& OutCount) const
{
OutFirst = 0;
OutCount = 0;
if (!bValid)
{
return false;
}
if (CellsX <= 0 || CellX < MinBinX || CellX >= MinBinX + CellsX
|| CellY < MinBinY || CellY >= MinBinY + CellsY
|| CellZ < MinBinZ || CellZ >= MinBinZ + CellsZ)
{
return true;
}
const int64 LocalX = static_cast<int64>(CellX) - MinBinX;
const int64 LocalY = static_cast<int64>(CellY) - MinBinY;
const int64 LocalZ = static_cast<int64>(CellZ) - MinBinZ;
const int64 BinIndex = (LocalZ * CellsY + LocalY) * CellsX + LocalX;
const FCaveSpatialIndexBin& Bin = Bins[static_cast<int32>(BinIndex)];
OutFirst = Bin.First;
OutCount = Bin.Count;
return true;
}
bool FCaveSpatialIndex::GetBin(
float X, float Y, float Z,
int32& OutFirst, int32& OutCount) const
{
OutFirst = 0;
OutCount = 0;
if (!bValid)
{
return false;
}
int32 CellX, CellY, CellZ;
if (!VF_IndexCell(X, CellX) || !VF_IndexCell(Y, CellY) || !VF_IndexCell(Z, CellZ))
{
return false;
}
return GetBinAt(CellX, CellY, CellZ, OutFirst, OutCount);
}
template <typename AllocatorType>
bool FCaveSpatialIndex::GetColumnCandidates(
float X, float Y, TArray<int32, AllocatorType>& OutCandidates) const
{
OutCandidates.Reset();
if (!bValid)
{
return false;
}
int32 CellX, CellY;
if (!VF_IndexCell(X, CellX) || !VF_IndexCell(Y, CellY))
{
return false;
}
if (CellsX <= 0 || CellX < MinBinX || CellX >= MinBinX + CellsX
|| CellY < MinBinY || CellY >= MinBinY + CellsY)
{
return true;
}
// The support decision is a column query, so intentionally inspect every height bin for this
// XY cell. This work occurs once per column, not once per sample; the regular SDF/core query
// still uses the full 3D bin lookup above.
for (int32 CellZ = MinBinZ; CellZ < MinBinZ + CellsZ; ++CellZ)
{
int32 First, Count;
GetBinAt(CellX, CellY, CellZ, First, Count);
for (int32 Offset = 0; Offset < Count; ++Offset)
{
const int32 Candidate = CandidateIndices[First + Offset];
if (!OutCandidates.Contains(Candidate))
{
OutCandidates.Add(Candidate);
}
}
}
return true;
}
SIZE_T FChunkSDFCache::GetAllocatedSize() const
@@ -378,14 +105,6 @@ VoxelDensityProfile::FCacheMemoryBreakdown FChunkSDFCache::GetAllocatedSizeBreak
Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldControlRadii);
}
Breakdown.RoomIndexBinsBytes = ArrayBytes(RoomIndex.Bins);
Breakdown.RoomIndexCandidatesBytes = ArrayBytes(RoomIndex.CandidateIndices);
Breakdown.RoomFloorJoinIndexBinsBytes = ArrayBytes(RoomFloorJoinIndex.Bins);
Breakdown.RoomFloorJoinIndexCandidatesBytes = ArrayBytes(RoomFloorJoinIndex.CandidateIndices);
Breakdown.TunnelIndexBinsBytes = ArrayBytes(TunnelIndex.Bins);
Breakdown.TunnelIndexCandidatesBytes = ArrayBytes(TunnelIndex.CandidateIndices);
Breakdown.TunnelWorldIndexBinsBytes = ArrayBytes(TunnelWorldIndex.Bins);
Breakdown.TunnelWorldIndexCandidatesBytes = ArrayBytes(TunnelWorldIndex.CandidateIndices);
return Breakdown;
}
@@ -447,115 +166,6 @@ static void BakeRoomFeature(
}
}
namespace
{
FORCEINLINE void VF_AddSpatialEntry(
TArray<FCaveSpatialIndexEntry>& Entries,
int32 CandidateIndex,
const FVector& InMin,
const FVector& InMax)
{
constexpr float Epsilon = 0.001f;
FCaveSpatialIndexEntry& Entry = Entries.Emplace_GetRef();
Entry.CandidateIndex = CandidateIndex;
Entry.Min = FVector(
FMath::Min(InMin.X, InMax.X) - Epsilon,
FMath::Min(InMin.Y, InMax.Y) - Epsilon,
FMath::Min(InMin.Z, InMax.Z) - Epsilon);
Entry.Max = FVector(
FMath::Max(InMin.X, InMax.X) + Epsilon,
FMath::Max(InMin.Y, InMax.Y) + Epsilon,
FMath::Max(InMin.Z, InMax.Z) + Epsilon);
}
FORCEINLINE void VF_AddSegmentSpatialEntry(
TArray<FCaveSpatialIndexEntry>& Entries,
int32 CandidateIndex,
const FVector& A,
const FVector& B,
float PadXY,
float PadZ)
{
const FVector Pad(PadXY, PadXY, PadZ);
VF_AddSpatialEntry(Entries, CandidateIndex,
FVector(FMath::Min(A.X, B.X), FMath::Min(A.Y, B.Y), FMath::Min(A.Z, B.Z)) - Pad,
FVector(FMath::Max(A.X, B.X), FMath::Max(A.Y, B.Y), FMath::Max(A.Z, B.Z)) + Pad);
}
static void VF_AppendTunnelSpatialEntries(
TArray<FCaveSpatialIndexEntry>& Entries,
int32 TunnelIndex,
const TArray<FVector>& Points,
const TArray<float>& Radii,
float BlendPad,
float VerticalPad)
{
if (Points.Num() >= 2 && Radii.Num() == Points.Num())
{
for (int32 SegmentIndex = 0; SegmentIndex + 1 < Points.Num(); ++SegmentIndex)
{
const float Radius = FMath::Max(
FMath::Abs(Radii[SegmentIndex]),
FMath::Abs(Radii[SegmentIndex + 1]));
VF_AddSegmentSpatialEntry(
Entries, TunnelIndex, Points[SegmentIndex], Points[SegmentIndex + 1],
Radius + BlendPad, Radius + VerticalPad);
}
return;
}
// Keep the descriptor compatibility path conservative as well. Current caches always use
// the chain, but cooked/diagnostic caches can still contain the old midpoint descriptor.
if (Points.Num() >= 2)
{
VF_AddSegmentSpatialEntry(
Entries, TunnelIndex, Points[0], Points[1],
BlendPad, VerticalPad);
}
}
static void VF_AppendTunnelDescriptorSpatialEntries(
TArray<FCaveSpatialIndexEntry>& Entries,
int32 TunnelIndex,
const FCachedTunnel& Tunnel,
float BlendPad,
float VerticalPad)
{
if (Tunnel.ControlPoints.Num() >= 2
&& Tunnel.ControlRadii.Num() == Tunnel.ControlPoints.Num())
{
VF_AppendTunnelSpatialEntries(
Entries, TunnelIndex, Tunnel.ControlPoints, Tunnel.ControlRadii,
BlendPad, VerticalPad);
return;
}
// Keep old cooked/diagnostic descriptors conservative too. The current
// cache always has a control chain, but the evaluator still supports the
// endpoint/midpoint representation and its index must cover that path.
if (Tunnel.bHasMidpoint)
{
const float RadiusA = FMath::Max(
FMath::Abs(Tunnel.RadiusA), FMath::Abs(Tunnel.RadiusMid));
const float RadiusB = FMath::Max(
FMath::Abs(Tunnel.RadiusMid), FMath::Abs(Tunnel.RadiusB));
VF_AddSegmentSpatialEntry(
Entries, TunnelIndex, Tunnel.EndpointA, Tunnel.Midpoint,
RadiusA + BlendPad, RadiusA + VerticalPad);
VF_AddSegmentSpatialEntry(
Entries, TunnelIndex, Tunnel.Midpoint, Tunnel.EndpointB,
RadiusB + BlendPad, RadiusB + VerticalPad);
}
else
{
const float Radius = FMath::Max(
FMath::Abs(Tunnel.RadiusA), FMath::Abs(Tunnel.RadiusB));
VF_AddSegmentSpatialEntry(
Entries, TunnelIndex, Tunnel.EndpointA, Tunnel.EndpointB,
Radius + BlendPad, Radius + VerticalPad);
}
}
static void VF_FinalizeTunnelBroadPhaseBounds(FCachedTunnel& Tunnel, float BlendK)
{
const float BlendPad = FMath::Max(BlendK, 0.0f);
@@ -637,68 +247,16 @@ namespace
}
}
static void VF_BuildCacheSpatialIndices(
static void VF_FinalizeCacheBroadPhaseBounds(
FChunkSDFCache& Cache,
float BlendK)
{
TArray<FCaveSpatialIndexEntry> RoomEntries;
TArray<FCaveSpatialIndexEntry> JoinEntries;
TArray<FCaveSpatialIndexEntry> TunnelEntries;
TArray<FCaveSpatialIndexEntry> WorldTunnelEntries;
RoomEntries.Reserve(Cache.Rooms.Num());
JoinEntries.Reserve(Cache.RoomFloorJoins.Num());
for (int32 Index = 0; Index < Cache.Rooms.Num(); ++Index)
{
const FCachedRoom& Room = Cache.Rooms[Index];
const float Radius = FMath::Sqrt(FMath::Max(Room.CullRadiusSq, 0.0f));
const FVector Extent(Radius, Radius, Radius);
VF_AddSpatialEntry(RoomEntries, Index, Room.Center - Extent, Room.Center + Extent);
}
for (int32 Index = 0; Index < Cache.RoomFloorJoins.Num(); ++Index)
{
const FCachedRoomFloorJoin& Join = Cache.RoomFloorJoins[Index];
const float Radius = FMath::Sqrt(FMath::Max(Join.BoundRadiusSq, 0.0f));
const FVector Extent(Radius, Radius, Radius);
VF_AddSpatialEntry(JoinEntries, Index,
Join.BoundCenter - Extent, Join.BoundCenter + Extent);
}
for (int32 Index = 0; Index < Cache.Tunnels.Num(); ++Index)
{
FCachedTunnel& Tunnel = Cache.Tunnels[Index];
VF_FinalizeTunnelBroadPhaseBounds(Tunnel, BlendK);
const float SDFPad = FMath::Max(BlendK, 0.0f) * 3.0f;
VF_AppendTunnelDescriptorSpatialEntries(
TunnelEntries, Index, Tunnel, SDFPad, SDFPad);
const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2
&& Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num();
if (bHasWorldChain)
{
VF_AppendTunnelSpatialEntries(
WorldTunnelEntries, Index,
Tunnel.WorldControlPoints, Tunnel.WorldControlRadii,
VoxelPassageGeometry::LandingFloorThicknessVoxels,
VoxelPassageGeometry::LandingFloorThicknessVoxels);
}
else
{
VF_AppendTunnelDescriptorSpatialEntries(
WorldTunnelEntries, Index, Tunnel,
VoxelPassageGeometry::LandingFloorThicknessVoxels,
VoxelPassageGeometry::LandingFloorThicknessVoxels);
}
}
Cache.RoomIndex.Build(RoomEntries);
Cache.RoomFloorJoinIndex.Build(JoinEntries);
Cache.TunnelIndex.Build(TunnelEntries);
Cache.TunnelWorldIndex.Build(WorldTunnelEntries);
}
}
namespace
{
// Runtime landing queries use the same pinned capsule dimensions and support rules as the
@@ -3277,10 +2835,6 @@ void VoxelCaveMorphology::BuildChunkCache(
OutCache.Pits.Reset();
OutCache.Chimneys.Reset();
OutCache.Columns.Reset();
OutCache.RoomIndex.Reset();
OutCache.RoomFloorJoinIndex.Reset();
OutCache.TunnelIndex.Reset();
OutCache.TunnelWorldIndex.Reset();
OutCache.SupportColumnMinX = 0;
OutCache.SupportColumnMinY = 0;
OutCache.SupportColumnCellsX = 0;
@@ -3480,7 +3034,7 @@ void VoxelCaveMorphology::BuildChunkCache(
const int32 NumRooms = BuildRooms.Num();
if (NumRooms == 0)
{
VF_BuildCacheSpatialIndices(OutCache, BlendK);
VF_FinalizeCacheBroadPhaseBounds(OutCache, BlendK);
return;
}
@@ -4366,7 +3920,7 @@ void VoxelCaveMorphology::BuildChunkCache(
// All candidate arrays are complete now. Build the immutable broad phase only after every
// deterministic room/tunnel decision and feature bake has finished.
VF_BuildCacheSpatialIndices(OutCache, BlendK);
VF_FinalizeCacheBroadPhaseBounds(OutCache, BlendK);
}
//=============================================================================
@@ -4394,15 +3948,11 @@ float VoxelCaveMorphology::EvaluateSDFCached(
float NearestRoomRawSDF = FLT_MAX;
int32 NearestIdx = -1;
int32 RoomFirst = 0;
int32 RoomCount = 0;
const bool bIndexedRooms = Cache.RoomIndex.GetBin(
WorldX, WorldY, WorldZ, RoomFirst, RoomCount);
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::CaveRoomCandidates,
static_cast<uint64>(bIndexedRooms ? RoomCount : Cache.Rooms.Num()));
static_cast<uint64>(Cache.Rooms.Num()));
}
//=========================================================================
@@ -4459,20 +4009,10 @@ float VoxelCaveMorphology::EvaluateSDFCached(
MinSDF = VoxelSDF::SmoothMin(MinSDF, RoomSDF, BlendK);
};
if (bIndexedRooms)
{
for (int32 Offset = 0; Offset < RoomCount; ++Offset)
{
EvaluateRoom(Cache.RoomIndex.CandidateIndices[RoomFirst + Offset]);
}
}
else
{
for (int32 RoomIdx = 0; RoomIdx < Cache.Rooms.Num(); ++RoomIdx)
{
EvaluateRoom(RoomIdx);
}
}
//==========================================================================
// Flattened room joins
@@ -4481,10 +4021,6 @@ float VoxelCaveMorphology::EvaluateSDFCached(
// single horizontal plane, so a player crossing the shared volume cannot meet the tall lip
// produced by two independent floor cuts. They are deliberately not reported as rooms and do
// not participate in nearest-room terrain-op ownership.
int32 JoinFirst = 0;
int32 JoinCount = 0;
const bool bIndexedJoins = Cache.RoomFloorJoinIndex.GetBin(
WorldX, WorldY, WorldZ, JoinFirst, JoinCount);
auto EvaluateJoin = [&](int32 JoinIdx)
{
const FCachedRoomFloorJoin& Join = Cache.RoomFloorJoins[JoinIdx];
@@ -4499,33 +4035,19 @@ float VoxelCaveMorphology::EvaluateSDFCached(
const float JoinSDF = FMath::Max(HorizontalSDF, VerticalSDF);
MinSDF = VoxelSDF::SmoothMin(MinSDF, JoinSDF, BlendK);
};
if (bIndexedJoins)
{
for (int32 Offset = 0; Offset < JoinCount; ++Offset)
{
EvaluateJoin(Cache.RoomFloorJoinIndex.CandidateIndices[JoinFirst + Offset]);
}
}
else
{
for (int32 JoinIdx = 0; JoinIdx < Cache.RoomFloorJoins.Num(); ++JoinIdx)
{
EvaluateJoin(JoinIdx);
}
}
//=========================================================================
// Tunnel SDFs
//=========================================================================
int32 TunnelFirst = 0;
int32 TunnelCount = 0;
const bool bIndexedTunnels = Cache.TunnelIndex.GetBin(
WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount);
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::CaveTunnelCandidates,
static_cast<uint64>(bIndexedTunnels ? TunnelCount : Cache.Tunnels.Num()));
static_cast<uint64>(Cache.Tunnels.Num()));
}
auto EvaluateTunnel = [&](int32 TunnelIdx)
@@ -4590,24 +4112,10 @@ float VoxelCaveMorphology::EvaluateSDFCached(
MinSDF = VoxelSDF::SmoothMin(MinSDF, TunnelSDF, BlendK);
};
if (bIndexedTunnels)
{
int32 LastTunnelIdx = INDEX_NONE;
for (int32 Offset = 0; Offset < TunnelCount; ++Offset)
{
const int32 TunnelIdx = Cache.TunnelIndex.CandidateIndices[TunnelFirst + Offset];
if (TunnelIdx == LastTunnelIdx) continue;
LastTunnelIdx = TunnelIdx;
EvaluateTunnel(TunnelIdx);
}
}
else
{
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx)
{
EvaluateTunnel(TunnelIdx);
}
}
// Write nearest room index for the caller (terrain ops system)
if (OutNearestRoomIdx)
@@ -4625,10 +4133,6 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF(
const FVector Pos(WorldX, WorldY, WorldZ);
float MinSDF = FLT_MAX;
int32 TunnelFirst = 0;
int32 TunnelCount = 0;
const bool bIndexedTunnels = Cache.TunnelIndex.GetBin(
WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount);
auto EvaluateTunnel = [&](int32 TunnelIdx)
{
const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx];
@@ -4686,24 +4190,10 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF(
}
};
if (bIndexedTunnels)
{
int32 LastTunnelIdx = INDEX_NONE;
for (int32 Offset = 0; Offset < TunnelCount; ++Offset)
{
const int32 TunnelIdx = Cache.TunnelIndex.CandidateIndices[TunnelFirst + Offset];
if (TunnelIdx == LastTunnelIdx) continue;
LastTunnelIdx = TunnelIdx;
EvaluateTunnel(TunnelIdx);
}
}
else
{
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx)
{
EvaluateTunnel(TunnelIdx);
}
}
return MinSDF;
}
@@ -4716,10 +4206,6 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
const FVector Pos(WorldX, WorldY, WorldZ);
FTunnelCoreWorldEvaluation Result;
int32 TunnelFirst = 0;
int32 TunnelCount = 0;
const bool bIndexedTunnels = Cache.TunnelWorldIndex.GetBin(
WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount);
if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
@@ -4729,7 +4215,7 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCoreCandidates,
static_cast<uint64>(bIndexedTunnels ? TunnelCount : Cache.Tunnels.Num()));
static_cast<uint64>(Cache.Tunnels.Num()));
}
auto EvaluateTunnel = [&](int32 TunnelIdx)
@@ -4848,24 +4334,10 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
}
};
if (bIndexedTunnels)
{
int32 LastTunnelIdx = INDEX_NONE;
for (int32 Offset = 0; Offset < TunnelCount; ++Offset)
{
const int32 TunnelIdx = Cache.TunnelWorldIndex.CandidateIndices[TunnelFirst + Offset];
if (TunnelIdx == LastTunnelIdx) continue;
LastTunnelIdx = TunnelIdx;
EvaluateTunnel(TunnelIdx);
}
}
else
{
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx)
{
EvaluateTunnel(TunnelIdx);
}
}
return Result;
}
@@ -4883,15 +4355,11 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
{
const FVector Pos(WorldX, WorldY, WorldZ);
int32 TunnelFirst = 0;
int32 TunnelCount = 0;
const bool bIndexedTunnels = Cache.TunnelWorldIndex.GetBin(
WorldX, WorldY, WorldZ, TunnelFirst, TunnelCount);
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCoreCandidates,
static_cast<uint64>(bIndexedTunnels ? TunnelCount : Cache.Tunnels.Num()));
static_cast<uint64>(Cache.Tunnels.Num()));
}
auto TestTunnel = [&](int32 TunnelIdx) -> bool
@@ -4936,28 +4404,6 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
};
bool bSupport = false;
if (bIndexedTunnels)
{
int32 LastTunnelIdx = INDEX_NONE;
for (int32 Offset = 0; Offset < TunnelCount; ++Offset)
{
const int32 TunnelIdx = Cache.TunnelWorldIndex.CandidateIndices[TunnelFirst + Offset];
if (TunnelIdx == LastTunnelIdx) continue;
LastTunnelIdx = TunnelIdx;
if (TestTunnel(TunnelIdx))
{
bSupport = true;
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
}
break;
}
}
}
else
{
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx)
{
if (TestTunnel(TunnelIdx))
@@ -4971,7 +4417,6 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
break;
}
}
}
return bSupport;
}
@@ -4988,25 +4433,14 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
VoxelDensityProfile::ECounter::TunnelSupportColumnBuilds);
}
TArray<int32> CandidateIds;
const bool bIndexed = Cache.TunnelWorldIndex.GetColumnCandidates(WorldX, WorldY, CandidateIds);
if (!bIndexed)
{
CandidateIds.Reserve(Cache.Tunnels.Num());
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx)
{
CandidateIds.Add(TunnelIdx);
}
}
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportColumnCandidates,
static_cast<uint64>(CandidateIds.Num()));
static_cast<uint64>(Cache.Tunnels.Num()));
}
for (const int32 TunnelIdx : CandidateIds)
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num(); ++TunnelIdx)
{
if (!Cache.Tunnels.IsValidIndex(TunnelIdx))
{
+623 -40
View File
@@ -89,6 +89,12 @@ namespace
*/
static constexpr float VF_PerlinAbsBound = 1.5f;
// Cellular3D returns 2*(F2-F1)-1 without a clamp. Every searched feature point is at most
// sqrt(12) from the query's unit cell, so F2-F1 <= sqrt(12) and the positive supremum is
// 2*sqrt(12)-1 = 5.9282... . Round that up for float evaluation instead of trusting the
// "~[-1,1]" comment in the noise implementation (which is an observation, not a proof).
static constexpr float VF_CellularAbsBound = 6.0f;
/** La même enveloppe que `FractalNoise3D` de VoxelGenerator.cpp (qui y est `static`, donc
* invisible ici). Les coordonnées de bruit restent en float : FVector est double dans UE5
* et son aller-retour ne servait qu'à conserver l'ancien arrondi, exigence abandonnée. */
@@ -1637,6 +1643,22 @@ namespace
? EVoxelOpEffect::Both : EVoxelOpEffect::CarveOnly;
}
// The landing carve interpolates from the incoming density toward an air target, so its
// delta is unbounded without an input-density interval. It is nevertheless exactly
// inactive outside the geometric influence box; return zero there so an unrelated block
// does not inherit the interface's conservative FLT_MAX sentinel.
float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
{
const EVoxelOpEffect Effect = EffectOverBox(VoxelBox, Ctx);
return (Effect == EVoxelOpEffect::CarveOnly || Effect == EVoxelOpEffect::Both)
? FLT_MAX : 0.0f;
}
float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
{
return EffectOverBox(VoxelBox, Ctx) == EVoxelOpEffect::Both ? FLT_MAX : 0.0f;
}
const TCHAR* DebugName() const override { return TEXT("OriginSpineOp"); }
private:
@@ -1709,6 +1731,15 @@ namespace
return EVoxelOpEffect::FillOnly;
}
// A partial seal can raise an arbitrarily negative incoming density, so its fill delta is
// genuinely unbounded through this API. Outside both bands it is an exact identity.
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
{
return EffectOverBox(VoxelBox, Ctx) == EVoxelOpEffect::Identity ? 0.0f : FLT_MAX;
}
const TCHAR* DebugName() const override { return TEXT("BoundarySealOp"); }
private:
@@ -1768,6 +1799,13 @@ namespace
? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity;
}
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
{
return EffectOverBox(VoxelBox, Ctx) == EVoxelOpEffect::Identity ? 0.0f : FLT_MAX;
}
bool IsXYPure() const override { return true; }
const TCHAR* DebugName() const override { return TEXT("XYEdgeSealOp"); }
@@ -1827,6 +1865,21 @@ namespace
? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity;
}
// The current-density Lerp/Min passage carve has no finite delta bound for an arbitrary
// incoming density. Keep that honest where it can affect a box, but expose exact identity
// for the overwhelmingly common no-passage case.
float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
{
const EVoxelOpEffect Effect = EffectOverBox(VoxelBox, Ctx);
return (Effect == EVoxelOpEffect::CarveOnly || Effect == EVoxelOpEffect::Both)
? FLT_MAX : 0.0f;
}
float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const override
{
return EffectOverBox(VoxelBox, Ctx) == EVoxelOpEffect::Both ? FLT_MAX : 0.0f;
}
const TCHAR* DebugName() const override { return TEXT("PassageCarveOp"); }
private:
@@ -3486,6 +3539,93 @@ namespace
return EVoxelOpEffect::Identity;
}
// This operator publishes SDF and does not change Density. Reporting zero here is
// important: FLT_MAX means "an unknown density delta", whereas this source has no density
// delta at all. The SDF interval remains the source's separate box contract below.
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
/**
* State-aware detail gate support. A room terrain op can replace the strate-level
* activation/strength fields for the room selected by Eval. Until the cache tells us
* that the queried box contains no such override, a detail bound based on P alone would be
* unsound. Returning true on an invalid box state deliberately loses a skip rather than
* manufacturing one.
*/
bool HasRoomTerrainOverrideForLastBox() const
{
const FBoxState& B = BoxState();
if (!B.bValid) { return true; }
for (const FCachedRoom& Room : B.Cache.Rooms)
{
if (Room.RoomOp != nullptr) { return true; }
}
return false;
}
bool HasRoomColumnsForLastBox() const
{
const FBoxState& B = BoxState();
return !B.bValid || B.Cache.Columns.Num() > 0;
}
float RoomColumnFillSupremumForLastBox() const
{
const FBoxState& B = BoxState();
if (!B.bValid) { return FLT_MAX; }
float Supremum = 0.0f;
for (const FCachedColumn& Column : B.Cache.Columns)
{
if (!FMath::IsFinite(Column.BaseDensity) || Column.BaseDensity < 0.0f)
{
// Eval multiplies by BaseDensity. A negative authored value reverses the
// declared FillOnly direction, so the state-aware caller must fall back to
// Both/unknown rather than pretend this is a fill envelope.
return FLT_MAX;
}
const float Contribution = Column.BaseDensity * 1.5f;
if (!FMath::IsFinite(Contribution)
|| Supremum > FLT_MAX - Contribution)
{
return FLT_MAX;
}
Supremum += Contribution;
}
return Supremum;
}
float FloorBiasFillSupremumForLastBox() const
{
const FBoxState& B = BoxState();
if (!B.bValid) { return FLT_MAX; }
// Eval uses NormZ² only below the room centre. The largest value over the queried
// box is therefore the squared distance from the room centre to Box.Min.Z, divided by
// max(RadiusZ, 1)². Taking the maximum over cached rooms is conservative for whichever
// room wins the nearest-SDF query, and is finite without assuming a global room height.
float Supremum = 0.0f;
const float BoxMinZ = static_cast<float>(B.KeyBox.Min.Z);
if (!FMath::IsFinite(BoxMinZ)) { return FLT_MAX; }
for (const FCachedRoom& Room : B.Cache.Rooms)
{
const float CenterZ = static_cast<float>(Room.Center.Z);
const float RadiusZ = Room.RadiusZ;
if (!FMath::IsFinite(CenterZ) || !FMath::IsFinite(RadiusZ)
|| !FMath::IsFinite(P.FloorBias) || P.FloorBias < 0.0f)
{
return FLT_MAX;
}
const float Denominator = FMath::Max(RadiusZ, 1.0f);
const float Below = FMath::Max(CenterZ - BoxMinZ, 0.0f);
const float Normalized = Below / Denominator;
const float Contribution = Normalized * Normalized * P.FloorBias;
if (!FMath::IsFinite(Contribution)) { return FLT_MAX; }
Supremum = FMath::Max(Supremum, Contribution);
}
return Supremum;
}
/**
* Publish a conservative interval for this source alone.
*
@@ -4007,17 +4147,29 @@ namespace
{
// A detail op with no room source has the same no-op guard as Eval. An unknown interval is
// not a reason to claim Identity: uncertainty costs a skip, while a false skip is a hole.
return (Rooms == nullptr || VF_CaveBoxIsFar(H, SDFBlendRadius))
? EVoxelOpEffect::Identity : Intrinsic;
if (Rooms == nullptr || VF_CaveBoxIsFar(H, SDFBlendRadius))
{
return EVoxelOpEffect::Identity;
}
// A terrain op is allowed to replace the strate-level activation fields for the room that
// Eval selects. Its exact direction is not part of the generic cache proof, so preserve
// safety with Both whenever one is present. The common no-override TunnelNetwork path
// gets the real intrinsic direction and finite amplitude below.
return Rooms->HasRoomTerrainOverrideForLastBox()
? EVoxelOpEffect::Both : Intrinsic;
}
FORCEINLINE float VF_CaveDetailMax(const FRoomGraphSource* Rooms,
const FVoxelBoxHypotheses& H,
float SDFBlendRadius)
float SDFBlendRadius,
float IntrinsicMax)
{
// Per-room overrides can activate or enlarge a modifier even when the strate-level
// parameter is zero. Near/unknown therefore returns the safe default amplitude.
return (Rooms == nullptr || VF_CaveBoxIsFar(H, SDFBlendRadius)) ? 0.0f : FLT_MAX;
// parameter is zero. A valid cache with no override makes the strate-level supremum
// exact for these modifiers; an unknown/overridden cache keeps the old safe default.
if (Rooms == nullptr || VF_CaveBoxIsFar(H, SDFBlendRadius)) { return 0.0f; }
if (Rooms->HasRoomTerrainOverrideForLastBox()) { return FLT_MAX; }
return IntrinsicMax;
}
//=========================================================================
@@ -4197,11 +4349,11 @@ namespace
/**
* ✅ Borne consommée par le pliage numérique. `RoughNoise` et `FineNoise` respectent tous
* deux sont couverts par la borne prouvée `VF_PerlinAbsBound = 1.5`, mis à l'échelle par
* `VOXEL_NOISE_SCALE`, et `TotalRough = Rough·S + Fine·S·0.4` ⇒
* `|TotalRough| ≤ 2.1 · S · SCALE`. Le fade est
* dans `[0,1]`. Le clamp anti-remplissage ne fait que RÉDUIRE côté fill ; on ne s'appuie pas
* dessus (il ne s'applique que dans l'air certain), donc la borne fill reste la même.
* `FBM` est couvert par la borne prouvée `VF_PerlinAbsBound = 1.5`, mis à l'échelle par
* `VOXEL_NOISE_SCALE`. `Ridged` is in [-1,1] after its square/fold, while the unclamped
* cellular mapping needs its own finite envelope below. The two octaves contribute
* `|Rough| + 0.4·|Fine|`; the fade is in `[0,1]`. The anti-fill clamp only reduces the
* positive side, so the same envelope is valid in both directions.
*/
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); }
float MaxFillOverBox (const FBox&, const FVoxelOpContext&) const override { return MaxAmplitude(); }
@@ -4221,8 +4373,29 @@ namespace
/** La borne d'amplitude, en unités de densité. */
float MaxAmplitude() const
{
return (P.SurfaceRoughness > 0.0f)
? (2.1f * P.SurfaceRoughness * VOXEL_NOISE_SCALE) : 0.0f;
if (!(P.SurfaceRoughness > 0.0f)) { return 0.0f; }
if (!FMath::IsFinite(P.SurfaceRoughness)) { return FLT_MAX; }
float NoiseBound = VF_PerlinAbsBound;
switch (P.RoughnessNoiseType)
{
case EVoxelNoiseType::Ridged:
NoiseBound = 1.0f;
break;
case EVoxelNoiseType::Mixed:
NoiseBound = 0.5f * (VF_PerlinAbsBound + 1.0f);
break;
case EVoxelNoiseType::Cellular:
NoiseBound = VF_CellularAbsBound;
break;
case EVoxelNoiseType::FBM:
default:
NoiseBound = VF_PerlinAbsBound;
break;
}
const float Result = 1.4f * NoiseBound * P.SurfaceRoughness * VOXEL_NOISE_SCALE;
return FMath::IsFinite(Result) ? Result : FLT_MAX;
}
private:
@@ -4339,7 +4512,45 @@ namespace
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius,
EVoxelOpEffect::Both);
EffectOverBox(VoxelBox, Ctx));
}
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override
{
return MaxAmplitude();
}
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override
{
return MaxAmplitude();
}
float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius,
MaxCarveOverBox(VoxelBox, Ctx));
}
float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius,
MaxFillOverBox(VoxelBox, Ctx));
}
/**
* `TerracedZ` is one staircase step below or above `NoisedZ`; the modulo phase is in
* [0,1], so the safe supremum of `abs(TerracedZ - NoisedZ)` is one full step, not StepH/2.
* The noise displacement changes which phase is selected but cannot enlarge that interval.
*/
float MaxAmplitude() const
{
return (P.TerraceStepHeight > 0.0f
&& FMath::IsFinite(P.TerraceStepHeight)
&& FMath::IsFinite(P.TerraceHardness)
&& P.TerraceHardness >= 0.0f && P.TerraceHardness <= 1.0f)
? P.TerraceStepHeight : (P.TerraceStepHeight > 0.0f ? FLT_MAX : 0.0f);
}
private:
@@ -4407,7 +4618,7 @@ namespace
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius,
EVoxelOpEffect::CarveOnly);
EffectOverBox(VoxelBox, Ctx));
}
/** `LineValue = max(sin,0)³ ∈ [0,1]`, `Fade ∈ [0,1]` ⇒ retrait ≤ `LayerLineDepth`.
@@ -4417,18 +4628,25 @@ namespace
* réglée par le même correctif — AVANT que `ClassifyTile` ne consomme `ClassifyBox`. */
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override
{
return (P.LayerLineSpacing > 0.0f) ? FMath::Max(P.LayerLineDepth, 0.0f) : 0.0f;
return MaxAmplitude();
}
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius);
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxAmplitude());
}
float MaxFillOverBox(const FBox&, const FVoxelOpContext&,
const FVoxelBoxHypotheses&) const override { return 0.0f; }
float MaxAmplitude() const
{
if (!(P.LayerLineSpacing > 0.0f)) { return 0.0f; }
return FMath::IsFinite(P.LayerLineDepth)
? FMath::Max(P.LayerLineDepth, 0.0f) : FLT_MAX;
}
private:
FStrateGenerationParams P;
const FRoomGraphSource* Rooms; // NON possédant
@@ -4496,7 +4714,7 @@ namespace
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius,
EVoxelOpEffect::FillOnly);
EffectOverBox(VoxelBox, Ctx));
}
/** `RibValue = max(sin,0)² ∈ [0,1]`, `Fade ∈ [0,1]` ⇒ ajout ≤ `RibbingDepth`.
@@ -4504,7 +4722,7 @@ namespace
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override
{
return (P.RibbingSpacing > 0.0f) ? FMath::Max(P.RibbingDepth, 0.0f) : 0.0f;
return MaxAmplitude();
}
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&,
@@ -4512,7 +4730,14 @@ namespace
float MaxFillOverBox(const FBox&, const FVoxelOpContext&,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius);
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxAmplitude());
}
float MaxAmplitude() const
{
if (!(P.RibbingSpacing > 0.0f)) { return 0.0f; }
return FMath::IsFinite(P.RibbingDepth)
? FMath::Max(P.RibbingDepth, 0.0f) : FLT_MAX;
}
private:
@@ -4588,7 +4813,7 @@ namespace
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius,
EVoxelOpEffect::FillOnly);
EffectOverBox(VoxelBox, Ctx));
}
/** fBM ∈ [-1,1] × `VOXEL_NOISE_SCALE`, lobe positif seulement, `Fade ∈ [0,1]` ⇒ ajout
@@ -4596,9 +4821,7 @@ namespace
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override
{
return VOXEL_NOISE_SCALE * VF_PerlinAbsBound
* FMath::Max(P.OverhangDepth, 0.0f)
* FMath::Max(P.OverhangStrength, 0.0f);
return MaxAmplitude();
}
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&,
@@ -4606,7 +4829,20 @@ namespace
float MaxFillOverBox(const FBox&, const FVoxelOpContext&,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius);
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxAmplitude());
}
float MaxAmplitude() const
{
if (!(P.OverhangStrength > 0.0f && P.OverhangDepth > 0.0f)) { return 0.0f; }
if (!FMath::IsFinite(P.OverhangDepth)
|| !FMath::IsFinite(P.OverhangStrength))
{
return FLT_MAX;
}
const float Result = VOXEL_NOISE_SCALE * VF_PerlinAbsBound
* P.OverhangDepth * P.OverhangStrength;
return FMath::IsFinite(Result) ? Result : FLT_MAX;
}
private:
@@ -4689,7 +4925,41 @@ namespace
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius,
EVoxelOpEffect::Both);
EffectOverBox(VoxelBox, Ctx));
}
/** `|VertGrad| ≤ SCALE·1.5`, `|CaveSDF| < 8`, `Fade ≤ 1`, final multiplier `3`. */
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override
{
return MaxAmplitude();
}
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override
{
return MaxAmplitude();
}
float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius,
MaxCarveOverBox(VoxelBox, Ctx));
}
float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius,
MaxFillOverBox(VoxelBox, Ctx));
}
float MaxAmplitude() const
{
if (!(P.CliffStrength > 0.0f)) { return 0.0f; }
if (!FMath::IsFinite(P.CliffStrength)) { return FLT_MAX; }
const float Result = 8.0f * 3.0f * VOXEL_NOISE_SCALE
* VF_PerlinAbsBound * P.CliffStrength;
return FMath::IsFinite(Result) ? Result : FLT_MAX;
}
private:
@@ -4765,25 +5035,34 @@ namespace
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius,
EVoxelOpEffect::CarveOnly);
EffectOverBox(VoxelBox, Ctx));
}
/** `Cellular3D ∈ [-1,1]`, lobe positif seulement, `Fade ∈ [0,1]` ⇒ retrait ≤
* `ScallopStrength`. Même réserve « params de strate ». */
/** The implementation maps an unclamped F2-F1 distance to `2*d-1`; the proven finite
* envelope used here is `VF_CellularAbsBound`, not the approximate [-1,1] comment beside
* the noise function. The positive lobe and `Fade ∈ [0,1]` then bound the carve. */
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override
{
return VF_PerlinAbsBound * FMath::Max(P.ScallopStrength, 0.0f);
return MaxAmplitude();
}
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius);
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, MaxAmplitude());
}
float MaxFillOverBox(const FBox&, const FVoxelOpContext&,
const FVoxelBoxHypotheses&) const override { return 0.0f; }
float MaxAmplitude() const
{
if (!(P.ScallopStrength > 0.0f)) { return 0.0f; }
if (!FMath::IsFinite(P.ScallopStrength)) { return FLT_MAX; }
const float Result = VF_CellularAbsBound * P.ScallopStrength;
return FMath::IsFinite(Result) ? Result : FLT_MAX;
}
private:
FStrateGenerationParams P;
uint32 SeedU;
@@ -4863,14 +5142,49 @@ namespace
* consomme l'intervalle SDF publié par la source, comme les autres détails de salle. */
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
{
return (P.ArchDensity > 0.0f) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Identity;
if (!(P.ArchDensity > 0.0f)) { return EVoxelOpEffect::Identity; }
if (P.BaseDensity > 0.0f) { return EVoxelOpEffect::FillOnly; }
if (P.BaseDensity < 0.0f) { return EVoxelOpEffect::CarveOnly; }
return EVoxelOpEffect::Identity;
}
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius,
EVoxelOpEffect::FillOnly);
EffectOverBox(VoxelBox, Ctx));
}
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override
{
return P.BaseDensity < 0.0f ? MaxAmplitude() : 0.0f;
}
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override
{
return P.BaseDensity > 0.0f ? MaxAmplitude() : 0.0f;
}
float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius,
MaxCarveOverBox(VoxelBox, Ctx));
}
float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius,
MaxFillOverBox(VoxelBox, Ctx));
}
float MaxAmplitude() const
{
if (!(P.ArchDensity > 0.0f)) { return 0.0f; }
if (!FMath::IsFinite(P.BaseDensity)) { return FLT_MAX; }
const float Result = FMath::Abs(P.BaseDensity) * 3.0f * 1.5f;
return FMath::IsFinite(Result) ? Result : FLT_MAX;
}
private:
@@ -4935,8 +5249,8 @@ namespace
}
}
/** N'AJOUTE que du solide ⇒ `FillOnly`. On ne peut pas rendre `Identity` sans consulter le
* cache pour la boîte interrogée — même dette que le graphe de salles. */
/** The cached column list is the complete source of this op's work. A valid empty list is
* therefore an exact Identity result; an invalid box cache stays conservative. */
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
{
return EVoxelOpEffect::FillOnly;
@@ -4945,8 +5259,35 @@ namespace
EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius,
EVoxelOpEffect::FillOnly);
if (Rooms == nullptr || VF_CaveBoxIsFar(H, P.SDFBlendRadius))
{
return EVoxelOpEffect::Identity;
}
const float Bound = Rooms->RoomColumnFillSupremumForLastBox();
if (!(Bound > 0.0f)) { return EVoxelOpEffect::Identity; }
return FMath::IsFinite(Bound) ? EVoxelOpEffect::FillOnly : EVoxelOpEffect::Both;
}
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override
{
return FLT_MAX;
}
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&,
const FVoxelBoxHypotheses& H) const override
{
if (Rooms == nullptr || VF_CaveBoxIsFar(H, P.SDFBlendRadius)) { return 0.0f; }
const float Bound = Rooms->RoomColumnFillSupremumForLastBox();
return FMath::IsFinite(Bound) ? 0.0f : Bound;
}
float MaxFillOverBox(const FBox&, const FVoxelOpContext&,
const FVoxelBoxHypotheses& H) const override
{
if (Rooms == nullptr || VF_CaveBoxIsFar(H, P.SDFBlendRadius)) { return 0.0f; }
return Rooms->RoomColumnFillSupremumForLastBox();
}
private:
@@ -5053,7 +5394,38 @@ namespace
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius,
EVoxelOpEffect::CarveOnly);
EffectOverBox(VoxelBox, Ctx));
}
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override
{
return MaxAmplitude();
}
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override
{
return 0.0f;
}
float MaxCarveOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius,
MaxCarveOverBox(VoxelBox, Ctx));
}
float MaxFillOverBox(const FBox&, const FVoxelOpContext&,
const FVoxelBoxHypotheses&) const override
{
return 0.0f;
}
float MaxAmplitude() const
{
if (!(P.DomeDensity > 0.0f)) { return 0.0f; }
if (!FMath::IsFinite(P.BaseDensity)) { return FLT_MAX; }
const float Result = FMath::Abs(P.BaseDensity) * 2.0f * 1.5f;
return FMath::IsFinite(Result) ? Result : FLT_MAX;
}
private:
@@ -5142,7 +5514,35 @@ namespace
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius,
EVoxelOpEffect::FillOnly);
EffectOverBox(VoxelBox, Ctx));
}
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override
{
return MaxAmplitude();
}
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius, 0.0f);
}
float MaxFillOverBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailMax(Rooms, H, P.SDFBlendRadius,
MaxFillOverBox(VoxelBox, Ctx));
}
float MaxAmplitude() const
{
if (!(P.PinchDensity > 0.0f)) { return 0.0f; }
if (!FMath::IsFinite(P.BaseDensity)) { return FLT_MAX; }
const float Result = FMath::Abs(P.BaseDensity) * 3.0f * 1.5f;
return FMath::IsFinite(Result) ? Result : FLT_MAX;
}
private:
@@ -5217,7 +5617,28 @@ namespace
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveDetailEffect(Rooms, H, P.SDFBlendRadius,
EVoxelOpEffect::FillOnly);
EffectOverBox(VoxelBox, Ctx));
}
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&) const override { return 0.0f; }
float MaxFillOverBox(const FBox&, const FVoxelOpContext&) const override
{
return FMath::IsFinite(P.FloorBias) && P.FloorBias >= 0.0f
? P.FloorBias : (P.FloorBias < 0.0f ? 0.0f : FLT_MAX);
}
float MaxCarveOverBox(const FBox&, const FVoxelOpContext&,
const FVoxelBoxHypotheses& H) const override
{
return VF_CaveBoxIsFar(H, P.SDFBlendRadius) ? 0.0f : 0.0f;
}
float MaxFillOverBox(const FBox&, const FVoxelOpContext&,
const FVoxelBoxHypotheses& H) const override
{
if (Rooms == nullptr || VF_CaveBoxIsFar(H, P.SDFBlendRadius)) { return 0.0f; }
return Rooms->FloorBiasFillSupremumForLastBox();
}
private:
@@ -5359,7 +5780,8 @@ namespace
* réseau, c'est `MaxCarveOverBox` qui l'applique. */
float MaxCarveAmplitude() const
{
return (P.WormStrength > 0.0f && P.WormThreshold > 0.0f) ? P.WormStrength : 0.0f;
if (!(P.WormStrength > 0.0f && P.WormThreshold > 0.0f)) { return 0.0f; }
return FMath::IsFinite(P.WormStrength) ? P.WormStrength : FLT_MAX;
}
const TCHAR* DebugName() const override { return TEXT("WormFieldSource"); }
@@ -5582,6 +6004,167 @@ SIZE_T FVoxelOpStack::GetAllocatedSize() const
return Bytes;
}
void FVoxelOpStack::DiagnoseBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
int32 SampleStep, TArray<FOpBoxDiagnostic>& OutDiagnostics,
EVoxelTileClass* OutVerdict) const
{
OutDiagnostics.Reset();
OutDiagnostics.SetNum(Ops.Num());
if (OutVerdict != nullptr)
{
*OutVerdict = EVoxelTileClass::Mixed;
}
if (!VoxelBox.IsValid || SampleStep <= 0)
{
return;
}
FVoxelBoxHypotheses H;
for (int32 Index = 0; Index < Ops.Num(); ++Index)
{
const FOpEntry& Entry = Ops[Index];
FOpBoxDiagnostic& Diagnostic = OutDiagnostics[Index];
Diagnostic.Index = Index;
Diagnostic.Name = Entry.Op.IsValid() ? FString(Entry.Op->DebugName()) : TEXT("(null op)");
Diagnostic.bWritesSdf = (Entry.Writes & VoxelOpChannels::Sdf) != 0;
if (!Entry.Op.IsValid())
{
continue;
}
// This is intentionally the same order and branch structure as VF_FoldOp. The diagnostic
// must explain the production classifier, not an approximation of it.
const EVoxelTileClass Forced = Entry.Op->ClassifyBox(VoxelBox, Ctx);
Diagnostic.ForcedVerdict = Forced;
Diagnostic.bForced = Forced != EVoxelTileClass::Mixed;
if (Diagnostic.bForced)
{
Diagnostic.Effect = EVoxelOpEffect::Identity;
Diagnostic.ForcedMargin = Entry.Op->ForcedMarginOverBox(VoxelBox, Ctx);
VF_ForceHypotheses(H, Forced, Diagnostic.ForcedMargin);
if (Diagnostic.bWritesSdf)
{
Entry.Op->PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx);
Diagnostic.bHasSdfBound = H.Sdf.IsKnown();
if (Diagnostic.bHasSdfBound)
{
Diagnostic.SdfBoundMin = H.Sdf.Min;
Diagnostic.SdfBoundMax = H.Sdf.Max;
}
}
}
else
{
Diagnostic.Effect = Entry.Op->EffectOverBox(VoxelBox, Ctx, H);
Diagnostic.MaxCarve = Entry.Op->MaxCarveOverBox(VoxelBox, Ctx, H);
Diagnostic.MaxFill = Entry.Op->MaxFillOverBox(VoxelBox, Ctx, H);
VF_FoldEffect(H, Diagnostic.Effect, Diagnostic.MaxCarve, Diagnostic.MaxFill);
if (Diagnostic.bWritesSdf)
{
Entry.Op->PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx);
Diagnostic.bHasSdfBound = H.Sdf.IsKnown();
if (Diagnostic.bHasSdfBound)
{
Diagnostic.SdfBoundMin = H.Sdf.Min;
Diagnostic.SdfBoundMax = H.Sdf.Max;
}
}
}
}
if (OutVerdict != nullptr)
{
*OutVerdict = H.Resolve();
}
const int32 MinX = FMath::CeilToInt(static_cast<float>(VoxelBox.Min.X));
const int32 MinY = FMath::CeilToInt(static_cast<float>(VoxelBox.Min.Y));
const int32 MinZ = FMath::CeilToInt(static_cast<float>(VoxelBox.Min.Z));
const int32 MaxX = FMath::FloorToInt(static_cast<float>(VoxelBox.Max.X));
const int32 MaxY = FMath::FloorToInt(static_cast<float>(VoxelBox.Max.Y));
const int32 MaxZ = FMath::FloorToInt(static_cast<float>(VoxelBox.Max.Z));
if (MinX > MaxX || MinY > MaxY || MinZ > MaxZ)
{
return;
}
constexpr float BoundEpsilon = 1.0e-4f;
for (int32 Z = MinZ; Z <= MaxZ; )
{
for (int32 Y = MinY; Y <= MaxY; )
{
for (int32 X = MinX; X <= MaxX; )
{
FVoxelOpSample Sample;
for (int32 Index = 0; Index < Ops.Num(); ++Index)
{
const FOpEntry& Entry = Ops[Index];
FOpBoxDiagnostic& Diagnostic = OutDiagnostics[Index];
if (!Entry.Op.IsValid())
{
continue;
}
const float DensityBefore = Sample.Density;
Entry.Op->Eval(static_cast<float>(X), static_cast<float>(Y),
static_cast<float>(Z), Sample);
const float DensityDelta = Sample.Density - DensityBefore;
if (FMath::IsFinite(DensityDelta))
{
Diagnostic.bHasDensityDelta = true;
Diagnostic.ActualDensityDeltaMin =
FMath::Min(Diagnostic.ActualDensityDeltaMin, DensityDelta);
Diagnostic.ActualDensityDeltaMax =
FMath::Max(Diagnostic.ActualDensityDeltaMax, DensityDelta);
// A forced classifier is a stronger statement than a directional delta
// bound. Its density can legitimately replace an arbitrary incoming
// value (the constant source is the canonical example), so do not call
// that replacement a MaxFill/MaxCarve violation. ForcedMargin records the
// proof margin separately for the report.
if (!Diagnostic.bForced)
{
const float CarveEpsilon = BoundEpsilon
* FMath::Max(1.0f, FMath::Abs(Diagnostic.MaxCarve));
const float FillEpsilon = BoundEpsilon
* FMath::Max(1.0f, FMath::Abs(Diagnostic.MaxFill));
if (FMath::IsFinite(Diagnostic.MaxCarve)
&& DensityDelta < -Diagnostic.MaxCarve - CarveEpsilon)
{
Diagnostic.bCarveBoundViolated = true;
}
if (FMath::IsFinite(Diagnostic.MaxFill)
&& DensityDelta > Diagnostic.MaxFill + FillEpsilon)
{
Diagnostic.bFillBoundViolated = true;
}
}
}
if (Diagnostic.bWritesSdf && FMath::IsFinite(Sample.Sdf))
{
Diagnostic.bHasSdfValue = true;
Diagnostic.ActualSdfMin = FMath::Min(
Diagnostic.ActualSdfMin, Sample.Sdf);
Diagnostic.ActualSdfMax = FMath::Max(
Diagnostic.ActualSdfMax, Sample.Sdf);
}
++Diagnostic.SampleCount;
}
if (X > MaxX - SampleStep) { break; }
X += SampleStep;
}
if (Y > MaxY - SampleStep) { break; }
Y += SampleStep;
}
if (Z > MaxZ - SampleStep) { break; }
Z += SampleStep;
}
}
bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
{
if (OutError != nullptr) { OutError->Reset(); }
@@ -416,6 +416,9 @@ namespace VoxelDensityProfile
case ECounter::TunnelSupportFloorChecks:return TEXT("TunnelSupportFloorChecks");
case ECounter::PassageCandidates: return TEXT("PassageCandidates");
case ECounter::PassageEvaluated: return TEXT("PassageEvaluated");
case ECounter::MesherBlockTests: return TEXT("MesherBlockTests");
case ECounter::MesherBlockAllSolid:return TEXT("MesherBlockAllSolid");
case ECounter::MesherBlockAllAir: return TEXT("MesherBlockAllAir");
case ECounter::Count: break;
}
return TEXT("Unknown");
@@ -285,6 +285,58 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
}
MesherDensityGridTimer.End();
// Optional 8x8x8-cell block early-out. ClassifyTile uses the same one-point halo and the
// same cave/disturbance/diff-layer guards as production tile loading. It is deliberately
// performed after the density grid is complete: skipping only the Marching Cubes work keeps
// the grid, capture buffer, geometry, normals, and all density-side effects identical.
TArray<uint8> BlockSkip;
int32 BlocksPerAxis = 0;
if (bUseBlockEarlyOut)
{
constexpr int32 BlockCells = 8;
BlocksPerAxis = (CellsPerAxis + BlockCells - 1) / BlockCells;
BlockSkip.Init(0, BlocksPerAxis * BlocksPerAxis * BlocksPerAxis);
for (int32 BlockZ = 0; BlockZ < BlocksPerAxis; ++BlockZ)
{
const int32 BlockMinZ = BlockZ * BlockCells;
const int32 BlockMaxZ = FMath::Min(CellsPerAxis - 1,
BlockMinZ + BlockCells - 1);
if (BlockMaxZ < CzLo || BlockMinZ > CzHi)
{
continue;
}
for (int32 BlockY = 0; BlockY < BlocksPerAxis; ++BlockY)
{
for (int32 BlockX = 0; BlockX < BlocksPerAxis; ++BlockX)
{
const FIntVector BlockOrigin = OriginVoxels + FIntVector(
BlockX * BlockCells * Step,
BlockY * BlockCells * Step,
BlockMinZ * Step);
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::MesherBlockTests);
}
const EVoxelTileClass Verdict = Generator->ClassifyTile(
BlockOrigin, Step, BlockCells);
if (Verdict == EVoxelTileClass::AllSolid
|| Verdict == EVoxelTileClass::AllAir)
{
BlockSkip[((BlockZ * BlocksPerAxis) + BlockY) * BlocksPerAxis + BlockX] = 1;
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
Verdict == EVoxelTileClass::AllSolid
? VoxelDensityProfile::ECounter::MesherBlockAllSolid
: VoxelDensityProfile::ECounter::MesherBlockAllAir);
}
}
}
}
}
}
// ── CAPTURE-DURING-MESHING ──
// Si demandé et que la tuile est pleine résolution (CellsPerAxis==CHUNK_SIZE ⇒ Step==1<<Level,
// donc chaque point de grille = exactement une cellule du clipmap de densité), on recopie les
@@ -344,6 +396,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 cx = 0; cx < CellsPerAxis; cx++)
{
if (bUseBlockEarlyOut
&& BlockSkip[((cz / 8) * BlocksPerAxis + (cy / 8)) * BlocksPerAxis + (cx / 8)] != 0)
{
continue;
}
const bool bSampleCell = bProfileMesher
&& VoxelDensityProfile::ShouldSample(
VoxelDensityProfile::EBucket::MesherCellClassification);
+3 -60
View File
@@ -656,56 +656,6 @@ struct FCachedTunnel
float BoundRadiusSq; // Squared radius — if voxel is further, skip this tunnel
};
// A deterministic uniform-grid broad phase. Build() is called while a chunk cache is being
// assembled; after that the arrays are immutable and workers only read a bin range. Entries are
// supplied in candidate order, so each bin is already sorted by candidate ID and no runtime sort
// or allocation is needed. A failed/overflowing build leaves bValid=false and callers fall back
// to the exact legacy scan rather than risking a false negative.
struct FCaveSpatialIndexEntry
{
int32 CandidateIndex = INDEX_NONE;
FVector Min = FVector::ZeroVector;
FVector Max = FVector::ZeroVector;
};
struct FCaveSpatialIndexBin
{
int32 First = 0;
int32 Count = 0;
};
struct FCaveSpatialIndex
{
static constexpr int32 BinSizeVoxels = 16;
bool bValid = false;
int32 MinBinX = 0;
int32 MinBinY = 0;
int32 MinBinZ = 0;
int32 CellsX = 0;
int32 CellsY = 0;
int32 CellsZ = 0;
TArray<FCaveSpatialIndexBin> Bins;
TArray<int32> CandidateIndices;
void Reset();
void Build(const TArray<FCaveSpatialIndexEntry>& Entries);
// Returns false only when the index is invalid. A valid query outside its extent returns
// true with an empty range, which is the normal cheap reject for that sample.
bool GetBin(float X, float Y, float Z, int32& OutFirst, int32& OutCount) const;
// Build-time/column-query helper. Coordinates are already integer cell coordinates.
bool GetBinAt(int32 CellX, int32 CellY, int32 CellZ,
int32& OutFirst, int32& OutCount) const;
// A support-floor column is Z-dependent but not Z-search-dependent. Gather the unique
// tunnel IDs from every height bin crossing one XY cell once, then reuse the resulting floor
// intervals for all samples in that column.
template <typename AllocatorType>
bool GetColumnCandidates(float X, float Y, TArray<int32, AllocatorType>& OutCandidates) const;
};
struct FTunnelSupportFloorInterval
{
int32 TunnelIndex = INDEX_NONE;
@@ -806,13 +756,6 @@ struct FChunkSDFCache
TArray<FCachedChimney> Chimneys;
TArray<FCachedColumn> Columns;
// Immutable broad-phase indices. The world index is also the source for per-column support
// intervals; its Z bins are traversed only when a column is first built.
FCaveSpatialIndex RoomIndex;
FCaveSpatialIndex RoomFloorJoinIndex;
FCaveSpatialIndex TunnelIndex;
FCaveSpatialIndex TunnelWorldIndex;
// Sparse immutable support-column table. SupportColumnEntries is a dense
// integer-XY slot map whose values index only non-empty SupportColumns.
int32 SupportColumnMinX = 0;
@@ -822,9 +765,9 @@ struct FChunkSDFCache
TArray<int32> SupportColumnEntries;
TArray<FTunnelSupportFloorColumn> SupportColumns;
// Allocator-backed bytes owned by this cache, including the nested tunnel chains and index
// arrays. The returned value excludes sizeof(FChunkSDFCache) itself so callers can add the
// enclosing entry's inline storage exactly once.
// Allocator-backed bytes owned by this cache, including the nested tunnel chains. The returned
// value excludes sizeof(FChunkSDFCache) itself so callers can add the enclosing entry's inline
// storage exactly once.
SIZE_T GetAllocatedSize() const;
VoxelDensityProfile::FCacheMemoryBreakdown GetAllocatedSizeBreakdown() const;
};
@@ -231,6 +231,40 @@ public:
return H.Resolve();
}
/**
* Per-operator box-bound audit. This deliberately replays the same fold as ClassifyBox while
* also brute-forcing Eval on the queried lattice. It is report/test infrastructure only: it
* must never be used to make a generation decision.
*/
struct FOpBoxDiagnostic
{
int32 Index = INDEX_NONE;
FString Name;
EVoxelOpEffect Effect = EVoxelOpEffect::Identity;
EVoxelTileClass ForcedVerdict = EVoxelTileClass::Mixed;
bool bForced = false;
bool bWritesSdf = false;
float ForcedMargin = 0.0f;
float MaxCarve = 0.0f;
float MaxFill = 0.0f;
bool bHasDensityDelta = false;
float ActualDensityDeltaMin = FLT_MAX;
float ActualDensityDeltaMax = -FLT_MAX;
bool bHasSdfValue = false;
float ActualSdfMin = FLT_MAX;
float ActualSdfMax = -FLT_MAX;
bool bHasSdfBound = false;
float SdfBoundMin = FLT_MAX;
float SdfBoundMax = FLT_MAX;
bool bCarveBoundViolated = false;
bool bFillBoundViolated = false;
uint64 SampleCount = 0;
};
VOXELFORGE_API void DiagnoseBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx,
int32 SampleStep, TArray<FOpBoxDiagnostic>& OutDiagnostics,
EVoxelTileClass* OutVerdict = nullptr) const;
/** Nom lisible d'un opérateur, pour les rapports de test. Voir `IVoxelDensityOp::DebugName`. */
const TCHAR* GetOpDebugName(int32 Index) const
{
+4 -22
View File
@@ -100,6 +100,9 @@ namespace VoxelDensityProfile
TunnelSupportFloorChecks,
PassageCandidates,
PassageEvaluated,
MesherBlockTests,
MesherBlockAllSolid,
MesherBlockAllAir,
Count
};
@@ -122,25 +125,12 @@ namespace VoxelDensityProfile
uint64 SupportColumnEntriesBytes = 0;
uint64 SupportColumnsBytes = 0;
uint64 SupportColumnIntervalsBytes = 0;
uint64 RoomIndexBinsBytes = 0;
uint64 RoomIndexCandidatesBytes = 0;
uint64 RoomFloorJoinIndexBinsBytes = 0;
uint64 RoomFloorJoinIndexCandidatesBytes = 0;
uint64 TunnelIndexBinsBytes = 0;
uint64 TunnelIndexCandidatesBytes = 0;
uint64 TunnelWorldIndexBinsBytes = 0;
uint64 TunnelWorldIndexCandidatesBytes = 0;
uint64 TotalBytes() const
{
return SlotStorageBytes + OpStackBytes
+ RoomsBytes + RoomFloorJoinsBytes + TunnelsBytes + PitsBytes
+ ChimneysBytes + ColumnsBytes + SupportColumnEntriesBytes
+ SupportColumnsBytes + SupportColumnIntervalsBytes
+ RoomIndexBinsBytes + RoomIndexCandidatesBytes
+ RoomFloorJoinIndexBinsBytes + RoomFloorJoinIndexCandidatesBytes
+ TunnelIndexBinsBytes + TunnelIndexCandidatesBytes
+ TunnelWorldIndexBinsBytes + TunnelWorldIndexCandidatesBytes;
+ SupportColumnsBytes + SupportColumnIntervalsBytes;
}
uint64 DynamicBytes() const
@@ -161,14 +151,6 @@ namespace VoxelDensityProfile
SupportColumnEntriesBytes += Other.SupportColumnEntriesBytes;
SupportColumnsBytes += Other.SupportColumnsBytes;
SupportColumnIntervalsBytes += Other.SupportColumnIntervalsBytes;
RoomIndexBinsBytes += Other.RoomIndexBinsBytes;
RoomIndexCandidatesBytes += Other.RoomIndexCandidatesBytes;
RoomFloorJoinIndexBinsBytes += Other.RoomFloorJoinIndexBinsBytes;
RoomFloorJoinIndexCandidatesBytes += Other.RoomFloorJoinIndexCandidatesBytes;
TunnelIndexBinsBytes += Other.TunnelIndexBinsBytes;
TunnelIndexCandidatesBytes += Other.TunnelIndexCandidatesBytes;
TunnelWorldIndexBinsBytes += Other.TunnelWorldIndexBinsBytes;
TunnelWorldIndexCandidatesBytes += Other.TunnelWorldIndexCandidatesBytes;
return *this;
}
};
@@ -147,6 +147,11 @@ public:
// l'écart vers un voisin un niveau plus grossier (cellule 2×). Monter si des fissures persistent.
float SkirtCells = 2.0f;
// Optional conservative block classifier. When enabled, 8x8x8 cell regions proven uniformly
// solid or air by UVoxelGenerator::ClassifyTile skip the marching-cubes cell loop. The density
// grid is still produced in full, so this switch cannot change samples or geometry.
bool bUseBlockEarlyOut = false;
// T2.b — LOD-aware octave reduction (opt-in, copied from UVoxelSettings::LODOctaveDrop).
// Octaves dropped from per-voxel volumetric noise PER Step doubling: a tile at Step=S
// drops LODOctaveDrop * log2(S) octaves (see VoxelGenLOD in VoxelGenerator.h).
@@ -25,7 +25,9 @@
#include "UObject/Package.h"
#include "UObject/StrongObjectPtr.h"
#include "UObject/UObjectGlobals.h"
#include "VoxelCaveMorphology.h"
#include "VoxelDiffLayer.h"
#include "VoxelDensityOpStack.h"
#include "VoxelDensityProfile.h"
#include "VoxelGenerator.h"
#include "VoxelMarchingCubesMesher.h"
@@ -202,6 +204,7 @@ struct FExploreArguments
bool bProfileDensity = false;
bool bProfileDensityFull = false;
bool bProfileLod = false;
bool bOpBounds = false;
bool bFailureFocusRender = false;
FString OutDirectory;
@@ -214,6 +217,7 @@ struct FExploreArguments
// The canonical export owns one immutable lattice for the whole run. Batch cases can pass
// density_grid_reuse=false to produce a matched wall-clock control.
bool bReuseDensityGrid = true;
bool bBlockEarlyOut = false;
int32 MeshMinBatchSize = 1;
int32 MaxWalkCells = DefaultMaxWalkCells;
float BudgetMinutes = DefaultBudgetMinutes;
@@ -226,6 +230,7 @@ struct FExploreArguments
if (bRender) Result += TEXT("render,");
if (bWalk) Result += TEXT("walk,");
if (bExport) Result += TEXT("export,");
if (bOpBounds) Result += TEXT("opbounds,");
if (Result.EndsWith(TEXT(",")))
{
Result.LeftChopInline(1);
@@ -265,6 +270,7 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
OutArguments.bProfileDensityFull = FParse::Param(*Params, TEXT("profiledensityfull"));
OutArguments.bProfileDensity |= OutArguments.bProfileDensityFull;
OutArguments.bProfileLod = FParse::Param(*Params, TEXT("profilelod"));
OutArguments.bOpBounds = FParse::Param(*Params, TEXT("opbounds"));
FParse::Value(*Params, TEXT("out="), OutText);
FParse::Value(*Params, TEXT("renderwidth="), OutArguments.RenderWidth);
FParse::Value(*Params, TEXT("renderheight="), OutArguments.RenderHeight);
@@ -275,6 +281,9 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
int32 DensityGridReuse = OutArguments.bReuseDensityGrid ? 1 : 0;
FParse::Value(*Params, TEXT("densitygridreuse="), DensityGridReuse);
OutArguments.bReuseDensityGrid = DensityGridReuse != 0;
int32 BlockEarlyOut = OutArguments.bBlockEarlyOut ? 1 : 0;
FParse::Value(*Params, TEXT("blockearlyout="), BlockEarlyOut);
OutArguments.bBlockEarlyOut = BlockEarlyOut != 0;
FParse::Value(*Params, TEXT("meshminbatch="), OutArguments.MeshMinBatchSize);
OutArguments.MeshMinBatchSize = FMath::Clamp(OutArguments.MeshMinBatchSize, 1, 64);
int32 Lod = 0;
@@ -322,6 +331,10 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
OutArguments.bWalk = true;
OutArguments.bExport = true;
}
else if (Mode == TEXT("opbounds"))
{
OutArguments.bOpBounds = true;
}
else if (Mode == TEXT("render"))
{
OutArguments.bRender = true;
@@ -337,11 +350,12 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
else
{
OutError = FString::Printf(
TEXT("Unknown mode '%s'. Expected render, walk, export, or all."), *Mode);
TEXT("Unknown mode '%s'. Expected render, walk, export, opbounds, or all."), *Mode);
return false;
}
}
if (!OutArguments.bRender && !OutArguments.bWalk && !OutArguments.bExport)
if (!OutArguments.bRender && !OutArguments.bWalk && !OutArguments.bExport
&& !OutArguments.bOpBounds)
{
OutError = TEXT("At least one mode must be selected.");
return false;
@@ -717,6 +731,7 @@ struct FExploreWorld
Mesher = TStrongObjectPtr<UVoxelMarchingCubesMesher>(
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
Mesher->SetGenerator(Generator.Get());
Mesher->bUseBlockEarlyOut = Arguments.bBlockEarlyOut;
if (Settings->WorldRadiusVoxels != 0.0f
|| Generator->WorldRadiusVoxels != 0.0f
@@ -3861,26 +3876,31 @@ FString BuildExploreJson(
if (Arguments.bRender) Writer->WriteValue(TEXT("render"));
if (Arguments.bWalk) Writer->WriteValue(TEXT("walk"));
if (Arguments.bExport) Writer->WriteValue(TEXT("export"));
if (Arguments.bOpBounds) Writer->WriteValue(TEXT("opbounds"));
Writer->WriteArrayEnd();
Writer->WriteValue(TEXT("failure_focus_render"), Arguments.bFailureFocusRender);
Writer->WriteValue(TEXT("render_step_voxels"), static_cast<double>(Arguments.RenderStepVoxels));
Writer->WriteValue(TEXT("export_size"), Arguments.ExportSize);
Writer->WriteValue(TEXT("export_step"), Arguments.ExportStep);
Writer->WriteValue(TEXT("density_grid_reuse"), Arguments.bReuseDensityGrid);
Writer->WriteValue(TEXT("block_early_out"), Arguments.bBlockEarlyOut);
Writer->WriteValue(TEXT("mesh_min_batch_size"), Arguments.MeshMinBatchSize);
Writer->WriteValue(TEXT("profile_density"), Arguments.bProfileDensity);
Writer->WriteValue(TEXT("profile_density_full"), Arguments.bProfileDensityFull);
Writer->WriteValue(TEXT("profile_lod"), Arguments.bProfileLod);
Writer->WriteValue(TEXT("op_bounds"), Arguments.bOpBounds);
Writer->WriteValue(TEXT("budget_minutes"), static_cast<double>(Arguments.BudgetMinutes));
Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory);
Writer->WriteValue(TEXT("canonical_invocation"), FString::Printf(
TEXT("UnrealEditor-Cmd VoxelM.uproject -run=VoxelForgeExplore -seed=%d -archetype=%s "
"-slot=%d -opstack=%d -modes=%s -out=<ABSOLUTE_PLUGIN_SAVED_PATH>"),
"-slot=%d -opstack=%d -modes=%s -blockearlyout=%d "
"-out=<ABSOLUTE_PLUGIN_SAVED_PATH>"),
Arguments.Seed,
ArchetypeName(Arguments.Archetype),
Arguments.Slot,
Arguments.bUseOperatorStack ? 1 : 0,
*Arguments.CanonicalModes()));
*Arguments.CanonicalModes(),
Arguments.bBlockEarlyOut ? 1 : 0));
Writer->WriteObjectEnd();
Writer->WriteObjectStart(TEXT("summary"));
@@ -4319,14 +4339,6 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("support_column_entries_bytes"), static_cast<int64>(Breakdown.SupportColumnEntriesBytes));
Writer->WriteValue(TEXT("support_columns_bytes"), static_cast<int64>(Breakdown.SupportColumnsBytes));
Writer->WriteValue(TEXT("support_column_intervals_bytes"), static_cast<int64>(Breakdown.SupportColumnIntervalsBytes));
Writer->WriteValue(TEXT("room_index_bins_bytes"), static_cast<int64>(Breakdown.RoomIndexBinsBytes));
Writer->WriteValue(TEXT("room_index_candidates_bytes"), static_cast<int64>(Breakdown.RoomIndexCandidatesBytes));
Writer->WriteValue(TEXT("room_floor_join_index_bins_bytes"), static_cast<int64>(Breakdown.RoomFloorJoinIndexBinsBytes));
Writer->WriteValue(TEXT("room_floor_join_index_candidates_bytes"), static_cast<int64>(Breakdown.RoomFloorJoinIndexCandidatesBytes));
Writer->WriteValue(TEXT("tunnel_index_bins_bytes"), static_cast<int64>(Breakdown.TunnelIndexBinsBytes));
Writer->WriteValue(TEXT("tunnel_index_candidates_bytes"), static_cast<int64>(Breakdown.TunnelIndexCandidatesBytes));
Writer->WriteValue(TEXT("tunnel_world_index_bins_bytes"), static_cast<int64>(Breakdown.TunnelWorldIndexBinsBytes));
Writer->WriteValue(TEXT("tunnel_world_index_candidates_bytes"), static_cast<int64>(Breakdown.TunnelWorldIndexCandidatesBytes));
Writer->WriteValue(TEXT("dynamic_bytes"), static_cast<int64>(Breakdown.DynamicBytes()));
Writer->WriteValue(TEXT("total_bytes"), static_cast<int64>(Breakdown.TotalBytes()));
Writer->WriteObjectEnd();
@@ -4398,6 +4410,513 @@ FString BuildExploreJson(
return Writer->Close() ? Json : FString();
}
const TCHAR* ExploreEffectName(EVoxelOpEffect Effect)
{
switch (Effect)
{
case EVoxelOpEffect::Identity: return TEXT("Identity");
case EVoxelOpEffect::CarveOnly: return TEXT("CarveOnly");
case EVoxelOpEffect::FillOnly: return TEXT("FillOnly");
case EVoxelOpEffect::Both: return TEXT("Both");
default: return TEXT("Unknown");
}
}
const TCHAR* ExploreTileClassName(EVoxelTileClass Class)
{
switch (Class)
{
case EVoxelTileClass::AllSolid: return TEXT("AllSolid");
case EVoxelTileClass::AllAir: return TEXT("AllAir");
case EVoxelTileClass::Mixed: return TEXT("Mixed");
default: return TEXT("Unknown");
}
}
// Density-bound APIs use FLT_MAX as the documented "unknown/unbounded" sentinel. It is finite
// according to IEEE-754, so FMath::IsFinite alone would misreport the very failure this audit is
// intended to expose as a real numeric envelope.
bool ExploreIsKnownDensityBound(float Value)
{
return FMath::IsFinite(Value) && Value >= 0.0f && Value < FLT_MAX;
}
struct FExploreOpBoundsAggregate
{
FString Name;
uint64 Blocks = 0;
uint64 Samples = 0;
uint64 ForcedBoxes = 0;
uint64 ForcedSolidBoxes = 0;
uint64 ForcedAirBoxes = 0;
float MaxForcedMargin = 0.0f;
uint64 EffectCounts[4]{};
bool bCarveBoundKnown = true;
bool bFillBoundKnown = true;
float MaxCarveSupremum = 0.0f;
float MaxFillSupremum = 0.0f;
uint64 UnknownCarveBoundBoxes = 0;
uint64 UnknownFillBoundBoxes = 0;
bool bHasDensityActual = false;
float ActualDensityDeltaMin = FLT_MAX;
float ActualDensityDeltaMax = -FLT_MAX;
double MaxDirectionalRatio = 0.0;
double MaxConservativeRatio = 0.0;
bool bRatioFinite = true;
uint64 CarveBoundViolations = 0;
uint64 FillBoundViolations = 0;
bool bSdfBoundKnown = true;
bool bHasSdfBound = false;
float SdfBoundMin = FLT_MAX;
float SdfBoundMax = -FLT_MAX;
bool bHasSdfActual = false;
float ActualSdfMin = FLT_MAX;
float ActualSdfMax = -FLT_MAX;
uint64 SdfBoundViolations = 0;
};
int32 ExploreEffectIndex(EVoxelOpEffect Effect)
{
switch (Effect)
{
case EVoxelOpEffect::Identity: return 0;
case EVoxelOpEffect::CarveOnly: return 1;
case EVoxelOpEffect::FillOnly: return 2;
case EVoxelOpEffect::Both: return 3;
default: return 0;
}
}
void AccumulateExploreOpDiagnostic(
const FVoxelOpStack::FOpBoxDiagnostic& Diagnostic,
FExploreOpBoundsAggregate& Aggregate)
{
Aggregate.Name = Diagnostic.Name;
++Aggregate.Blocks;
Aggregate.Samples += Diagnostic.SampleCount;
++Aggregate.EffectCounts[ExploreEffectIndex(Diagnostic.Effect)];
if (Diagnostic.bForced)
{
++Aggregate.ForcedBoxes;
if (FMath::IsFinite(Diagnostic.ForcedMargin))
{
Aggregate.MaxForcedMargin = FMath::Max(
Aggregate.MaxForcedMargin, Diagnostic.ForcedMargin);
}
if (Diagnostic.ForcedVerdict == EVoxelTileClass::AllSolid)
{
++Aggregate.ForcedSolidBoxes;
}
else if (Diagnostic.ForcedVerdict == EVoxelTileClass::AllAir)
{
++Aggregate.ForcedAirBoxes;
}
}
else
{
if (!ExploreIsKnownDensityBound(Diagnostic.MaxCarve))
{
Aggregate.bCarveBoundKnown = false;
++Aggregate.UnknownCarveBoundBoxes;
if (Diagnostic.Effect == EVoxelOpEffect::CarveOnly
|| Diagnostic.Effect == EVoxelOpEffect::Both)
{
Aggregate.bRatioFinite = false;
}
}
else
{
Aggregate.MaxCarveSupremum = FMath::Max(
Aggregate.MaxCarveSupremum, Diagnostic.MaxCarve);
}
if (!ExploreIsKnownDensityBound(Diagnostic.MaxFill))
{
Aggregate.bFillBoundKnown = false;
++Aggregate.UnknownFillBoundBoxes;
if (Diagnostic.Effect == EVoxelOpEffect::FillOnly
|| Diagnostic.Effect == EVoxelOpEffect::Both)
{
Aggregate.bRatioFinite = false;
}
}
else
{
Aggregate.MaxFillSupremum = FMath::Max(
Aggregate.MaxFillSupremum, Diagnostic.MaxFill);
}
}
if (Diagnostic.bCarveBoundViolated) { ++Aggregate.CarveBoundViolations; }
if (Diagnostic.bFillBoundViolated) { ++Aggregate.FillBoundViolations; }
if (Diagnostic.bHasDensityDelta)
{
Aggregate.bHasDensityActual = true;
Aggregate.ActualDensityDeltaMin = FMath::Min(
Aggregate.ActualDensityDeltaMin, Diagnostic.ActualDensityDeltaMin);
Aggregate.ActualDensityDeltaMax = FMath::Max(
Aggregate.ActualDensityDeltaMax, Diagnostic.ActualDensityDeltaMax);
if (!Diagnostic.bForced)
{
const double ActualCarve = FMath::Max(
0.0, -static_cast<double>(Diagnostic.ActualDensityDeltaMin));
const double ActualFill = FMath::Max(
0.0, static_cast<double>(Diagnostic.ActualDensityDeltaMax));
double Ratio = 0.0;
if (ExploreIsKnownDensityBound(Diagnostic.MaxCarve)
&& Diagnostic.MaxCarve > 0.0f)
{
Ratio = FMath::Max(Ratio, ActualCarve / Diagnostic.MaxCarve);
if (ActualCarve > 1.0e-4)
{
Aggregate.MaxConservativeRatio = FMath::Max(
Aggregate.MaxConservativeRatio,
static_cast<double>(Diagnostic.MaxCarve) / ActualCarve);
}
}
else if (ActualCarve > 1.0e-4)
{
Aggregate.bRatioFinite = false;
}
if (ExploreIsKnownDensityBound(Diagnostic.MaxFill)
&& Diagnostic.MaxFill > 0.0f)
{
Ratio = FMath::Max(Ratio, ActualFill / Diagnostic.MaxFill);
if (ActualFill > 1.0e-4)
{
Aggregate.MaxConservativeRatio = FMath::Max(
Aggregate.MaxConservativeRatio,
static_cast<double>(Diagnostic.MaxFill) / ActualFill);
}
}
else if (ActualFill > 1.0e-4)
{
Aggregate.bRatioFinite = false;
}
if (FMath::IsFinite(Ratio))
{
Aggregate.MaxDirectionalRatio = FMath::Max(
Aggregate.MaxDirectionalRatio, Ratio);
}
else
{
Aggregate.bRatioFinite = false;
}
}
}
if (Diagnostic.bHasSdfBound)
{
Aggregate.bHasSdfBound = true;
Aggregate.SdfBoundMin = FMath::Min(
Aggregate.SdfBoundMin, Diagnostic.SdfBoundMin);
Aggregate.SdfBoundMax = FMath::Max(
Aggregate.SdfBoundMax, Diagnostic.SdfBoundMax);
if (Diagnostic.bHasSdfValue)
{
const float Epsilon = 1.0e-4f * FMath::Max(
1.0f, FMath::Max(FMath::Abs(Diagnostic.SdfBoundMin),
FMath::Abs(Diagnostic.SdfBoundMax)));
if (Diagnostic.ActualSdfMin < Diagnostic.SdfBoundMin - Epsilon
|| Diagnostic.ActualSdfMax > Diagnostic.SdfBoundMax + Epsilon)
{
++Aggregate.SdfBoundViolations;
}
}
}
else if (Diagnostic.bWritesSdf)
{
Aggregate.bSdfBoundKnown = false;
}
if (Diagnostic.bHasSdfValue)
{
Aggregate.bHasSdfActual = true;
Aggregate.ActualSdfMin = FMath::Min(
Aggregate.ActualSdfMin, Diagnostic.ActualSdfMin);
Aggregate.ActualSdfMax = FMath::Max(
Aggregate.ActualSdfMax, Diagnostic.ActualSdfMax);
}
}
bool WriteExploreOpBoundsReport(
const FExploreArguments& Arguments,
const FExploreWorld& World,
int32 BlocksTested,
int32 RandomBlocksTested,
int32 AllSolidBlocks,
int32 AllAirBlocks,
int32 MixedBlocks,
const TArray<int32>& StackSolidKillerCounts,
const TArray<int32>& StackAirKillerCounts,
int32 StackVerdictMismatches,
const TArray<FExploreOpBoundsAggregate>& Aggregates,
FString& OutError)
{
FString Json;
TSharedRef<FExploreJsonWriter> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
Writer->WriteObjectStart();
Writer->WriteValue(TEXT("schema_version"), 1);
Writer->WriteValue(TEXT("tool"), TEXT("VoxelForgeExplore"));
Writer->WriteValue(TEXT("purpose"), TEXT(
"Conservative EffectOverBox/ClassifyBox bounds compared with brute-force Eval over the same "
"one-voxel-halo domain of an 8-cell block."));
Writer->WriteValue(TEXT("seed"), Arguments.Seed);
Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype));
Writer->WriteValue(TEXT("slot"), Arguments.Slot);
Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack);
Writer->WriteValue(TEXT("sample_step"), 1);
Writer->WriteValue(TEXT("block_cells"), 8);
Writer->WriteValue(TEXT("classifier_blocks_tested"), BlocksTested);
Writer->WriteValue(TEXT("classifier_all_solid_blocks"), AllSolidBlocks);
Writer->WriteValue(TEXT("classifier_all_air_blocks"), AllAirBlocks);
Writer->WriteValue(TEXT("classifier_mixed_blocks"), MixedBlocks);
Writer->WriteValue(TEXT("classifier_skip_rate"), BlocksTested > 0
? static_cast<double>(AllSolidBlocks + AllAirBlocks) / BlocksTested : 0.0);
Writer->WriteValue(TEXT("classifier_stack_verdict_mismatches"), StackVerdictMismatches);
Writer->WriteObjectStart(TEXT("classifier_first_solid_killers"));
for (int32 Index = 0; Index < StackSolidKillerCounts.Num(); ++Index)
{
if (StackSolidKillerCounts[Index] <= 0) { continue; }
Writer->WriteValue(
FString::Printf(TEXT("op_%d"), Index), StackSolidKillerCounts[Index]);
}
Writer->WriteObjectEnd();
Writer->WriteObjectStart(TEXT("classifier_first_air_killers"));
for (int32 Index = 0; Index < StackAirKillerCounts.Num(); ++Index)
{
if (StackAirKillerCounts[Index] <= 0) { continue; }
Writer->WriteValue(
FString::Printf(TEXT("op_%d"), Index), StackAirKillerCounts[Index]);
}
Writer->WriteObjectEnd();
Writer->WriteValue(TEXT("random_blocks_tested"), RandomBlocksTested);
Writer->WriteValue(TEXT("bruteforce_block_index_min"), 1);
Writer->WriteValue(TEXT("bruteforce_block_index_max_inclusive"), 14);
Writer->WriteValue(TEXT("bounds_validated"), true);
Writer->WriteValue(TEXT("target_bottom_world_z"), World.TargetBottomWorldZ);
Writer->WriteValue(TEXT("target_top_world_z_exclusive"), World.TargetTopWorldZ);
Writer->WriteArrayStart(TEXT("operators"));
for (const FExploreOpBoundsAggregate& Aggregate : Aggregates)
{
Writer->WriteObjectStart();
Writer->WriteValue(TEXT("name"), Aggregate.Name);
Writer->WriteValue(TEXT("blocks"), static_cast<int64>(Aggregate.Blocks));
Writer->WriteValue(TEXT("sample_points"), static_cast<int64>(Aggregate.Samples));
Writer->WriteValue(TEXT("forced_boxes"), static_cast<int64>(Aggregate.ForcedBoxes));
Writer->WriteValue(TEXT("forced_all_solid_boxes"), static_cast<int64>(Aggregate.ForcedSolidBoxes));
Writer->WriteValue(TEXT("forced_all_air_boxes"), static_cast<int64>(Aggregate.ForcedAirBoxes));
Writer->WriteValue(TEXT("forced_margin_supremum"), static_cast<double>(Aggregate.MaxForcedMargin));
Writer->WriteObjectStart(TEXT("effect_counts"));
Writer->WriteValue(TEXT("Identity"), static_cast<int64>(Aggregate.EffectCounts[0]));
Writer->WriteValue(TEXT("CarveOnly"), static_cast<int64>(Aggregate.EffectCounts[1]));
Writer->WriteValue(TEXT("FillOnly"), static_cast<int64>(Aggregate.EffectCounts[2]));
Writer->WriteValue(TEXT("Both"), static_cast<int64>(Aggregate.EffectCounts[3]));
Writer->WriteObjectEnd();
Writer->WriteValue(TEXT("carve_bound_known"), Aggregate.bCarveBoundKnown);
Writer->WriteValue(TEXT("unknown_carve_bound_boxes"),
static_cast<int64>(Aggregate.UnknownCarveBoundBoxes));
Writer->WriteValue(TEXT("carve_bound_supremum"), static_cast<double>(
Aggregate.bCarveBoundKnown ? Aggregate.MaxCarveSupremum : 0.0f));
Writer->WriteValue(TEXT("fill_bound_known"), Aggregate.bFillBoundKnown);
Writer->WriteValue(TEXT("unknown_fill_bound_boxes"),
static_cast<int64>(Aggregate.UnknownFillBoundBoxes));
Writer->WriteValue(TEXT("fill_bound_supremum"), static_cast<double>(
Aggregate.bFillBoundKnown ? Aggregate.MaxFillSupremum : 0.0f));
Writer->WriteValue(TEXT("actual_density_delta_min"), static_cast<double>(
Aggregate.bHasDensityActual ? Aggregate.ActualDensityDeltaMin : 0.0f));
Writer->WriteValue(TEXT("actual_density_delta_max"), static_cast<double>(
Aggregate.bHasDensityActual ? Aggregate.ActualDensityDeltaMax : 0.0f));
Writer->WriteValue(TEXT("actual_carve_supremum"), Aggregate.bHasDensityActual
? static_cast<double>(FMath::Max(0.0f, -Aggregate.ActualDensityDeltaMin)) : 0.0);
Writer->WriteValue(TEXT("actual_fill_supremum"), Aggregate.bHasDensityActual
? static_cast<double>(FMath::Max(0.0f, Aggregate.ActualDensityDeltaMax)) : 0.0);
Writer->WriteValue(TEXT("directional_ratio_known"), Aggregate.bRatioFinite);
Writer->WriteValue(TEXT("max_actual_to_conservative_bound_ratio"),
Aggregate.bRatioFinite ? Aggregate.MaxDirectionalRatio : 0.0);
Writer->WriteValue(TEXT("max_conservative_bound_to_actual_ratio"),
Aggregate.bRatioFinite ? Aggregate.MaxConservativeRatio : 0.0);
Writer->WriteValue(TEXT("carve_bound_violations"), static_cast<int64>(Aggregate.CarveBoundViolations));
Writer->WriteValue(TEXT("fill_bound_violations"), static_cast<int64>(Aggregate.FillBoundViolations));
Writer->WriteValue(TEXT("sdf_bound_known"), Aggregate.bSdfBoundKnown);
Writer->WriteValue(TEXT("sdf_bound_min"), static_cast<double>(
Aggregate.bHasSdfBound ? Aggregate.SdfBoundMin : 0.0f));
Writer->WriteValue(TEXT("sdf_bound_max"), static_cast<double>(
Aggregate.bHasSdfBound ? Aggregate.SdfBoundMax : 0.0f));
Writer->WriteValue(TEXT("actual_sdf_min"), static_cast<double>(
Aggregate.bHasSdfActual ? Aggregate.ActualSdfMin : 0.0f));
Writer->WriteValue(TEXT("actual_sdf_max"), static_cast<double>(
Aggregate.bHasSdfActual ? Aggregate.ActualSdfMax : 0.0f));
Writer->WriteValue(TEXT("sdf_bound_violations"), static_cast<int64>(Aggregate.SdfBoundViolations));
Writer->WriteObjectEnd();
}
Writer->WriteArrayEnd();
Writer->WriteObjectEnd();
if (!Writer->Close() || Json.IsEmpty()
|| !FFileHelper::SaveStringToFile(
Json,
*FPaths::Combine(Arguments.OutDirectory, TEXT("op_bounds.json")),
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
OutError = TEXT("Could not write op_bounds.json.");
return false;
}
OutError.Reset();
return true;
}
bool RunOpBounds(
const FExploreArguments& Arguments,
FExploreWorld& World,
FExploreBudget& Budget,
FString& OutError)
{
if (Arguments.Archetype != ECaveGeneratorType::TunnelNetwork
|| !Arguments.bUseOperatorStack)
{
OutError = TEXT("opbounds requires archetype=TunnelNetwork and opstack=1.");
return false;
}
const FIntVector TargetChunk(0, 0, World.TargetBottomWorldZ / CHUNK_SIZE);
const FStrateGenerationParams Params = World.Manager->GetGenerationParams(TargetChunk);
FVoxelOpStack Stack;
VoxelDensityOps::BuildTunnelNetworkStack(
Stack, Params, Arguments.Seed, World.Settings->OriginSpineRadius,
World.Manager.Get());
FVoxelOpContext Context;
Context.ChunkCoord = TargetChunk;
Context.Step = 1;
Context.Seed = static_cast<uint32>(Arguments.Seed);
Context.LayoutVersion = World.Manager->GetLayoutVersion();
Context.WorldRadiusVoxels = World.Settings->WorldRadiusVoxels;
Context.EdgeSealThickness = World.Settings->EdgeSealThickness;
Context.StrateTopWorldZ = World.TargetTopWorldZ;
Context.StrateBottomWorldZ = World.TargetBottomWorldZ;
Stack.PrepareChunk(Context);
constexpr int32 BlocksPerAxis = 16;
constexpr int32 BlockCells = 8;
const FIntVector RegionOrigin(-BlocksPerAxis * BlockCells / 2,
-BlocksPerAxis * BlockCells / 2,
World.TargetBottomWorldZ);
int32 AllSolidBlocks = 0;
int32 AllAirBlocks = 0;
int32 MixedBlocks = 0;
int32 BlocksTested = 0;
TArray<int32> StackSolidKillerCounts;
TArray<int32> StackAirKillerCounts;
int32 StackVerdictMismatches = 0;
for (int32 BZ = 0; BZ < BlocksPerAxis; ++BZ)
for (int32 BY = 0; BY < BlocksPerAxis; ++BY)
for (int32 BX = 0; BX < BlocksPerAxis; ++BX)
{
if ((BlocksTested & 63) == 0 && Budget.ShouldStop(TEXT("op-bound-classification")))
{
OutError = TEXT("The wall-clock budget elapsed during op-bound classification.");
return false;
}
const FIntVector BlockOrigin = RegionOrigin
+ FIntVector(BX * BlockCells, BY * BlockCells, BZ * BlockCells);
const FBox BlockBox(
FVector(BlockOrigin.X - 1, BlockOrigin.Y - 1, BlockOrigin.Z - 1),
FVector(BlockOrigin.X + BlockCells + 1,
BlockOrigin.Y + BlockCells + 1,
BlockOrigin.Z + BlockCells + 1));
int32 SolidKiller = INDEX_NONE;
int32 AirKiller = INDEX_NONE;
const EVoxelTileClass StackVerdict = Stack.ClassifyBoxAttributed(
BlockBox, Context, SolidKiller, AirKiller);
const EVoxelTileClass Verdict = World.Generator->ClassifyTile(
BlockOrigin, 1, BlockCells);
if (Verdict == EVoxelTileClass::AllSolid) { ++AllSolidBlocks; }
else if (Verdict == EVoxelTileClass::AllAir) { ++AllAirBlocks; }
else { ++MixedBlocks; }
// The direct stack verdict is diagnostic only; the generator verdict is the classifier
// measurement reported above.
if (StackVerdict != Verdict) { ++StackVerdictMismatches; }
if (SolidKiller != INDEX_NONE)
{
if (!StackSolidKillerCounts.IsValidIndex(SolidKiller))
{
StackSolidKillerCounts.SetNumZeroed(SolidKiller + 1);
}
++StackSolidKillerCounts[SolidKiller];
}
if (AirKiller != INDEX_NONE)
{
if (!StackAirKillerCounts.IsValidIndex(AirKiller))
{
StackAirKillerCounts.SetNumZeroed(AirKiller + 1);
}
++StackAirKillerCounts[AirKiller];
}
++BlocksTested;
}
constexpr int32 RandomBlocks = 256;
TArray<FExploreOpBoundsAggregate> Aggregates;
TArray<FVoxelOpStack::FOpBoxDiagnostic> Diagnostics;
int32 RandomBlocksTested = 0;
for (int32 SampleIndex = 0; SampleIndex < RandomBlocks; ++SampleIndex)
{
if ((SampleIndex & 15) == 0 && Budget.ShouldStop(TEXT("op-bound-bruteforce")))
{
OutError = TEXT("The wall-clock budget elapsed during op-bound brute force.");
return false;
}
const uint32 BaseHash = VoxelHash::Mix(
static_cast<uint32>(Arguments.Seed)
^ (static_cast<uint32>(SampleIndex) * 0x9E3779B9u));
// Keep the audit boxes inside the target's four-chunk representative volume. Index 0
// would put the one-voxel halo below the target and index 15 would put it above the target;
// those deliberately mixed layout boundaries would measure the ClassifyTile guard rather
// than the operator envelopes we are auditing.
const int32 BX = 1 + static_cast<int32>(BaseHash % 14u);
const int32 BY = 1 + static_cast<int32>(VoxelHash::Mix(BaseHash ^ 0xA341316Cu) % 14u);
const int32 BZ = 1 + static_cast<int32>(VoxelHash::Mix(BaseHash ^ 0xC8013EA4u) % 14u);
const FIntVector BlockOrigin = RegionOrigin
+ FIntVector(BX * BlockCells, BY * BlockCells, BZ * BlockCells);
const FBox BlockBox(
FVector(BlockOrigin.X - 1, BlockOrigin.Y - 1, BlockOrigin.Z - 1),
FVector(BlockOrigin.X + BlockCells + 1,
BlockOrigin.Y + BlockCells + 1,
BlockOrigin.Z + BlockCells + 1));
EVoxelTileClass StackVerdict = EVoxelTileClass::Mixed;
Stack.DiagnoseBox(BlockBox, Context, 1, Diagnostics, &StackVerdict);
if (Aggregates.Num() == 0)
{
Aggregates.SetNum(Diagnostics.Num());
}
for (int32 OpIndex = 0; OpIndex < Diagnostics.Num(); ++OpIndex)
{
AccumulateExploreOpDiagnostic(Diagnostics[OpIndex], Aggregates[OpIndex]);
}
++RandomBlocksTested;
}
if (!WriteExploreOpBoundsReport(
Arguments, World, BlocksTested, RandomBlocksTested,
AllSolidBlocks, AllAirBlocks, MixedBlocks,
StackSolidKillerCounts, StackAirKillerCounts,
StackVerdictMismatches, Aggregates, OutError))
{
return false;
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeOpBounds] blocks=%d all_solid=%d all_air=%d mixed=%d skip_rate=%.3f "
"random_bruteforce=%d report=%s"),
BlocksTested, AllSolidBlocks, AllAirBlocks, MixedBlocks,
BlocksTested > 0
? static_cast<double>(AllSolidBlocks + AllAirBlocks) / BlocksTested : 0.0,
RandomBlocksTested,
*FPaths::Combine(Arguments.OutDirectory, TEXT("op_bounds.json")));
return true;
}
} // namespace
UVoxelForgeExploreCommandlet::UVoxelForgeExploreCommandlet()
@@ -4450,6 +4969,26 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
}
const double SetupSeconds = FPlatformTime::Seconds() - SetupStartSeconds;
bool bRequestedModeFailed = false;
if (Arguments.bOpBounds)
{
const double Start = FPlatformTime::Seconds();
const bool bOpBoundsOk = !Budget.bTruncated
&& RunOpBounds(Arguments, World, Budget, Error);
if (!bOpBoundsOk && !Budget.bTruncated)
{
bRequestedModeFailed = true;
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] opbounds failed: %s"), *Error);
}
if (bOpBoundsOk && !Budget.bTruncated)
{
Budget.CompleteMode(TEXT("opbounds"));
}
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] opbounds %.3fs (%s)"),
FPlatformTime::Seconds() - Start,
bOpBoundsOk ? TEXT("ok") : (Budget.bTruncated ? TEXT("truncated") : TEXT("error")));
}
if (Arguments.bProfileLod)
{
const FIntVector ProbeOrigin(0, 0, World.TargetBottomWorldZ);
@@ -4523,7 +5062,6 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
UE_LOG(LogTemp, Warning,
TEXT("[VoxelForgeExplore] setup consumed the wall-clock budget; no mode was started."));
}
bool bRequestedModeFailed = false;
FExploreWalkOutput RenderSeedWalk;
const FExploreWalkOutput* CameraSeedWalk = nullptr;
if (Arguments.bWalk)
@@ -4570,7 +5108,7 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
// it cannot affect the profiled world's generation or its deterministic output.
if (Arguments.bProfileDensity
&& (Arguments.bRender || Arguments.bExport)
&& CameraSeedWalk != nullptr
&& (CameraSeedWalk != nullptr || Arguments.bExport)
&& !Budget.bTruncated)
{
Output.ProfilerComparison.Scope = TEXT("canonical_mesh_export");
@@ -5101,6 +5639,8 @@ bool BuildBatchCaseParams(
bool bReuseDensityGrid = true;
JsonBool(Case, TEXT("density_grid_reuse"), bReuseDensityGrid);
bool bBlockEarlyOut = false;
JsonBool(Case, TEXT("block_early_out"), bBlockEarlyOut);
int32 MeshMinBatchSize = 1;
if (JsonNumber(Case, TEXT("mesh_min_batch_size"), Number))
{
@@ -5118,15 +5658,19 @@ bool BuildBatchCaseParams(
bProfileDensity |= bProfileDensityFull;
bool bProfileLod = false;
JsonBool(Case, TEXT("profile_lod"), bProfileLod);
bool bOpBounds = false;
JsonBool(Case, TEXT("op_bounds"), bOpBounds);
bool bFailureFocus = false;
JsonBool(Case, TEXT("failure_focus_render"), bFailureFocus);
OutParams = FString::Printf(
TEXT("-seed=%d -archetype=%s -slot=%d -modes=%s -opstack=%d "
"-exportsize=%d -exportstep=%d -densitygridreuse=%d -meshminbatch=%d "
"-exportsize=%d -exportstep=%d -densitygridreuse=%d "
"-blockearlyout=%d -meshminbatch=%d "
"-failurefocus=%d -out=\"%s\""),
Seed, *Archetype, Slot, *Modes, bOperatorStack ? 1 : 0,
ExportSize, ExportStep, bReuseDensityGrid ? 1 : 0, MeshMinBatchSize,
ExportSize, ExportStep, bReuseDensityGrid ? 1 : 0,
bBlockEarlyOut ? 1 : 0, MeshMinBatchSize,
bFailureFocus ? 1 : 0, *CaseOutDirectory);
if (bProfileDensity)
@@ -5139,6 +5683,10 @@ bool BuildBatchCaseParams(
{
OutParams += TEXT(" -profilelod");
}
if (bOpBounds)
{
OutParams += TEXT(" -opbounds");
}
const TPair<const TCHAR*, const TCHAR*> IntegerFields[] = {
{ TEXT("render_width"), TEXT("renderwidth") },