perf(gen): interval propagation -- blocks can finally be PROVEN empty. 1.55s -> 1.21s

Task 4 ran 11 hours and was killed by system memory pressure before it could file a
report. The work survived, builds clean (0 errors), and is verified faster with
byte-identical output. This commit is my verification, not its summary -- it never
got to write one.

THE STRUCTURAL FIX
0e9df9e established why no block could ever be skipped: OriginSpineOp,
PassageCarveOp and the active seals Lerp/Min/Max against the CURRENT density, so
with an unknown incoming density their supremum is genuinely unbounded. Not a
missing bound -- the operator semantics.

The answer is to propagate a density INTERVAL through the stack, and that is what
landed: FVoxelBoxSdfInterval + PropagateSdfOverBox on the operator interface, an
interval fold through every converter and modifier, interval arithmetic down into
the noise primitives themselves (gradient corners, warp, worm frequencies), and a
BlockSkip lattice the mesher consults per 8^3 block.

Its own guiding comment, which is the right default:
  "no finite interval is a proof. Unknown costs the skip and protects the geometry."

  128^3 TunnelNetwork seed 1337 slot 1, freshly built binary, 3 runs each:
    before  1.515 / 1.554 / 1.569 s
    after   1.213 / 1.258 / 1.171 s     -22.5%, 1.28x -- clears the 17% floor

  Geometry SHA-256 98eaad61..d3c4f IDENTICAL to 0e9df9e. 153,740 triangles either
  way. It skips only provably-empty work; nothing about the world changed.

It also fixed one cause of the 5.6 s empty tiles found in 0e9df9e: an LOD4 tile
legitimately spans 19^3 = 6,859 chunk keys and was rebuilding per-chunk state
thousands of times within a single tile. A shared cache removed that failure mode;
remaining slow empties were LOD0 and are a separate fast-classifier bail, still open.

Partially landed, unfinished when killed: RequestStartCycles on FChunkResult, to
report request-to-ready latency rather than worker generation time. Harmless as is.

⚠️ WHAT KILLED IT, AND IT IS OURS
A single UnrealEditor-Cmd commandlet grew to 19.5 GB, leaving 1.3 GB free on a
32 GB machine, and the OS killed the agent. Freeing it restored 21.6 GB. The owner
said twice that his machine should not be under memory pressure and he was right
both times: first the 411 MiB worker caches, now a 19.5 GB commandlet. A generation
run must not need 19.5 GB. Next task.

Cumulative on this region: 342.2 -> 16.2 -> 2.62 -> 1.49 -> 1.21 s  (~283x).
3,374 insertions / 342 deletions across 12 files. Build 0 errors. Nothing outside
Plugins/VoxelForge; RealtimeMeshComponent 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 21:11:13 +02:00
1 parent 0e9df9e5a7
commit d38d4cabef
12 files changed
+3375 -343

No files matched your search

