standalone: per-tile speed at plugin level, streaming like the game, softer lighting

Cause found by profiling: room/SDF graph-cache churn (exact XY bounds + sample order rebuilt it
constantly). Region grouping (one LOD3 tile: 1,084 -> 229 builds), taller-tile traversal for LOD6
(36,480 -> 12,160), extent-dependent cache slots, lambdas instead of recursive std::function.
Serial ms/tile, 64 nearest tiles per level (before -> after):
  L0 50.4 -> 50.6, L1 112 -> 110, L2 24 -> 22, L3 223 -> 73, L4 269 -> 134, L5 393 -> 127, L6 283 -> 145.
Hashes of all levels unchanged; parity 3,436/3,436 bit-identical.
Full clipmap, 16 workers BelowNormal: full detail 66.6 s, all 12,054 tiles 175 s. In-stream L0 tail is
high (median 83 ms, mean 198 ms, p95 669 ms) vs 51 ms serial.
Streaming: recentre keeps the intersection, generates only missing tiles nearest/finest first;
one crossing = exactly 441 new tiles in 6.1 s, no stale results.
Lighting: normals already come from the density gradient; the striping was the squared headlamp;
broad linear fill + warmer palette. Daytime CPU rule documented in Standalone/README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-03 06:49:41 +02:00
1 parent 3f300b912a
commit 81b7d6d0ff
24 files changed
+1096 -196

No files matched your search

