perf(gen): the landing work was per-voxel -- 342s to 2.6s, same world
The owner played64f5d61and reported the truth: "generation still takes ages... TunnelNetwork doesn't even finish generating, as big as the surface world" while SurfaceWorld completed in under 10s, and stopping PIE hung for 30+ seconds. e08b49a's 21x was real and irrelevant. It fixed per-CHUNK state rebuilds, which dominate the commandlet export path. The game is bound by per-VOXEL evaluation. Same symptom, different bottleneck, and nobody had measured the second one. WHY PIE HUNG bShuttingDown was never checked inside VoxelMarchingCubesMesher.cpp -- zero occurrences. A worker inside GenerateMesh could not abort, so 64f5d61's correct unconditional EndPlay wait was waiting on tiles physically unable to stop. The hang was not a teardown bug; it was evidence that one tile took tens of seconds. Added shutdown polling per slab/row in the mesher and through GenerateTileResult; aborted results carry Epoch + bAborted and ProcessQueue drops them. EndPlay still waits unconditionally -- the 3s mark only logs now. PIE stop 3.409s / 44.759s -> 4 ms teardown, 38 us worker drain WHERE THE PER-VOXEL COST WAS The owner's own comparison was the sharpest instrument available. Profiled both: GetDensityAt TunnelNetwork 12.64 us/sample SurfaceWorld 1.32 tunnel candidates evaluated 7.088 per sample 0 tunnel-core candidates evaluated 7.201 per sample 0 room candidates evaluated 1.000 per sample 0 largest buckets: RoomGraphSource 2.57, TunnelCorePosts 0.76, PassageCarveOp 0.22, PassageStructuralPosts 0.20 The three regressing commits map cleanly, and one is innocent:7a20eeedrainage tree -- cache-build work, NOT per-voxel413c15dlandings -- repeated full passage modification per sample55bf807landing rooms -- room SDF + tunnel/core evaluation per sample Fixes: FPassageCarveOp carves only, landing/support moved to the structural MC post; tunnel core support and SDF fused into one pass; room graph caches shared per worker on complete deterministic keys; tunnel density cache uses open addressing with 4096 slots. Room cache rebuilds fell to 9 per 42,875 samples, so what remains is genuine O(local cached room/tunnel count) work. TunnelNetwork 128^3, seed 1337 slot 1, same region: 342.2 s / 163.161 us/vox -> 2.623 s / 1.251 us/vox (130x) Geometry SHA-256 aaf95559..066b4, IDENTICAL to the pre-optimisation run at the same roughness. 153,692 triangles either way. Verified independently of the agent's own report. NOT FIXED, and the first one is the next real target: - L4 tiles still cost 16.93 s. Hit rate falls 99.9% (L0) -> 23.7% (L4), but 1024 -> 4096 slots only moved 18.39s -> 16.93s and the 1,927 misses ARE the distinct key count. That is low reuse, not eviction. A bigger cache will not save it. - TunnelNetwork is still ~10x SurfaceWorld (0.354s vs 0.028s on a matched 32^3). - The live PIE tile table is SurfaceWorld: this checkout's PIE map is configured that way. TunnelNetwork went through the same mesher and density path via the commandlet, but no TunnelNetwork tile has been timed in an actual game session. Export numbers are not presented as game numbers. Still outstanding, untouched here: the inter-strate passage has intersections it should not have, routes through open passage when it should not, and should descend from a low point of the current strate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
64f5d61e08
commit
0576e800c4
15 files changed
+1035
-124
No files matched your search
@@ -28,6 +28,7 @@
|
||||
// this chunk are kept for the per-voxel loop, so hot-path cost stays low.
|
||||
|
||||
#include "VoxelCaveMorphology.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
#include "VoxelDensityPrimitives.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
#include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01
|
||||
@@ -3643,6 +3644,16 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
||||
float NearestRoomRawSDF = FLT_MAX;
|
||||
int32 NearestIdx = -1;
|
||||
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::CaveRoomCandidates,
|
||||
static_cast<uint64>(Cache.Rooms.Num()));
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::CaveTunnelCandidates,
|
||||
static_cast<uint64>(Cache.Tunnels.Num()));
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// Room SDFs
|
||||
//=========================================================================
|
||||
@@ -3653,6 +3664,11 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
||||
// --- DISTANCE CULL ---
|
||||
const float DistSq = FVector::DistSquared(Pos, Room.Center);
|
||||
if (DistSq > Room.CullRadiusSq) continue;
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::CaveRoomEvaluated);
|
||||
}
|
||||
|
||||
// --- SHAPE (pre-baked in BuildChunkCache — no per-voxel hash roll / trig) ---
|
||||
float RoomSDF;
|
||||
@@ -3721,6 +3737,11 @@ float VoxelCaveMorphology::EvaluateSDFCached(
|
||||
// --- BOUNDING SPHERE CULL ---
|
||||
const float DistSq = FVector::DistSquared(Pos, Tunnel.BoundCenter);
|
||||
if (DistSq > Tunnel.BoundRadiusSq) continue;
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::CaveTunnelEvaluated);
|
||||
}
|
||||
|
||||
float TunnelSDF;
|
||||
|
||||
@@ -3830,12 +3851,19 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF(
|
||||
return MinSDF;
|
||||
}
|
||||
|
||||
float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||
FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
||||
float WorldX, float WorldY, float WorldZ,
|
||||
const FChunkSDFCache& Cache)
|
||||
{
|
||||
const FVector Pos(WorldX, WorldY, WorldZ);
|
||||
float MinSDF = FLT_MAX;
|
||||
FTunnelCoreWorldEvaluation Result;
|
||||
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCoreCandidates,
|
||||
static_cast<uint64>(Cache.Tunnels.Num()));
|
||||
}
|
||||
|
||||
for (const FCachedTunnel& Tunnel : Cache.Tunnels)
|
||||
{
|
||||
@@ -3850,6 +3878,11 @@ float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
|
||||
}
|
||||
|
||||
const TArray<FVector>& ControlPoints = bHasWorldChain
|
||||
? Tunnel.WorldControlPoints : Tunnel.ControlPoints;
|
||||
@@ -3859,9 +3892,20 @@ float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||
{
|
||||
float FloorZ = 0.0f;
|
||||
float SupportRadius = 0.0f;
|
||||
if (VoxelPassageGeometry::ProjectWalkableTunnelFloor(
|
||||
ControlPoints, ControlRadii, Pos, FloorZ, SupportRadius)
|
||||
const bool bProjectedFloor = VoxelPassageGeometry::ProjectWalkableTunnelFloor(
|
||||
ControlPoints, ControlRadii, Pos, FloorZ, SupportRadius);
|
||||
const bool bWithinWorldBounds = Tunnel.WorldBoundRadiusSq <= 0.0f
|
||||
|| FVector::DistSquared(Pos, Tunnel.WorldBoundCenter)
|
||||
<= Tunnel.WorldBoundRadiusSq;
|
||||
if (bProjectedFloor && bWithinWorldBounds
|
||||
&& WorldZ <= FloorZ
|
||||
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
|
||||
&& WorldZ >= FloorZ
|
||||
- VoxelPassageGeometry::LandingFloorThicknessVoxels)
|
||||
{
|
||||
Result.bSupportFloor = true;
|
||||
}
|
||||
if (bProjectedFloor && WorldZ <= FloorZ
|
||||
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels)
|
||||
{
|
||||
// The finite floor slab owns the bottom of this tunnel. Another intersecting
|
||||
@@ -3877,8 +3921,8 @@ float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||
SegmentIndex + 1 < ControlPoints.Num();
|
||||
++SegmentIndex)
|
||||
{
|
||||
MinSDF = FMath::Min(
|
||||
MinSDF,
|
||||
Result.SDF = FMath::Min(
|
||||
Result.SDF,
|
||||
VoxelSDF::TaperedCapsule(
|
||||
Pos,
|
||||
ControlPoints[SegmentIndex],
|
||||
@@ -3889,28 +3933,35 @@ float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||
}
|
||||
else if (Tunnel.bHasMidpoint)
|
||||
{
|
||||
MinSDF = FMath::Min(
|
||||
MinSDF,
|
||||
Result.SDF = FMath::Min(
|
||||
Result.SDF,
|
||||
VoxelSDF::TaperedCapsule(
|
||||
Pos, Tunnel.EndpointA, Tunnel.Midpoint,
|
||||
Tunnel.RadiusA, Tunnel.RadiusMid));
|
||||
MinSDF = FMath::Min(
|
||||
MinSDF,
|
||||
Result.SDF = FMath::Min(
|
||||
Result.SDF,
|
||||
VoxelSDF::TaperedCapsule(
|
||||
Pos, Tunnel.Midpoint, Tunnel.EndpointB,
|
||||
Tunnel.RadiusMid, Tunnel.RadiusB));
|
||||
}
|
||||
else
|
||||
{
|
||||
MinSDF = FMath::Min(
|
||||
MinSDF,
|
||||
Result.SDF = FMath::Min(
|
||||
Result.SDF,
|
||||
VoxelSDF::TaperedCapsule(
|
||||
Pos, Tunnel.EndpointA, Tunnel.EndpointB,
|
||||
Tunnel.RadiusA, Tunnel.RadiusB));
|
||||
}
|
||||
}
|
||||
|
||||
return MinSDF;
|
||||
return Result;
|
||||
}
|
||||
|
||||
float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||
float WorldX, float WorldY, float WorldZ,
|
||||
const FChunkSDFCache& Cache)
|
||||
{
|
||||
return EvaluateTunnelCoreWorld(WorldX, WorldY, WorldZ, Cache).SDF;
|
||||
}
|
||||
|
||||
bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
@@ -3918,6 +3969,13 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
const FChunkSDFCache& Cache)
|
||||
{
|
||||
const FVector Pos(WorldX, WorldY, WorldZ);
|
||||
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCoreCandidates,
|
||||
static_cast<uint64>(Cache.Tunnels.Num()));
|
||||
}
|
||||
for (const FCachedTunnel& Tunnel : Cache.Tunnels)
|
||||
{
|
||||
if (Tunnel.WorldControlPoints.Num() < 2
|
||||
@@ -3944,6 +4002,11 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
|| FVector::DistSquared(Pos, Tunnel.WorldBoundCenter)
|
||||
<= Tunnel.WorldBoundRadiusSq)
|
||||
{
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +340,6 @@ namespace
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
if (!(Strength > 0.0f)) { return; }
|
||||
@@ -1816,7 +1815,9 @@ namespace
|
||||
{
|
||||
if (const UVoxelStrateManager* LiveManager = Manager.Get())
|
||||
{
|
||||
LiveManager->ApplyPassageModifier(InOut.Density, X, Y, Z, Base, Seal);
|
||||
// Landing/tunnel support is a shared MC-space post after disturbances. Calling the
|
||||
// legacy full modifier here repeats those same writers before the shared post.
|
||||
LiveManager->ApplyPassageCarvingOnly(InOut.Density, X, Y, Z, Base, Seal);
|
||||
}
|
||||
// Keep the source and op-stack operation order identical: the origin room is carved
|
||||
// before the boundary seal, then its support floor is reasserted after the passage
|
||||
@@ -2780,6 +2781,85 @@ namespace
|
||||
uint32 Salt;
|
||||
};
|
||||
|
||||
// The mesher's coarse samples are sparse enough that one sample can land in a different
|
||||
// exact 32-voxel chunk every time. Keeping one SDF cache inside every exact-chunk op-stack
|
||||
// therefore turns L4 into thousands of BuildChunkCache calls. These worker-local entries
|
||||
// share the immutable room graph cache by its complete deterministic input key. A wider
|
||||
// region is safe because BuildChunkCache's collect/store construction is window-invariant;
|
||||
// the key still includes every value that can change the graph or its terrain-op rolls.
|
||||
constexpr int32 RoomGraphCacheSlotCount = 128;
|
||||
|
||||
struct FRoomGraphCacheEntry
|
||||
{
|
||||
bool bValid = false;
|
||||
uint32 Seed = 0;
|
||||
int32 StrateIndex = INT32_MIN;
|
||||
uint32 ParamsFingerprint = 0xFFFFFFFFu;
|
||||
uint32 LayoutVersion = 0xFFFFFFFFu;
|
||||
uint64 ManagerLifetimeId = 0;
|
||||
const TArray<FStrateTerrainOpEntry>* TerrainOps = nullptr;
|
||||
int32 RegionMinX = 0;
|
||||
int32 RegionMinY = 0;
|
||||
int32 RegionSize = 0;
|
||||
FChunkSDFCache Cache;
|
||||
};
|
||||
|
||||
static thread_local FRoomGraphCacheEntry GRoomGraphCache[RoomGraphCacheSlotCount];
|
||||
|
||||
FORCEINLINE int32 RoomGraphCacheSlot(
|
||||
uint32 Seed, int32 StrateIndex, uint32 ParamsFingerprint, uint32 LayoutVersion,
|
||||
uint64 ManagerLifetimeId, int32 RegionMinX, int32 RegionMinY, int32 RegionSize)
|
||||
{
|
||||
uint32 Hash = Seed * 0x9E3779B9u;
|
||||
Hash ^= static_cast<uint32>(StrateIndex) * 0x85EBCA6Bu;
|
||||
Hash ^= ParamsFingerprint * 0xC2B2AE35u;
|
||||
Hash ^= LayoutVersion * 0x27D4EB2Fu;
|
||||
Hash ^= static_cast<uint32>(ManagerLifetimeId)
|
||||
^ static_cast<uint32>(ManagerLifetimeId >> 32);
|
||||
Hash ^= static_cast<uint32>(RegionMinX) * 0x165667B1u;
|
||||
Hash ^= static_cast<uint32>(RegionMinY) * 0xD3A2646Cu;
|
||||
Hash ^= static_cast<uint32>(RegionSize) * 0xFD7046C5u;
|
||||
Hash ^= Hash >> 16;
|
||||
return static_cast<int32>(Hash & (RoomGraphCacheSlotCount - 1));
|
||||
}
|
||||
|
||||
FRoomGraphCacheEntry* FindRoomGraphCacheEntry(
|
||||
uint32 Seed, int32 StrateIndex, uint32 ParamsFingerprint, uint32 LayoutVersion,
|
||||
uint64 ManagerLifetimeId, const TArray<FStrateTerrainOpEntry>* TerrainOps,
|
||||
int32 RegionMinX, int32 RegionMinY, int32 RegionSize)
|
||||
{
|
||||
const int32 StartSlot = RoomGraphCacheSlot(
|
||||
Seed, StrateIndex, ParamsFingerprint, LayoutVersion, ManagerLifetimeId,
|
||||
RegionMinX, RegionMinY, RegionSize);
|
||||
FRoomGraphCacheEntry* ReuseEntry = nullptr;
|
||||
for (int32 Probe = 0; Probe < RoomGraphCacheSlotCount; ++Probe)
|
||||
{
|
||||
FRoomGraphCacheEntry& Candidate = GRoomGraphCache[
|
||||
(StartSlot + Probe) & (RoomGraphCacheSlotCount - 1)];
|
||||
if (!Candidate.bValid)
|
||||
{
|
||||
return ReuseEntry != nullptr ? ReuseEntry : &Candidate;
|
||||
}
|
||||
if (Candidate.Seed == Seed
|
||||
&& Candidate.StrateIndex == StrateIndex
|
||||
&& Candidate.ParamsFingerprint == ParamsFingerprint
|
||||
&& Candidate.LayoutVersion == LayoutVersion
|
||||
&& Candidate.ManagerLifetimeId == ManagerLifetimeId
|
||||
&& Candidate.TerrainOps == TerrainOps
|
||||
&& Candidate.RegionMinX == RegionMinX
|
||||
&& Candidate.RegionMinY == RegionMinY
|
||||
&& Candidate.RegionSize == RegionSize)
|
||||
{
|
||||
return &Candidate;
|
||||
}
|
||||
if (ReuseEntry == nullptr && Candidate.ManagerLifetimeId != ManagerLifetimeId)
|
||||
{
|
||||
ReuseEntry = &Candidate;
|
||||
}
|
||||
}
|
||||
return ReuseEntry != nullptr ? ReuseEntry : &GRoomGraphCache[StartSlot];
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// RÔLE 1 — SOURCE : GRAPHE DE SALLES / ROOM GRAPH (TunnelNetwork)
|
||||
//=========================================================================
|
||||
@@ -2833,29 +2913,17 @@ namespace
|
||||
// Dans l'original tout cela vit dans des `thread_local` d'une seule fonction de 1080 lignes ;
|
||||
// ici la source les possède et les expose.
|
||||
//
|
||||
// ⚠️ `static` (donc PARTAGÉ ENTRE INSTANCES), pas un membre : c'est exactement ce que fait
|
||||
// l'original, et le contrôle 3 du test en DÉPEND — deux piles construites côte à côte se
|
||||
// partagent ce cache, et c'est l'empreinte de params dans la clé (pas une copie par pile) qui
|
||||
// les empêche de se servir mutuellement leurs salles. En faire un membre ferait passer ce
|
||||
// contrôle pour de mauvaises raisons.
|
||||
//
|
||||
// ⚠️ MÊME MOTIF QUE `FOverhangShelfMod` ← `FSurfaceColumnSource` et `FShaftLedgeMod` ←
|
||||
// `FShaftFieldSource` : un opérateur possède l'état, les autres le lisent par pointeur non
|
||||
// possédant remis à la construction. C'est un motif ÉTABLI dans ce fichier, pas une invention.
|
||||
//
|
||||
// Per-worker state, deliberately `static` (shared between instances) because that is what the
|
||||
// original does and what the test's stale-cache check rests on. Detail ops read it through a
|
||||
// non-owning pointer, the same way the surface and shaft ports already do.
|
||||
// The state belongs to this source instance. The outer tunnel cache retains the immutable
|
||||
// op stack, so retaining the source's prepared SDF window with that stack avoids rebuilding
|
||||
// the room graph every time a coarse tile switches between cached chunk keys. It remains
|
||||
// worker-local because each op stack lives in the thread-local density cache.
|
||||
struct FState
|
||||
{
|
||||
// Fallback only for the invalid-owner path. Normal evaluations point at the worker-local
|
||||
// shared cache bank below, so switching exact chunk op-stacks does not rebuild the room
|
||||
// graph merely because the stack object changed.
|
||||
FChunkSDFCache Cache;
|
||||
float CachedSMinX = 1.0f, CachedSMaxX = -1.0f; // invalide au départ (min > max)
|
||||
float CachedSMinY = 0.0f, CachedSMaxY = 0.0f;
|
||||
int32 CachedStrate = INT32_MIN;
|
||||
uint32 CachedSeed = 0;
|
||||
uint32 CachedFingerprint = 0xFFFFFFFFu;
|
||||
uint32 CachedLayout = 0xFFFFFFFFu;
|
||||
uint64 CachedManagerLifetimeId = 0;
|
||||
const FChunkSDFCache* ActiveCache = nullptr;
|
||||
|
||||
/** La salle de SDF minimal pour le dernier voxel évalué. -1 = aucune. */
|
||||
int32 NearestRoom = -1;
|
||||
@@ -2874,14 +2942,23 @@ namespace
|
||||
bool bLocalParamsValid = false;
|
||||
};
|
||||
|
||||
static FState& State()
|
||||
FState& State() const
|
||||
{
|
||||
thread_local FState S;
|
||||
return S;
|
||||
// The prepared SDF data lives in the worker-local keyed cache bank above. This state
|
||||
// is only scratch for the current EvalSample: Eval resets NearestRoom and the local
|
||||
// parameter memo before its detail ops consume them. A single TLS slot is therefore
|
||||
// safe even when this worker switches between immutable source instances, and avoids
|
||||
// a map lookup on every sample. A mutable member is not safe because stacks can be
|
||||
// shared by concurrent readers during generation.
|
||||
static thread_local FState Scratch;
|
||||
return Scratch;
|
||||
}
|
||||
|
||||
/** Le cache que la source vient de bâtir/servir pour ce voxel. Lu par les colonnes (4d). */
|
||||
const FChunkSDFCache& GetCache() const { return State().Cache; }
|
||||
const FChunkSDFCache& GetCache() const
|
||||
{
|
||||
return State().ActiveCache != nullptr ? *State().ActiveCache : State().Cache;
|
||||
}
|
||||
|
||||
/** L'index de la salle la plus proche pour le dernier voxel évalué. -1 = aucune.
|
||||
* Lu par les arches, les dômes, le pincement et le biais de sol. */
|
||||
@@ -2896,7 +2973,7 @@ namespace
|
||||
{
|
||||
const FVector& Position = State().LastWorldPosition;
|
||||
return VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||
Position.X, Position.Y, Position.Z, State().Cache);
|
||||
Position.X, Position.Y, Position.Z, GetCache());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2933,9 +3010,10 @@ namespace
|
||||
if (!S.bLocalParamsValid)
|
||||
{
|
||||
S.LocalParams = P;
|
||||
if (S.NearestRoom >= 0 && S.Cache.Rooms.IsValidIndex(S.NearestRoom))
|
||||
const FChunkSDFCache& Cache = GetCache();
|
||||
if (S.NearestRoom >= 0 && Cache.Rooms.IsValidIndex(S.NearestRoom))
|
||||
{
|
||||
const FCachedRoom& NR = S.Cache.Rooms[S.NearestRoom];
|
||||
const FCachedRoom& NR = Cache.Rooms[S.NearestRoom];
|
||||
if (NR.RoomOp)
|
||||
{
|
||||
// N'écrit que les champs propres au type de l'op ; tout le reste garde la
|
||||
@@ -2964,7 +3042,7 @@ namespace
|
||||
*/
|
||||
float ProbeSdfUnwarped(float X, float Y, float Z) const
|
||||
{
|
||||
return VoxelCaveMorphology::EvaluateSDFCached(X, Y, Z, State().Cache, P.SDFBlendRadius);
|
||||
return VoxelCaveMorphology::EvaluateSDFCached(X, Y, Z, GetCache(), P.SDFBlendRadius);
|
||||
}
|
||||
|
||||
/** Le Z « effectif » : `VerticalScale` étire le monde AVANT le bruit. Pure fonction de Z et
|
||||
@@ -3002,11 +3080,16 @@ namespace
|
||||
if (ManagerLifetimeId != 0 && LiveManager == nullptr)
|
||||
{
|
||||
S.Cache = FChunkSDFCache();
|
||||
S.ActiveCache = &S.Cache;
|
||||
InOut.Sdf = FLT_MAX;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f)) { return; } // Sdf reste FLT_MAX
|
||||
if (!(P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f))
|
||||
{
|
||||
S.ActiveCache = &S.Cache;
|
||||
return; // Sdf reste FLT_MAX
|
||||
}
|
||||
|
||||
const float EffectiveZ = EffZ(WorldZ);
|
||||
|
||||
@@ -3078,51 +3161,78 @@ namespace
|
||||
StrateIdx = SI_Index;
|
||||
}
|
||||
|
||||
const bool bNeedRebuild =
|
||||
StrateIdx != S.CachedStrate || SeedU != S.CachedSeed ||
|
||||
ParamsFingerprint != S.CachedFingerprint || LayoutVersion != S.CachedLayout ||
|
||||
ManagerLifetimeId != S.CachedManagerLifetimeId ||
|
||||
WarpedX < S.CachedSMinX || WarpedX > S.CachedSMaxX ||
|
||||
WarpedY < S.CachedSMinY || WarpedY > S.CachedSMaxY;
|
||||
|
||||
if (bNeedRebuild)
|
||||
{
|
||||
const int32 CacheChunkX = FMath::FloorToInt(WorldX / (float)CHUNK_SIZE);
|
||||
const int32 CacheChunkY = FMath::FloorToInt(WorldY / (float)CHUNK_SIZE);
|
||||
const float ChunkMinX = CacheChunkX * (float)CHUNK_SIZE;
|
||||
const float ChunkMinY = CacheChunkY * (float)CHUNK_SIZE;
|
||||
const float ChunkMaxX = ChunkMinX + (float)CHUNK_SIZE;
|
||||
const float ChunkMaxY = ChunkMinY + (float)CHUNK_SIZE;
|
||||
const float Expansion = P.CaveWarpStrength + 2.0f;
|
||||
|
||||
const float SMinX = ChunkMinX - Expansion;
|
||||
const float SMinY = ChunkMinY - Expansion;
|
||||
const float SMaxX = ChunkMaxX + Expansion;
|
||||
const float SMaxY = ChunkMaxY + Expansion;
|
||||
// A coarse LOD samples a new exact 32-voxel chunk at nearly every point. The graph is
|
||||
// window-invariant, so group those samples into a larger deterministic XY region. The
|
||||
// step is supplied by the mesher through VoxelGenLOD TLS; ordinary point queries keep
|
||||
// the original one-chunk window. The cache key retains the exact params/strate/op-pool
|
||||
// identity, so no blended chunk can borrow another chunk's room graph.
|
||||
const int32 SampleStep = FMath::Max(VoxelGenLOD::SampleStep, 1);
|
||||
const int32 RegionChunks = SampleStep <= 1
|
||||
? 1
|
||||
: SampleStep >= 32
|
||||
? 4
|
||||
: FMath::Clamp((SampleStep + 3) / 4, 1, 4);
|
||||
const int32 RegionSize = CHUNK_SIZE * RegionChunks;
|
||||
const int32 RegionCellX = FMath::FloorToInt(WorldX / (float)RegionSize);
|
||||
const int32 RegionCellY = FMath::FloorToInt(WorldY / (float)RegionSize);
|
||||
const int32 RegionMinX = RegionCellX * RegionSize;
|
||||
const int32 RegionMinY = RegionCellY * RegionSize;
|
||||
|
||||
const TArray<FStrateTerrainOpEntry>* TerrainOps = nullptr;
|
||||
if (LiveManager)
|
||||
{
|
||||
const int32 ChunkX = FMath::FloorToInt(WorldX / (float)CHUNK_SIZE);
|
||||
const int32 ChunkY = FMath::FloorToInt(WorldY / (float)CHUNK_SIZE);
|
||||
const int32 ChunkZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE);
|
||||
UVoxelStrateDefinition* Def = LiveManager->GetStrateForChunk(
|
||||
FIntVector(CacheChunkX, CacheChunkY, ChunkZ));
|
||||
FIntVector(ChunkX, ChunkY, ChunkZ));
|
||||
if (Def) { TerrainOps = &Def->TerrainOperations; }
|
||||
}
|
||||
|
||||
VoxelCaveMorphology::BuildChunkCache(
|
||||
S.Cache, SMinX, SMinY, SMaxX, SMaxY, P, SeedU, StrateIdx, TerrainOps);
|
||||
|
||||
S.CachedSMinX = SMinX; S.CachedSMaxX = SMaxX;
|
||||
S.CachedSMinY = SMinY; S.CachedSMaxY = SMaxY;
|
||||
S.CachedStrate = StrateIdx;
|
||||
S.CachedSeed = SeedU;
|
||||
S.CachedFingerprint = ParamsFingerprint;
|
||||
S.CachedLayout = LayoutVersion;
|
||||
S.CachedManagerLifetimeId = ManagerLifetimeId;
|
||||
FRoomGraphCacheEntry* SharedCache = FindRoomGraphCacheEntry(
|
||||
SeedU, StrateIdx, ParamsFingerprint, LayoutVersion, ManagerLifetimeId,
|
||||
TerrainOps, RegionMinX, RegionMinY, RegionSize);
|
||||
const bool bCacheMatches = SharedCache->bValid
|
||||
&& SharedCache->Seed == SeedU
|
||||
&& SharedCache->StrateIndex == StrateIdx
|
||||
&& SharedCache->ParamsFingerprint == ParamsFingerprint
|
||||
&& SharedCache->LayoutVersion == LayoutVersion
|
||||
&& SharedCache->ManagerLifetimeId == ManagerLifetimeId
|
||||
&& SharedCache->TerrainOps == TerrainOps
|
||||
&& SharedCache->RegionMinX == RegionMinX
|
||||
&& SharedCache->RegionMinY == RegionMinY
|
||||
&& SharedCache->RegionSize == RegionSize;
|
||||
if (!bCacheMatches)
|
||||
{
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::SdfCacheBuild);
|
||||
}
|
||||
SharedCache->bValid = false;
|
||||
SharedCache->Seed = SeedU;
|
||||
SharedCache->StrateIndex = StrateIdx;
|
||||
SharedCache->ParamsFingerprint = ParamsFingerprint;
|
||||
SharedCache->LayoutVersion = LayoutVersion;
|
||||
SharedCache->ManagerLifetimeId = ManagerLifetimeId;
|
||||
SharedCache->TerrainOps = TerrainOps;
|
||||
SharedCache->RegionMinX = RegionMinX;
|
||||
SharedCache->RegionMinY = RegionMinY;
|
||||
SharedCache->RegionSize = RegionSize;
|
||||
const float Expansion = P.CaveWarpStrength + 2.0f;
|
||||
VoxelCaveMorphology::BuildChunkCache(
|
||||
SharedCache->Cache,
|
||||
(float)RegionMinX - Expansion,
|
||||
(float)RegionMinY - Expansion,
|
||||
(float)(RegionMinX + RegionSize) + Expansion,
|
||||
(float)(RegionMinY + RegionSize) + Expansion,
|
||||
P, SeedU, StrateIdx, TerrainOps);
|
||||
SharedCache->bValid = true;
|
||||
}
|
||||
S.ActiveCache = &SharedCache->Cache;
|
||||
|
||||
float CaveSDF = VoxelCaveMorphology::EvaluateSDFCached(
|
||||
WarpedX, WarpedY, WarpedZ, S.Cache, P.SDFBlendRadius, &S.NearestRoom);
|
||||
WarpedX, WarpedY, WarpedZ, GetCache(), P.SDFBlendRadius, &S.NearestRoom);
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// PITS & CHEMINÉES — coordonnées RÉELLES, SmoothMin dans le même canal SDF
|
||||
@@ -3132,7 +3242,7 @@ namespace
|
||||
// mais à des coordonnées NON warpées. Sous un modèle de frames il aurait fallu les sortir
|
||||
// du frame tout en gardant le canal — exprimable, mais tordu. Dans un opérateur unique la
|
||||
// difficulté disparaît : le warp est une variable locale, pas un contexte hérité.
|
||||
for (const FCachedPit& Pit : S.Cache.Pits)
|
||||
for (const FCachedPit& Pit : GetCache().Pits)
|
||||
{
|
||||
const float DZ = WorldZ - Pit.TopZ;
|
||||
if (DZ >= Pit.BlendK) { continue; }
|
||||
@@ -3160,7 +3270,7 @@ namespace
|
||||
CaveSDF = VoxelSDF::SmoothMin(CaveSDF, PitSDF, Pit.BlendK);
|
||||
}
|
||||
|
||||
for (const FCachedChimney& Chim : S.Cache.Chimneys)
|
||||
for (const FCachedChimney& Chim : GetCache().Chimneys)
|
||||
{
|
||||
const float DZ = WorldZ - Chim.BottomZ;
|
||||
if (-DZ >= Chim.BlendK) { continue; }
|
||||
@@ -3641,7 +3751,7 @@ namespace
|
||||
{
|
||||
const FVector& Position = State().LastWorldPosition;
|
||||
return VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
Position.X, Position.Y, Position.Z, State().Cache);
|
||||
Position.X, Position.Y, Position.Z, GetCache());
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -3820,6 +3930,7 @@ namespace
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
const TCHAR* DebugName() const override { return TEXT("CaveRoughnessMod"); }
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
@@ -4017,6 +4128,7 @@ namespace
|
||||
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
const TCHAR* DebugName() const override { return TEXT("CaveTerraceMod"); }
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
@@ -4127,6 +4239,7 @@ namespace
|
||||
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
const TCHAR* DebugName() const override { return TEXT("LayerLineMod"); }
|
||||
|
||||
void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
@@ -4216,6 +4329,7 @@ namespace
|
||||
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
const TCHAR* DebugName() const override { return TEXT("RibbingMod"); }
|
||||
|
||||
void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
@@ -4300,6 +4414,7 @@ namespace
|
||||
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
const TCHAR* DebugName() const override { return TEXT("CaveOverhangMod"); }
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
@@ -4401,6 +4516,7 @@ namespace
|
||||
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
const TCHAR* DebugName() const override { return TEXT("CaveCliffMod"); }
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
@@ -4476,6 +4592,7 @@ namespace
|
||||
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
const TCHAR* DebugName() const override { return TEXT("ScallopMod"); }
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
@@ -4568,6 +4685,7 @@ namespace
|
||||
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
const TCHAR* DebugName() const override { return TEXT("CaveArchMod"); }
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
@@ -4685,6 +4803,7 @@ namespace
|
||||
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
const TCHAR* DebugName() const override { return TEXT("RoomColumnMod"); }
|
||||
|
||||
void Eval(float WorldX, float WorldY, float, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
@@ -4749,6 +4868,7 @@ namespace
|
||||
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
const TCHAR* DebugName() const override { return TEXT("DomeMod"); }
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
@@ -4857,6 +4977,7 @@ namespace
|
||||
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
const TCHAR* DebugName() const override { return TEXT("PinchMod"); }
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
@@ -4977,6 +5098,7 @@ namespace
|
||||
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
const TCHAR* DebugName() const override { return TEXT("FloorBiasMod"); }
|
||||
|
||||
void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
{
|
||||
@@ -5815,10 +5937,9 @@ namespace VoxelDensityOps
|
||||
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
|
||||
P.BoundarySealThickness, P.BaseDensity,
|
||||
SpineRadius, StrateManager, false);
|
||||
OutStack.Add(MakeUnique<FCaveTunnelFloorOp>(RoomPtr, P.BaseDensity));
|
||||
OutStack.Add(MakeUnique<FCaveTunnelAirOp>(
|
||||
RoomPtr, P.BaseDensity,
|
||||
VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels));
|
||||
// The generator's shared MC post evaluates the prepared tunnel-core cache after
|
||||
// disturbances. Keeping these pre-disturbance graph scans here duplicated both the
|
||||
// support and air queries for every voxel; the feature remains in that shared post.
|
||||
OutStack.Add(MakeUnique<FXYEdgeSealOp>(P.BaseDensity));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
#include "VoxelDensityProfile.h"
|
||||
|
||||
namespace VoxelDensityProfile
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::atomic<bool> GEnabled(false);
|
||||
std::atomic<uint64> GCalls[BucketCount];
|
||||
std::atomic<uint64> GCycles[BucketCount];
|
||||
std::atomic<uint64> GCounters[CounterCount];
|
||||
|
||||
int32 ToIndex(EBucket Bucket)
|
||||
{
|
||||
const int32 Index = static_cast<int32>(Bucket);
|
||||
return (Index >= 0 && Index < BucketCount)
|
||||
? Index
|
||||
: static_cast<int32>(EBucket::OtherOp);
|
||||
}
|
||||
}
|
||||
|
||||
void SetEnabled(bool bEnabled)
|
||||
{
|
||||
GEnabled.store(bEnabled, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool IsEnabled()
|
||||
{
|
||||
return GEnabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
for (int32 Index = 0; Index < BucketCount; ++Index)
|
||||
{
|
||||
GCalls[Index].store(0, std::memory_order_relaxed);
|
||||
GCycles[Index].store(0, std::memory_order_relaxed);
|
||||
}
|
||||
for (int32 Index = 0; Index < CounterCount; ++Index)
|
||||
{
|
||||
GCounters[Index].store(0, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
FSnapshot Snapshot()
|
||||
{
|
||||
FSnapshot Result;
|
||||
for (int32 Index = 0; Index < BucketCount; ++Index)
|
||||
{
|
||||
Result.Calls[Index] = GCalls[Index].load(std::memory_order_relaxed);
|
||||
Result.Cycles[Index] = GCycles[Index].load(std::memory_order_relaxed);
|
||||
}
|
||||
for (int32 Index = 0; Index < CounterCount; ++Index)
|
||||
{
|
||||
Result.Counters[Index] = GCounters[Index].load(std::memory_order_relaxed);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
void AddCounter(ECounter Counter, uint64 Amount)
|
||||
{
|
||||
const int32 Index = static_cast<int32>(Counter);
|
||||
if (Index >= 0 && Index < CounterCount)
|
||||
{
|
||||
GCounters[Index].fetch_add(Amount, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
const TCHAR* CounterName(ECounter Counter)
|
||||
{
|
||||
switch (Counter)
|
||||
{
|
||||
case ECounter::TunnelCacheLookup: return TEXT("TunnelCacheLookup");
|
||||
case ECounter::TunnelCacheHit: return TEXT("TunnelCacheHit");
|
||||
case ECounter::TunnelCacheMiss: return TEXT("TunnelCacheMiss");
|
||||
case ECounter::SdfCacheBuild: return TEXT("SdfCacheBuild");
|
||||
case ECounter::CaveRoomCandidates:return TEXT("CaveRoomCandidates");
|
||||
case ECounter::CaveRoomEvaluated: return TEXT("CaveRoomEvaluated");
|
||||
case ECounter::CaveTunnelCandidates:return TEXT("CaveTunnelCandidates");
|
||||
case ECounter::CaveTunnelEvaluated:return TEXT("CaveTunnelEvaluated");
|
||||
case ECounter::TunnelCoreCandidates:return TEXT("TunnelCoreCandidates");
|
||||
case ECounter::TunnelCoreEvaluated:return TEXT("TunnelCoreEvaluated");
|
||||
case ECounter::PassageCandidates: return TEXT("PassageCandidates");
|
||||
case ECounter::PassageEvaluated: return TEXT("PassageEvaluated");
|
||||
case ECounter::Count: break;
|
||||
}
|
||||
return TEXT("Unknown");
|
||||
}
|
||||
|
||||
EBucket BucketFromName(const TCHAR* Name)
|
||||
{
|
||||
if (Name == nullptr) { return EBucket::OtherOp; }
|
||||
|
||||
struct FNameBucket
|
||||
{
|
||||
const TCHAR* Name;
|
||||
EBucket Bucket;
|
||||
};
|
||||
static const FNameBucket Names[] =
|
||||
{
|
||||
{ TEXT("ApplyDisturbances"), EBucket::ApplyDisturbances },
|
||||
{ TEXT("PassageModifier"), EBucket::PassageModifier },
|
||||
{ TEXT("PassageLandingAir"), EBucket::PassageLandingAir },
|
||||
{ TEXT("PassageLandingFloor"), EBucket::PassageLandingFloor },
|
||||
{ TEXT("PassageLandingRoomFloor"), EBucket::PassageLandingRoomFloor },
|
||||
{ TEXT("PassageTunnelAir"), EBucket::PassageTunnelAir },
|
||||
{ TEXT("PassageStructuralPosts"), EBucket::PassageStructuralPosts },
|
||||
{ TEXT("TunnelCoreSupport"), EBucket::TunnelCoreSupport },
|
||||
{ TEXT("TunnelCoreSdf"), EBucket::TunnelCoreSdf },
|
||||
{ TEXT("TunnelCorePosts"), EBucket::TunnelCorePosts },
|
||||
{ TEXT("StructuralTail"), EBucket::StructuralTail },
|
||||
{ TEXT("RoomGraphSource"), EBucket::RoomGraphSource },
|
||||
{ TEXT("SdfCarve"), EBucket::SdfCarve },
|
||||
{ TEXT("SdfConvertOp"), EBucket::SdfCarve },
|
||||
{ TEXT("CaveRoughnessMod"), EBucket::CaveRoughnessMod },
|
||||
{ TEXT("CaveTerraceMod"), EBucket::CaveTerraceMod },
|
||||
{ TEXT("LayerLineMod"), EBucket::LayerLineMod },
|
||||
{ TEXT("RibbingMod"), EBucket::RibbingMod },
|
||||
{ TEXT("CaveOverhangMod"), EBucket::CaveOverhangMod },
|
||||
{ TEXT("CaveCliffMod"), EBucket::CaveCliffMod },
|
||||
{ TEXT("ScallopMod"), EBucket::ScallopMod },
|
||||
{ TEXT("CaveArchMod"), EBucket::CaveArchMod },
|
||||
{ TEXT("RoomColumnMod"), EBucket::RoomColumnMod },
|
||||
{ TEXT("DomeMod"), EBucket::DomeMod },
|
||||
{ TEXT("PinchMod"), EBucket::PinchMod },
|
||||
{ TEXT("FloorBiasMod"), EBucket::FloorBiasMod },
|
||||
{ TEXT("WormFieldSource"), EBucket::WormFieldSource },
|
||||
{ TEXT("OriginSpineOp"), EBucket::OriginSpineOp },
|
||||
{ TEXT("BoundarySealOp"), EBucket::BoundarySealOp },
|
||||
{ TEXT("PassageCarveOp"), EBucket::PassageCarveOp },
|
||||
{ TEXT("CaveTunnelFloorOp"), EBucket::CaveTunnelFloorOp },
|
||||
{ TEXT("CaveTunnelAirOp"), EBucket::CaveTunnelAirOp },
|
||||
{ TEXT("XYEdgeSealOp"), EBucket::XYEdgeSealOp },
|
||||
};
|
||||
for (const FNameBucket& Entry : Names)
|
||||
{
|
||||
if (FCString::Strcmp(Name, Entry.Name) == 0)
|
||||
{
|
||||
return Entry.Bucket;
|
||||
}
|
||||
}
|
||||
return EBucket::OtherOp;
|
||||
}
|
||||
|
||||
const TCHAR* BucketName(EBucket Bucket)
|
||||
{
|
||||
switch (Bucket)
|
||||
{
|
||||
case EBucket::GetDensityAt: return TEXT("GetDensityAt");
|
||||
case EBucket::ApplyDisturbances:return TEXT("ApplyDisturbances");
|
||||
case EBucket::PassageModifier: return TEXT("PassageModifier");
|
||||
case EBucket::PassageLandingAir:return TEXT("PassageLandingAir");
|
||||
case EBucket::PassageLandingFloor:return TEXT("PassageLandingFloor");
|
||||
case EBucket::PassageLandingRoomFloor:return TEXT("PassageLandingRoomFloor");
|
||||
case EBucket::PassageTunnelAir:return TEXT("PassageTunnelAir");
|
||||
case EBucket::PassageStructuralPosts:return TEXT("PassageStructuralPosts");
|
||||
case EBucket::TunnelCoreSupport:return TEXT("TunnelCoreSupport");
|
||||
case EBucket::TunnelCoreSdf: return TEXT("TunnelCoreSdf");
|
||||
case EBucket::TunnelCorePosts: return TEXT("TunnelCorePosts");
|
||||
case EBucket::StructuralTail: return TEXT("StructuralTail");
|
||||
case EBucket::RoomGraphSource: return TEXT("RoomGraphSource");
|
||||
case EBucket::SdfCarve: return TEXT("SdfCarve");
|
||||
case EBucket::CaveRoughnessMod: return TEXT("CaveRoughnessMod");
|
||||
case EBucket::CaveTerraceMod: return TEXT("CaveTerraceMod");
|
||||
case EBucket::LayerLineMod: return TEXT("LayerLineMod");
|
||||
case EBucket::RibbingMod: return TEXT("RibbingMod");
|
||||
case EBucket::CaveOverhangMod: return TEXT("CaveOverhangMod");
|
||||
case EBucket::CaveCliffMod: return TEXT("CaveCliffMod");
|
||||
case EBucket::ScallopMod: return TEXT("ScallopMod");
|
||||
case EBucket::CaveArchMod: return TEXT("CaveArchMod");
|
||||
case EBucket::RoomColumnMod: return TEXT("RoomColumnMod");
|
||||
case EBucket::DomeMod: return TEXT("DomeMod");
|
||||
case EBucket::PinchMod: return TEXT("PinchMod");
|
||||
case EBucket::FloorBiasMod: return TEXT("FloorBiasMod");
|
||||
case EBucket::WormFieldSource: return TEXT("WormFieldSource");
|
||||
case EBucket::OriginSpineOp: return TEXT("OriginSpineOp");
|
||||
case EBucket::BoundarySealOp: return TEXT("BoundarySealOp");
|
||||
case EBucket::PassageCarveOp: return TEXT("PassageCarveOp");
|
||||
case EBucket::CaveTunnelFloorOp:return TEXT("CaveTunnelFloorOp");
|
||||
case EBucket::CaveTunnelAirOp: return TEXT("CaveTunnelAirOp");
|
||||
case EBucket::XYEdgeSealOp: return TEXT("XYEdgeSealOp");
|
||||
case EBucket::OtherOp: return TEXT("OtherOp");
|
||||
case EBucket::Count: break;
|
||||
}
|
||||
return TEXT("Unknown");
|
||||
}
|
||||
|
||||
FScopedTimer::~FScopedTimer()
|
||||
{
|
||||
if (!bActive) { return; }
|
||||
const int32 Index = ToIndex(Bucket);
|
||||
GCalls[Index].fetch_add(1, std::memory_order_relaxed);
|
||||
GCycles[Index].fetch_add(FPlatformTime::Cycles64() - StartCycles,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "VoxelDensityOpStack.h" // OPSTACK Phase 1: the opt-in per-strate operator stack
|
||||
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
|
||||
#include "VoxelStats.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "VoxelStrateComposer.h"
|
||||
@@ -174,6 +175,7 @@ static thread_local FSurfaceColumnCache GSurfColCache;
|
||||
// T2.b — per-thread octave bias for the tile being meshed (see VoxelGenerator.h).
|
||||
// 0 = full quality; set by the mesher per tile from Step + Settings->LODOctaveDrop.
|
||||
thread_local int32 VoxelGenLOD::OctaveBias = 0;
|
||||
thread_local int32 VoxelGenLOD::SampleStep = 1;
|
||||
|
||||
// NOTE (T2.a): the fBm/Ridged bodies moved to VoxelNoise.h, where octaves are evaluated
|
||||
// 4-wide via SSE (Perlin3D_x4). These thin wrappers keep every call site unchanged. They
|
||||
@@ -273,6 +275,8 @@ static FORCEINLINE void ApplyOriginLandingFloor(float& Density,
|
||||
static void ApplyDisturbances(float& MC, float X, float Y, float Z,
|
||||
const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir)
|
||||
{
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::ApplyDisturbances);
|
||||
const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness;
|
||||
const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness;
|
||||
if (Z <= InnerBot || Z >= InnerTop) return;
|
||||
@@ -487,7 +491,12 @@ namespace
|
||||
// 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
|
||||
// moving them on every voxel.
|
||||
constexpr int32 TunnelDensityCacheSlotCount = 128;
|
||||
// The largest production coarse tile can touch thousands of exact chunk keys once its
|
||||
// gradient halo is included. A 128-slot direct map repeatedly evicts nearby keys (the LOD
|
||||
// probe measured 1,734 misses for only 6,859 samples at L4), rebuilding the room/tunnel cache
|
||||
// in the hot loop. Keep the same deterministic key and open-addressed lookup, but size the
|
||||
// bounded table for the measured L4 working set rather than the old 128-slot guess.
|
||||
constexpr int32 TunnelDensityCacheSlotCount = 4096;
|
||||
|
||||
struct FTunnelNetworkDensityCacheEntry
|
||||
{
|
||||
@@ -728,6 +737,8 @@ void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
|
||||
|
||||
float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) const
|
||||
{
|
||||
VoxelDensityProfile::FScopedTimer DensityProfileTimer(
|
||||
VoxelDensityProfile::EBucket::GetDensityAt);
|
||||
// ── STRATE SYSTEM ──
|
||||
// Query per-chunk params from the manager so each strate has different caves.
|
||||
float Result;
|
||||
@@ -827,20 +838,62 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
FVoxelOpStack* ActiveOpStack = &CP_OpStack;
|
||||
FTunnelCoreCacheState* ActiveTunnelCoreCache = >unnelCoreCache;
|
||||
bool bLoadedTunnelDensityCache = false;
|
||||
FTunnelNetworkDensityCacheEntry* TunnelDensityCacheEntryForWrite = nullptr;
|
||||
|
||||
// The table is open-addressed rather than one-slot direct-mapped. A direct map is very
|
||||
// cheap until a coarse tile's regular lattice happens to put two chunk keys in the same
|
||||
// bucket; then the two keys evict each other on every row and rebuild the prepared graph.
|
||||
// Probe deterministically to the first empty/stale slot, retaining the bounded memory cap.
|
||||
auto FindTunnelDensityCacheEntry = [&](const FIntVector& QueryChunk,
|
||||
FTunnelNetworkDensityCacheEntry*& OutEntry) -> bool
|
||||
{
|
||||
const int32 StartSlot = TunnelDensityCacheSlot(QueryChunk);
|
||||
FTunnelNetworkDensityCacheEntry* ReuseEntry = nullptr;
|
||||
for (int32 Probe = 0; Probe < TunnelDensityCacheSlotCount; ++Probe)
|
||||
{
|
||||
const int32 Slot = (StartSlot + Probe) & (TunnelDensityCacheSlotCount - 1);
|
||||
FTunnelNetworkDensityCacheEntry& Candidate = CP_TunnelDensityCache[Slot];
|
||||
if (!Candidate.bValid || Candidate.OwnerId != DensityCacheOwnerId)
|
||||
{
|
||||
if (ReuseEntry == nullptr) { ReuseEntry = &Candidate; }
|
||||
if (!Candidate.bValid) { break; }
|
||||
continue;
|
||||
}
|
||||
const bool bKeyMatches = Candidate.ManagerLifetimeId == ManagerLifetimeId
|
||||
&& Candidate.Chunk == QueryChunk
|
||||
&& Candidate.LayoutVersion == LayoutVersion
|
||||
&& (Candidate.GenType == ECaveGeneratorType::TunnelNetwork
|
||||
|| Candidate.GenType == ECaveGeneratorType::Underwater);
|
||||
if (bKeyMatches)
|
||||
{
|
||||
OutEntry = &Candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
OutEntry = ReuseEntry != nullptr
|
||||
? ReuseEntry
|
||||
: &CP_TunnelDensityCache[StartSlot];
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!bIsGapChunk)
|
||||
{
|
||||
FTunnelNetworkDensityCacheEntry& CachedEntry =
|
||||
CP_TunnelDensityCache[TunnelDensityCacheSlot(ChunkCoord)];
|
||||
const bool bCacheKeyMatches = CachedEntry.bValid
|
||||
&& CachedEntry.OwnerId == DensityCacheOwnerId
|
||||
&& CachedEntry.ManagerLifetimeId == ManagerLifetimeId
|
||||
&& CachedEntry.Chunk == ChunkCoord
|
||||
&& CachedEntry.LayoutVersion == LayoutVersion
|
||||
&& (CachedEntry.GenType == ECaveGeneratorType::TunnelNetwork
|
||||
|| CachedEntry.GenType == ECaveGeneratorType::Underwater);
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCacheLookup);
|
||||
}
|
||||
FTunnelNetworkDensityCacheEntry* CachedEntryPtr = nullptr;
|
||||
const bool bCacheKeyMatches = FindTunnelDensityCacheEntry(ChunkCoord, CachedEntryPtr);
|
||||
FTunnelNetworkDensityCacheEntry& CachedEntry = *CachedEntryPtr;
|
||||
TunnelDensityCacheEntryForWrite = CachedEntryPtr;
|
||||
if (bCacheKeyMatches)
|
||||
{
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCacheHit);
|
||||
}
|
||||
if (CP_ActiveTunnelDensityCache != &CachedEntry)
|
||||
{
|
||||
CP_OwnerId = CachedEntry.OwnerId;
|
||||
@@ -862,6 +915,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
ActiveTunnelCoreCache = &CachedEntry.TunnelCore;
|
||||
bLoadedTunnelDensityCache = true;
|
||||
}
|
||||
else if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TunnelCacheMiss);
|
||||
}
|
||||
}
|
||||
|
||||
const bool bOwnerChanged = DensityCacheOwnerId != CP_OwnerId;
|
||||
@@ -1083,8 +1141,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
#endif
|
||||
if (!bLoadedTunnelDensityCache && bCanStoreTunnelDensityCache)
|
||||
{
|
||||
FTunnelNetworkDensityCacheEntry& CachedEntry =
|
||||
CP_TunnelDensityCache[TunnelDensityCacheSlot(ChunkCoord)];
|
||||
FTunnelNetworkDensityCacheEntry& CachedEntry = TunnelDensityCacheEntryForWrite
|
||||
? *TunnelDensityCacheEntryForWrite
|
||||
: CP_TunnelDensityCache[TunnelDensityCacheSlot(ChunkCoord)];
|
||||
CachedEntry.bValid = false;
|
||||
CachedEntry.OwnerId = DensityCacheOwnerId;
|
||||
CachedEntry.ManagerLifetimeId = ManagerLifetimeId;
|
||||
@@ -1253,34 +1312,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
Result, WorldX, WorldY, WorldZ,
|
||||
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
|
||||
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
|
||||
StrateManager->ApplyPassageLandingAirMC(
|
||||
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
|
||||
CP_Dist.BoundarySealThickness);
|
||||
VF_ApplyOriginLandingFloorMC(
|
||||
Result, WorldX, WorldY, WorldZ,
|
||||
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
|
||||
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
|
||||
StrateManager->ApplyPassageLandingFloorMC(
|
||||
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
|
||||
// A walkable tunnel's guaranteed air core is the final structural writer above its
|
||||
// floor. This prevents a nearby passage's floor slab from capping the active route;
|
||||
// the tunnel-floor predicate is false on the active floor itself, so support remains.
|
||||
StrateManager->ApplyPassageTunnelAirMC(
|
||||
StrateManager->ApplyPassageStructuralPostsMC(
|
||||
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
|
||||
CP_Dist.BoundarySealThickness);
|
||||
VF_ApplyOriginLandingAirMC(
|
||||
Result, WorldX, WorldY, WorldZ,
|
||||
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
|
||||
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
|
||||
StrateManager->ApplyPassageLandingAirMC(
|
||||
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
|
||||
CP_Dist.BoundarySealThickness);
|
||||
VF_ApplyOriginLandingFloorMC(
|
||||
Result, WorldX, WorldY, WorldZ,
|
||||
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
|
||||
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
|
||||
StrateManager->ApplyPassageLandingRoomFloorMC(
|
||||
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
|
||||
|
||||
// Disturbance features are authored as a generic MC-space post and may add a ridge or
|
||||
// bridge over a graph tunnel. Reassert the native cached tunnel core here, after every
|
||||
@@ -1288,17 +1326,21 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
// never once per voxel.
|
||||
if (ActiveTunnelCoreCache->bValid)
|
||||
{
|
||||
const bool bTunnelSupportFloor =
|
||||
VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||
FTunnelCoreWorldEvaluation TunnelCore;
|
||||
{
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::TunnelCorePosts);
|
||||
TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
||||
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache);
|
||||
}
|
||||
const bool bTunnelSupportFloor = TunnelCore.bSupportFloor;
|
||||
if (bTunnelSupportFloor)
|
||||
{
|
||||
Result = FMath::Min(
|
||||
Result,
|
||||
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
|
||||
}
|
||||
const float CoreSDF = VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache);
|
||||
const float CoreSDF = TunnelCore.SDF;
|
||||
if (!bTunnelSupportFloor
|
||||
&& CoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels)
|
||||
{
|
||||
@@ -1563,6 +1605,11 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
|
||||
if (bNeedRebuild)
|
||||
{
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::SdfCacheBuild);
|
||||
}
|
||||
// Center the search box on this query's chunk. Search area = chunk XY extent
|
||||
// + CaveWarpStrength margin (covers warp displacement) + gradient sampling.
|
||||
// MaxInfluence (room/tunnel reach) is added internally by BuildChunkCache.
|
||||
|
||||
@@ -29,6 +29,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
const int32 OctaveBias = (LODOctaveDrop > 0 && Step > 1)
|
||||
? LODOctaveDrop * (int32)FMath::FloorLog2((uint32)Step) : 0;
|
||||
TGuardValue<int32> OctaveBiasGuard(VoxelGenLOD::OctaveBias, OctaveBias);
|
||||
TGuardValue<int32> SampleStepGuard(VoxelGenLOD::SampleStep, Step);
|
||||
|
||||
// World-cm origin of the tile's min corner (positions are built relative to this).
|
||||
const FVector ChunkWorldPos = FVector(OriginVoxels) * VOXEL_SIZE;
|
||||
@@ -225,8 +226,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
// gradient) sont échantillonnées — le reste du tampon reste non initialisé et non lu.
|
||||
for (int32 gz = GzLo; gz <= GzHi; gz++)
|
||||
{
|
||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
||||
for (int32 gy = -1; gy <= GridDim; gy++)
|
||||
{
|
||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
||||
for (int32 gx = -1; gx <= GridDim; gx++)
|
||||
{
|
||||
// World voxel = tile origin + grid offset scaled by the cell size (Step).
|
||||
@@ -289,8 +292,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
CapTris.Reset();
|
||||
for (int32 cz = CzLo; cz <= CzHi; cz++) // bande de strate : cf. CzLo/CzHi plus haut
|
||||
{
|
||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
||||
for (int32 cy = 0; cy < CellsPerAxis; cy++)
|
||||
{
|
||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
||||
for (int32 cx = 0; cx < CellsPerAxis; cx++)
|
||||
{
|
||||
// PASSE 1 : densités aux 8 coins + index de cas MC SEULEMENT.
|
||||
@@ -498,6 +503,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateSheetMesh(FIntVector OriginVox
|
||||
SheetCols.SetNumUninitialized(MDim * MDim);
|
||||
for (int32 gy = -1; gy <= GridDim; ++gy)
|
||||
{
|
||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
||||
for (int32 gx = -1; gx <= GridDim; ++gx)
|
||||
{
|
||||
float Tz = 0.0f, Cz = 0.0f;
|
||||
@@ -572,6 +578,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateSheetMesh(FIntVector OriginVox
|
||||
|
||||
for (int32 cy = 0; cy < CellsXY; ++cy)
|
||||
{
|
||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
||||
for (int32 cx = 0; cx < CellsXY; ++cx)
|
||||
{
|
||||
if (CellInHole(cx, cy)) continue; // couverte par les coquilles MC proches
|
||||
@@ -638,6 +645,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateSheetMesh(FIntVector OriginVox
|
||||
// (pas dans le trou XY) — sinon mur flottant sans surface.
|
||||
for (int32 c = 0; c < CellsXY; ++c)
|
||||
{
|
||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
||||
for (int32 Pass = 0; Pass < 2; ++Pass)
|
||||
{
|
||||
const bool bCap = (Pass == 1);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
|
||||
#include "VoxelCaveMorphology.h" // For VoxelSDF and VoxelHash
|
||||
#include "VoxelDensityPrimitives.h" // Shared passage carve polarity/strength
|
||||
#include "VoxelDensityProfile.h" // Opt-in targeted per-voxel attribution
|
||||
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
||||
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
@@ -1687,6 +1688,12 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
|
||||
// The shortlist is rebuilt once for a (manager, version, chunk) and is shared by the tube,
|
||||
// landing, and floor paths. No source-fit stencil or topology search is allowed below it.
|
||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::PassageCandidates,
|
||||
static_cast<uint64>(Nearby.Num()));
|
||||
}
|
||||
|
||||
if (Nearby.Num() == 0) return FLT_MAX; // no passage near this chunk → no carve
|
||||
|
||||
@@ -1707,6 +1714,11 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
|
||||
// capsule chain unconditionally — the dominant lag source once passages became
|
||||
// 12-segment worms. Now far passages cost a single squared-distance compare.
|
||||
if (FVector::DistSquared(Pos, P.BoundCenter) > P.BoundRadiusSq) continue;
|
||||
if (VoxelDensityProfile::IsEnabled())
|
||||
{
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::PassageEvaluated);
|
||||
}
|
||||
|
||||
if (P.ControlPoints.Num() >= 2)
|
||||
{
|
||||
@@ -1748,6 +1760,8 @@ void UVoxelStrateManager::ApplyPassageModifier(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity, float SealThickness) const
|
||||
{
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageModifier);
|
||||
const float ModSDF = EvaluateModifierSDF(WorldX, WorldY, WorldZ);
|
||||
VF_ApplyPassageCarving(Density, ModSDF, BaseDensity, SealThickness);
|
||||
ApplyPassageLandingAir(Density, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
|
||||
@@ -1781,10 +1795,22 @@ void UVoxelStrateManager::ApplyPassageModifier(
|
||||
ApplyPassageTunnelAir(Density, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
|
||||
}
|
||||
|
||||
void UVoxelStrateManager::ApplyPassageCarvingOnly(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity, float SealThickness) const
|
||||
{
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageModifier);
|
||||
const float ModSDF = EvaluateModifierSDF(WorldX, WorldY, WorldZ);
|
||||
VF_ApplyPassageCarving(Density, ModSDF, BaseDensity, SealThickness);
|
||||
}
|
||||
|
||||
void UVoxelStrateManager::ApplyPassageLandingAir(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity, float SealThickness) const
|
||||
{
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageLandingAir);
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
@@ -1846,6 +1872,8 @@ void UVoxelStrateManager::ApplyPassageTunnelAir(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity, float SealThickness) const
|
||||
{
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageTunnelAir);
|
||||
const FVector Position(WorldX, WorldY, WorldZ);
|
||||
int32 PassageIndex = INDEX_NONE;
|
||||
float FloorZ = 0.0f;
|
||||
@@ -1874,6 +1902,8 @@ void UVoxelStrateManager::ApplyPassageTunnelAirMC(
|
||||
void UVoxelStrateManager::ApplyPassageLandingFloorMC(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity) const
|
||||
{
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageLandingFloor);
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
@@ -1914,10 +1944,102 @@ static bool VF_IsPassageRoomFloor(
|
||||
<= FMath::Max(Landing.HalfWidth - 1.0f, 0.0f);
|
||||
}
|
||||
|
||||
void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity, float SealThickness) const
|
||||
{
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageStructuralPosts);
|
||||
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE));
|
||||
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord);
|
||||
if (Nearby.Num() == 0) return;
|
||||
|
||||
const FVector Position(WorldX, WorldY, WorldZ);
|
||||
const bool bSuppressFloor =
|
||||
VoxelPassageGeometry::VerticalShaftConnectorAirMarker();
|
||||
bool bAnyPassageFloor = false;
|
||||
bool bAnyRoomFloor = false;
|
||||
bool bWalkableAir = false;
|
||||
float MinLandingSDF = FLT_MAX;
|
||||
|
||||
// The four old MC backstops all walked the same immutable per-chunk shortlist. Gather their
|
||||
// predicates in one pass; the writes below retain the old order (landing air, landing floor,
|
||||
// tunnel air, room floor), including the vertical-shaft floor suppression marker.
|
||||
for (const int32 PassageIndex : Nearby)
|
||||
{
|
||||
if (!Passages.IsValidIndex(PassageIndex)) continue;
|
||||
const FVoxelPassage& Passage = Passages[PassageIndex];
|
||||
|
||||
if (!bSuppressFloor
|
||||
&& (VF_IsPassageLandingFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageLandingFloor(Position, Passage.LowerLanding)
|
||||
|| VF_IsWalkableTunnelFloor(Passage, Position)))
|
||||
{
|
||||
bAnyPassageFloor = true;
|
||||
}
|
||||
|
||||
if (!bSuppressFloor
|
||||
&& (VF_IsPassageRoomFloor(Position, Passage.UpperLanding)
|
||||
|| VF_IsPassageRoomFloor(Position, Passage.LowerLanding)))
|
||||
{
|
||||
bAnyRoomFloor = true;
|
||||
}
|
||||
|
||||
if (!bWalkableAir
|
||||
&& FVector::DistSquared(Position, Passage.BoundCenter) <= Passage.BoundRadiusSq
|
||||
&& VF_IsWalkableTunnelAir(Passage, Position))
|
||||
{
|
||||
bWalkableAir = true;
|
||||
}
|
||||
|
||||
if (FVector::DistSquared(Position, Passage.BoundCenter) <= Passage.BoundRadiusSq)
|
||||
{
|
||||
const FVoxelPassageLanding* Landings[] = {
|
||||
&Passage.UpperLanding, &Passage.LowerLanding };
|
||||
for (const FVoxelPassageLanding* Landing : Landings)
|
||||
{
|
||||
MinLandingSDF = FMath::Min(
|
||||
MinLandingSDF,
|
||||
VF_EvaluatePassageLandingSDF(Position, *Landing));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bAnyPassageFloor && MinLandingSDF < FLT_MAX)
|
||||
{
|
||||
float InternalDensity = -Density;
|
||||
VF_ApplyPassageLandingCarving(
|
||||
InternalDensity, MinLandingSDF, BaseDensity, SealThickness);
|
||||
Density = -InternalDensity;
|
||||
}
|
||||
|
||||
if (bAnyPassageFloor)
|
||||
{
|
||||
Density = FMath::Min(Density, -BaseDensity);
|
||||
}
|
||||
|
||||
if (bWalkableAir)
|
||||
{
|
||||
Density = FMath::Max(
|
||||
Density, BaseDensity * 2.0f + SealThickness + 4.0f);
|
||||
}
|
||||
|
||||
if (bAnyRoomFloor)
|
||||
{
|
||||
Density = FMath::Min(Density, -BaseDensity);
|
||||
}
|
||||
}
|
||||
|
||||
void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC(
|
||||
float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity) const
|
||||
{
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(
|
||||
VoxelDensityProfile::EBucket::PassageLandingRoomFloor);
|
||||
const FIntVector ChunkCoord(
|
||||
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
|
||||
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
|
||||
|
||||
#if WITH_EDITOR
|
||||
@@ -38,6 +39,15 @@
|
||||
#include "VoxelStrateMeasure.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
int32 GVoxelForgeProfileTileGeneration = 0;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration(
|
||||
TEXT("voxel.ProfileTileGeneration"),
|
||||
GVoxelForgeProfileTileGeneration,
|
||||
TEXT("Log worker tile generation time with level/step/cell count."));
|
||||
}
|
||||
|
||||
AVoxelWorld::AVoxelWorld()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
@@ -855,7 +865,10 @@ void AVoxelWorld::OnObjectModifiedInEditor(UObject* ModifiedObject)
|
||||
void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
// Signal all async tasks to bail out ASAP
|
||||
const double EndPlayStartSeconds = FPlatformTime::Seconds();
|
||||
bShuttingDown.store(true, std::memory_order_release);
|
||||
UE_LOG(LogTemp, Display, TEXT("[VoxelWorld] EndPlay: shutdown signalled; waiting for %d tasks"),
|
||||
ActiveTaskCount.load(std::memory_order_relaxed));
|
||||
|
||||
// Wait for every running task before destroying UObjects. A timeout here is unsafe: a task
|
||||
// already inside GenerateTileResult may still be evaluating a cached op stack that points at
|
||||
@@ -872,6 +885,8 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
}
|
||||
FPlatformProcess::Yield(); // Give CPU to other threads
|
||||
}
|
||||
UE_LOG(LogTemp, Display, TEXT("[VoxelWorld] EndPlay: workers drained in %.6fs"),
|
||||
FPlatformTime::Seconds() - EndPlayStartSeconds);
|
||||
|
||||
// Drain any queued results
|
||||
FChunkResult Discard;
|
||||
@@ -965,6 +980,7 @@ void AVoxelWorld::BeginPlay()
|
||||
|
||||
Generator->InitializeSettings(Settings);
|
||||
Mesher->SetGenerator(Generator);
|
||||
Mesher->SetShutdownFlag(&bShuttingDown);
|
||||
Mesher->bGenerateSkirts = Settings->bGenerateSkirts;
|
||||
Mesher->SkirtCells = Settings->SkirtCells;
|
||||
Mesher->LODOctaveDrop = Settings->LODOctaveDrop; // T2.b — 0 = off
|
||||
@@ -1161,6 +1177,13 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
{
|
||||
PendingTiles.Remove(DequeuedChunk.Tile);
|
||||
|
||||
// A worker can observe shutdown after entering the mesher. Do not turn that partial
|
||||
// result into an all-air tile: it must remain eligible for a future generation epoch.
|
||||
if (DequeuedChunk.bAborted)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// ApplyTileResult does epoch check, mark-loaded, capture ingest, empty-release / mesh upload.
|
||||
// Only a real (visible) upload counts against the per-frame budget — stale/empty drain free.
|
||||
if (ApplyTileResult(DequeuedChunk))
|
||||
@@ -1179,7 +1202,7 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
||||
{
|
||||
// Discard results from a previous generation epoch (stale).
|
||||
if (Result.Epoch != GenerationEpoch)
|
||||
if (Result.bAborted || Result.Epoch != GenerationEpoch)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -2078,11 +2101,44 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
|
||||
FChunkResult& Result)
|
||||
{
|
||||
const bool bProfileTile = GVoxelForgeProfileTileGeneration != 0;
|
||||
const double TileStartSeconds = bProfileTile ? FPlatformTime::Seconds() : 0.0;
|
||||
auto EmitTileProfile = [&]()
|
||||
{
|
||||
if (bProfileTile)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeTileProfile] level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d seconds=%.6f"),
|
||||
Tile.Level, Step, Cells, bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0,
|
||||
Result.bEmpty ? 1 : 0, FPlatformTime::Seconds() - TileStartSeconds);
|
||||
}
|
||||
};
|
||||
|
||||
Result.Tile = Tile;
|
||||
Result.Epoch = Epoch;
|
||||
Result.bAborted = false;
|
||||
Result.bEmpty = true;
|
||||
Result.Streams.Reset();
|
||||
Result.CaptureGrid.Reset();
|
||||
Result.BandChunkLo = BandChunkLo; // strate content cut (MIN/MAX = uncut)
|
||||
Result.BandChunkHi = BandChunkHi;
|
||||
|
||||
// Shared by async workers and the synchronous carve path. An interrupted result is empty but
|
||||
// explicitly discarded, never an all-air tile that gets marked loaded.
|
||||
auto AbortResult = [&]()
|
||||
{
|
||||
Result.bAborted = true;
|
||||
Result.bEmpty = true;
|
||||
Result.Streams.Reset();
|
||||
Result.CaptureGrid.Reset();
|
||||
};
|
||||
if (ShouldAbortWork())
|
||||
{
|
||||
AbortResult();
|
||||
EmitTileProfile();
|
||||
return;
|
||||
}
|
||||
|
||||
// T1.d — TRIVIAL-TILE REJECT: ~84 % des tuiles générées sortaient vides (tout-roc /
|
||||
// tout-air) en payant quand même le pré-échantillonnage complet. Le classifieur prouve
|
||||
// (bornes exactes sur le treillis du mesher + gardes conservatives) qu'une tuile est
|
||||
@@ -2096,6 +2152,12 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile);
|
||||
INC_DWORD_STAT(STAT_VoxelForgeTilesClassified);
|
||||
const EVoxelTileClass Verdict = Generator->ClassifyTile(OriginVoxels, Step, Cells);
|
||||
if (ShouldAbortWork())
|
||||
{
|
||||
AbortResult();
|
||||
EmitTileProfile();
|
||||
return;
|
||||
}
|
||||
if (Verdict == EVoxelTileClass::AllSolid)
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeTilesSkippedAllSolid);
|
||||
@@ -2123,6 +2185,12 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
bWantCapture ? &Result.CaptureGrid : nullptr,
|
||||
BandVoxLo, BandVoxHi);
|
||||
INC_DWORD_STAT(STAT_VoxelForgeTilesMeshed);
|
||||
if (ShouldAbortWork())
|
||||
{
|
||||
AbortResult();
|
||||
EmitTileProfile();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// T1.f — build the RMC geometry buffers HERE (worker), not on the game thread. Empty/all-air
|
||||
@@ -2132,6 +2200,12 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_BuildStreams);
|
||||
Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>();
|
||||
BuildTileStreamSet(*Result.Streams, MeshData);
|
||||
if (ShouldAbortWork())
|
||||
{
|
||||
AbortResult();
|
||||
EmitTileProfile();
|
||||
return;
|
||||
}
|
||||
Result.bEmpty = false;
|
||||
|
||||
// F17 — the mesher classified every triangle semantically (sky-cap vs ground, per
|
||||
@@ -2143,6 +2217,8 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
Result.bHasCeilingTris = MeshData.NumCeilingTriangles > 0;
|
||||
Result.bHasGroundTris = NumTris > MeshData.NumCeilingTriangles;
|
||||
}
|
||||
|
||||
EmitTileProfile();
|
||||
}
|
||||
|
||||
// Section-group key shared by every tile component ("the" tile geometry group).
|
||||
|
||||
@@ -669,6 +669,13 @@ struct FChunkSDFCache
|
||||
TArray<FCachedColumn> Columns;
|
||||
};
|
||||
|
||||
/** Combined world-space tunnel post query; one cache scan supplies both support and air tests. */
|
||||
struct FTunnelCoreWorldEvaluation
|
||||
{
|
||||
float SDF = FLT_MAX;
|
||||
bool bSupportFloor = false;
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// CAVE MORPHOLOGY EVALUATOR
|
||||
//=============================================================================
|
||||
@@ -757,6 +764,11 @@ namespace VoxelCaveMorphology
|
||||
const FChunkSDFCache& Cache
|
||||
);
|
||||
|
||||
VOXELFORGE_API FTunnelCoreWorldEvaluation EvaluateTunnelCoreWorld(
|
||||
float WorldX, float WorldY, float WorldZ,
|
||||
const FChunkSDFCache& Cache
|
||||
);
|
||||
|
||||
// True for the finite support slab beneath a world-space graph tunnel. The native density
|
||||
// path and the operator stack use this same predicate before reopening the air core, keeping
|
||||
// the rounded capsule's bottom from becoming an unsupported point.
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "VoxelDensityOp.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
#include "VoxelStrateTypes.h" // FMazeGenerationParams
|
||||
#include "VoxelHeightOp.h" // IVoxelBiomeField — BuildSurfaceStack takes ownership of one
|
||||
|
||||
@@ -85,6 +86,7 @@ private:
|
||||
bool bAdditive = false;
|
||||
EVoxelOpResourceMask RequiredResources = VoxelOpResources::None;
|
||||
EVoxelOpResourceMask ProvidedResources = VoxelOpResources::None;
|
||||
const TCHAR* ProfileName = nullptr;
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -121,6 +123,7 @@ public:
|
||||
Entry.bAdditive = Entry.Op->IsAdditive();
|
||||
Entry.RequiredResources = Entry.Op->RequiredResources();
|
||||
Entry.ProvidedResources = Entry.Op->ProvidedResources();
|
||||
Entry.ProfileName = Entry.Op->DebugName();
|
||||
}
|
||||
Ops.Add(MoveTemp(Entry));
|
||||
}
|
||||
@@ -153,7 +156,11 @@ public:
|
||||
FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const
|
||||
{
|
||||
FVoxelOpSample S;
|
||||
for (const FOpEntry& Entry : Ops) { Entry.Op->Eval(WorldX, WorldY, WorldZ, S); }
|
||||
for (const FOpEntry& Entry : Ops)
|
||||
{
|
||||
VoxelDensityProfile::FScopedTimer ProfileTimer(Entry.ProfileName);
|
||||
Entry.Op->Eval(WorldX, WorldY, WorldZ, S);
|
||||
}
|
||||
return S;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// Opt-in density-path profiling used by the editor commandlet and targeted PIE diagnostics.
|
||||
// Disabled by default so the shipping density path pays only one predictable branch in the
|
||||
// scoped timers below.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "HAL/PlatformTime.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace VoxelDensityProfile
|
||||
{
|
||||
enum class EBucket : uint8
|
||||
{
|
||||
GetDensityAt,
|
||||
ApplyDisturbances,
|
||||
PassageModifier,
|
||||
PassageLandingAir,
|
||||
PassageLandingFloor,
|
||||
PassageLandingRoomFloor,
|
||||
PassageTunnelAir,
|
||||
PassageStructuralPosts,
|
||||
TunnelCoreSupport,
|
||||
TunnelCoreSdf,
|
||||
TunnelCorePosts,
|
||||
StructuralTail,
|
||||
RoomGraphSource,
|
||||
SdfCarve,
|
||||
CaveRoughnessMod,
|
||||
CaveTerraceMod,
|
||||
LayerLineMod,
|
||||
RibbingMod,
|
||||
CaveOverhangMod,
|
||||
CaveCliffMod,
|
||||
ScallopMod,
|
||||
CaveArchMod,
|
||||
RoomColumnMod,
|
||||
DomeMod,
|
||||
PinchMod,
|
||||
FloorBiasMod,
|
||||
WormFieldSource,
|
||||
OriginSpineOp,
|
||||
BoundarySealOp,
|
||||
PassageCarveOp,
|
||||
CaveTunnelFloorOp,
|
||||
CaveTunnelAirOp,
|
||||
XYEdgeSealOp,
|
||||
OtherOp,
|
||||
Count
|
||||
};
|
||||
|
||||
constexpr int32 BucketCount = static_cast<int32>(EBucket::Count);
|
||||
|
||||
enum class ECounter : uint8
|
||||
{
|
||||
TunnelCacheLookup,
|
||||
TunnelCacheHit,
|
||||
TunnelCacheMiss,
|
||||
SdfCacheBuild,
|
||||
CaveRoomCandidates,
|
||||
CaveRoomEvaluated,
|
||||
CaveTunnelCandidates,
|
||||
CaveTunnelEvaluated,
|
||||
TunnelCoreCandidates,
|
||||
TunnelCoreEvaluated,
|
||||
PassageCandidates,
|
||||
PassageEvaluated,
|
||||
Count
|
||||
};
|
||||
|
||||
constexpr int32 CounterCount = static_cast<int32>(ECounter::Count);
|
||||
|
||||
struct FSnapshot
|
||||
{
|
||||
uint64 Calls[BucketCount]{};
|
||||
uint64 Cycles[BucketCount]{};
|
||||
uint64 Counters[CounterCount]{};
|
||||
};
|
||||
|
||||
VOXELFORGE_API void SetEnabled(bool bEnabled);
|
||||
VOXELFORGE_API bool IsEnabled();
|
||||
VOXELFORGE_API void Reset();
|
||||
VOXELFORGE_API FSnapshot Snapshot();
|
||||
VOXELFORGE_API void AddCounter(ECounter Counter, uint64 Amount = 1);
|
||||
VOXELFORGE_API const TCHAR* CounterName(ECounter Counter);
|
||||
VOXELFORGE_API EBucket BucketFromName(const TCHAR* Name);
|
||||
VOXELFORGE_API const TCHAR* BucketName(EBucket Bucket);
|
||||
|
||||
class FScopedTimer final
|
||||
{
|
||||
public:
|
||||
explicit FScopedTimer(EBucket InBucket)
|
||||
: Bucket(InBucket)
|
||||
, bActive(IsEnabled())
|
||||
, StartCycles(bActive ? FPlatformTime::Cycles64() : 0)
|
||||
{
|
||||
}
|
||||
|
||||
explicit FScopedTimer(const TCHAR* InName)
|
||||
: Bucket(EBucket::OtherOp)
|
||||
, bActive(IsEnabled())
|
||||
, StartCycles(0)
|
||||
{
|
||||
if (bActive)
|
||||
{
|
||||
Bucket = BucketFromName(InName);
|
||||
StartCycles = FPlatformTime::Cycles64();
|
||||
}
|
||||
}
|
||||
|
||||
~FScopedTimer();
|
||||
|
||||
FScopedTimer(const FScopedTimer&) = delete;
|
||||
FScopedTimer& operator=(const FScopedTimer&) = delete;
|
||||
|
||||
private:
|
||||
EBucket Bucket = EBucket::OtherOp;
|
||||
bool bActive = false;
|
||||
uint64 StartCycles = 0;
|
||||
};
|
||||
}
|
||||
@@ -45,6 +45,11 @@ namespace VoxelGenLOD
|
||||
// (generator + mesher) are inside this module, so no export is needed anyway.
|
||||
extern thread_local int32 OctaveBias;
|
||||
|
||||
// Sampling step of the tile currently being meshed on this worker. GetDensityAt itself has
|
||||
// no Step parameter because callers outside the mesher are point queries; this TLS is only a
|
||||
// read-only hint for caches that may safely widen their spatial window for coarse tiles.
|
||||
extern thread_local int32 SampleStep;
|
||||
|
||||
// Effective octave count for a per-voxel noise call site.
|
||||
// At least 1 octave always survives (the coarse base shape).
|
||||
FORCEINLINE int32 Eff(int32 Octaves) { return FMath::Max(1, Octaves - OctaveBias); }
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include <atomic>
|
||||
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
|
||||
#include "VoxelGenerator.h"
|
||||
#include "VoxelMarchingCubesMesher.generated.h"
|
||||
@@ -88,6 +89,20 @@ public:
|
||||
|
||||
void SetGenerator(const UVoxelGenerator* InGenerator) { Generator = InGenerator; }
|
||||
|
||||
// AVoxelWorld installs its bShuttingDown flag here. The commandlet path leaves it null.
|
||||
// This is intentionally read-only: workers may observe shutdown, but never mutate world state.
|
||||
void SetShutdownFlag(const std::atomic<bool>* InShutdownFlag) { ShutdownFlag = InShutdownFlag; }
|
||||
|
||||
private:
|
||||
FORCEINLINE bool ShouldAbortWork() const
|
||||
{
|
||||
return ShutdownFlag && ShutdownFlag->load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
const std::atomic<bool>* ShutdownFlag = nullptr;
|
||||
|
||||
public:
|
||||
|
||||
//=========================================================================
|
||||
// SETTINGS
|
||||
//=========================================================================
|
||||
|
||||
@@ -378,6 +378,10 @@ public:
|
||||
void ApplyPassageModifier(float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity, float SealThickness) const;
|
||||
|
||||
/** Apply only the passage SDF carve. The shared MC post owns landing/tunnel support writes. */
|
||||
void ApplyPassageCarvingOnly(float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity, float SealThickness) const;
|
||||
|
||||
/** Reapply the landing-only air carve in internal density (used after visual disturbances). */
|
||||
void ApplyPassageLandingAir(float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity, float SealThickness) const;
|
||||
@@ -394,6 +398,10 @@ public:
|
||||
void ApplyPassageTunnelAirMC(float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity, float SealThickness) const;
|
||||
|
||||
/** Apply the landing/tunnel MC backstop in one cached passage scan after disturbances. */
|
||||
void ApplyPassageStructuralPostsMC(float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity, float SealThickness) const;
|
||||
|
||||
/** Re-assert a landing floor after the landing air pass, still before the final XY seal. */
|
||||
void ApplyPassageLandingFloorMC(float& Density, float WorldX, float WorldY, float WorldZ,
|
||||
float BaseDensity) const;
|
||||
|
||||
@@ -84,6 +84,7 @@ struct FChunkResult
|
||||
// Null ⇒ empty/all-air tile (no component).
|
||||
TSharedPtr<RealtimeMesh::FRealtimeMeshStreamSet> Streams;
|
||||
uint32 Epoch = 0; // Generation epoch — discard if stale
|
||||
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
|
||||
// F17 — the mesher classifies every triangle semantically (sky-cap = down-facing near the
|
||||
// column's CeilSurf; overhangs/cave roofs stay ground) and packs them as two contiguous runs
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "UObject/StrongObjectPtr.h"
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
#include "VoxelDiffLayer.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
#include "VoxelGenerator.h"
|
||||
#include "VoxelMarchingCubesMesher.h"
|
||||
#include "VoxelNoise.h"
|
||||
@@ -172,6 +173,8 @@ struct FExploreArguments
|
||||
bool bRender = true;
|
||||
bool bWalk = true;
|
||||
bool bExport = true;
|
||||
bool bProfileDensity = false;
|
||||
bool bProfileLod = false;
|
||||
bool bFailureFocusRender = false;
|
||||
FString OutDirectory;
|
||||
|
||||
@@ -209,6 +212,8 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
FParse::Value(*Params, TEXT("archetype="), ArchetypeText);
|
||||
FParse::Value(*Params, TEXT("slot="), OutArguments.Slot);
|
||||
FParse::Value(*Params, TEXT("modes="), ModesText);
|
||||
OutArguments.bProfileDensity = FParse::Param(*Params, TEXT("profiledensity"));
|
||||
OutArguments.bProfileLod = FParse::Param(*Params, TEXT("profilelod"));
|
||||
FParse::Value(*Params, TEXT("out="), OutText);
|
||||
FParse::Value(*Params, TEXT("renderwidth="), OutArguments.RenderWidth);
|
||||
FParse::Value(*Params, TEXT("renderheight="), OutArguments.RenderHeight);
|
||||
@@ -3840,6 +3845,12 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] %s"), *Error);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (Arguments.bProfileDensity)
|
||||
{
|
||||
VoxelDensityProfile::Reset();
|
||||
VoxelDensityProfile::SetEnabled(true);
|
||||
}
|
||||
if (!IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] Could not create output directory '%s'."),
|
||||
@@ -3860,6 +3871,61 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
||||
}
|
||||
const double SetupSeconds = FPlatformTime::Seconds() - SetupStartSeconds;
|
||||
|
||||
if (Arguments.bProfileLod)
|
||||
{
|
||||
const FIntVector ProbeOrigin(0, 0, World.TargetBottomWorldZ);
|
||||
const int32 FullRes = World.Settings
|
||||
? FMath::Max(1, World.Settings->FullResClipLevels)
|
||||
: 2;
|
||||
const int32 CoarseCells = World.Settings
|
||||
? FMath::Clamp(World.Settings->CoarseTileCells, 4, CHUNK_SIZE)
|
||||
: 16;
|
||||
const int32 CutMin = World.Settings
|
||||
? World.Settings->StrateContentCutMinLevel
|
||||
: 9;
|
||||
for (int32 Level = 0; Level <= 4; ++Level)
|
||||
{
|
||||
// Match AVoxelWorld::LoadTile exactly: the probe is a production tile at this LOD,
|
||||
// including the configured coarse sample count. Using 1<<Level here would measure
|
||||
// a denser mesh than the game generates at levels >= FullResClipLevels.
|
||||
const int32 Extent = CHUNK_SIZE << Level;
|
||||
const int32 Cells = Level < FullRes ? CHUNK_SIZE : CoarseCells;
|
||||
const int32 Step = FMath::Max(1, Extent / Cells);
|
||||
VoxelDensityProfile::Reset();
|
||||
VoxelDensityProfile::SetEnabled(true);
|
||||
const double Start = FPlatformTime::Seconds();
|
||||
const bool bBanded = Level >= CutMin;
|
||||
const FVoxelMeshData Mesh = World.Mesher->GenerateMesh(
|
||||
ProbeOrigin, Step, Cells, nullptr,
|
||||
bBanded ? World.TargetBottomWorldZ : INT32_MIN,
|
||||
bBanded ? World.TargetTopWorldZ - 1 : INT32_MAX);
|
||||
const double Seconds = FPlatformTime::Seconds() - Start;
|
||||
const VoxelDensityProfile::FSnapshot Profile = VoxelDensityProfile::Snapshot();
|
||||
const int32 DensityIndex = static_cast<int32>(VoxelDensityProfile::EBucket::GetDensityAt);
|
||||
const uint64 Calls = Profile.Calls[DensityIndex];
|
||||
const uint64 Lookups = Profile.Counters[static_cast<int32>(VoxelDensityProfile::ECounter::TunnelCacheLookup)];
|
||||
const uint64 Hits = Profile.Counters[static_cast<int32>(VoxelDensityProfile::ECounter::TunnelCacheHit)];
|
||||
const uint64 SdfBuilds = Profile.Counters[static_cast<int32>(VoxelDensityProfile::ECounter::SdfCacheBuild)];
|
||||
const double DensityUs = static_cast<double>(Profile.Cycles[DensityIndex])
|
||||
* FPlatformTime::GetSecondsPerCycle64() * 1.0e6;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeLODProfile] level=%d step=%d cells=%d banded=%d seconds=%.6f density_calls=%llu "
|
||||
"density_us_per_call=%.3f sdf_builds=%llu cache_lookups=%llu cache_hits=%llu cache_misses=%llu "
|
||||
"hit_rate=%.3f vertices=%d triangles=%d mesh_empty=%d"),
|
||||
Level, Step, Cells, bBanded ? 1 : 0, Seconds,
|
||||
static_cast<unsigned long long>(Calls),
|
||||
Calls > 0 ? DensityUs / static_cast<double>(Calls) : 0.0,
|
||||
static_cast<unsigned long long>(SdfBuilds),
|
||||
static_cast<unsigned long long>(Lookups),
|
||||
static_cast<unsigned long long>(Hits),
|
||||
static_cast<unsigned long long>(Profile.Counters[static_cast<int32>(VoxelDensityProfile::ECounter::TunnelCacheMiss)]),
|
||||
Lookups > 0 ? static_cast<double>(Hits) / static_cast<double>(Lookups) : 0.0,
|
||||
Mesh.Vertices.Num(), Mesh.Triangles.Num() / 3,
|
||||
Mesh.IsEmpty() ? 1 : 0);
|
||||
}
|
||||
VoxelDensityProfile::SetEnabled(false);
|
||||
}
|
||||
|
||||
FExploreRunOutput Output;
|
||||
if (Budget.ShouldStop(TEXT("setup")))
|
||||
{
|
||||
@@ -4044,5 +4110,48 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
||||
FPlatformTime::Seconds() - MainStartSeconds,
|
||||
*ReportPath,
|
||||
(bPayloadRepeatEqual && bFinalRepeatEqual) ? TEXT("yes") : TEXT("no"));
|
||||
|
||||
if (Arguments.bProfileDensity)
|
||||
{
|
||||
const VoxelDensityProfile::FSnapshot Profile = VoxelDensityProfile::Snapshot();
|
||||
const double CycleToMicroseconds = FPlatformTime::GetSecondsPerCycle64() * 1.0e6;
|
||||
const uint64 DensityCalls = Profile.Calls[static_cast<int32>(VoxelDensityProfile::EBucket::GetDensityAt)];
|
||||
const uint64 DensityCycles = Profile.Cycles[static_cast<int32>(VoxelDensityProfile::EBucket::GetDensityAt)];
|
||||
const double DensityTotalUs = static_cast<double>(DensityCycles) * CycleToMicroseconds;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeDensityProfile] total calls=%llu total_us=%.3f us_per_call=%.6f"),
|
||||
static_cast<unsigned long long>(DensityCalls), DensityTotalUs,
|
||||
DensityCalls > 0 ? DensityTotalUs / static_cast<double>(DensityCalls) : 0.0);
|
||||
|
||||
for (int32 Index = 0; Index < VoxelDensityProfile::CounterCount; ++Index)
|
||||
{
|
||||
const VoxelDensityProfile::ECounter Counter =
|
||||
static_cast<VoxelDensityProfile::ECounter>(Index);
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeDensityProfile] counter=%s value=%llu per_density_call=%.3f"),
|
||||
VoxelDensityProfile::CounterName(Counter),
|
||||
static_cast<unsigned long long>(Profile.Counters[Index]),
|
||||
DensityCalls > 0
|
||||
? static_cast<double>(Profile.Counters[Index])
|
||||
/ static_cast<double>(DensityCalls)
|
||||
: 0.0);
|
||||
}
|
||||
|
||||
for (int32 Index = 0; Index < VoxelDensityProfile::BucketCount; ++Index)
|
||||
{
|
||||
const uint64 Calls = Profile.Calls[Index];
|
||||
if (Calls == 0 || Index == static_cast<int32>(VoxelDensityProfile::EBucket::GetDensityAt))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double TotalUs = static_cast<double>(Profile.Cycles[Index]) * CycleToMicroseconds;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeDensityProfile] op=%s calls=%llu total_us=%.3f us_per_call=%.6f"),
|
||||
VoxelDensityProfile::BucketName(static_cast<VoxelDensityProfile::EBucket>(Index)),
|
||||
static_cast<unsigned long long>(Calls), TotalUs,
|
||||
TotalUs / static_cast<double>(Calls));
|
||||
}
|
||||
VoxelDensityProfile::SetEnabled(false);
|
||||
}
|
||||
return bRequestedModeFailed ? 2 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user