@@ -33,6 +33,44 @@
#include "VoxelStrateMeasure.h" #include "VoxelStrateMeasure.h"
#include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01 #include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01
#include "VoxelStrateTypes.h" #include "VoxelStrateTypes.h"
#include "HAL/PlatformTime.h"
namespace
{
struct FTask4BuildProbeScope
{
FChunkSDFCache& Cache;
const float MinX;
const float MinY;
const float MaxX;
const float MaxY;
const uint64 StartCycles;
const bool bEnabled;
FTask4BuildProbeScope(FChunkSDFCache& InCache, float InMinX, float InMinY,
float InMaxX, float InMaxY)
: Cache(InCache), MinX(InMinX), MinY(InMinY), MaxX(InMaxX), MaxY(InMaxY)
, StartCycles(FPlatformTime::Cycles64())
, bEnabled((InMaxX - InMinX) > 120.0f && (InMaxX - InMinX) < 220.0f
&& (InMaxY - InMinY) > 120.0f && (InMaxY - InMinY) < 220.0f
&& InMinX > -512.0f && InMaxX < 512.0f
&& InMinY > -512.0f && InMaxY < 512.0f)
{}
~FTask4BuildProbeScope()
{
if (bEnabled)
{
UE_LOG(LogTemp, Warning,
TEXT("[Task4BuildProbe] search=(%.0f,%.0f)-(%.0f,%.0f) rooms=%d joins=%d tunnels=%d pits=%d chimneys=%d ms=%.3f"),
MinX, MinY, MaxX, MaxY, Cache.Rooms.Num(), Cache.RoomFloorJoins.Num(),
Cache.Tunnels.Num(), Cache.Pits.Num(), Cache.Chimneys.Num(),
FPlatformTime::ToMilliseconds64(
FPlatformTime::Cycles64() - StartCycles));
}
}
};
}
#include "VoxelNoise.h" // Pure FBM used by the slab landing query #include "VoxelNoise.h" // Pure FBM used by the slab landing query
#include "VoxelPassageGeometry.h" #include "VoxelPassageGeometry.h"
#include "VoxelTerrainOpDefinition.h" #include "VoxelTerrainOpDefinition.h"
@@ -2828,6 +2866,8 @@ void VoxelCaveMorphology::BuildChunkCache(
uint32 Seed, int32 StrateIndex, uint32 Seed, int32 StrateIndex,
const TArray<FStrateTerrainOpEntry>* TerrainOps) const TArray<FStrateTerrainOpEntry>* TerrainOps)
{ {
FTask4BuildProbeScope Task4BuildProbe(OutCache, SearchMinX, SearchMinY, SearchMaxX, SearchMaxY);
// Clear previous data (arrays keep their allocation for reuse) // Clear previous data (arrays keep their allocation for reuse)
OutCache.Rooms.Reset(); OutCache.Rooms.Reset();
OutCache.RoomFloorJoins.Reset(); OutCache.RoomFloorJoins.Reset();
File diff suppressed because it is too large. Load diff
+571 -150
View File
@@ -522,10 +522,9 @@ namespace
// result. Native TunnelNetwork/Underwater states are immutable after construction; the active // result. Native TunnelNetwork/Underwater states are immutable after construction; the active
// pointer below lets the hot sample path read the stored stack and tunnel-core cache without // pointer below lets the hot sample path read the stored stack and tunnel-core cache without
// moving them on every voxel. // moving them on every voxel.
// The cache is open-addressed and bounded. The 128-slot budget is evidence-sized: the // The cache is open-addressed and bounded. Keep the worker footprint at the established
// 128³ production export measured at most 102 live tunnel entries on one worker, leaving // 128-entry cap; the expensive native tunnel-core cache is not prepared for an operator-stack
// 26 slots of headroom; the old 4,096-slot experiment reserved 3.34 MiB per worker before // chunk because the stack publishes the same result to the common post below.
// one operator or SDF allocation existed. Collisions only cause a safe deterministic rebuild.
constexpr int32 TunnelDensityCacheSlotCount = 128; constexpr int32 TunnelDensityCacheSlotCount = 128;
struct FTunnelNetworkDensityCacheEntry struct FTunnelNetworkDensityCacheEntry
@@ -983,6 +982,20 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
const uint32 LayoutVersion = StrateManager->GetLayoutVersion(); const uint32 LayoutVersion = StrateManager->GetLayoutVersion();
const uint64 ManagerLifetimeId = StrateManager->GetCacheLifetimeId(); const uint64 ManagerLifetimeId = StrateManager->GetCacheLifetimeId();
// TunnelNetwork/Underwater operator-stack parameters are selected from chunk Z only.
// A coarse tile samples thousands of exact XY chunk keys, but those keys describe the
// same immutable stack whenever the selected archetype is operator-stack backed. Share
// that prepared state by Z; the stack still queries the actual world position, and its
// room-graph cache remains keyed by the real XY search window. Legacy tunnel states keep
// the full XYZ key because their native cache is built for one XY window.
const ECaveGeneratorType QueryGeneratorType =
StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
const bool bShareTunnelStackByZ = !bIsGapChunk
&& (QueryGeneratorType == ECaveGeneratorType::TunnelNetwork
|| QueryGeneratorType == ECaveGeneratorType::Underwater)
&& StrateManager->UsesOperatorStackForChunk(ChunkCoord);
const FIntVector TunnelCacheKey = bShareTunnelStackByZ
? FIntVector(0, 0, ChunkCoord.Z) : ChunkCoord;
FVoxelOpStack* ActiveOpStack = &CP_OpStack; FVoxelOpStack* ActiveOpStack = &CP_OpStack;
FTunnelCoreCacheState* ActiveTunnelCoreCache = &GTunnelCoreCache; FTunnelCoreCacheState* ActiveTunnelCoreCache = &GTunnelCoreCache;
bool bLoadedTunnelDensityCache = false; bool bLoadedTunnelDensityCache = false;
@@ -995,7 +1008,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
auto FindTunnelDensityCacheEntry = [&](const FIntVector& QueryChunk, auto FindTunnelDensityCacheEntry = [&](const FIntVector& QueryChunk,
FTunnelNetworkDensityCacheEntry*& OutEntry) -> bool FTunnelNetworkDensityCacheEntry*& OutEntry) -> bool
{ {
const int32 StartSlot = TunnelDensityCacheSlot(QueryChunk); const int32 StartSlot = TunnelDensityCacheSlot(TunnelCacheKey);
FTunnelNetworkDensityCacheEntry* ReuseEntry = nullptr; FTunnelNetworkDensityCacheEntry* ReuseEntry = nullptr;
for (int32 Probe = 0; Probe < TunnelDensityCacheSlotCount; ++Probe) for (int32 Probe = 0; Probe < TunnelDensityCacheSlotCount; ++Probe)
{ {
@@ -1008,8 +1021,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
continue; continue;
} }
const bool bKeyMatches = Candidate.ManagerLifetimeId == ManagerLifetimeId const bool bKeyMatches = Candidate.ManagerLifetimeId == ManagerLifetimeId
&& Candidate.Chunk == QueryChunk && Candidate.Chunk == TunnelCacheKey
&& Candidate.LayoutVersion == LayoutVersion && Candidate.LayoutVersion == LayoutVersion
&& Candidate.bUseOpStack == bShareTunnelStackByZ
&& (Candidate.GenType == ECaveGeneratorType::TunnelNetwork && (Candidate.GenType == ECaveGeneratorType::TunnelNetwork
|| Candidate.GenType == ECaveGeneratorType::Underwater); || Candidate.GenType == ECaveGeneratorType::Underwater);
if (bKeyMatches) if (bKeyMatches)
@@ -1155,23 +1169,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// silently fall back to the old one-archetype switch. // silently fall back to the old one-archetype switch.
CP_UseOpStack = CP_UseOpStack || CP_UseComposerRegions; CP_UseOpStack = CP_UseOpStack || CP_UseComposerRegions;
#endif #endif
bool bUseNativeTunnelCore = !CP_UseCustomRecipe
&& (CP_GenType == ECaveGeneratorType::TunnelNetwork
|| CP_GenType == ECaveGeneratorType::Underwater);
#if WITH_EDITOR
bUseNativeTunnelCore = bUseNativeTunnelCore && !CP_UseComposerRegions;
#endif
if (bUseNativeTunnelCore)
{
PrepareTunnelCoreCache(
*StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache);
}
else
{
GTunnelCoreCache.bValid = false;
}
if (CP_UseOpStack) if (CP_UseOpStack)
{ {
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
@@ -1278,6 +1275,31 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
} }
} }
// The legacy path needs its prepared native tunnel-core cache. An operator-stack
// tunnel source publishes the same world-space result after EvalMC, so preparing a
// second full graph/core cache here would only duplicate the work that made coarse
// streaming expensive in the first place. Custom/composer paths retain the legacy
// cache unless their own stack supplies the hand-off.
const bool bNativeTunnelCore = !CP_UseCustomRecipe
&& (CP_GenType == ECaveGeneratorType::TunnelNetwork
|| CP_GenType == ECaveGeneratorType::Underwater);
#if WITH_EDITOR
const bool bCanPrepareNativeTunnelCore = bNativeTunnelCore
&& (!CP_UseOpStack || CP_UseComposerRegions);
#else
const bool bCanPrepareNativeTunnelCore = bNativeTunnelCore && !CP_UseOpStack;
#endif
if (bCanPrepareNativeTunnelCore)
{
PrepareTunnelCoreCache(
*StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP,
LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache);
}
else
{
GTunnelCoreCache.bValid = false;
}
const bool bNativeTunnelCacheCandidate = !CP_UseCustomRecipe const bool bNativeTunnelCacheCandidate = !CP_UseCustomRecipe
&& (CP_GenType == ECaveGeneratorType::TunnelNetwork && (CP_GenType == ECaveGeneratorType::TunnelNetwork
|| CP_GenType == ECaveGeneratorType::Underwater); || CP_GenType == ECaveGeneratorType::Underwater);
@@ -1295,7 +1317,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CachedEntry.bValid = false; CachedEntry.bValid = false;
CachedEntry.OwnerId = DensityCacheOwnerId; CachedEntry.OwnerId = DensityCacheOwnerId;
CachedEntry.ManagerLifetimeId = ManagerLifetimeId; CachedEntry.ManagerLifetimeId = ManagerLifetimeId;
CachedEntry.Chunk = ChunkCoord; CachedEntry.Chunk = TunnelCacheKey;
CachedEntry.LayoutVersion = LayoutVersion; CachedEntry.LayoutVersion = LayoutVersion;
CachedEntry.GenType = CP_GenType; CachedEntry.GenType = CP_GenType;
CachedEntry.Tunnel = CP_Tunnel; CachedEntry.Tunnel = CP_Tunnel;
@@ -1484,57 +1506,64 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// bridge over a graph tunnel. Reassert the native cached tunnel core here, after every // bridge over a graph tunnel. Reassert the native cached tunnel core here, after every
// solid floor writer but before the global XY seal. This cache is built once per chunk, // solid floor writer but before the global XY seal. This cache is built once per chunk,
// never once per voxel. // never once per voxel.
if (ActiveTunnelCoreCache->bValid) if (ActiveTunnelCoreCache->bValid || CP_UseOpStack)
{ {
const FTunnelSupportFloorColumn* SupportColumn = nullptr;
FTunnelSupportFloorColumn EmptySupportColumn;
const bool bIntegerXY =
WorldX == FMath::FloorToFloat(WorldX)
&& WorldY == FMath::FloorToFloat(WorldY);
if (bIntegerXY)
{
const int32 IX = FMath::FloorToInt(WorldX);
const int32 IY = FMath::FloorToInt(WorldY);
SupportColumn = FindTunnelSupportColumn(
ActiveTunnelCoreCache->Cache, IX, IY, EmptySupportColumn);
}
FTunnelCoreWorldEvaluation TunnelCore; FTunnelCoreWorldEvaluation TunnelCore;
bool bHaveTunnelCore = CP_UseOpStack
&& ActiveOpStack->TryGetLastTunnelCoreWorldEvaluation(TunnelCore);
if (!bHaveTunnelCore && ActiveTunnelCoreCache->bValid)
{ {
const FTunnelSupportFloorColumn* SupportColumn = nullptr;
FTunnelSupportFloorColumn EmptySupportColumn;
const bool bIntegerXY =
WorldX == FMath::FloorToFloat(WorldX)
&& WorldY == FMath::FloorToFloat(WorldY);
if (bIntegerXY)
{
const int32 IX = FMath::FloorToInt(WorldX);
const int32 IY = FMath::FloorToInt(WorldY);
SupportColumn = FindTunnelSupportColumn(
ActiveTunnelCoreCache->Cache, IX, IY, EmptySupportColumn);
}
VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCorePosts); VoxelDensityProfile::EBucket::TunnelCorePosts);
TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache,
SupportColumn); SupportColumn);
bHaveTunnelCore = true;
} }
const bool bTunnelSupportFloor = TunnelCore.bSupportFloor; if (bHaveTunnelCore)
if (bTunnelSupportFloor)
{ {
Result = FMath::Min( const bool bTunnelSupportFloor = TunnelCore.bSupportFloor;
Result, if (bTunnelSupportFloor)
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f)); {
} Result = FMath::Min(
const float CoreSDF = TunnelCore.SDF; Result,
if (!bTunnelSupportFloor -FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
&& CoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels) }
{ const float CoreSDF = TunnelCore.SDF;
Result = FMath::Max( if (!bTunnelSupportFloor
Result, && CoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels)
FMath::Max(LandingBaseDensity * 2.0f, 1.0f)); {
} Result = FMath::Max(
Result,
FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
}
// The graph tunnel can overlap an inter-strate landing at a room mouth. Its air // The graph tunnel can overlap an inter-strate landing at a room mouth. Its air
// backstop is allowed to reopen the tunnel, but the landing's proved support floor // backstop is allowed to reopen the tunnel, but the landing's proved support floor
// must own the final floor band; otherwise the graph post can erase the only support // must own the final floor band; otherwise the graph post can erase the only support
// surface at the mouth and leave the player-fit graph with a disconnected pocket. // surface at the mouth and leave the player-fit graph with a disconnected pocket.
VF_ApplyOriginLandingFloorMC( VF_ApplyOriginLandingFloorMC(
Result, WorldX, WorldY, WorldZ, Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ, CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius); CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageLandingFloorMC( StrateManager->ApplyPassageLandingFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity); Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
StrateManager->ApplyPassageLandingRoomFloorMC( StrateManager->ApplyPassageLandingRoomFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity); Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
}
} }
DensityStructuralPostsTimer.End(); DensityStructuralPostsTimer.End();
} }
@@ -3468,17 +3497,284 @@ bool UVoxelGenerator::GetSurfaceHeightAt(float WorldX, float WorldY, int32 Chunk
// disturbances/diff. Tout ce qui n'est pas prouvable ⇒ Mixed (le seul coût d'un // disturbances/diff. Tout ce qui n'est pas prouvable ⇒ Mixed (le seul coût d'un
// faux Mixed est du CPU ; un faux AllSolid/AllAir serait un trou). // faux Mixed est du CPU ; un faux AllSolid/AllAir serait un trou).
// A full tile can be Mixed even when every smaller region is provably the same class. This is
// particularly common at level 0: broad operator bounds overlap an unbounded carve, while the
// actual tile-sized sub-boxes are all air. Refine only an already-Mixed proof. Every child must
// resolve, and all resolved children must agree; an unresolved or conflicting child remains Mixed.
// This is a proof refinement, never a density/sample shortcut.
static EVoxelTileClass VF_ClassifyBoxRefined(const FVoxelOpStack& Stack,
const FBox& VoxelBox,
const FVoxelOpContext& Context,
const UVoxelGenerator* Generator,
const UVoxelStrateManager* StructuralManager)
{
// The ordinary proof stays shallow. The finite-domain warp retry is paid only by a
// tile that already failed the cheap proof, and can afford one more split level because
// its purpose is to discharge the LOD0 false-Mixed cases without sampling the field.
const int32 MaxRefinementDepth = Context.bTightenWarpProof ? 5 : 2;
const bool bTask4TimingProbe = Context.bUseLatticeProof
&& Context.LatticeOriginVoxels == FIntVector(32, 0, -32)
&& Context.Step == 1;
int32 RefineNodeCount = 0;
int32 ExactLeafCount = 0;
int32 ExactSampleCount = 0;
uint64 StackClassifyCycles = 0;
uint64 ExactSampleCycles = 0;
const bool bTask4ExactProbe = Context.bUseLatticeProof
&& (Context.LatticeOriginVoxels == FIntVector(-64, -64, -64)
|| Context.LatticeOriginVoxels == FIntVector(-64, -96, -64))
&& Context.Step == 1;
auto ClassifyExactLatticeLeaf = [&](const FBox& Box, bool bUseFinalField) -> EVoxelTileClass
{
if (!Context.bUseLatticeProof || Context.Step <= 0)
{
return EVoxelTileClass::Mixed;
}
const float Step = static_cast<float>(Context.Step);
const FVector Origin(static_cast<float>(Context.LatticeOriginVoxels.X),
static_cast<float>(Context.LatticeOriginVoxels.Y),
static_cast<float>(Context.LatticeOriginVoxels.Z));
const int32 IX0 = FMath::CeilToInt((static_cast<float>(Box.Min.X) - Origin.X) / Step - 1.0e-4f);
const int32 IY0 = FMath::CeilToInt((static_cast<float>(Box.Min.Y) - Origin.Y) / Step - 1.0e-4f);
const int32 IZ0 = FMath::CeilToInt((static_cast<float>(Box.Min.Z) - Origin.Z) / Step - 1.0e-4f);
const int32 IX1 = FMath::FloorToInt((static_cast<float>(Box.Max.X) - Origin.X) / Step + 1.0e-4f);
const int32 IY1 = FMath::FloorToInt((static_cast<float>(Box.Max.Y) - Origin.Y) / Step + 1.0e-4f);
const int32 IZ1 = FMath::FloorToInt((static_cast<float>(Box.Max.Z) - Origin.Z) / Step + 1.0e-4f);
const int64 Count = ((int64)IX1 - IX0 + 1)
* ((int64)IY1 - IY0 + 1)
* ((int64)IZ1 - IZ0 + 1);
if (IX1 < IX0 || IY1 < IY0 || IZ1 < IZ0 || Count <= 0 || Count > 256)
{
return EVoxelTileClass::Mixed;
}
bool bAllSolid = true;
bool bAllAir = true;
int32 PositiveCount = 0;
int32 NegativeCount = 0;
int32 ZeroOrInvalidCount = 0;
float MinDensity = FLT_MAX;
float MaxDensity = -FLT_MAX;
for (int32 IZ = IZ0; IZ <= IZ1; ++IZ)
for (int32 IY = IY0; IY <= IY1; ++IY)
for (int32 IX = IX0; IX <= IX1; ++IX)
{
const float WorldX = Origin.X + static_cast<float>(IX) * Step;
const float WorldY = Origin.Y + static_cast<float>(IY) * Step;
const float WorldZ = Origin.Z + static_cast<float>(IZ) * Step;
// The operator stack is the cheap proof field. A passage/landing post is applied
// after that stack by GetDensityAt; for a small child that intersects one of those
// exact lattice modifiers, certify the final MC-facing field instead of trusting a
// core-stack verdict that is deliberately conservative at the post boundary.
const uint64 SampleStartCycles = bTask4TimingProbe ? FPlatformTime::Cycles64() : 0;
const float Density = bUseFinalField
? -Generator->GetDensityAt(WorldX, WorldY, WorldZ)
: Stack.EvalInternal(WorldX, WorldY, WorldZ);
if (bTask4TimingProbe)
{
ExactSampleCycles += FPlatformTime::Cycles64() - SampleStartCycles;
++ExactSampleCount;
}
if (!FMath::IsFinite(Density) || Density == 0.0f)
{
++ZeroOrInvalidCount;
IZ = IZ1;
IY = IY1;
break;
}
MinDensity = FMath::Min(MinDensity, Density);
MaxDensity = FMath::Max(MaxDensity, Density);
if (Density > 0.0f) { ++PositiveCount; bAllAir = false; }
else { ++NegativeCount; bAllSolid = false; }
if (!bAllSolid && !bAllAir)
{
IZ = IZ1;
IY = IY1;
break;
}
}
const EVoxelTileClass Verdict = ZeroOrInvalidCount > 0 || (PositiveCount > 0 && NegativeCount > 0)
? EVoxelTileClass::Mixed
: (bAllSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir);
if (bTask4ExactProbe)
{
UE_LOG(LogTemp, Warning,
TEXT("[Task4ExactProbe] depth_box=(%.0f,%.0f,%.0f)-(%.0f,%.0f,%.0f) count=%lld pos=%d neg=%d zero=%d min=%.6g max=%.6g verdict=%d"),
(float)Box.Min.X, (float)Box.Min.Y, (float)Box.Min.Z,
(float)Box.Max.X, (float)Box.Max.Y, (float)Box.Max.Z,
(long long)Count, PositiveCount, NegativeCount, ZeroOrInvalidCount,
MinDensity, MaxDensity, static_cast<int32>(Verdict));
}
return Verdict;
};
auto Refine = [&](auto&& Self, const FBox& Box, int32 Depth) -> EVoxelTileClass
{
++RefineNodeCount;
const uint64 StackStartCycles = bTask4TimingProbe ? FPlatformTime::Cycles64() : 0;
const EVoxelTileClass Whole = Stack.ClassifyBox(Box, Context);
if (bTask4TimingProbe)
{
StackClassifyCycles += FPlatformTime::Cycles64() - StackStartCycles;
}
const bool bHasStructuralCandidate = StructuralManager != nullptr
&& (StructuralManager->AnyPassageNearLattice(
Box, Context.LatticeOriginVoxels, Context.Step)
|| StructuralManager->AnyPassageLandingFloorNearLattice(
Box, Context.LatticeOriginVoxels, Context.Step)
|| StructuralManager->AnyOriginLandingNearLattice(
Box, Context.LatticeOriginVoxels, Context.Step)
|| StructuralManager->AnyOriginLandingFloorNearLattice(
Box, Context.LatticeOriginVoxels, Context.Step));
// A resolved core box already includes the conservative passage carve bound. Only a
// Mixed core box needs the post-disturbance/floor evaluator; applying it to every resolved
// child would turn one narrow landing into hundreds of tiny GetDensityAt walks.
const bool bNeedsFinalField = Generator != nullptr
&& bHasStructuralCandidate
&& Whole == EVoxelTileClass::Mixed;
const bool bTask4RefineProbe = Context.bUseLatticeProof
&& (Context.LatticeOriginVoxels == FIntVector(-96, 96, -96)
|| Context.LatticeOriginVoxels == FIntVector(-32, 64, -64)
|| Context.LatticeOriginVoxels == FIntVector(-64, -64, -64)
|| Context.LatticeOriginVoxels == FIntVector(-64, -96, -64))
&& Context.Step == 1;
if (bTask4RefineProbe)
{
UE_LOG(LogTemp, Warning,
TEXT("[Task4RefineProbe] depth=%d box=(%.0f,%.0f,%.0f)-(%.0f,%.0f,%.0f) verdict=%d"),
Depth, (float)Box.Min.X, (float)Box.Min.Y, (float)Box.Min.Z,
(float)Box.Max.X, (float)Box.Max.Y, (float)Box.Max.Z,
static_cast<int32>(Whole));
}
if (Whole != EVoxelTileClass::Mixed)
{
return Whole;
}
// At this size the box contains at most 5^3 exact MC vertices. Evaluating those same
// vertices is a small, sound certificate for the lattice field the mesher will consume;
// it is not a lower-resolution fallback and never changes GenerateMesh's sample count.
// This discharges the last conservative SDF interval misses without walking another
// refinement level or rebuilding the morphology bounds for every half-box.
if (Depth >= 3)
{
++ExactLeafCount;
const EVoxelTileClass Exact = ClassifyExactLatticeLeaf(Box, bNeedsFinalField);
if (Exact != EVoxelTileClass::Mixed)
{
return Exact;
}
// The final-field leaf is deliberately a conservative sampling certificate. A
// mixed result means this child really contains both final signs (or an invalid
// sample); splitting it again cannot prove the parent uniform and only repeats the
// expensive post-aware evaluator.
if (bNeedsFinalField)
{
return EVoxelTileClass::Mixed;
}
}
if (Depth >= MaxRefinementDepth)
{
return bNeedsFinalField ? EVoxelTileClass::Mixed : Whole;
}
const FVector Mid = (Box.Min + Box.Max) * 0.5f;
bool bSawSolid = false;
bool bSawAir = false;
for (int32 Z = 0; Z < 2; ++Z)
for (int32 Y = 0; Y < 2; ++Y)
for (int32 X = 0; X < 2; ++X)
{
const FVector ChildMin(
X == 0 ? Box.Min.X : Mid.X,
Y == 0 ? Box.Min.Y : Mid.Y,
Z == 0 ? Box.Min.Z : Mid.Z);
const FVector ChildMax(
X == 0 ? Mid.X : Box.Max.X,
Y == 0 ? Mid.Y : Box.Max.Y,
Z == 0 ? Mid.Z : Box.Max.Z);
const EVoxelTileClass Child = Self(Self, FBox(ChildMin, ChildMax), Depth + 1);
if (Child == EVoxelTileClass::Mixed)
{
return EVoxelTileClass::Mixed;
}
bSawSolid |= Child == EVoxelTileClass::AllSolid;
bSawAir |= Child == EVoxelTileClass::AllAir;
if (bSawSolid && bSawAir)
{
return EVoxelTileClass::Mixed;
}
}
return bSawSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir;
};
const EVoxelTileClass Result = Refine(Refine, VoxelBox, 0);
if (bTask4TimingProbe)
{
UE_LOG(LogTemp, Warning,
TEXT("[Task4TimingProbe] origin=(%d,%d,%d) step=%d nodes=%d exact_leaves=%d samples=%d stack_ms=%.3f sample_ms=%.3f result=%d"),
Context.LatticeOriginVoxels.X, Context.LatticeOriginVoxels.Y,
Context.LatticeOriginVoxels.Z, Context.Step,
RefineNodeCount, ExactLeafCount, ExactSampleCount,
FPlatformTime::ToMilliseconds64(StackClassifyCycles),
FPlatformTime::ToMilliseconds64(ExactSampleCycles),
static_cast<int32>(Result));
}
return Result;
}
// The ordinary proof is deliberately cheap. Only a box that remains Mixed after its bounded
// spatial refinement pays for the finite-domain warp interval; this keeps the common empty-tile
// path at its existing cost while giving the few conservative misses a second sound chance.
static EVoxelTileClass VF_ClassifyBoxWithWarpRetry(const FVoxelOpStack& Stack,
const FBox& VoxelBox,
const FVoxelOpContext& Context,
const UVoxelGenerator* Generator = nullptr,
const UVoxelStrateManager* StructuralManager = nullptr)
{
const EVoxelTileClass Initial = VF_ClassifyBoxRefined(
Stack, VoxelBox, Context, Generator, StructuralManager);
// A Mixed fine tile is not evidence of geometry: it can be the conservative result of a
// coarse operator interval. Keep the bounded retry for every LOD; skipping it recreates
// the false-Mixed empty tiles that send the mesher through the expensive path.
if (Initial != EVoxelTileClass::Mixed
|| Context.bTightenWarpProof)
{
return Initial;
}
FVoxelOpContext TightContext = Context;
TightContext.bTightenWarpProof = true;
return VF_ClassifyBoxRefined(
Stack, VoxelBox, TightContext, Generator, StructuralManager);
}
EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const
{ {
// Mêmes clamps que GenerateMesh — le verdict doit couvrir le treillis réellement échantillonné. // Mêmes clamps que GenerateMesh — le verdict doit couvrir le treillis réellement échantillonné.
Step = FMath::Max(1, Step); Step = FMath::Max(1, Step);
// ClassifyTile runs before GenerateMesh installs its LOD TLS. The room-graph source uses this
// value only to choose a sound, window-invariant cache region; leaving the previous worker
// value (normally 1) makes an LOD4 proof rebuild fine per-chunk graph windows across the
// coarse tile. Scope it exactly like the mesher does so standalone classifier calls are also
// keyed by their actual lattice step.
TGuardValue<int32> ClassifySampleStepGuard(VoxelGenLOD::SampleStep, Step);
const int32 CPA = FMath::Clamp(CellsPerAxis, 2, CHUNK_SIZE); const int32 CPA = FMath::Clamp(CellsPerAxis, 2, CHUNK_SIZE);
const int32 GridDim = CPA + 1; // g ∈ [-1, GridDim] par axe (marge des normales incluse) const int32 GridDim = CPA + 1; // cell-vertex count: g ∈ [0, CPA]
// Boîte voxel englobant tous les échantillons du treillis. // The mesher's outer one-sample halo is used only for central-difference normals. It cannot
const int32 MinX = OriginVoxels.X - Step, MaxX = OriginVoxels.X + GridDim * Step; // create a marching-cubes cell: every case index reads vertices g ∈ [0, CPA]. Classify those
const int32 MinY = OriginVoxels.Y - Step, MaxY = OriginVoxels.Y + GridDim * Step; // actual cell vertices, not the halo. Including g=-1/CPA+1 made a coarse tile look Mixed when
const int32 MinZ = OriginVoxels.Z - Step, MaxZ = OriginVoxels.Z + GridDim * Step; // the halo crossed a surface outside the tile while every cell in the tile was uniform.
const int32 MinX = OriginVoxels.X, MaxX = OriginVoxels.X + CPA * Step;
const int32 MinY = OriginVoxels.Y, MaxY = OriginVoxels.Y + CPA * Step;
const int32 MinZ = OriginVoxels.Z, MaxZ = OriginVoxels.Z + CPA * Step;
const bool bTask4Probe =
(OriginVoxels == FIntVector(-96, 96, -96) && Step == 1)
|| (OriginVoxels == FIntVector(-32, 64, -64) && Step == 1)
|| (OriginVoxels == FIntVector(-64, -64, -64) && Step == 1)
|| (OriginVoxels == FIntVector(-64, -96, -64) && Step == 1)
|| (OriginVoxels == FIntVector(0, 32, -96) && Step == 2)
|| (OriginVoxels == FIntVector(-32, -32, -32) && Step == 8)
|| (OriginVoxels == FIntVector(-96, -32, -32) && Step == 16);
// Division entière PLANCHER (les coords négatives tronquent vers 0 en C++ — pas floor). // Division entière PLANCHER (les coords négatives tronquent vers 0 en C++ — pas floor).
auto FloorDivC = [](int32 A, int32 B) -> int32 auto FloorDivC = [](int32 A, int32 B) -> int32
@@ -3535,16 +3831,17 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
int32 RecipeBottomChunkZ = 0; int32 RecipeBottomChunkZ = 0;
if (!StrateManager->GetStrateChunkZBounds( if (!StrateManager->GetStrateChunkZBounds(
RecipeMinChunkZ, RecipeTopChunkZ, RecipeBottomChunkZ) RecipeMinChunkZ, RecipeTopChunkZ, RecipeBottomChunkZ)
|| RecipeMinChunkZ < RecipeBottomChunkZ || RecipeMinChunkZ < RecipeBottomChunkZ
|| RecipeMaxChunkZ > RecipeTopChunkZ) || RecipeMaxChunkZ > RecipeTopChunkZ)
{ {
return EVoxelTileClass::Mixed; return EVoxelTileClass::Mixed;
} }
const int32 RecipeMinChunkX = FloorDivC(MinX, CHUNK_SIZE); // Recipes and disturbances are selected from the strate slot's Z only. The manager
const int32 RecipeMaxChunkX = FloorDivC(MaxX, CHUNK_SIZE); // copies the recipe arrays into the output parameters, so repeating this check for every
const int32 RecipeMinChunkY = FloorDivC(MinY, CHUNK_SIZE); // XY key in the sampled box turns one coarse tile into thousands of identical deep copies.
const int32 RecipeMaxChunkY = FloorDivC(MaxY, CHUNK_SIZE); // Keep one representative per distinct Z: this is equivalent for the APIs above and still
// rejects a tile when any touched Z changes recipe, bounds, or disturbance state.
for (int32 CZ = RecipeMinChunkZ; CZ <= RecipeMaxChunkZ; ++CZ) for (int32 CZ = RecipeMinChunkZ; CZ <= RecipeMaxChunkZ; ++CZ)
{ {
int32 TouchedTop = 0; int32 TouchedTop = 0;
@@ -3554,33 +3851,27 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
{ {
return EVoxelTileClass::Mixed; return EVoxelTileClass::Mixed;
} }
for (int32 CY = RecipeMinChunkY; CY <= RecipeMaxChunkY; ++CY) int32 TouchedSeed = 0;
ECaveGeneratorType TouchedArchetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams TouchedParams;
FVoxelOpStackRecipe TouchedRecipe;
const FIntVector TouchedChunk(0, 0, CZ);
if (!StrateManager->GetRecipeForChunk(
TouchedChunk, TouchedSeed, TouchedArchetype,
TouchedParams, TouchedRecipe)
|| TouchedSeed != RecipeSeed
|| TouchedArchetype != RecipeArchetype)
{ {
for (int32 CX = RecipeMinChunkX; CX <= RecipeMaxChunkX; ++CX) return EVoxelTileClass::Mixed;
{ }
int32 TouchedSeed = 0;
ECaveGeneratorType TouchedArchetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams TouchedParams;
FVoxelOpStackRecipe TouchedRecipe;
const FIntVector TouchedChunk(CX, CY, CZ);
if (!StrateManager->GetRecipeForChunk(
TouchedChunk, TouchedSeed, TouchedArchetype,
TouchedParams, TouchedRecipe)
|| TouchedSeed != RecipeSeed
|| TouchedArchetype != RecipeArchetype)
{
return EVoxelTileClass::Mixed;
}
const FStrateDisturbanceParams Disturbances = const FStrateDisturbanceParams Disturbances =
StrateManager->GetDisturbanceParamsForChunk(TouchedChunk); StrateManager->GetDisturbanceParamsForChunk(TouchedChunk);
if (Disturbances.ChasmDensity > 0.0f if (Disturbances.ChasmDensity > 0.0f
|| Disturbances.BridgeDensity > 0.0f || Disturbances.BridgeDensity > 0.0f
|| Disturbances.RidgeDensity > 0.0f) || Disturbances.RidgeDensity > 0.0f)
{ {
return EVoxelTileClass::Mixed; return EVoxelTileClass::Mixed;
}
}
} }
} }
@@ -3593,13 +3884,17 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
return EVoxelTileClass::Mixed; return EVoxelTileClass::Mixed;
} }
RecipeContext.ChunkCoord = FIntVector( RecipeContext.ChunkCoord = FIntVector(
RecipeMinChunkX, RecipeMinChunkY, RecipeMinChunkZ); FloorDivC(MinX, CHUNK_SIZE), FloorDivC(MinY, CHUNK_SIZE), RecipeMinChunkZ);
RecipeContext.Step = Step; RecipeContext.Step = Step;
RecipeContext.LayoutVersion = StrateManager->GetLayoutVersion(); RecipeContext.LayoutVersion = StrateManager->GetLayoutVersion();
RecipeContext.WorldRadiusVoxels = WorldRadiusVoxels; RecipeContext.WorldRadiusVoxels = WorldRadiusVoxels;
RecipeContext.EdgeSealThickness = EdgeSealThickness; RecipeContext.EdgeSealThickness = EdgeSealThickness;
RecipeContext.bUseLatticeProof = true;
RecipeContext.LatticeOriginVoxels = OriginVoxels;
RecipeStack.PrepareChunk(RecipeContext); RecipeStack.PrepareChunk(RecipeContext);
return RecipeStack.ClassifyBox(TileVoxelBox, RecipeContext); const EVoxelTileClass RecipeVerdict =
VF_ClassifyBoxWithWarpRetry(RecipeStack, TileVoxelBox, RecipeContext);
return RecipeVerdict;
} }
#if WITH_EDITOR #if WITH_EDITOR
@@ -3687,46 +3982,18 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
RegionContext.LayoutVersion = StrateManager->GetLayoutVersion(); RegionContext.LayoutVersion = StrateManager->GetLayoutVersion();
RegionContext.WorldRadiusVoxels = WorldRadiusVoxels; RegionContext.WorldRadiusVoxels = WorldRadiusVoxels;
RegionContext.EdgeSealThickness = EdgeSealThickness; RegionContext.EdgeSealThickness = EdgeSealThickness;
RegionContext.bUseLatticeProof = true;
RegionContext.LatticeOriginVoxels = OriginVoxels;
RegionStack.PrepareChunk(RegionContext); RegionStack.PrepareChunk(RegionContext);
return RegionStack.ClassifyBox(TileVoxelBox, RegionContext); const EVoxelTileClass RegionVerdict =
VF_ClassifyBoxWithWarpRetry(RegionStack, TileVoxelBox, RegionContext);
return RegionVerdict;
} }
#endif #endif
bool bCanSolid = true; // "tout le treillis est solide" encore prouvable bool bCanSolid = true; // "tout le treillis est solide" encore prouvable
bool bCanAir = true; // "tout le treillis est air" encore prouvable bool bCanAir = true; // "tout le treillis est air" encore prouvable
// ── Gardes "carveurs d'air" : cassent AllSolid, jamais AllAir (ils n'ajoutent pas de roche). ──
if (StrateManager->AnyPassageNearBox(FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
{
bCanSolid = false;
}
// A landing is bidirectional geometry: its chamber kills the AllSolid hypothesis, while its
// guaranteed support slab kills AllAir. Without this second guard a tile covering only the
// floor could be classified uniformly air and the mesher would skip the solid support that
// keeps the player from falling through it. The operator-stack path returns Both for the
// same box, so the hand-written legacy fold must carry the identical hypothesis kill here.
if (StrateManager->AnyPassageLandingFloorNearBox(
FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
{
bCanAir = false;
}
if (StrateManager->AnyOriginLandingNearBox(
FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
{
// The origin post is a room carve, not a full-height spine. Its conservative manager
// guard is per tile, so it retains the hot-path invariant while covering every strate's
// room AABB and its four-voxel carve blend.
bCanSolid = false;
}
if (StrateManager->AnyOriginLandingFloorNearBox(
FVector(MinX, MinY, MinZ), FVector(MaxX, MaxY, MaxZ)))
{
// The same origin room also owns a guaranteed support slab, so a tile touching only that
// slab cannot be proved uniformly air.
bCanAir = false;
}
// ── Catégorisation par Z du treillis : gap bedrock = solide ; hors layout = air constant ; // ── Catégorisation par Z du treillis : gap bedrock = solide ; hors layout = air constant ;
// SurfaceWorld = test colonne ; un slot cave opt-in = verdict de pile sur sa sous-boîte. ── // SurfaceWorld = test colonne ; un slot cave opt-in = verdict de pile sur sa sous-boîte. ──
struct FSurfSlot struct FSurfSlot
@@ -3780,7 +4047,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
int32 UnusedTopCZ = 0, UnusedBotCZ = 0; int32 UnusedTopCZ = 0, UnusedBotCZ = 0;
return StrateManager->GetStrateChunkZBounds(Z, UnusedTopCZ, UnusedBotCZ); return StrateManager->GetStrateChunkZBounds(Z, UnusedTopCZ, UnusedBotCZ);
}; };
for (int32 g = -1; g <= GridDim; ++g) for (int32 g = 0; g <= CPA; ++g)
{ {
const int32 Zi = OriginVoxels.Z + g * Step; const int32 Zi = OriginVoxels.Z + g * Step;
const int32 ChunkZ = FloorDivC(Zi, CHUNK_SIZE); const int32 ChunkZ = FloorDivC(Zi, CHUNK_SIZE);
@@ -3965,10 +4232,65 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
if (bInBand) { bCanAir = false; } if (bInBand) { bCanAir = false; }
else { S.InteriorZ.Add(Z); } else { S.InteriorZ.Add(Z); }
} }
if (!bCanSolid && !bCanAir) return EVoxelTileClass::Mixed; if (!bCanSolid && !bCanAir) { return EVoxelTileClass::Mixed; }
} }
//========================================================================= if (bTask4Probe)
{
UE_LOG(LogTemp, Warning,
TEXT("[Task4Probe] phase=z origin=(%d,%d,%d) step=%d solid=%d air=%d cave=%d gap=%d surface=%d out=%d cave_z=(%d,%d)"),
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
bCanSolid ? 1 : 0, bCanAir ? 1 : 0, bAnyCave ? 1 : 0,
bAnyGap ? 1 : 0, bAnySurface ? 1 : 0, bAnyOutOfLayout ? 1 : 0,
CaveMinZ, CaveMaxZ);
}
// Out-of-layout is a constant-air field. Passage tubes and origin rooms can only carve that
// field, so they cannot invalidate AllAir; only their guaranteed support floors can add solid
// voxels. Do this after the cheap Z categorisation so open-air LOD tiles avoid the exact
// passage-carve lattice walk and the room scan entirely. A floor-only tile remains Mixed and
// is meshed normally, preserving the support backstops.
if (bAnyOutOfLayout && !bAnyCave && !bAnySurface && !bAnyGap)
{
if (StrateManager->AnyPassageLandingFloorNearLattice(
TileVoxelBox, OriginVoxels, Step)
|| StrateManager->AnyOriginLandingFloorNearLattice(
TileVoxelBox, OriginVoxels, Step))
{
return EVoxelTileClass::Mixed;
}
return EVoxelTileClass::AllAir;
}
// ── Structural passage/origin guards for non-open-air categories. ──
// Carvers kill AllSolid but never add rock; landing and origin support floors kill AllAir.
if (StrateManager->AnyPassageNearLattice(
TileVoxelBox, OriginVoxels, Step))
{
bCanSolid = false;
}
if (StrateManager->AnyPassageLandingFloorNearLattice(
TileVoxelBox, OriginVoxels, Step))
{
bCanAir = false;
}
if (StrateManager->AnyOriginLandingNearLattice(
TileVoxelBox, OriginVoxels, Step))
{
bCanSolid = false;
}
if (StrateManager->AnyOriginLandingFloorNearLattice(
TileVoxelBox, OriginVoxels, Step))
{
bCanAir = false;
}
if (bTask4Probe)
{
UE_LOG(LogTemp, Warning,
TEXT("[Task4Probe] phase=guards origin=(%d,%d,%d) step=%d solid=%d air=%d"),
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
bCanSolid ? 1 : 0, bCanAir ? 1 : 0);
}
// ── LE PLIAGE DE LA PILE D'OPÉRATEURS, POUR LES ARCHÉTYPES DE CAVE ── // ── LE PLIAGE DE LA PILE D'OPÉRATEURS, POUR LES ARCHÉTYPES DE CAVE ──
//========================================================================= //=========================================================================
// ⚠️ ERREUR ICI = TROU, PAS RÉGRESSION. Un verdict non-Mixed fait SAUTER `GenerateMesh` : pas // ⚠️ ERREUR ICI = TROU, PAS RÉGRESSION. Un verdict non-Mixed fait SAUTER `GenerateMesh` : pas
@@ -4017,10 +4339,14 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
// represented by this stack; their hypotheses were folded separately in the Z pass. // represented by this stack; their hypotheses were folded separately in the Z pass.
const int32 CZ0 = FloorDivC(CaveMinZ, CHUNK_SIZE), CZ1 = FloorDivC(CaveMaxZ, CHUNK_SIZE); const int32 CZ0 = FloorDivC(CaveMinZ, CHUNK_SIZE), CZ1 = FloorDivC(CaveMaxZ, CHUNK_SIZE);
// Une tuile très étalée (Step élevé) toucherait trop de chunks pour que cette vérification // Keep the parameter identity check bounded, but do not use the old 27-key cutoff here:
// reste bon marché. Au-delà, `Mixed` — on renonce au gain, jamais à la sûreté. // LOD4 legitimately covers 19^3 = 6,859 chunk keys. Returning Mixed at that point sent
// the tile straight to GenerateMesh, where GetDensityAt rebuilt one exact-chunk state for
// almost every coarse sample. The operator proof now shares its root morphology cache
// across refined children, so this scan is cheap compared with the fallback it prevents.
const int64 NumChunkCoords = (int64)(CX1 - CX0 + 1) * (int64)(CY1 - CY0 + 1) * (int64)(CZ1 - CZ0 + 1); const int64 NumChunkCoords = (int64)(CX1 - CX0 + 1) * (int64)(CY1 - CY0 + 1) * (int64)(CZ1 - CZ0 + 1);
if (NumChunkCoords > 27) constexpr int64 MaxClassifiedChunkCoords = 262144;
if (NumChunkCoords > MaxClassifiedChunkCoords)
{ {
INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams);
return EVoxelTileClass::Mixed; return EVoxelTileClass::Mixed;
@@ -4144,6 +4470,9 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
OpCtx.Step = Step; OpCtx.Step = Step;
OpCtx.WorldRadiusVoxels = WorldRadiusVoxels; OpCtx.WorldRadiusVoxels = WorldRadiusVoxels;
OpCtx.EdgeSealThickness = EdgeSealThickness; OpCtx.EdgeSealThickness = EdgeSealThickness;
OpCtx.bUseLatticeProof = true;
OpCtx.bTightenWarpProof = Step <= 2;
OpCtx.LatticeOriginVoxels = OriginVoxels;
FVoxelOpStack TileStack; FVoxelOpStack TileStack;
if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius, if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius,
@@ -4158,15 +4487,90 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
const FBox CaveBox(FVector((float)MinX, (float)MinY, (float)CaveMinZ), const FBox CaveBox(FVector((float)MinX, (float)MinY, (float)CaveMinZ),
FVector((float)MaxX, (float)MaxY, (float)CaveMaxZ)); FVector((float)MaxX, (float)MaxY, (float)CaveMaxZ));
const EVoxelTileClass StackVerdict = TileStack.ClassifyBox(CaveBox, OpCtx); const EVoxelTileClass StackVerdict = VF_ClassifyBoxWithWarpRetry(
TileStack, CaveBox, OpCtx, this, StrateManager);
if (bTask4Probe)
{
UE_LOG(LogTemp, Warning,
TEXT("[Task4Probe] phase=stack origin=(%d,%d,%d) step=%d verdict=%d solid=%d air=%d"),
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
static_cast<int32>(StackVerdict), bCanSolid ? 1 : 0, bCanAir ? 1 : 0);
}
if (StackVerdict == EVoxelTileClass::Mixed) if (StackVerdict == EVoxelTileClass::Mixed)
{ {
if (bTask4Probe)
{
int32 SolidKiller = INDEX_NONE;
int32 AirKiller = INDEX_NONE;
const EVoxelTileClass Attributed = TileStack.ClassifyBoxAttributed(
CaveBox, OpCtx, SolidKiller, AirKiller);
UE_LOG(LogTemp, Warning,
TEXT("[Task4Probe] stack_attributed origin=(%d,%d,%d) step=%d verdict=%d solid_killer=%d:%s air_killer=%d:%s"),
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
static_cast<int32>(Attributed), SolidKiller,
TileStack.GetOpDebugName(SolidKiller), AirKiller,
TileStack.GetOpDebugName(AirKiller));
int32 FinalPositive = 0;
int32 FinalNegative = 0;
int32 FinalZeroOrInvalid = 0;
float FinalMin = FLT_MAX;
float FinalMax = -FLT_MAX;
for (int32 Z = MinZ; Z <= MaxZ; Z += Step)
for (int32 Y = MinY; Y <= MaxY; Y += Step)
for (int32 X = MinX; X <= MaxX; X += Step)
{
const float FinalDensity = GetDensityAt((float)X, (float)Y, (float)Z);
if (!FMath::IsFinite(FinalDensity) || FinalDensity == 0.0f)
{
++FinalZeroOrInvalid;
}
else if (FinalDensity > 0.0f)
{
++FinalPositive;
FinalMin = FMath::Min(FinalMin, FinalDensity);
FinalMax = FMath::Max(FinalMax, FinalDensity);
}
else
{
++FinalNegative;
FinalMin = FMath::Min(FinalMin, FinalDensity);
FinalMax = FMath::Max(FinalMax, FinalDensity);
}
}
UE_LOG(LogTemp, Warning,
TEXT("[Task4Probe] final_samples origin=(%d,%d,%d) step=%d pos=%d neg=%d zero=%d min=%.6g max=%.6g"),
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
FinalPositive, FinalNegative, FinalZeroOrInvalid, FinalMin, FinalMax);
}
INC_DWORD_STAT(STAT_VoxelForgeCaveBailStackVerdict); INC_DWORD_STAT(STAT_VoxelForgeCaveBailStackVerdict);
return EVoxelTileClass::Mixed; return EVoxelTileClass::Mixed;
} }
if (StackVerdict == EVoxelTileClass::AllSolid) { bCanAir = false; } const bool bCaveOnlyTile = bAnyCave
else { bCanSolid = false; } && !bAnyGap && !bAnySurface && !bAnyOutOfLayout
&& CaveMinZ == MinZ && CaveMaxZ == MaxZ;
if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllSolid)
{
// The stack verdict covers the complete cave-only MC lattice. Its exact structural
// post path was supplied above, so a broad conservative passage guard must not force
// this already-certified solid tile through GenerateMesh.
bCanSolid = true;
bCanAir = false;
}
else if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllAir)
{
bCanSolid = false;
bCanAir = true;
}
else if (StackVerdict == EVoxelTileClass::AllSolid)
{
bCanAir = false;
}
else
{
bCanSolid = false;
}
// Le verdict cave se plie avec gap=solide, hors-layout=air, seals surface=solide. Si les // Le verdict cave se plie avec gap=solide, hors-layout=air, seals surface=solide. Si les
// deux hypothèses sont mortes ici, les catégories se contredisent : ce n'est PAS un échec // deux hypothèses sont mortes ici, les catégories se contredisent : ce n'est PAS un échec
@@ -4240,18 +4644,28 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
&& (Slots[0].InteriorZ.Num() > 0 || (NumSlots > 1 && Slots[1].InteriorZ.Num() > 0)); && (Slots[0].InteriorZ.Num() > 0 || (NumSlots > 1 && Slots[1].InteriorZ.Num() > 0));
if (bNeedColumns) if (bNeedColumns)
{ {
const int32 PreStride = FMath::Max(1, (GridDim + 1) / 4); const int32 PreStride = FMath::Max(1, GridDim / 4);
for (int32 gy = -1; gy <= GridDim; gy += PreStride) for (int32 gy = 0; gy <= CPA; gy += PreStride)
for (int32 gx = -1; gx <= GridDim; gx += PreStride) for (int32 gx = 0; gx <= CPA; gx += PreStride)
if (!TestColumn(gx, gy)) return EVoxelTileClass::Mixed; if (!TestColumn(gx, gy)) return EVoxelTileClass::Mixed;
for (int32 gy = -1; gy <= GridDim; ++gy) for (int32 gy = 0; gy <= CPA; ++gy)
for (int32 gx = -1; gx <= GridDim; ++gx) for (int32 gx = 0; gx <= CPA; ++gx)
if (!TestColumn(gx, gy)) return EVoxelTileClass::Mixed; if (!TestColumn(gx, gy)) return EVoxelTileClass::Mixed;
} }
// Ici exactement UNE hypothèse doit survivre (chaque point testé en tue une ; les tuiles // Ici exactement UNE hypothèse doit survivre (chaque point testé en tue une ; les tuiles
// sans point intérieur ont tué AllAir via gap/seal). Égalité = prudence → Mixed. // sans point intérieur ont tué AllAir via gap/seal). Égalité = prudence → Mixed.
if (bCanSolid == bCanAir) return EVoxelTileClass::Mixed; if (bCanSolid == bCanAir)
{
if (bTask4Probe)
{
UE_LOG(LogTemp, Warning,
TEXT("[Task4Probe] phase=final origin=(%d,%d,%d) step=%d verdict=0 solid=%d air=%d"),
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
bCanSolid ? 1 : 0, bCanAir ? 1 : 0);
}
return EVoxelTileClass::Mixed;
}
if (bAnyCave) if (bAnyCave)
{ {
// Compte seulement les verdicts FINAUX qui sautent réellement une tuile. Une pile peut avoir // Compte seulement les verdicts FINAUX qui sautent réellement une tuile. Une pile peut avoir
@@ -4261,6 +4675,13 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
if (bCanSolid) { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid); } if (bCanSolid) { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid); }
else { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackAir); } else { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackAir); }
} }
if (bTask4Probe)
{
UE_LOG(LogTemp, Warning,
TEXT("[Task4Probe] phase=final origin=(%d,%d,%d) step=%d verdict=%d solid=%d air=%d"),
OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z, Step,
bCanSolid ? 1 : 2, bCanSolid ? 1 : 0, bCanAir ? 1 : 0);
}
return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir; return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir;
} }
@@ -227,6 +227,85 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
MesherSetupTimer.End(); MesherSetupTimer.End();
// Optional 8x8x8-cell block proof. The tile classifier has already returned Mixed, but
// that is a conservative answer over the whole tile. A tile can still be uniformly solid
// (or uniformly air) when its smaller blocks are each provable. Do this before allocating
// or filling the density grid: the grid is only needed when at least one block can contain
// a surface. A whole-tile early return is allowed only when every relevant block has the
// SAME verdict; adjacent uniform blocks with opposite signs can contain a surface between
// them. This is a proof-only optimization: ClassifyTile is conservative and no samples or
// resolution are removed from a tile that reaches the mesher.
TArray<uint8> BlockSkip;
int32 BlocksPerAxis = 0;
if (bUseBlockEarlyOut && !OutCaptureGrid && Step == 1)
{
constexpr int32 BlockCells = 8;
BlocksPerAxis = (CellsPerAxis + BlockCells - 1) / BlockCells;
BlockSkip.Init(0, BlocksPerAxis * BlocksPerAxis * BlocksPerAxis);
bool bHaveUniformBlock = false;
EVoxelTileClass FirstUniformVerdict = EVoxelTileClass::Mixed;
bool bAllRelevantBlocksUniform = true;
bool bAllRelevantBlocksSame = true;
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);
}
if (!bHaveUniformBlock)
{
FirstUniformVerdict = Verdict;
bHaveUniformBlock = true;
}
else if (FirstUniformVerdict != Verdict)
{
bAllRelevantBlocksSame = false;
}
}
else
{
bAllRelevantBlocksUniform = false;
}
}
}
}
if (bHaveUniformBlock && bAllRelevantBlocksUniform && bAllRelevantBlocksSame)
{
return MeshData;
}
}
// Réutilise le tampon entre tuiles (thread_local) : SetNumUninitialized garde la // Réutilise le tampon entre tuiles (thread_local) : SetNumUninitialized garde la
// capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile. // capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile.
static thread_local TArray<float> DensityGrid; static thread_local TArray<float> DensityGrid;
@@ -285,58 +364,6 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
} }
MesherDensityGridTimer.End(); 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 ── // ── CAPTURE-DURING-MESHING ──
// Si demandé et que la tuile est pleine résolution (CellsPerAxis==CHUNK_SIZE ⇒ Step==1<<Level, // 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 // donc chaque point de grille = exactement une cellule du clipmap de densité), on recopie les
@@ -396,7 +423,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
if (ShouldAbortWork()) return FVoxelMeshData(); if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 cx = 0; cx < CellsPerAxis; cx++) for (int32 cx = 0; cx < CellsPerAxis; cx++)
{ {
if (bUseBlockEarlyOut if (bUseBlockEarlyOut && Step == 1
&& BlockSkip[((cz / 8) * BlocksPerAxis + (cy / 8)) * BlocksPerAxis + (cx / 8)] != 0) && BlockSkip[((cz / 8) * BlocksPerAxis + (cy / 8)) * BlocksPerAxis + (cx / 8)] != 0)
{ {
continue; continue;
@@ -2157,6 +2157,444 @@ bool UVoxelStrateManager::AnyPassageLandingFloorNearBox(
return false; return false;
} }
namespace
{
static bool VF_LatticeAxisRange(
float Min, float Max, float Origin, int32 Step,
int32& OutFirst, int32& OutLast)
{
if (Step <= 0 || !FMath::IsFinite(Min) || !FMath::IsFinite(Max)
|| !FMath::IsFinite(Origin) || Min > Max)
{
return false;
}
const float InvStep = 1.0f / static_cast<float>(Step);
OutFirst = FMath::CeilToInt((Min - Origin) * InvStep - 1.0e-4f);
OutLast = FMath::FloorToInt((Max - Origin) * InvStep + 1.0e-4f);
return OutFirst <= OutLast;
}
static bool VF_LatticeAxisNearestDistance(
float Min, float Max, float Origin, int32 Step, float Target, float& OutDistance)
{
int32 First = 0, Last = -1;
if (!VF_LatticeAxisRange(Min, Max, Origin, Step, First, Last))
{
return false;
}
const int32 Nearest = FMath::Clamp(
FMath::RoundToInt((Target - Origin) / static_cast<float>(Step)), First, Last);
OutDistance = FMath::Abs(
Origin + static_cast<float>(Nearest * Step) - Target);
return true;
}
static bool VF_LatticeBoxTouchesSphere(
const FBox& Box, const FIntVector& Origin, int32 Step,
const FVector& Center, float Radius)
{
float DX = 0.0f, DY = 0.0f, DZ = 0.0f;
if (!VF_LatticeAxisNearestDistance(
(float)Box.Min.X, (float)Box.Max.X, (float)Origin.X,
Step, (float)Center.X, DX)
|| !VF_LatticeAxisNearestDistance(
(float)Box.Min.Y, (float)Box.Max.Y, (float)Origin.Y,
Step, (float)Center.Y, DY)
|| !VF_LatticeAxisNearestDistance(
(float)Box.Min.Z, (float)Box.Max.Z, (float)Origin.Z,
Step, (float)Center.Z, DZ))
{
return false;
}
return DX * DX + DY * DY + DZ * DZ <= FMath::Square(Radius);
}
static bool VF_AnyPassageBoundTouchesLattice(
const TArray<FVoxelPassage>& Passages,
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step)
{
if (Step <= 0 || !VoxelBox.IsValid)
{
// An invalid query must never manufacture an identity proof.
return true;
}
for (const FVoxelPassage& Passage : Passages)
{
const float Reach = Passage.BoundRadius + 4.0f;
if (!FMath::IsFinite(Reach) || Reach < 0.0f)
{
// Invalid generated geometry is an unknown, not an empty passage set.
return true;
}
if (VF_LatticeBoxTouchesSphere(
VoxelBox, LatticeOrigin, Step, Passage.BoundCenter, Reach))
{
return true;
}
}
return false;
}
// MaxPassageCarveFactorNearLattice is queried once for the root and once for every refined
// child. The child lattice is a strict subset of the root lattice, so retaining the exact
// carve factor at each root point is a sound memo: a child reads the same values the old
// implementation would have recomputed, in the same deterministic order. The cache is
// worker-local because classifier calls can run concurrently and no mutable state may be
// published through the immutable strate manager.
struct FPassageLatticeBoundCache
{
const UVoxelStrateManager* Owner = nullptr;
uint64 OwnerLifetimeId = 0;
uint32 Version = 0xFFFFFFFFu;
FIntVector Origin = FIntVector::ZeroValue;
int32 Step = 0;
int32 FirstX = 0, LastX = -1;
int32 FirstY = 0, LastY = -1;
int32 FirstZ = 0, LastZ = -1;
bool bValid = false;
bool bNoCandidate = false;
TArray<float> Factors;
void Reset()
{
Owner = nullptr;
OwnerLifetimeId = 0;
Version = 0xFFFFFFFFu;
Origin = FIntVector::ZeroValue;
Step = 0;
FirstX = 0; LastX = -1;
FirstY = 0; LastY = -1;
FirstZ = 0; LastZ = -1;
bValid = false;
bNoCandidate = false;
Factors.Reset();
}
void Begin(const UVoxelStrateManager* InOwner, uint64 InLifetimeId,
uint32 InVersion, const FIntVector& InOrigin, int32 InStep,
int32 InFirstX, int32 InLastX,
int32 InFirstY, int32 InLastY,
int32 InFirstZ, int32 InLastZ,
bool bInNoCandidate)
{
Owner = InOwner;
OwnerLifetimeId = InLifetimeId;
Version = InVersion;
Origin = InOrigin;
Step = InStep;
FirstX = InFirstX; LastX = InLastX;
FirstY = InFirstY; LastY = InLastY;
FirstZ = InFirstZ; LastZ = InLastZ;
bValid = true;
bNoCandidate = bInNoCandidate;
Factors.Reset();
}
bool Contains(const UVoxelStrateManager* InOwner, uint64 InLifetimeId,
uint32 InVersion, const FIntVector& InOrigin, int32 InStep,
int32 InFirstX, int32 InLastX,
int32 InFirstY, int32 InLastY,
int32 InFirstZ, int32 InLastZ) const
{
return bValid && Owner == InOwner && OwnerLifetimeId == InLifetimeId
&& Version == InVersion && Origin == InOrigin && Step == InStep
&& InFirstX >= FirstX && InLastX <= LastX
&& InFirstY >= FirstY && InLastY <= LastY
&& InFirstZ >= FirstZ && InLastZ <= LastZ;
}
int32 Index(int32 IX, int32 IY, int32 IZ) const
{
const int32 DimX = LastX - FirstX + 1;
const int32 DimY = LastY - FirstY + 1;
return ((IZ - FirstZ) * DimY + (IY - FirstY)) * DimX + (IX - FirstX);
}
float MaxIn(const int32 InFirstX, const int32 InLastX,
const int32 InFirstY, const int32 InLastY,
const int32 InFirstZ, const int32 InLastZ) const
{
if (bNoCandidate) { return 0.0f; }
float Result = 0.0f;
for (int32 IZ = InFirstZ; IZ <= InLastZ; ++IZ)
{
for (int32 IY = InFirstY; IY <= InLastY; ++IY)
{
for (int32 IX = InFirstX; IX <= InLastX; ++IX)
{
Result = FMath::Max(Result, Factors[Index(IX, IY, IZ)]);
if (Result >= 1.0f)
{
return Result;
}
}
}
}
return Result;
}
};
FPassageLatticeBoundCache& VF_GetPassageLatticeBoundCache()
{
thread_local FPassageLatticeBoundCache Cache;
return Cache;
}
static bool VF_LatticeBoxTouchesAABB(
const FBox& Box, const FIntVector& Origin, int32 Step,
float MinX, float MaxX, float MinY, float MaxY, float MinZ, float MaxZ)
{
return VoxelPassageGeometry::LatticeAxisHasSampleInInterval(
FMath::Max((float)Box.Min.X, MinX),
FMath::Min((float)Box.Max.X, MaxX),
(float)Origin.X, Step)
&& VoxelPassageGeometry::LatticeAxisHasSampleInInterval(
FMath::Max((float)Box.Min.Y, MinY),
FMath::Min((float)Box.Max.Y, MaxY),
(float)Origin.Y, Step)
&& VoxelPassageGeometry::LatticeAxisHasSampleInInterval(
FMath::Max((float)Box.Min.Z, MinZ),
FMath::Min((float)Box.Max.Z, MaxZ),
(float)Origin.Z, Step);
}
}
bool UVoxelStrateManager::AnyPassageNearLattice(
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step) const
{
// This is deliberately only the conservative spatial candidate test. The old implementation
// evaluated the complete modifier SDF here and then evaluated it a second time in
// MaxPassageCarveFactorNearLattice during the same fold. A bound hit is sufficient to keep
// the carve hypothesis alive; MaxPassageCarveFactorNearLattice supplies the exact lattice
// amplitude before the fold can preserve AllSolid.
return VF_AnyPassageBoundTouchesLattice(
Passages, VoxelBox, LatticeOrigin, Step);
}
float UVoxelStrateManager::MaxPassageCarveFactorNearLattice(
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step) const
{
// The passage bound is only a cheap candidate test. It encloses an entire multi-segment
// passage, so using it as the final lattice proof turns a long tunnel into a false positive
// for every tile inside its enclosing sphere. That is particularly damaging at LOD0: the
// classifier then falls through to a full density grid even when every sampled point is rock.
//
// Once a bound reaches this lattice, ask the same exact modifier SDF used by
// ApplyPassageCarvingOnly at the actual MC samples. This is still a proof, not a sampling
// shortcut: the operator is evaluated at precisely the points the mesher will evaluate, and
// a non-finite result remains conservative. The common no-candidate path stays O(P), while a
// false-positive bound pays one exact lattice walk and can then be classified as Identity.
if (Step <= 0 || !VoxelBox.IsValid)
{
return 1.0f;
}
int32 FirstX = 0, LastX = -1;
int32 FirstY = 0, LastY = -1;
int32 FirstZ = 0, LastZ = -1;
if (!VF_LatticeAxisRange(
VoxelBox.Min.X, VoxelBox.Max.X, (float)LatticeOrigin.X, Step, FirstX, LastX)
|| !VF_LatticeAxisRange(
VoxelBox.Min.Y, VoxelBox.Max.Y, (float)LatticeOrigin.Y, Step, FirstY, LastY)
|| !VF_LatticeAxisRange(
VoxelBox.Min.Z, VoxelBox.Max.Z, (float)LatticeOrigin.Z, Step, FirstZ, LastZ))
{
return 1.0f;
}
FPassageLatticeBoundCache& Cache = VF_GetPassageLatticeBoundCache();
const uint64 LifetimeId = GetCacheLifetimeId();
const uint32 Version = GetLayoutVersion();
if (Cache.Contains(
this, LifetimeId, Version, LatticeOrigin, Step,
FirstX, LastX, FirstY, LastY, FirstZ, LastZ))
{
return Cache.MaxIn(FirstX, LastX, FirstY, LastY, FirstZ, LastZ);
}
if (!VF_AnyPassageBoundTouchesLattice(
Passages, VoxelBox, LatticeOrigin, Step))
{
Cache.Begin(
this, LifetimeId, Version, LatticeOrigin, Step,
FirstX, LastX, FirstY, LastY, FirstZ, LastZ,
/*bInNoCandidate*/ true);
return 0.0f;
}
constexpr float PassageCarveThreshold = 4.0f;
auto CarveFactorFromSDF = [](float ModifierSDF)
{
if (!(ModifierSDF < PassageCarveThreshold))
{
return 0.0f;
}
float CarveFactor = FMath::Clamp(
(PassageCarveThreshold - ModifierSDF)
/ (PassageCarveThreshold * 2.0f),
0.0f, 1.0f);
return SmoothStep01(CarveFactor);
};
const int64 DimX = static_cast<int64>(LastX) - FirstX + 1;
const int64 DimY = static_cast<int64>(LastY) - FirstY + 1;
const int64 DimZ = static_cast<int64>(LastZ) - FirstZ + 1;
const int64 SampleCount = DimX * DimY * DimZ;
constexpr int64 MaxCachedLatticeSamples = 65536;
if (SampleCount > 0 && SampleCount <= MaxCachedLatticeSamples)
{
Cache.Begin(
this, LifetimeId, Version, LatticeOrigin, Step,
FirstX, LastX, FirstY, LastY, FirstZ, LastZ,
/*bInNoCandidate*/ false);
Cache.Factors.SetNumUninitialized(static_cast<int32>(SampleCount));
float MaxCarveFactor = 0.0f;
for (int32 IZ = FirstZ; IZ <= LastZ; ++IZ)
{
const float Z = (float)LatticeOrigin.Z + (float)(IZ * Step);
for (int32 IY = FirstY; IY <= LastY; ++IY)
{
const float Y = (float)LatticeOrigin.Y + (float)(IY * Step);
for (int32 IX = FirstX; IX <= LastX; ++IX)
{
const float X = (float)LatticeOrigin.X + (float)(IX * Step);
const float ModifierSDF = EvaluateModifierSDF(X, Y, Z);
const float CarveFactor = FMath::IsFinite(ModifierSDF)
? CarveFactorFromSDF(ModifierSDF) : 1.0f;
Cache.Factors[Cache.Index(IX, IY, IZ)] = CarveFactor;
MaxCarveFactor = FMath::Max(MaxCarveFactor, CarveFactor);
if (MaxCarveFactor >= 1.0f)
{
// The remaining values are still filled below. Descendant queries need
// the complete exact lattice, not merely this query's maximum.
for (int32 RemainingZ = IZ; RemainingZ <= LastZ; ++RemainingZ)
{
const float RemainingWorldZ =
(float)LatticeOrigin.Z + (float)(RemainingZ * Step);
const int32 StartY = RemainingZ == IZ ? IY : FirstY;
for (int32 RemainingY = StartY; RemainingY <= LastY; ++RemainingY)
{
const float RemainingWorldY =
(float)LatticeOrigin.Y + (float)(RemainingY * Step);
const int32 StartX =
(RemainingZ == IZ && RemainingY == IY) ? IX + 1 : FirstX;
for (int32 RemainingX = StartX; RemainingX <= LastX; ++RemainingX)
{
const float RemainingWorldX =
(float)LatticeOrigin.X + (float)(RemainingX * Step);
const float RemainingSDF = EvaluateModifierSDF(
RemainingWorldX, RemainingWorldY, RemainingWorldZ);
Cache.Factors[Cache.Index(RemainingX, RemainingY, RemainingZ)] =
FMath::IsFinite(RemainingSDF)
? CarveFactorFromSDF(RemainingSDF) : 1.0f;
}
}
}
return MaxCarveFactor;
}
}
}
}
return MaxCarveFactor;
}
// Very large or unusual external queries do not enter the bounded worker memo. Keep the
// original exact walk for them; refusing the cache must never change the proof.
Cache.Reset();
float MaxCarveFactor = 0.0f;
for (int32 IZ = FirstZ; IZ <= LastZ; ++IZ)
{
const float Z = (float)LatticeOrigin.Z + (float)(IZ * Step);
for (int32 IY = FirstY; IY <= LastY; ++IY)
{
const float Y = (float)LatticeOrigin.Y + (float)(IY * Step);
for (int32 IX = FirstX; IX <= LastX; ++IX)
{
const float X = (float)LatticeOrigin.X + (float)(IX * Step);
const float ModifierSDF = EvaluateModifierSDF(X, Y, Z);
if (!FMath::IsFinite(ModifierSDF))
{
return 1.0f;
}
MaxCarveFactor = FMath::Max(
MaxCarveFactor, CarveFactorFromSDF(ModifierSDF));
if (MaxCarveFactor >= 1.0f)
{
return MaxCarveFactor;
}
}
}
}
return MaxCarveFactor;
}
bool UVoxelStrateManager::AnyPassageLandingFloorNearLattice(
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step) const
{
for (const FVoxelPassage& Passage : Passages)
{
const FVoxelPassageLanding* Landings[] = {
&Passage.UpperLanding, &Passage.LowerLanding };
for (const FVoxelPassageLanding* Landing : Landings)
{
if (Landing->HalfWidth <= 0.0f || Landing->FloorThickness <= 0.0f)
{
continue;
}
constexpr float Pad = 1.0f;
if (VF_LatticeBoxTouchesAABB(
VoxelBox, LatticeOrigin, Step,
Landing->StandingPoint.X - Landing->HalfWidth - Pad,
Landing->StandingPoint.X + Landing->HalfWidth + Pad,
Landing->StandingPoint.Y - Landing->HalfWidth - Pad,
Landing->StandingPoint.Y + Landing->HalfWidth + Pad,
Landing->FloorZ - Landing->FloorThickness - Pad,
Landing->FloorZ + Pad))
{
return true;
}
}
if (Passage.bWalkableTunnelContract
&& Passage.ControlPoints.Num() >= 2
&& Passage.ControlRadii.Num() == Passage.ControlPoints.Num())
{
constexpr float Pad = 1.0f;
for (int32 SegmentIndex = 0;
SegmentIndex + 1 < Passage.ControlPoints.Num(); ++SegmentIndex)
{
const FVector& A = Passage.ControlPoints[SegmentIndex];
const FVector& B = Passage.ControlPoints[SegmentIndex + 1];
const float SupportRadius = FMath::Max(
FMath::Min(
FMath::Abs(Passage.ControlRadii[SegmentIndex]),
FMath::Abs(Passage.ControlRadii[SegmentIndex + 1])) - 0.5f,
VoxelPassageGeometry::PlayerRadiusVoxels);
const float FloorA = VoxelPassageGeometry::TunnelFloorZ(
A, Passage.ControlRadii[SegmentIndex]);
const float FloorB = VoxelPassageGeometry::TunnelFloorZ(
B, Passage.ControlRadii[SegmentIndex + 1]);
if (VF_LatticeBoxTouchesAABB(
VoxelBox, LatticeOrigin, Step,
FMath::Min(A.X, B.X) - SupportRadius - Pad,
FMath::Max(A.X, B.X) + SupportRadius + Pad,
FMath::Min(A.Y, B.Y) - SupportRadius - Pad,
FMath::Max(A.Y, B.Y) + SupportRadius + Pad,
FMath::Min(FloorA, FloorB)
- VoxelPassageGeometry::LandingFloorThicknessVoxels - Pad,
FMath::Max(FloorA, FloorB) + Pad))
{
return true;
}
}
}
}
return false;
}
bool UVoxelStrateManager::AnyOriginLandingNearBox( bool UVoxelStrateManager::AnyOriginLandingNearBox(
const FVector& MinVoxel, const FVector& MaxVoxel) const const FVector& MinVoxel, const FVector& MaxVoxel) const
{ {
@@ -2199,6 +2637,44 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearBox(
return false; return false;
} }
bool UVoxelStrateManager::AnyOriginLandingNearLattice(
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step) const
{
if (OriginSpineRadius <= 0.0f) return false;
for (const FStrateSlot& Slot : StrateLayout)
{
if (Slot.Definition == nullptr) continue;
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
if (VoxelPassageGeometry::OriginLandingRoomTouchesLattice(
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius))
{
return true;
}
}
return false;
}
bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step) const
{
if (OriginSpineRadius <= 0.0f) return false;
for (const FStrateSlot& Slot : StrateLayout)
{
if (Slot.Definition == nullptr) continue;
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE;
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * CHUNK_SIZE;
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
if (VoxelPassageGeometry::OriginLandingFloorTouchesLattice(
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius))
{
return true;
}
}
return false;
}
//============================================================================= //=============================================================================
// QUERIES // QUERIES
//============================================================================= //=============================================================================
+34 -4
View File
@@ -1207,6 +1207,14 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
return false; return false;
} }
if (GVoxelForgeProfileTileGeneration != 0 && Result.RequestStartCycles != 0)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTileReady] level=%d empty=%d request_to_ready=%.6f"),
Result.Tile.Level, Result.bEmpty || !Result.Streams ? 1 : 0,
FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - Result.RequestStartCycles));
}
// Mark the tile loaded (even if empty — so we don't re-submit it). // Mark the tile loaded (even if empty — so we don't re-submit it).
LoadedTiles.Add(Result.Tile); LoadedTiles.Add(Result.Tile);
@@ -1270,6 +1278,8 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
} }
FChunkResult Result; FChunkResult Result;
Result.RequestStartCycles = GVoxelForgeProfileTileGeneration != 0
? FPlatformTime::Cycles64() : 0;
GenerateTileResult(Tile, OriginVoxels, Step, Cells, GenerationEpoch, /*bWantCapture*/ false, GenerateTileResult(Tile, OriginVoxels, Step, Cells, GenerationEpoch, /*bWantCapture*/ false,
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi, BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
/*bSheetTile*/ false, /*SheetChunkZ*/ 0, /*bSheetTile*/ false, /*SheetChunkZ*/ 0,
@@ -1966,6 +1976,8 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
return; // Budget full — wait for a task to finish. return; // Budget full — wait for a task to finish.
} }
PendingTiles.Add(Tile); PendingTiles.Add(Tile);
const uint64 RequestStartCycles = GVoxelForgeProfileTileGeneration != 0
? FPlatformTime::Cycles64() : 0;
const FIntVector OriginVoxels = Tile.OriginVoxels(); // min corner, voxel coords const FIntVector OriginVoxels = Tile.OriginVoxels(); // min corner, voxel coords
@@ -2019,6 +2031,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
FChunkResult Empty; FChunkResult Empty;
Empty.Tile = Tile; Empty.Tile = Tile;
Empty.Epoch = TaskEpoch; Empty.Epoch = TaskEpoch;
Empty.RequestStartCycles = RequestStartCycles;
ProcessQueue.Enqueue(MoveTemp(Empty)); ProcessQueue.Enqueue(MoveTemp(Empty));
return; return;
} }
@@ -2048,6 +2061,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
FChunkResult Empty; FChunkResult Empty;
Empty.Tile = Tile; Empty.Tile = Tile;
Empty.Epoch = TaskEpoch; Empty.Epoch = TaskEpoch;
Empty.RequestStartCycles = RequestStartCycles;
Empty.BandChunkLo = BandChunkLo; Empty.BandChunkLo = BandChunkLo;
Empty.BandChunkHi = BandChunkHi; Empty.BandChunkHi = BandChunkHi;
ProcessQueue.Enqueue(MoveTemp(Empty)); ProcessQueue.Enqueue(MoveTemp(Empty));
@@ -2069,7 +2083,8 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
: UE::Tasks::ETaskPriority::BackgroundNormal; : UE::Tasks::ETaskPriority::BackgroundNormal;
UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture, UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi, BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY]() bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
RequestStartCycles]()
{ {
// RAII: decrement the counter on every exit path. // RAII: decrement the counter on every exit path.
struct FTaskGuard struct FTaskGuard
@@ -2084,6 +2099,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture, GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi, BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY, Result); bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY, Result);
Result.RequestStartCycles = RequestStartCycles;
if (!ShouldAbortWork()) if (!ShouldAbortWork())
{ {
@@ -2103,14 +2119,21 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
{ {
const bool bProfileTile = GVoxelForgeProfileTileGeneration != 0; const bool bProfileTile = GVoxelForgeProfileTileGeneration != 0;
const double TileStartSeconds = bProfileTile ? FPlatformTime::Seconds() : 0.0; const double TileStartSeconds = bProfileTile ? FPlatformTime::Seconds() : 0.0;
double ClassifySeconds = 0.0;
double MeshSeconds = 0.0;
double StreamSeconds = 0.0;
int32 ClassifyVerdict = -1; // Mixed = 0, AllSolid = 1, AllAir = 2
auto EmitTileProfile = [&]() auto EmitTileProfile = [&]()
{ {
if (bProfileTile) if (bProfileTile)
{ {
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTileProfile] level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d seconds=%.6f"), TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d "
Tile.Level, Step, Cells, bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0, "verdict=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f"),
Result.bEmpty ? 1 : 0, FPlatformTime::Seconds() - TileStartSeconds); Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells,
bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0,
Result.bEmpty ? 1 : 0, ClassifyVerdict, ClassifySeconds, MeshSeconds, StreamSeconds,
FPlatformTime::Seconds() - TileStartSeconds);
} }
}; };
@@ -2151,7 +2174,10 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
{ {
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile); TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile);
INC_DWORD_STAT(STAT_VoxelForgeTilesClassified); INC_DWORD_STAT(STAT_VoxelForgeTilesClassified);
const double ClassifyStartSeconds = bProfileTile ? FPlatformTime::Seconds() : 0.0;
const EVoxelTileClass Verdict = Generator->ClassifyTile(OriginVoxels, Step, Cells); const EVoxelTileClass Verdict = Generator->ClassifyTile(OriginVoxels, Step, Cells);
ClassifySeconds = bProfileTile ? FPlatformTime::Seconds() - ClassifyStartSeconds : 0.0;
ClassifyVerdict = static_cast<int32>(Verdict);
if (ShouldAbortWork()) if (ShouldAbortWork())
{ {
AbortResult(); AbortResult();
@@ -2178,12 +2204,14 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
if (!bTrivialEmpty) if (!bTrivialEmpty)
{ {
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GenerateMesh); TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GenerateMesh);
const double MeshStartSeconds = bProfileTile ? FPlatformTime::Seconds() : 0.0;
MeshData = bSheetTile MeshData = bSheetTile
? Mesher->GenerateSheetMesh(OriginVoxels, Step, Cells, SheetChunkZ, ? Mesher->GenerateSheetMesh(OriginVoxels, Step, Cells, SheetChunkZ,
HoleMinX, HoleMinY, HoleMaxX, HoleMaxY) HoleMinX, HoleMinY, HoleMaxX, HoleMaxY)
: Mesher->GenerateMesh(OriginVoxels, Step, Cells, : Mesher->GenerateMesh(OriginVoxels, Step, Cells,
bWantCapture ? &Result.CaptureGrid : nullptr, bWantCapture ? &Result.CaptureGrid : nullptr,
BandVoxLo, BandVoxHi); BandVoxLo, BandVoxHi);
MeshSeconds = bProfileTile ? FPlatformTime::Seconds() - MeshStartSeconds : 0.0;
INC_DWORD_STAT(STAT_VoxelForgeTilesMeshed); INC_DWORD_STAT(STAT_VoxelForgeTilesMeshed);
if (ShouldAbortWork()) if (ShouldAbortWork())
{ {
@@ -2198,8 +2226,10 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
if (!MeshData.IsEmpty()) if (!MeshData.IsEmpty())
{ {
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_BuildStreams); TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_BuildStreams);
const double StreamStartSeconds = bProfileTile ? FPlatformTime::Seconds() : 0.0;
Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>(); Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>();
BuildTileStreamSet(*Result.Streams, MeshData); BuildTileStreamSet(*Result.Streams, MeshData);
StreamSeconds = bProfileTile ? FPlatformTime::Seconds() - StreamStartSeconds : 0.0;
if (ShouldAbortWork()) if (ShouldAbortWork())
{ {
AbortResult(); AbortResult();
+74 -5
View File
@@ -102,6 +102,7 @@
#include "VoxelTypes.h" // CHUNK_SIZE, EVoxelTileClass #include "VoxelTypes.h" // CHUNK_SIZE, EVoxelTileClass
struct FBiomeContext; struct FBiomeContext;
struct FTunnelCoreWorldEvaluation;
//============================================================================= //=============================================================================
// LES QUATRE RÔLES / THE FOUR ROLES // LES QUATRE RÔLES / THE FOUR ROLES
@@ -284,6 +285,18 @@ struct FVoxelOpContext
float StrateTopWorldZ = 0.0f; float StrateTopWorldZ = 0.0f;
float StrateBottomWorldZ = 0.0f; float StrateBottomWorldZ = 0.0f;
// ClassifyTile may ask box bounds about the exact marching-cubes lattice rather than the
// continuous volume between samples. Production voxel evaluation leaves this disabled;
// when enabled, operators may tighten a conservative bound only for points
// LatticeOriginVoxels + n * Step that lie inside the queried box.
bool bUseLatticeProof = false;
FIntVector LatticeOriginVoxels = FIntVector::ZeroValue;
// A mixed tile may opt into a tighter, still conservative finite-box bound for domain-warped
// sources. The normal classifier leaves this off; ClassifyTile retries only the few boxes
// that the cheap global proof could not resolve.
bool bTightenWarpProof = false;
// null = cette strate n'a pas de champ de biome. // null = cette strate n'a pas de champ de biome.
const FBiomeContext* Biome = nullptr; const FBiomeContext* Biome = nullptr;
}; };
@@ -457,6 +470,14 @@ public:
*/ */
virtual void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const = 0; virtual void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const = 0;
/** Optional hand-off for the common structural tail. The default keeps custom operators
* conservative: the caller uses its canonical cache path when no source publishes a result. */
virtual bool TryGetLastTunnelCoreWorldEvaluation(
FTunnelCoreWorldEvaluation& OutEvaluation) const
{
return false;
}
/** /**
* CONSERVATIF. The two-argument method is the intrinsic fallback. The state-aware overload * CONSERVATIF. The two-argument method is the intrinsic fallback. The state-aware overload
* below is what the stack calls: it sees the SDF interval already produced by earlier ops. * below is what the stack calls: it sees the SDF interval already produced by earlier ops.
@@ -717,7 +738,9 @@ FORCEINLINE void VF_ForceHypotheses(FVoxelBoxHypotheses& H, EVoxelTileClass Forc
* c'est la propriété de sûreté, et le pliage numérique ne l'affaiblit pas : une marge ne peut que * c'est la propriété de sûreté, et le pliage numérique ne l'affaiblit pas : une marge ne peut que
* DESCENDRE, jamais remonter, en dehors d'un opérateur forçant. * DESCENDRE, jamais remonter, en dehors d'un opérateur forçant.
* *
* ⚠️ `MaxCarve` / `MaxFill` valent `FLT_MAX` par défaut = « amplitude inconnue ». La soustraction * ⚠️ `MaxCarve` / `MaxFill` valent `FLT_MAX` par défaut = « amplitude inconnue ». Une borne finie
* nulle signifie au contraire « identité prouvée » pour cette direction : l'opérateur peut être
* géométriquement présent mais ne modifie aucun échantillon de la boîte. La soustraction
* fait alors passer la marge très en dessous de zéro et l'hypothèse meurt, exactement comme la * fait alors passer la marge très en dessous de zéro et l'hypothèse meurt, exactement comme la
* version purement directionnelle de ce pliage. Aucun opérateur existant ne change de verdict. * version purement directionnelle de ce pliage. Aucun opérateur existant ne change de verdict.
* (Arithmétique volontairement laissée en float sans garde : `0 − FLT_MAX` vaut `−FLT_MAX`, * (Arithmétique volontairement laissée en float sans garde : `0 − FLT_MAX` vaut `−FLT_MAX`,
@@ -733,21 +756,67 @@ FORCEINLINE void VF_FoldEffect(FVoxelBoxHypotheses& H, EVoxelOpEffect Effect,
break; break;
case EVoxelOpEffect::CarveOnly: case EVoxelOpEffect::CarveOnly:
// A finite zero bound is a proved identity for this direction. This matters for
// state-aware structural guards: a passage may be geometrically present in a box while
// its exact lattice carve factor is zero, and that must not kill an otherwise valid
// AllSolid hypothesis merely because its margin is zero/unknown.
if (MaxCarve == 0.0f)
{
break;
}
if (!(MaxCarve >= 0.0f))
{
H.bCanBeAllSolid = false;
H.SolidMargin = -FLT_MAX;
break;
}
H.SolidMargin -= MaxCarve; H.SolidMargin -= MaxCarve;
if (!(H.SolidMargin > 0.0f)) { H.bCanBeAllSolid = false; } if (!(H.SolidMargin > 0.0f)) { H.bCanBeAllSolid = false; }
break; break;
case EVoxelOpEffect::FillOnly: case EVoxelOpEffect::FillOnly:
if (MaxFill == 0.0f)
{
break;
}
if (!(MaxFill >= 0.0f))
{
H.bCanBeAllAir = false;
H.AirMargin = -FLT_MAX;
break;
}
H.AirMargin -= MaxFill; H.AirMargin -= MaxFill;
if (!(H.AirMargin > 0.0f)) { H.bCanBeAllAir = false; } if (!(H.AirMargin > 0.0f)) { H.bCanBeAllAir = false; }
break; break;
case EVoxelOpEffect::Both: case EVoxelOpEffect::Both:
default: default:
H.SolidMargin -= MaxCarve; if (MaxCarve != 0.0f)
if (!(H.SolidMargin > 0.0f)) { H.bCanBeAllSolid = false; } {
H.AirMargin -= MaxFill; if (!(MaxCarve >= 0.0f))
if (!(H.AirMargin > 0.0f)) { H.bCanBeAllAir = false; } {
H.bCanBeAllSolid = false;
H.SolidMargin = -FLT_MAX;
}
else
{
H.SolidMargin -= MaxCarve;
if (!(H.SolidMargin > 0.0f)) { H.bCanBeAllSolid = false; }
}
}
if (MaxFill != 0.0f)
{
if (!(MaxFill >= 0.0f))
{
H.bCanBeAllAir = false;
H.AirMargin = -FLT_MAX;
}
else
{
H.AirMargin -= MaxFill;
if (!(H.AirMargin > 0.0f)) { H.bCanBeAllAir = false; }
}
}
break; break;
} }
} }
@@ -174,6 +174,10 @@ public:
return -EvalInternal(WorldX, WorldY, WorldZ); return -EvalInternal(WorldX, WorldY, WorldZ);
} }
/** Return a same-sample structural-core result published by a source in this stack. */
bool TryGetLastTunnelCoreWorldEvaluation(
FTunnelCoreWorldEvaluation& OutEvaluation) const;
/** /**
* Le pliage générique qui remplacera les gardes écrites à la main dans ClassifyTile. * Le pliage générique qui remplacera les gardes écrites à la main dans ClassifyTile.
* Voir `VF_FoldOp` (VoxelDensityOp.h) pour la sémantique — en particulier pourquoi un * Voir `VF_FoldOp` (VoxelDensityOp.h) pour la sémantique — en particulier pourquoi un
@@ -182,10 +186,25 @@ public:
EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
{ {
FVoxelBoxHypotheses H; FVoxelBoxHypotheses H;
const bool bTask4OpProbe = Ctx.bUseLatticeProof
&& Ctx.LatticeOriginVoxels == FIntVector(32, 0, -32)
&& Ctx.Step == 1;
for (const FOpEntry& Entry : Ops) for (const FOpEntry& Entry : Ops)
{ {
const uint64 Task4OpStartCycles = bTask4OpProbe
? FPlatformTime::Cycles64() : 0;
VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx, VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx,
(Entry.Writes & VoxelOpChannels::Sdf) != 0); (Entry.Writes & VoxelOpChannels::Sdf) != 0);
if (bTask4OpProbe)
{
UE_LOG(LogTemp, Warning,
TEXT("[Task4OpProbe] box=(%.0f,%.0f,%.0f)-(%.0f,%.0f,%.0f) op=%s ms=%.3f"),
(float)VoxelBox.Min.X, (float)VoxelBox.Min.Y, (float)VoxelBox.Min.Z,
(float)VoxelBox.Max.X, (float)VoxelBox.Max.Y, (float)VoxelBox.Max.Z,
Entry.Op->DebugName(),
FPlatformTime::ToMilliseconds64(
FPlatformTime::Cycles64() - Task4OpStartCycles));
}
// Do not early-out on a dead hypothesis: a later forcing structural post may // Do not early-out on a dead hypothesis: a later forcing structural post may
// deliberately overwrite it (the XY edge seal is appended after passage tube/landing // deliberately overwrite it (the XY edge seal is appended after passage tube/landing
// carving and support). // carving and support).
@@ -147,9 +147,12 @@ public:
// l'écart vers un voisin un niveau plus grossier (cellule 2×). Monter si des fissures persistent. // l'écart vers un voisin un niveau plus grossier (cellule 2×). Monter si des fissures persistent.
float SkirtCells = 2.0f; float SkirtCells = 2.0f;
// Optional conservative block classifier. When enabled, 8x8x8 cell regions proven uniformly // Conservative 8x8x8 block classifier. Uniform blocks skip the marching-cubes cell loop;
// solid or air by UVoxelGenerator::ClassifyTile skip the marching-cubes cell loop. The density // when every relevant block has the same verdict, the density grid is skipped as well.
// grid is still produced in full, so this switch cannot change samples or geometry. // ClassifyTile is proof-only, so no samples, resolution, or geometry are removed.
// The tile classifier is the cheap, whole-tile proof. Keep the optional 8^3-block refinement
// available for focused diagnostics, but leave it disabled in streaming: repeating a proof on
// every mixed tile can cost more than the density grid it is meant to avoid.
bool bUseBlockEarlyOut = false; bool bUseBlockEarlyOut = false;
// T2.b — LOD-aware octave reduction (opt-in, copied from UVoxelSettings::LODOctaveDrop). // T2.b — LOD-aware octave reduction (opt-in, copied from UVoxelSettings::LODOctaveDrop).
@@ -433,6 +433,73 @@ namespace VoxelPassageGeometry
return MaxA >= MinB && MinA <= MaxB; return MaxA >= MinB && MinA <= MaxB;
} }
/** True when the exact MC lattice has at least one sample in a closed interval. */
FORCEINLINE bool LatticeAxisHasSampleInInterval(
float Min, float Max, float Origin, int32 Step)
{
if (Step <= 0 || !FMath::IsFinite(Min) || !FMath::IsFinite(Max)
|| !FMath::IsFinite(Origin) || Min > Max)
{
return false;
}
const float InvStep = 1.0f / static_cast<float>(Step);
const int32 First = FMath::CeilToInt(
(Min - Origin) * InvStep - 1.0e-4f);
const int32 Last = FMath::FloorToInt(
(Max - Origin) * InvStep + 1.0e-4f);
return First <= Last;
}
FORCEINLINE bool OriginLandingRoomTouchesLattice(
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
if (!VoxelBox.IsValid) return false;
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
const float Pad = LandingCarveBlendVoxels;
return LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.X, -Geometry.HalfWidth - Pad),
FMath::Min((float)VoxelBox.Max.X, Geometry.HalfWidth + Pad),
(float)LatticeOrigin.X, Step)
&& LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.Y, -Geometry.HalfWidth - Pad),
FMath::Min((float)VoxelBox.Max.Y, Geometry.HalfWidth + Pad),
(float)LatticeOrigin.Y, Step)
&& LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.Z, Geometry.FloorZ - Pad),
FMath::Min((float)VoxelBox.Max.Z, Geometry.CeilingZ + Pad),
(float)LatticeOrigin.Z, Step);
}
FORCEINLINE bool OriginLandingFloorTouchesLattice(
const FBox& VoxelBox, const FIntVector& LatticeOrigin, int32 Step,
float StrateTopZ, float StrateBottomZ, float SealThickness, float OriginRadius)
{
if (!VoxelBox.IsValid) return false;
const FOriginLandingGeometry Geometry = BuildOriginLandingGeometry(
StrateTopZ, StrateBottomZ, SealThickness, OriginRadius);
if (!Geometry.bValid) return false;
constexpr float ProofPad = 1.0f;
const float FloorHalfWidth = FMath::Max(
Geometry.HalfWidth - 1.0f, 0.0f);
return LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.X, -FloorHalfWidth - ProofPad),
FMath::Min((float)VoxelBox.Max.X, FloorHalfWidth + ProofPad),
(float)LatticeOrigin.X, Step)
&& LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.Y, -FloorHalfWidth - ProofPad),
FMath::Min((float)VoxelBox.Max.Y, FloorHalfWidth + ProofPad),
(float)LatticeOrigin.Y, Step)
&& LatticeAxisHasSampleInInterval(
FMath::Max((float)VoxelBox.Min.Z,
Geometry.FloorZ - Geometry.FloorThickness - ProofPad),
FMath::Min((float)VoxelBox.Max.Z, Geometry.FloorZ + ProofPad),
(float)LatticeOrigin.Z, Step);
}
/** Conservative influence box for the origin room's air carve. */ /** Conservative influence box for the origin room's air carve. */
FORCEINLINE bool OriginLandingRoomTouchesBox( FORCEINLINE bool OriginLandingRoomTouchesBox(
const FBox& VoxelBox, const FBox& VoxelBox,
@@ -420,10 +420,25 @@ public:
/** Conservative box guard for the floor fill; unlike a tube it can affect the all-air proof. */ /** Conservative box guard for the floor fill; unlike a tube it can affect the all-air proof. */
bool AnyPassageLandingFloorNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const; bool AnyPassageLandingFloorNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
/** Exact-lattice counterparts used by ClassifyTile's MC-grid proof. */
bool AnyPassageNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step) const;
/** Exact maximum of the passage carve factor on the mesher lattice; 0 means no tube/landing
* carve reaches a sampled point. Invalid geometry returns 1 conservatively. */
float MaxPassageCarveFactorNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step) const;
bool AnyPassageLandingFloorNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step) const;
/** Conservative box guards for the per-strate (0,0) landing rooms and their floor slabs. */ /** Conservative box guards for the per-strate (0,0) landing rooms and their floor slabs. */
bool AnyOriginLandingNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const; bool AnyOriginLandingNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
bool AnyOriginLandingFloorNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const; bool AnyOriginLandingFloorNearBox(const FVector& MinVoxel, const FVector& MaxVoxel) const;
bool AnyOriginLandingNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step) const;
bool AnyOriginLandingFloorNearLattice(const FBox& VoxelBox,
const FIntVector& LatticeOrigin, int32 Step) const;
/** Get all generated passages (for debug display). */ /** Get all generated passages (for debug display). */
const TArray<FVoxelPassage>& GetPassages() const { return Passages; } const TArray<FVoxelPassage>& GetPassages() const { return Passages; }
+3
View File
@@ -84,6 +84,9 @@ struct FChunkResult
// Null ⇒ empty/all-air tile (no component). // Null ⇒ empty/all-air tile (no component).
TSharedPtr<RealtimeMesh::FRealtimeMeshStreamSet> Streams; TSharedPtr<RealtimeMesh::FRealtimeMeshStreamSet> Streams;
uint32 Epoch = 0; // Generation epoch — discard if stale uint32 Epoch = 0; // Generation epoch — discard if stale
// Monotonic request timestamp used only for streaming telemetry. It is carried through the
// worker queue so the profile can report request-to-ready, not just worker generation time.
uint64 RequestStartCycles = 0;
bool bAborted = false; // Worker observed shutdown; never mark this tile loaded bool bAborted = false; // Worker observed shutdown; never mark this tile loaded
bool bEmpty = true; // true ⇒ all-air tile (Streams null); still marked loaded so we don't re-submit bool bEmpty = true; // true ⇒ all-air tile (Streams null); still marked loaded so we don't re-submit
// F17 — the mesher classifies every triangle semantically (sky-cap = down-facing near the // F17 — the mesher classifies every triangle semantically (sky-cap = down-facing near the