@@ -7750,7 +7750,7 @@ void VoxelCaveMorphology::BuildChunkCache(
GraphLowLink.Init(INDEX_NONE, NumRooms);
GraphBridges.Init(0u, GraphEdges.Num());
int32 GraphDiscoveryTime = 0;
TFunction<void(int32, int32)> VisitGraph = [&](int32 Node, int32 ParentEdge)
const auto VisitGraph = [&](auto&& VisitGraphSelf, int32 Node, int32 ParentEdge) -> void
{
GraphDiscovery[Node] = GraphDiscoveryTime;
GraphLowLink[Node] = GraphDiscoveryTime;
@@ -7765,7 +7765,7 @@ void VoxelCaveMorphology::BuildChunkCache(
const int32 Other = Edge.RoomA == Node ? Edge.RoomB : Edge.RoomA;
if (GraphDiscovery[Other] == INDEX_NONE)
{
VisitGraph(Other, EdgeIndex);
VisitGraphSelf(VisitGraphSelf, Other, EdgeIndex);
GraphLowLink[Node] = FMath::Min(
GraphLowLink[Node], GraphLowLink[Other]);
if (GraphLowLink[Other] > GraphDiscovery[Node])
@@ -7784,7 +7784,7 @@ void VoxelCaveMorphology::BuildChunkCache(
{
if (GraphDiscovery[Node] == INDEX_NONE)
{
VisitGraph(Node, INDEX_NONE);
VisitGraph(VisitGraph, Node, INDEX_NONE);
}
}
for (int32 EdgeIndex = 0; EdgeIndex < GraphEdges.Num(); ++EdgeIndex)
@@ -7803,7 +7803,7 @@ void VoxelCaveMorphology::BuildChunkCache(
TArray<uint8, TInlineAllocator<128>> GraphForestEdges;
GraphForestVisited.Init(0u, NumRooms);
GraphForestEdges.Init(0u, GraphEdges.Num());
TFunction<void(int32)> VisitWalkForest = [&](int32 Node)
const auto VisitWalkForest = [&](auto&& VisitWalkForestSelf, int32 Node) -> void
{
GraphForestVisited[Node] = 1u;
for (const int32 EdgeIndex : GraphAdjacency[Node])
@@ -7815,14 +7815,14 @@ void VoxelCaveMorphology::BuildChunkCache(
continue;
}
GraphForestEdges[EdgeIndex] = 1u;
VisitWalkForest(Other);
VisitWalkForestSelf(VisitWalkForestSelf, Other);
}
};
for (int32 Node = 0; Node < NumRooms; ++Node)
{
if (GraphForestVisited[Node] == 0u)
{
VisitWalkForest(Node);
VisitWalkForest(VisitWalkForest, Node);
}
}
for (int32 EdgeIndex = 0; EdgeIndex < GraphEdges.Num(); ++EdgeIndex)
@@ -704,6 +704,28 @@ namespace VoxelDensityProfile
case ECounter::MesherOutputAllocatedBytes: return TEXT("MesherOutputAllocatedBytes");
case ECounter::OpBlockScratchBytes: return TEXT("OpBlockScratchBytes");
case ECounter::OpBlockCopyBytes: return TEXT("OpBlockCopyBytes");
case ECounter::SdfRebuildStrate: return TEXT("SdfRebuildStrate");
case ECounter::SdfRebuildSeed: return TEXT("SdfRebuildSeed");
case ECounter::SdfRebuildFingerprint: return TEXT("SdfRebuildFingerprint");
case ECounter::SdfRebuildLayout: return TEXT("SdfRebuildLayout");
case ECounter::SdfRebuildManagerLifetime: return TEXT("SdfRebuildManagerLifetime");
case ECounter::SdfRebuildTerrainOps: return TEXT("SdfRebuildTerrainOps");
case ECounter::SdfRebuildFusedPolicy: return TEXT("SdfRebuildFusedPolicy");
case ECounter::SdfRebuildTilePolicy: return TEXT("SdfRebuildTilePolicy");
case ECounter::SdfRebuildTileIdentity: return TEXT("SdfRebuildTileIdentity");
case ECounter::SdfRebuildBoundsX: return TEXT("SdfRebuildBoundsX");
case ECounter::SdfRebuildBoundsY: return TEXT("SdfRebuildBoundsY");
case ECounter::SdfTileWindowAccepted: return TEXT("SdfTileWindowAccepted");
case ECounter::SdfTileWindowRejected: return TEXT("SdfTileWindowRejected");
case ECounter::TilePolicyKeyOrigin: return TEXT("TilePolicyKeyOrigin");
case ECounter::TilePolicyKeyStep: return TEXT("TilePolicyKeyStep");
case ECounter::TilePolicyKeyCells: return TEXT("TilePolicyKeyCells");
case ECounter::TilePolicyKeyFingerprint: return TEXT("TilePolicyKeyFingerprint");
case ECounter::TilePolicyKeyLayout: return TEXT("TilePolicyKeyLayout");
case ECounter::TilePolicyKeyManager: return TEXT("TilePolicyKeyManager");
case ECounter::TilePolicyKeyTerrainOps: return TEXT("TilePolicyKeyTerrainOps");
case ECounter::TilePolicyKeyFusedPath: return TEXT("TilePolicyKeyFusedPath");
case ECounter::TilePolicyKeyContext: return TEXT("TilePolicyKeyContext");
case ECounter::Count: break;
}
return TEXT("Unknown");
@@ -32,6 +32,7 @@
#endif
#include <atomic>
#include <array>
namespace VoxelMath
{
@@ -139,6 +140,24 @@ namespace
TEXT("voxel.TileCacheWindowOpStackMinLOD"),
GVoxelForgeTileCacheWindowOpStackMinLOD,
TEXT("Minimum SampleStep LOD for the op-stack tile-sized room cache window."));
int32 GVoxelForgeRoomGraphCacheSlots = 1;
FAutoConsoleVariableRef CVarVoxelForgeRoomGraphCacheSlots(
TEXT("voxel.RoomGraphCacheSlots"),
GVoxelForgeRoomGraphCacheSlots,
TEXT("Per-worker exact room-graph windows to retain during density sampling (1-4)."));
bool GVoxelForgeRoomGraphCacheSlotsParsed = false;
void VF_ParseRoomGraphCacheSlots()
{
if (GVoxelForgeRoomGraphCacheSlotsParsed) return;
GVoxelForgeRoomGraphCacheSlotsParsed = true;
int32 CommandLineValue = GVoxelForgeRoomGraphCacheSlots;
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.RoomGraphCacheSlots="), CommandLineValue))
{
GVoxelForgeRoomGraphCacheSlots = CommandLineValue;
}
GVoxelForgeRoomGraphCacheSlots = FMath::Clamp(GVoxelForgeRoomGraphCacheSlots, 1, 4);
}
int32 GVoxelForgeSpatialIndex = 1;
FAutoConsoleVariableRef CVarVoxelForgeSpatialIndex(
@@ -879,10 +898,23 @@ void VoxelGenLOD::InitializeConsoleSwitches()
VF_ParseFusedEvaluatorSwitch();
VF_ParseWormBlockSkipSwitch();
VF_ParseTileCacheWindowSwitch();
VF_ParseRoomGraphCacheSlots();
VF_ParseSpatialIndexSwitch();
VF_ParseChunkRoutingCacheSwitch();
}
void VoxelGenLOD::SetUseFusedEvaluatorEnabled(bool bEnabled)
{
VF_ParseFusedEvaluatorSwitch();
GVoxelForgeUseFusedEvaluator = bEnabled ? 1 : 0;
}
bool VoxelGenLOD::IsUseFusedEvaluatorEnabled()
{
VF_ParseFusedEvaluatorSwitch();
return GVoxelForgeUseFusedEvaluator != 0;
}
bool VoxelGenLOD::IsTileCacheWindowEnabled(bool bFusedPath)
{
if (GVoxelForgeTileCacheWindow == 0)
@@ -4171,38 +4203,31 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
//
// Declared before the RoomDensity if-block so SDFCache and NearestRoomIdx
// are also visible to the terrain ops block further below.
thread_local FChunkSDFCache SDFCache;
// Cache validity is tracked by the SEARCH BOX the cache was built for, NOT by chunk
// equality. Gradient-normal sampling queries at WorldX±1 (and warp displacement) can
// step a voxel outside the chunk; with chunk-equality keying that flipped the key and
// rebuilt the (now expensive) cache every boundary cell. Since the stored rooms cover
// the search box + MaxInfluence, any query INSIDE the box is correct — so we only
// rebuild when the query actually leaves the box. Result: one build per chunk, no thrash.
thread_local float CachedSMinX = 1.0f, CachedSMaxX = -1.0f; // start invalid (min > max)
thread_local float CachedSMinY = 0.0f, CachedSMaxY = 0.0f;
thread_local int32 CachedStrate = INT32_MIN;
thread_local uint32 CachedSeed = 0;
// ⚠️ AUDIT §C2 — `CachedFingerprint` et `CachedLayout` ci-dessous sont OBLIGATOIRES dans cette clé.
// La clé ci-dessus décrit la GÉOMÉTRIE de la fenêtre (boîte, strate, seed) et rien de ce qui
// détermine les PARAMS avec lesquels les salles ont été cuites. Comme `GetGenerationParams`
// blende à l'intérieur d'une strate (`Alpha` = f(chunk Z), et f(chunk XY) aussi en
// `Interleaved`), deux chunks voisins produisent la MÊME clé avec des params DIFFÉRENTS, et le
// deuxième se sert des salles du premier. Non déterministe entre pairs, parce que l'ordre des
// workers décide lequel est « le premier » — exactement ce que §2.6.1 interdit.
//
// Pourquoi ça ne casse PAS l'invariant de perf de §8.10 : la clé reste une BOÎTE, donc les
// sondes de gradient à `WorldX ± 1` ne font toujours pas tourner le cache. Ce qui le fait
// tourner en plus, c'est un changement RÉEL de params — une fois par chunk dans une bande de
// transition, ce qui est exactement le nombre de reconstructions nécessaire.
thread_local uint32 CachedFingerprint = 0xFFFFFFFFu;
thread_local uint32 CachedLayout = 0xFFFFFFFFu;
thread_local bool CachedUsesFusedCacheWindow = false;
thread_local bool CachedUsesTileCacheWindow = false;
thread_local FIntVector CachedTileOrigin = FIntVector::ZeroValue;
thread_local int32 CachedTileStep = 0;
thread_local int32 CachedTileCells = 0;
thread_local uint64 CachedManagerLifetimeId = 0;
thread_local const TArray<FStrateTerrainOpEntry>* CachedTerrainOps = nullptr;
// Keep a small bounded set of exact room-graph windows. Tall 32-chunk tiles need two slots
// for their two strata; other tile extents start with one. Every slot uses the full cache
// identity/bounds.
struct FRoomGraphCacheSlot
{
FChunkSDFCache Cache;
float SMinX = 1.0f, SMaxX = -1.0f;
float SMinY = 0.0f, SMaxY = 0.0f;
int32 Strate = INT32_MIN;
uint32 Seed = 0;
uint32 Fingerprint = 0xFFFFFFFFu;
uint32 Layout = 0xFFFFFFFFu;
bool bUsesFusedCacheWindow = false;
bool bUsesTileCacheWindow = false;
FIntVector TileOrigin = FIntVector::ZeroValue;
int32 TileStep = 0;
int32 TileCells = 0;
uint64 ManagerLifetimeId = 0;
const TArray<FStrateTerrainOpEntry>* TerrainOps = nullptr;
uint64 LastUse = 0;
bool bValid = false;
};
static thread_local std::array<FRoomGraphCacheSlot, 4> RoomGraphCacheSlots{};
static thread_local uint64 RoomGraphCacheUseSerial = 0;
VF_ParseRoomGraphCacheSlots();
// The envelope proof is constant for a cache-policy key. Do not run the complete room/edge
// reach calculation for every density sample: that would turn a build-time guard into a
// per-sample cost on the LOD0 floor.
@@ -4267,6 +4292,32 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|| CachedTilePolicyTerrainOps != TerrainOps;
if (bTilePolicyKeyChanged)
{
if (VoxelDensityProfile::AreCountersEnabled())
{
const auto CountPolicyKeyChange = [](bool bCondition, VoxelDensityProfile::ECounter Counter)
{
if (bCondition) VoxelDensityProfile::AddCounter(Counter);
};
CountPolicyKeyChange(CachedTilePolicyOrigin != RequestedTileOrigin,
VoxelDensityProfile::ECounter::TilePolicyKeyOrigin);
CountPolicyKeyChange(CachedTilePolicySampleStep != RequestedTileStep
|| CachedTilePolicyStep != RequestedTileStep,
VoxelDensityProfile::ECounter::TilePolicyKeyStep);
CountPolicyKeyChange(CachedTilePolicyCells != RequestedTileCells,
VoxelDensityProfile::ECounter::TilePolicyKeyCells);
CountPolicyKeyChange(CachedTilePolicyFingerprint != ParamsFingerprint,
VoxelDensityProfile::ECounter::TilePolicyKeyFingerprint);
CountPolicyKeyChange(CachedTilePolicyLayout != LayoutVersion,
VoxelDensityProfile::ECounter::TilePolicyKeyLayout);
CountPolicyKeyChange(CachedTilePolicyManagerLifetimeId != CurrentManagerLifetimeId,
VoxelDensityProfile::ECounter::TilePolicyKeyManager);
CountPolicyKeyChange(CachedTilePolicyTerrainOps != TerrainOps,
VoxelDensityProfile::ECounter::TilePolicyKeyTerrainOps);
CountPolicyKeyChange(CachedTilePolicyFusedPath != bUseFusedCacheWindow,
VoxelDensityProfile::ECounter::TilePolicyKeyFusedPath);
CountPolicyKeyChange(CachedTilePolicyHasContext != (bTileWindowEligible && bHasTileWindowContext),
VoxelDensityProfile::ECounter::TilePolicyKeyContext);
}
CachedTilePolicyValid = true;
CachedTilePolicyHasContext = bTileWindowEligible && bHasTileWindowContext;
CachedTilePolicyFusedPath = bUseFusedCacheWindow;
@@ -4280,6 +4331,12 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
CachedTilePolicyTerrainOps = TerrainOps;
CachedTilePolicyResult = CachedTilePolicyHasContext
&& VoxelCaveMorphology::IsRoomGraphWindowInvariant(Params, TerrainOps);
if (VoxelDensityProfile::AreCountersEnabled() && CachedTilePolicyHasContext)
{
VoxelDensityProfile::AddCounter(CachedTilePolicyResult
? VoxelDensityProfile::ECounter::SdfTileWindowAccepted
: VoxelDensityProfile::ECounter::SdfTileWindowRejected);
}
}
const bool bUseTileCacheWindow = CachedTilePolicyResult;
@@ -4290,36 +4347,137 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
float CaveSDF = FLT_MAX;
// Get strate index for unique caves per strate. The index changes only at chunk-Z
// boundaries and is also part of the worker-local graph-cache identity.
int32 StrateIdx = 0;
if (StrateManager)
{
thread_local int32 SI_ChunkZ = INT32_MAX;
thread_local uint32 SI_Version = 0xFFFFFFFFu;
thread_local int32 SI_Index = 0;
const int32 QZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE);
const uint32 LV = StrateManager->GetLayoutVersion();
if (QZ != SI_ChunkZ || LV != SI_Version)
{
SI_ChunkZ = QZ;
SI_Version = LV;
SI_Index = StrateManager->GetStrateIndex(
((float)QZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE);
}
StrateIdx = SI_Index;
}
const float ChunkMinX = CacheRegionX * static_cast<float>(CacheRegionSize);
const float ChunkMinY = CacheRegionY * static_cast<float>(CacheRegionSize);
const float ChunkMaxX = ChunkMinX + static_cast<float>(CacheRegionSize);
const float ChunkMaxY = ChunkMinY + static_cast<float>(CacheRegionSize);
const float WarpMargin = FMath::Abs(Params.CaveWarpStrength)
* VOXEL_NOISE_SCALE * 1.5f;
float DesiredSMinX = ChunkMinX;
float DesiredSMinY = ChunkMinY;
float DesiredSMaxX = ChunkMaxX;
float DesiredSMaxY = ChunkMaxY;
if (bUseTileCacheWindow)
{
const float TileMinX = static_cast<float>(RequestedTileOrigin.X);
const float TileMinY = static_cast<float>(RequestedTileOrigin.Y);
const float TileExtent = static_cast<float>(
static_cast<int64>(RequestedTileStep)
* static_cast<int64>(RequestedTileCells));
const float TileMaxX = TileMinX + TileExtent;
const float TileMaxY = TileMinY + TileExtent;
const float TileHalo = static_cast<float>(RequestedTileStep) + 2.0f;
const float Expansion = WarpMargin + TileHalo;
DesiredSMinX = TileMinX - Expansion;
DesiredSMinY = TileMinY - Expansion;
DesiredSMaxX = TileMaxX + Expansion;
DesiredSMaxY = TileMaxY + Expansion;
}
else
{
const float Expansion = WarpMargin + 2.0f;
DesiredSMinX = ChunkMinX - Expansion;
DesiredSMinY = ChunkMinY - Expansion;
DesiredSMaxX = ChunkMaxX + Expansion;
DesiredSMaxY = ChunkMaxY + Expansion;
}
const int64 RequestedTileExtent = static_cast<int64>(RequestedTileStep)
* static_cast<int64>(RequestedTileCells);
const bool bNeedsTwoStrataWindows = bHasTileWindowContext
&& RequestedTileExtent == static_cast<int64>(CHUNK_SIZE) * 32;
const int32 ActiveCacheSlots = FMath::Max(
FMath::Clamp(GVoxelForgeRoomGraphCacheSlots, 1, 4),
bNeedsTwoStrataWindows ? 2 : 1);
FRoomGraphCacheSlot* CacheSlot = nullptr;
FRoomGraphCacheSlot* EvictionSlot = &RoomGraphCacheSlots[0];
for (int32 SlotIndex = 0; SlotIndex < ActiveCacheSlots; ++SlotIndex)
{
FRoomGraphCacheSlot& Candidate = RoomGraphCacheSlots[SlotIndex];
if (!Candidate.bValid)
{
EvictionSlot = &Candidate;
break;
}
if (Candidate.LastUse < EvictionSlot->LastUse)
{
EvictionSlot = &Candidate;
}
const bool bSameKey = Candidate.Strate == StrateIdx
&& Candidate.Seed == static_cast<uint32>(Seed)
&& Candidate.Fingerprint == ParamsFingerprint
&& Candidate.Layout == LayoutVersion
&& Candidate.ManagerLifetimeId == CurrentManagerLifetimeId
&& Candidate.TerrainOps == TerrainOps
&& Candidate.bUsesFusedCacheWindow == bUseFusedCacheWindow
&& Candidate.bUsesTileCacheWindow == bUseTileCacheWindow
&& (!bUseTileCacheWindow
|| (Candidate.TileOrigin == RequestedTileOrigin
&& Candidate.TileStep == RequestedTileStep
&& Candidate.TileCells == RequestedTileCells));
const bool bSameBounds = Candidate.SMinX == DesiredSMinX
&& Candidate.SMaxX == DesiredSMaxX
&& Candidate.SMinY == DesiredSMinY
&& Candidate.SMaxY == DesiredSMaxY;
const bool bContainsQuery = WarpedX >= Candidate.SMinX
&& WarpedX <= Candidate.SMaxX
&& WarpedY >= Candidate.SMinY
&& WarpedY <= Candidate.SMaxY;
if (bSameKey && bSameBounds && bContainsQuery)
{
CacheSlot = &Candidate;
break;
}
}
if (CacheSlot == nullptr)
{
CacheSlot = EvictionSlot;
}
CacheSlot->LastUse = ++RoomGraphCacheUseSerial;
FChunkSDFCache& SDFCache = CacheSlot->Cache;
float& CachedSMinX = CacheSlot->SMinX;
float& CachedSMaxX = CacheSlot->SMaxX;
float& CachedSMinY = CacheSlot->SMinY;
float& CachedSMaxY = CacheSlot->SMaxY;
int32& CachedStrate = CacheSlot->Strate;
uint32& CachedSeed = CacheSlot->Seed;
uint32& CachedFingerprint = CacheSlot->Fingerprint;
uint32& CachedLayout = CacheSlot->Layout;
bool& CachedUsesFusedCacheWindow = CacheSlot->bUsesFusedCacheWindow;
bool& CachedUsesTileCacheWindow = CacheSlot->bUsesTileCacheWindow;
FIntVector& CachedTileOrigin = CacheSlot->TileOrigin;
int32& CachedTileStep = CacheSlot->TileStep;
int32& CachedTileCells = CacheSlot->TileCells;
uint64& CachedManagerLifetimeId = CacheSlot->ManagerLifetimeId;
const TArray<FStrateTerrainOpEntry>*& CachedTerrainOps = CacheSlot->TerrainOps;
if (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
{
// Get strate index for unique caves per strate.
// MEMOISED: the index only changes at chunk-Z boundaries, yet this ran the strate layout's
// linear scan PER VOXEL. Key = (chunk-Z, layout version) — the version guards against editor
// rebuilds (RebuildStrates / live edit) serving a stale index. The lookup queries the BAND
// CENTRE so the result is a pure function of the key (order-independent, window-invariant).
int32 StrateIdx = 0;
if (StrateManager)
{
thread_local int32 SI_ChunkZ = INT32_MAX;
thread_local uint32 SI_Version = 0xFFFFFFFFu;
thread_local int32 SI_Index = 0;
const int32 QZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE);
const uint32 LV = StrateManager->GetLayoutVersion();
if (QZ != SI_ChunkZ || LV != SI_Version)
{
SI_ChunkZ = QZ;
SI_Version = LV;
// GetStrateIndex expects Unreal world units (divides by VOXEL_SIZE internally);
// WorldZ here is in voxels, so multiply by VOXEL_SIZE.
SI_Index = StrateManager->GetStrateIndex(((float)QZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE);
}
StrateIdx = SI_Index;
}
// Rebuild only when the WARPED query (what EvaluateSDFCached uses) leaves the
// cached search box, or the strate/seed changed.
const bool bNeedRebuild =
StrateIdx != CachedStrate || (uint32)Seed != CachedSeed ||
!CacheSlot->bValid || StrateIdx != CachedStrate || (uint32)Seed != CachedSeed ||
ParamsFingerprint != CachedFingerprint || LayoutVersion != CachedLayout ||
CurrentManagerLifetimeId != CachedManagerLifetimeId ||
TerrainOps != CachedTerrainOps ||
@@ -4334,58 +4492,44 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
if (bNeedRebuild)
{
// The lowered evaluator uses the same window-invariance contract as the prepared
// room-graph source. With the switch enabled, the search box is the exact requesting
// tile footprint plus the one-sample normal halo and deterministic warp margin. A
// tile worker can visit neighbouring chunk keys in an arbitrary order without
// rebuilding the graph at every coarse sample. Point queries without a tile context
// retain the historical fused four-chunk / ordinary one-chunk fallback.
const float ChunkMinX = CacheRegionX * (float)CacheRegionSize;
const float ChunkMinY = CacheRegionY * (float)CacheRegionSize;
const float ChunkMaxX = ChunkMinX + (float)CacheRegionSize;
const float ChunkMaxY = ChunkMinY + (float)CacheRegionSize;
const float WarpMargin = FMath::Abs(Params.CaveWarpStrength)
* VOXEL_NOISE_SCALE * 1.5f;
float SMinX = ChunkMinX;
float SMinY = ChunkMinY;
float SMaxX = ChunkMaxX;
float SMaxY = ChunkMaxY;
if (bUseTileCacheWindow)
if (VoxelDensityProfile::AreCountersEnabled())
{
const float TileMinX = static_cast<float>(RequestedTileOrigin.X);
const float TileMinY = static_cast<float>(RequestedTileOrigin.Y);
const float TileExtent = static_cast<float>(
static_cast<int64>(RequestedTileStep)
* static_cast<int64>(RequestedTileCells));
const float TileMaxX = TileMinX + TileExtent;
const float TileMaxY = TileMinY + TileExtent;
const float TileHalo = static_cast<float>(RequestedTileStep) + 2.0f;
const float Expansion = WarpMargin + TileHalo;
SMinX = TileMinX - Expansion;
SMinY = TileMinY - Expansion;
SMaxX = TileMaxX + Expansion;
SMaxY = TileMaxY + Expansion;
const auto CountIf = [](bool bCondition, VoxelDensityProfile::ECounter Counter)
{
if (bCondition) VoxelDensityProfile::AddCounter(Counter);
};
CountIf(StrateIdx != CachedStrate, VoxelDensityProfile::ECounter::SdfRebuildStrate);
CountIf(static_cast<uint32>(Seed) != CachedSeed, VoxelDensityProfile::ECounter::SdfRebuildSeed);
CountIf(ParamsFingerprint != CachedFingerprint, VoxelDensityProfile::ECounter::SdfRebuildFingerprint);
CountIf(LayoutVersion != CachedLayout, VoxelDensityProfile::ECounter::SdfRebuildLayout);
CountIf(CurrentManagerLifetimeId != CachedManagerLifetimeId,
VoxelDensityProfile::ECounter::SdfRebuildManagerLifetime);
CountIf(TerrainOps != CachedTerrainOps, VoxelDensityProfile::ECounter::SdfRebuildTerrainOps);
CountIf(bUseFusedCacheWindow != CachedUsesFusedCacheWindow,
VoxelDensityProfile::ECounter::SdfRebuildFusedPolicy);
CountIf(bUseTileCacheWindow != CachedUsesTileCacheWindow,
VoxelDensityProfile::ECounter::SdfRebuildTilePolicy);
CountIf(bUseTileCacheWindow
&& (RequestedTileOrigin != CachedTileOrigin
|| RequestedTileStep != CachedTileStep
|| RequestedTileCells != CachedTileCells),
VoxelDensityProfile::ECounter::SdfRebuildTileIdentity);
CountIf(WarpedX < CachedSMinX || WarpedX > CachedSMaxX,
VoxelDensityProfile::ECounter::SdfRebuildBoundsX);
CountIf(WarpedY < CachedSMinY || WarpedY > CachedSMaxY,
VoxelDensityProfile::ECounter::SdfRebuildBoundsY);
}
else
{
const float Expansion = WarpMargin + 2.0f;
SMinX = ChunkMinX - Expansion;
SMinY = ChunkMinY - Expansion;
SMaxX = ChunkMaxX + Expansion;
SMaxY = ChunkMaxY + Expansion;
}
VoxelCaveMorphology::BuildChunkCache(
SDFCache,
SMinX, SMinY, SMaxX, SMaxY,
DesiredSMinX, DesiredSMinY, DesiredSMaxX, DesiredSMaxY,
Params, (uint32)Seed, StrateIdx,
TerrainOps,
ERoomGraphBuildSite::GeneratorTile
);
UpdateTunnelCoreTileReach(SDFCache, bUseTileCacheWindow);
CachedSMinX = SMinX; CachedSMaxX = SMaxX;
CachedSMinY = SMinY; CachedSMaxY = SMaxY;
CachedSMinX = DesiredSMinX; CachedSMaxX = DesiredSMaxX;
CachedSMinY = DesiredSMinY; CachedSMaxY = DesiredSMaxY;
CachedStrate = StrateIdx;
CachedSeed = (uint32)Seed;
CachedFingerprint = ParamsFingerprint;
@@ -4398,6 +4542,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
CachedTileCells = bUseTileCacheWindow ? RequestedTileCells : 0;
CachedManagerLifetimeId = CurrentManagerLifetimeId;
CachedTerrainOps = TerrainOps;
CacheSlot->bValid = true;
}
// Evaluate SDF using cached rooms and tunnels (WARPED coordinates).
@@ -103,13 +103,15 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
int32 BandZMinVox, int32 BandZMaxVox,
int64* OutDensitySampleCount,
FVoxelSharedDensityGrid* OutPreEditDensityGrid,
const FVoxelSharedDensityGrid* InSharedDensityGrid)
const FVoxelSharedDensityGrid* InSharedDensityGrid,
std::vector<float>* OutDebugDensitySamples)
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh);
FVoxelMeshData MeshData;
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
if (OutPreEditDensityGrid) { OutPreEditDensityGrid->Reset(); }
if (OutDensitySampleCount) { *OutDensitySampleCount = 0; }
if (OutDebugDensitySamples) { OutDebugDensitySamples->clear(); }
if (!Generator) return MeshData;
VoxelDensityProfile::FScopedTimer MesherProfileTimer(
VoxelDensityProfile::EBucket::MesherGenerateMesh);
@@ -284,7 +286,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
//
// CellsPerAxis = nombre de cellules par axe ; GridDim = points de grille (+1).
// Le point de grille (gx,gy,gz) correspond au voxel monde (gx,gy,gz)*Step.
// Ordre de remplissage z→y→x : garde le cache SDF (search-box) bien chaud.
// The grid fill order is selected below by tile extent to preserve room-cache locality.
// On échantillonne avec un anneau de marge de 1 point de chaque côté (indices -1..GridDim)
// pour pouvoir calculer les normales par GRADIENT DE GRILLE (T1.b) — différences centrales
// sur la grille au lieu de 6 appels densité frais par vertex. La marge utilise les mêmes
@@ -869,56 +871,114 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
}
#endif
}
else for (int32 gz = GzLo; gz <= GzHi; gz++)
else
{
if (ShouldAbortWork())
auto BuildAxisGroups = [&](int32 AxisOrigin, int32 RegionSize)
{
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
return FVoxelMeshData();
}
for (int32 gy = -1; gy <= GridDim; gy++)
{
if (ShouldAbortWork())
std::vector<std::vector<int32>> Groups;
int32 PreviousRegion = INT32_MIN;
for (int32 Index = -1; Index <= GridDim; ++Index)
{
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
return FVoxelMeshData();
}
for (int32 gx = -1; gx <= GridDim; gx++)
{
const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1);
if (bUseSharedDensityGrid)
const int64 WorldCoordinate = static_cast<int64>(AxisOrigin)
+ static_cast<int64>(Index) * Step;
const int32 Region = FMath::FloorToInt(
static_cast<float>(WorldCoordinate) / static_cast<float>(RegionSize));
if (Groups.empty() || Region != PreviousRegion)
{
const int32 SharedX = SharedDeltaX + gx;
const int32 SharedY = SharedDeltaY + gy;
const int32 SharedZ = SharedDeltaZ + gz;
DensityGrid[LocalIndex] = ReuseGrid->Samples[
(SharedZ * ReuseGrid->Dim + SharedY) * ReuseGrid->Dim + SharedX];
Groups.emplace_back();
PreviousRegion = Region;
}
else
Groups.back().push_back(Index);
}
return Groups;
};
const int32 RoomCacheRegionSize = CHUNK_SIZE
* (VoxelGenLOD::IsUseFusedEvaluatorEnabled() ? 4 : 1);
const auto XGroups = BuildAxisGroups(OriginVoxels.X,RoomCacheRegionSize);
const auto YGroups = BuildAxisGroups(OriginVoxels.Y,RoomCacheRegionSize);
auto SampleGridPoint = [&](int32 gx, int32 gy, int32 gz)
{
const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1);
if (bUseSharedDensityGrid)
{
const int32 SharedX = SharedDeltaX + gx;
const int32 SharedY = SharedDeltaY + gy;
const int32 SharedZ = SharedDeltaZ + gz;
DensityGrid[LocalIndex] = ReuseGrid->Samples[
(SharedZ * ReuseGrid->Dim + SharedY) * ReuseGrid->Dim + SharedX];
}
else
{
const int32 ReachBlockX = FMath::Clamp(gx, 0, CellsPerAxis - 1)
/ VoxelGenLOD::TileReachBlockCells;
const int32 ReachBlockY = FMath::Clamp(gy, 0, CellsPerAxis - 1)
/ VoxelGenLOD::TileReachBlockCells;
const int32 ReachBlockZ = FMath::Clamp(gz, 0, CellsPerAxis - 1)
/ VoxelGenLOD::TileReachBlockCells;
VoxelGenLOD::TileReachBlockIndex =
((ReachBlockZ * ReachBlocksPerAxis) + ReachBlockY)
* ReachBlocksPerAxis + ReachBlockX;
float PreEditDensity = 0.0f;
DensityGrid[LocalIndex] = Generator->GetDensityAt(
OriginVoxels.X + gx * Step,
OriginVoxels.Y + gy * Step,
OriginVoxels.Z + gz * Step,
bCapturePreEditGrid ? &PreEditDensity : nullptr);
if (bCapturePreEditGrid)
{
// World voxel = tile origin + grid offset scaled by the cell size (Step).
const int32 ReachBlockX = FMath::Clamp(gx, 0, CellsPerAxis - 1)
/ VoxelGenLOD::TileReachBlockCells;
const int32 ReachBlockY = FMath::Clamp(gy, 0, CellsPerAxis - 1)
/ VoxelGenLOD::TileReachBlockCells;
const int32 ReachBlockZ = FMath::Clamp(gz, 0, CellsPerAxis - 1)
/ VoxelGenLOD::TileReachBlockCells;
VoxelGenLOD::TileReachBlockIndex =
((ReachBlockZ * ReachBlocksPerAxis) + ReachBlockY)
* ReachBlocksPerAxis + ReachBlockX;
float PreEditDensity = 0.0f;
DensityGrid[LocalIndex] = Generator->GetDensityAt(
OriginVoxels.X + gx * Step,
OriginVoxels.Y + gy * Step,
OriginVoxels.Z + gz * Step,
bCapturePreEditGrid ? &PreEditDensity : nullptr);
if (bCapturePreEditGrid)
OutPreEditDensityGrid->Samples[LocalIndex] = PreEditDensity;
}
if (OutDensitySampleCount)
{
++(*OutDensitySampleCount);
}
}
};
// Very coarse tiles revisit the same XY cache window across a tall Z run. Keep that
// vertical run contiguous; SampleGridPoint writes by lattice index, so output bits do
// not depend on this traversal. Finer tiles retain plane-first traversal, which has
// lower overhead when a tile spans only one room-graph region.
const int64 TileExtentVoxels = static_cast<int64>(Step) * CellsPerAxis;
const bool bUseVerticalCacheTraversal = TileExtentVoxels > CHUNK_SIZE * 16;
if (bUseVerticalCacheTraversal)
{
for (const std::vector<int32>& YGroup : YGroups)
for (const std::vector<int32>& XGroup : XGroups)
for (const int32 gy : YGroup)
for (const int32 gx : XGroup)
{
for (int32 gz = GzLo; gz <= GzHi; ++gz)
{
if (ShouldAbortWork())
{
OutPreEditDensityGrid->Samples[LocalIndex] = PreEditDensity;
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
return FVoxelMeshData();
}
if (OutDensitySampleCount)
SampleGridPoint(gx,gy,gz);
}
}
}
else
{
for (int32 gz = GzLo; gz <= GzHi; ++gz)
{
if (ShouldAbortWork())
{
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
return FVoxelMeshData();
}
for (const std::vector<int32>& YGroup : YGroups)
for (const std::vector<int32>& XGroup : XGroups)
for (const int32 gy : YGroup)
{
if (ShouldAbortWork())
{
++(*OutDensitySampleCount);
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
return FVoxelMeshData();
}
for (const int32 gx : XGroup)
{
SampleGridPoint(gx,gy,gz);
}
}
}
@@ -943,6 +1003,19 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
{
Generator->EndDensityBlock();
}
if (OutDebugDensitySamples)
{
const size_t SampleCount = static_cast<size_t>(GzHi - GzLo + 1)
* static_cast<size_t>(MDim) * static_cast<size_t>(MDim);
OutDebugDensitySamples->reserve(SampleCount);
for (int32 gz = GzLo; gz <= GzHi; ++gz)
for (int32 gy = -1; gy <= GridDim; ++gy)
for (int32 gx = -1; gx <= GridDim; ++gx)
{
const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1);
OutDebugDensitySamples->push_back(DensityGrid[LocalIndex]);
}
}
MesherDensityGridTimer.End();
if (bUseProofBlocks)
@@ -172,6 +172,28 @@ namespace VoxelDensityProfile
MesherOutputAllocatedBytes,
OpBlockScratchBytes,
OpBlockCopyBytes,
SdfRebuildStrate,
SdfRebuildSeed,
SdfRebuildFingerprint,
SdfRebuildLayout,
SdfRebuildManagerLifetime,
SdfRebuildTerrainOps,
SdfRebuildFusedPolicy,
SdfRebuildTilePolicy,
SdfRebuildTileIdentity,
SdfRebuildBoundsX,
SdfRebuildBoundsY,
SdfTileWindowAccepted,
SdfTileWindowRejected,
TilePolicyKeyOrigin,
TilePolicyKeyStep,
TilePolicyKeyCells,
TilePolicyKeyFingerprint,
TilePolicyKeyLayout,
TilePolicyKeyManager,
TilePolicyKeyTerrainOps,
TilePolicyKeyFusedPath,
TilePolicyKeyContext,
Count
};
@@ -433,6 +433,8 @@ private:
namespace VoxelGenLOD
{
VOXELFORGE_API void SetUseFusedEvaluatorEnabled(bool bEnabled);
VOXELFORGE_API bool IsUseFusedEvaluatorEnabled();
// -1 uses the measured path/LOD policy; 0/1 force the spatial broad phase off/on for an A/B
// run. The policy affects candidate enumeration only, never cache contents or ordering.
VOXELFORGE_API bool ShouldUseSpatialIndex(bool bFusedPath);
@@ -13,6 +13,7 @@
#include "CoreMinimal.h"
#include <atomic>
#include <vector>
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
#include "VoxelGenerator.h"
#include "VoxelMarchingCubesMesher.generated.h"
@@ -78,7 +79,8 @@ public:
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX,
int64* OutDensitySampleCount = nullptr,
FVoxelSharedDensityGrid* OutPreEditDensityGrid = nullptr,
const FVoxelSharedDensityGrid* InSharedDensityGrid = nullptr);
const FVoxelSharedDensityGrid* InSharedDensityGrid = nullptr,
std::vector<float>* OutDebugDensitySamples = nullptr);
//=========================================================================
// SERVICES (injectés par AVoxelWorld)