perf(gen): the landing work was per-voxel -- 342s to 2.6s, same world

The owner played 64f5d61 and 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:
  7a20eee drainage tree  -- cache-build work, NOT per-voxel
  413c15d landings       -- repeated full passage modification per sample
  55bf807 landing 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:
Fr0zkaandClaude Opus 5 committed 2026-09-08 23:47:53 +02:00
1 parent 64f5d61e08
commit 0576e800c4
15 files changed
+1042 -131

No files matched your search

@@ -28,6 +28,7 @@
// this chunk are kept for the per-voxel loop, so hot-path cost stays low. // this chunk are kept for the per-voxel loop, so hot-path cost stays low.
#include "VoxelCaveMorphology.h" #include "VoxelCaveMorphology.h"
#include "VoxelDensityProfile.h"
#include "VoxelDensityPrimitives.h" #include "VoxelDensityPrimitives.h"
#include "VoxelStrateMeasure.h" #include "VoxelStrateMeasure.h"
#include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01 #include "VoxelTypes.h" // Pour VOXEL_NOISE_SCALE, SmoothStep01
@@ -3643,6 +3644,16 @@ float VoxelCaveMorphology::EvaluateSDFCached(
float NearestRoomRawSDF = FLT_MAX; float NearestRoomRawSDF = FLT_MAX;
int32 NearestIdx = -1; 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 // Room SDFs
//========================================================================= //=========================================================================
@@ -3653,6 +3664,11 @@ float VoxelCaveMorphology::EvaluateSDFCached(
// --- DISTANCE CULL --- // --- DISTANCE CULL ---
const float DistSq = FVector::DistSquared(Pos, Room.Center); const float DistSq = FVector::DistSquared(Pos, Room.Center);
if (DistSq > Room.CullRadiusSq) continue; if (DistSq > Room.CullRadiusSq) continue;
if (VoxelDensityProfile::IsEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::CaveRoomEvaluated);
}
// --- SHAPE (pre-baked in BuildChunkCache — no per-voxel hash roll / trig) --- // --- SHAPE (pre-baked in BuildChunkCache — no per-voxel hash roll / trig) ---
float RoomSDF; float RoomSDF;
@@ -3721,6 +3737,11 @@ float VoxelCaveMorphology::EvaluateSDFCached(
// --- BOUNDING SPHERE CULL --- // --- BOUNDING SPHERE CULL ---
const float DistSq = FVector::DistSquared(Pos, Tunnel.BoundCenter); const float DistSq = FVector::DistSquared(Pos, Tunnel.BoundCenter);
if (DistSq > Tunnel.BoundRadiusSq) continue; if (DistSq > Tunnel.BoundRadiusSq) continue;
if (VoxelDensityProfile::IsEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::CaveTunnelEvaluated);
}
float TunnelSDF; float TunnelSDF;
@@ -3830,12 +3851,19 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF(
return MinSDF; return MinSDF;
} }
float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF( FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
float WorldX, float WorldY, float WorldZ, float WorldX, float WorldY, float WorldZ,
const FChunkSDFCache& Cache) const FChunkSDFCache& Cache)
{ {
const FVector Pos(WorldX, WorldY, WorldZ); 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) for (const FCachedTunnel& Tunnel : Cache.Tunnels)
{ {
@@ -3850,6 +3878,11 @@ float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
{ {
continue; continue;
} }
if (VoxelDensityProfile::IsEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
}
const TArray<FVector>& ControlPoints = bHasWorldChain const TArray<FVector>& ControlPoints = bHasWorldChain
? Tunnel.WorldControlPoints : Tunnel.ControlPoints; ? Tunnel.WorldControlPoints : Tunnel.ControlPoints;
@@ -3859,9 +3892,20 @@ float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
{ {
float FloorZ = 0.0f; float FloorZ = 0.0f;
float SupportRadius = 0.0f; float SupportRadius = 0.0f;
if (VoxelPassageGeometry::ProjectWalkableTunnelFloor( const bool bProjectedFloor = VoxelPassageGeometry::ProjectWalkableTunnelFloor(
ControlPoints, ControlRadii, Pos, FloorZ, SupportRadius) ControlPoints, ControlRadii, Pos, FloorZ, SupportRadius);
const bool bWithinWorldBounds = Tunnel.WorldBoundRadiusSq <= 0.0f
|| FVector::DistSquared(Pos, Tunnel.WorldBoundCenter)
<= Tunnel.WorldBoundRadiusSq;
if (bProjectedFloor && bWithinWorldBounds
&& WorldZ <= FloorZ && WorldZ <= FloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels
&& WorldZ >= FloorZ
- VoxelPassageGeometry::LandingFloorThicknessVoxels)
{
Result.bSupportFloor = true;
}
if (bProjectedFloor && WorldZ <= FloorZ
+ VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels) + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels)
{ {
// The finite floor slab owns the bottom of this tunnel. Another intersecting // The finite floor slab owns the bottom of this tunnel. Another intersecting
@@ -3877,8 +3921,8 @@ float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
SegmentIndex + 1 < ControlPoints.Num(); SegmentIndex + 1 < ControlPoints.Num();
++SegmentIndex) ++SegmentIndex)
{ {
MinSDF = FMath::Min( Result.SDF = FMath::Min(
MinSDF, Result.SDF,
VoxelSDF::TaperedCapsule( VoxelSDF::TaperedCapsule(
Pos, Pos,
ControlPoints[SegmentIndex], ControlPoints[SegmentIndex],
@@ -3889,28 +3933,35 @@ float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
} }
else if (Tunnel.bHasMidpoint) else if (Tunnel.bHasMidpoint)
{ {
MinSDF = FMath::Min( Result.SDF = FMath::Min(
MinSDF, Result.SDF,
VoxelSDF::TaperedCapsule( VoxelSDF::TaperedCapsule(
Pos, Tunnel.EndpointA, Tunnel.Midpoint, Pos, Tunnel.EndpointA, Tunnel.Midpoint,
Tunnel.RadiusA, Tunnel.RadiusMid)); Tunnel.RadiusA, Tunnel.RadiusMid));
MinSDF = FMath::Min( Result.SDF = FMath::Min(
MinSDF, Result.SDF,
VoxelSDF::TaperedCapsule( VoxelSDF::TaperedCapsule(
Pos, Tunnel.Midpoint, Tunnel.EndpointB, Pos, Tunnel.Midpoint, Tunnel.EndpointB,
Tunnel.RadiusMid, Tunnel.RadiusB)); Tunnel.RadiusMid, Tunnel.RadiusB));
} }
else else
{ {
MinSDF = FMath::Min( Result.SDF = FMath::Min(
MinSDF, Result.SDF,
VoxelSDF::TaperedCapsule( VoxelSDF::TaperedCapsule(
Pos, Tunnel.EndpointA, Tunnel.EndpointB, Pos, Tunnel.EndpointA, Tunnel.EndpointB,
Tunnel.RadiusA, Tunnel.RadiusB)); 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( bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
@@ -3918,6 +3969,13 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
const FChunkSDFCache& Cache) const FChunkSDFCache& Cache)
{ {
const FVector Pos(WorldX, WorldY, WorldZ); 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) for (const FCachedTunnel& Tunnel : Cache.Tunnels)
{ {
if (Tunnel.WorldControlPoints.Num() < 2 if (Tunnel.WorldControlPoints.Num() < 2
@@ -3944,6 +4002,11 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|| FVector::DistSquared(Pos, Tunnel.WorldBoundCenter) || FVector::DistSquared(Pos, Tunnel.WorldBoundCenter)
<= Tunnel.WorldBoundRadiusSq) <= Tunnel.WorldBoundRadiusSq)
{ {
if (VoxelDensityProfile::IsEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
}
return true; return true;
} }
} }
+199 -78
View File
@@ -340,7 +340,6 @@ namespace
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{ {
if (!(Strength > 0.0f)) { return; } if (!(Strength > 0.0f)) { return; }
@@ -1816,7 +1815,9 @@ namespace
{ {
if (const UVoxelStrateManager* LiveManager = Manager.Get()) 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 // 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 // before the boundary seal, then its support floor is reasserted after the passage
@@ -2780,6 +2781,85 @@ namespace
uint32 Salt; 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) // 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 ; // 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. // ici la source les possède et les expose.
// //
// ⚠️ `static` (donc PARTAGÉ ENTRE INSTANCES), pas un membre : c'est exactement ce que fait // The state belongs to this source instance. The outer tunnel cache retains the immutable
// l'original, et le contrôle 3 du test en DÉPEND — deux piles construites côte à côte se // op stack, so retaining the source's prepared SDF window with that stack avoids rebuilding
// partagent ce cache, et c'est l'empreinte de params dans la clé (pas une copie par pile) qui // the room graph every time a coarse tile switches between cached chunk keys. It remains
// les empêche de se servir mutuellement leurs salles. En faire un membre ferait passer ce // worker-local because each op stack lives in the thread-local density cache.
// 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.
struct FState 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; FChunkSDFCache Cache;
float CachedSMinX = 1.0f, CachedSMaxX = -1.0f; // invalide au départ (min > max) const FChunkSDFCache* ActiveCache = nullptr;
float CachedSMinY = 0.0f, CachedSMaxY = 0.0f;
int32 CachedStrate = INT32_MIN;
uint32 CachedSeed = 0;
uint32 CachedFingerprint = 0xFFFFFFFFu;
uint32 CachedLayout = 0xFFFFFFFFu;
uint64 CachedManagerLifetimeId = 0;
/** La salle de SDF minimal pour le dernier voxel évalué. -1 = aucune. */ /** La salle de SDF minimal pour le dernier voxel évalué. -1 = aucune. */
int32 NearestRoom = -1; int32 NearestRoom = -1;
@@ -2874,14 +2942,23 @@ namespace
bool bLocalParamsValid = false; bool bLocalParamsValid = false;
}; };
static FState& State() FState& State() const
{ {
thread_local FState S; // The prepared SDF data lives in the worker-local keyed cache bank above. This state
return S; // 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). */ /** 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. /** 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. */ * Lu par les arches, les dômes, le pincement et le biais de sol. */
@@ -2896,7 +2973,7 @@ namespace
{ {
const FVector& Position = State().LastWorldPosition; const FVector& Position = State().LastWorldPosition;
return VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF( 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) if (!S.bLocalParamsValid)
{ {
S.LocalParams = P; 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) if (NR.RoomOp)
{ {
// N'écrit que les champs propres au type de l'op ; tout le reste garde la // 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 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 /** 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) if (ManagerLifetimeId != 0 && LiveManager == nullptr)
{ {
S.Cache = FChunkSDFCache(); S.Cache = FChunkSDFCache();
S.ActiveCache = &S.Cache;
InOut.Sdf = FLT_MAX; InOut.Sdf = FLT_MAX;
return; 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); const float EffectiveZ = EffZ(WorldZ);
@@ -3078,51 +3161,78 @@ namespace
StrateIdx = SI_Index; StrateIdx = SI_Index;
} }
const bool bNeedRebuild = // A coarse LOD samples a new exact 32-voxel chunk at nearly every point. The graph is
StrateIdx != S.CachedStrate || SeedU != S.CachedSeed || // window-invariant, so group those samples into a larger deterministic XY region. The
ParamsFingerprint != S.CachedFingerprint || LayoutVersion != S.CachedLayout || // step is supplied by the mesher through VoxelGenLOD TLS; ordinary point queries keep
ManagerLifetimeId != S.CachedManagerLifetimeId || // the original one-chunk window. The cache key retains the exact params/strate/op-pool
WarpedX < S.CachedSMinX || WarpedX > S.CachedSMaxX || // identity, so no blended chunk can borrow another chunk's room graph.
WarpedY < S.CachedSMinY || WarpedY > S.CachedSMaxY; 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;
if (bNeedRebuild) const TArray<FStrateTerrainOpEntry>* TerrainOps = nullptr;
if (LiveManager)
{ {
const int32 CacheChunkX = FMath::FloorToInt(WorldX / (float)CHUNK_SIZE); const int32 ChunkX = FMath::FloorToInt(WorldX / (float)CHUNK_SIZE);
const int32 CacheChunkY = FMath::FloorToInt(WorldY / (float)CHUNK_SIZE); const int32 ChunkY = FMath::FloorToInt(WorldY / (float)CHUNK_SIZE);
const float ChunkMinX = CacheChunkX * (float)CHUNK_SIZE; const int32 ChunkZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE);
const float ChunkMinY = CacheChunkY * (float)CHUNK_SIZE; UVoxelStrateDefinition* Def = LiveManager->GetStrateForChunk(
const float ChunkMaxX = ChunkMinX + (float)CHUNK_SIZE; FIntVector(ChunkX, ChunkY, ChunkZ));
const float ChunkMaxY = ChunkMinY + (float)CHUNK_SIZE; if (Def) { TerrainOps = &Def->TerrainOperations; }
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;
const TArray<FStrateTerrainOpEntry>* TerrainOps = nullptr;
if (LiveManager)
{
const int32 ChunkZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE);
UVoxelStrateDefinition* Def = LiveManager->GetStrateForChunk(
FIntVector(CacheChunkX, CacheChunkY, 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( 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 // 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 // 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 // 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é. // 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; const float DZ = WorldZ - Pit.TopZ;
if (DZ >= Pit.BlendK) { continue; } if (DZ >= Pit.BlendK) { continue; }
@@ -3160,7 +3270,7 @@ namespace
CaveSDF = VoxelSDF::SmoothMin(CaveSDF, PitSDF, Pit.BlendK); 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; const float DZ = WorldZ - Chim.BottomZ;
if (-DZ >= Chim.BlendK) { continue; } if (-DZ >= Chim.BlendK) { continue; }
@@ -3641,7 +3751,7 @@ namespace
{ {
const FVector& Position = State().LastWorldPosition; const FVector& Position = State().LastWorldPosition;
return VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( return VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
Position.X, Position.Y, Position.Z, State().Cache); Position.X, Position.Y, Position.Z, GetCache());
} }
private: private:
@@ -3820,6 +3930,7 @@ namespace
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; } EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} 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 void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{ {
@@ -4017,6 +4128,7 @@ namespace
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} 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 void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{ {
@@ -4127,6 +4239,7 @@ namespace
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
const TCHAR* DebugName() const override { return TEXT("LayerLineMod"); }
void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override
{ {
@@ -4216,6 +4329,7 @@ namespace
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
const TCHAR* DebugName() const override { return TEXT("RibbingMod"); }
void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override
{ {
@@ -4300,6 +4414,7 @@ namespace
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} 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 void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{ {
@@ -4401,6 +4516,7 @@ namespace
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} 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 void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{ {
@@ -4476,6 +4592,7 @@ namespace
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} 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 void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{ {
@@ -4568,6 +4685,7 @@ namespace
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} 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 void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{ {
@@ -4685,6 +4803,7 @@ namespace
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
const TCHAR* DebugName() const override { return TEXT("RoomColumnMod"); }
void Eval(float WorldX, float WorldY, float, FVoxelOpSample& InOut) const override void Eval(float WorldX, float WorldY, float, FVoxelOpSample& InOut) const override
{ {
@@ -4749,6 +4868,7 @@ namespace
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} 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 void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{ {
@@ -4857,6 +4977,7 @@ namespace
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} 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 void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{ {
@@ -4977,6 +5098,7 @@ namespace
EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; }
bool IsAdditive() const override { return true; } bool IsAdditive() const override { return true; }
void PrepareChunk(const FVoxelOpContext&) override {} void PrepareChunk(const FVoxelOpContext&) override {}
const TCHAR* DebugName() const override { return TEXT("FloorBiasMod"); }
void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override
{ {
@@ -5815,10 +5937,9 @@ namespace VoxelDensityOps
OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ, OutStack.AppendStructuralPost(P.StrateTopWorldZ, P.StrateBottomWorldZ,
P.BoundarySealThickness, P.BaseDensity, P.BoundarySealThickness, P.BaseDensity,
SpineRadius, StrateManager, false); SpineRadius, StrateManager, false);
OutStack.Add(MakeUnique<FCaveTunnelFloorOp>(RoomPtr, P.BaseDensity)); // The generator's shared MC post evaluates the prepared tunnel-core cache after
OutStack.Add(MakeUnique<FCaveTunnelAirOp>( // disturbances. Keeping these pre-disturbance graph scans here duplicated both the
RoomPtr, P.BaseDensity, // support and air queries for every voxel; the feature remains in that shared post.
VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels));
OutStack.Add(MakeUnique<FXYEdgeSealOp>(P.BaseDensity)); 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);
}
}
+85 -38
View File
@@ -17,6 +17,7 @@
#include "VoxelDensityOpStack.h" // OPSTACK Phase 1: the opt-in per-strate operator stack #include "VoxelDensityOpStack.h" // OPSTACK Phase 1: the opt-in per-strate operator stack
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it #include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
#include "VoxelStats.h" #include "VoxelStats.h"
#include "VoxelDensityProfile.h"
#if WITH_EDITOR #if WITH_EDITOR
#include "VoxelStrateComposer.h" #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). // 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. // 0 = full quality; set by the mesher per tile from Step + Settings->LODOctaveDrop.
thread_local int32 VoxelGenLOD::OctaveBias = 0; 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 // 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 // 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, static void ApplyDisturbances(float& MC, float X, float Y, float Z,
const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir) const FStrateDisturbanceParams& D, uint32 Seed, bool bProtectVerticalShaftAir)
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::ApplyDisturbances);
const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness; const float InnerTop = D.StrateTopWorldZ - D.BoundarySealThickness;
const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness; const float InnerBot = D.StrateBottomWorldZ + D.BoundarySealThickness;
if (Z <= InnerBot || Z >= InnerTop) return; if (Z <= InnerBot || Z >= InnerTop) return;
@@ -487,7 +491,12 @@ namespace
// result. Native TunnelNetwork/Underwater states are immutable after construction; the active // result. Native TunnelNetwork/Underwater states are immutable after construction; the active
// pointer below lets the hot sample path read the stored stack and tunnel-core cache without // pointer below lets the hot sample path read the stored stack and tunnel-core cache without
// moving them on every voxel. // moving them on every voxel.
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 struct FTunnelNetworkDensityCacheEntry
{ {
@@ -728,6 +737,8 @@ void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) const float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) const
{ {
VoxelDensityProfile::FScopedTimer DensityProfileTimer(
VoxelDensityProfile::EBucket::GetDensityAt);
// ── STRATE SYSTEM ── // ── STRATE SYSTEM ──
// Query per-chunk params from the manager so each strate has different caves. // Query per-chunk params from the manager so each strate has different caves.
float Result; float Result;
@@ -827,20 +838,62 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
FVoxelOpStack* ActiveOpStack = &CP_OpStack; FVoxelOpStack* ActiveOpStack = &CP_OpStack;
FTunnelCoreCacheState* ActiveTunnelCoreCache = &GTunnelCoreCache; FTunnelCoreCacheState* ActiveTunnelCoreCache = &GTunnelCoreCache;
bool bLoadedTunnelDensityCache = false; 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) if (!bIsGapChunk)
{ {
FTunnelNetworkDensityCacheEntry& CachedEntry = if (VoxelDensityProfile::IsEnabled())
CP_TunnelDensityCache[TunnelDensityCacheSlot(ChunkCoord)]; {
const bool bCacheKeyMatches = CachedEntry.bValid VoxelDensityProfile::AddCounter(
&& CachedEntry.OwnerId == DensityCacheOwnerId VoxelDensityProfile::ECounter::TunnelCacheLookup);
&& CachedEntry.ManagerLifetimeId == ManagerLifetimeId }
&& CachedEntry.Chunk == ChunkCoord FTunnelNetworkDensityCacheEntry* CachedEntryPtr = nullptr;
&& CachedEntry.LayoutVersion == LayoutVersion const bool bCacheKeyMatches = FindTunnelDensityCacheEntry(ChunkCoord, CachedEntryPtr);
&& (CachedEntry.GenType == ECaveGeneratorType::TunnelNetwork FTunnelNetworkDensityCacheEntry& CachedEntry = *CachedEntryPtr;
|| CachedEntry.GenType == ECaveGeneratorType::Underwater); TunnelDensityCacheEntryForWrite = CachedEntryPtr;
if (bCacheKeyMatches) if (bCacheKeyMatches)
{ {
if (VoxelDensityProfile::IsEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCacheHit);
}
if (CP_ActiveTunnelDensityCache != &CachedEntry) if (CP_ActiveTunnelDensityCache != &CachedEntry)
{ {
CP_OwnerId = CachedEntry.OwnerId; CP_OwnerId = CachedEntry.OwnerId;
@@ -862,6 +915,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
ActiveTunnelCoreCache = &CachedEntry.TunnelCore; ActiveTunnelCoreCache = &CachedEntry.TunnelCore;
bLoadedTunnelDensityCache = true; bLoadedTunnelDensityCache = true;
} }
else if (VoxelDensityProfile::IsEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCacheMiss);
}
} }
const bool bOwnerChanged = DensityCacheOwnerId != CP_OwnerId; const bool bOwnerChanged = DensityCacheOwnerId != CP_OwnerId;
@@ -1083,8 +1141,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
#endif #endif
if (!bLoadedTunnelDensityCache && bCanStoreTunnelDensityCache) if (!bLoadedTunnelDensityCache && bCanStoreTunnelDensityCache)
{ {
FTunnelNetworkDensityCacheEntry& CachedEntry = FTunnelNetworkDensityCacheEntry& CachedEntry = TunnelDensityCacheEntryForWrite
CP_TunnelDensityCache[TunnelDensityCacheSlot(ChunkCoord)]; ? *TunnelDensityCacheEntryForWrite
: CP_TunnelDensityCache[TunnelDensityCacheSlot(ChunkCoord)];
CachedEntry.bValid = false; CachedEntry.bValid = false;
CachedEntry.OwnerId = DensityCacheOwnerId; CachedEntry.OwnerId = DensityCacheOwnerId;
CachedEntry.ManagerLifetimeId = ManagerLifetimeId; CachedEntry.ManagerLifetimeId = ManagerLifetimeId;
@@ -1253,34 +1312,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
Result, WorldX, WorldY, WorldZ, Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ, CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius); CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageLandingAirMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness);
VF_ApplyOriginLandingFloorMC( VF_ApplyOriginLandingFloorMC(
Result, WorldX, WorldY, WorldZ, Result, WorldX, WorldY, WorldZ,
CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ, CP_Dist.StrateTopWorldZ, CP_Dist.StrateBottomWorldZ,
CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius); CP_Dist.BoundarySealThickness, LandingBaseDensity, OriginSpineRadius);
StrateManager->ApplyPassageLandingFloorMC( StrateManager->ApplyPassageStructuralPostsMC(
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(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity, Result, WorldX, WorldY, WorldZ, LandingBaseDensity,
CP_Dist.BoundarySealThickness); 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 // 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 // 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. // never once per voxel.
if (ActiveTunnelCoreCache->bValid) if (ActiveTunnelCoreCache->bValid)
{ {
const bool bTunnelSupportFloor = FTunnelCoreWorldEvaluation TunnelCore;
VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( {
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCorePosts);
TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache); WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache);
}
const bool bTunnelSupportFloor = TunnelCore.bSupportFloor;
if (bTunnelSupportFloor) if (bTunnelSupportFloor)
{ {
Result = FMath::Min( Result = FMath::Min(
Result, Result,
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f)); -FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
} }
const float CoreSDF = VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF( const float CoreSDF = TunnelCore.SDF;
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache);
if (!bTunnelSupportFloor if (!bTunnelSupportFloor
&& CoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels) && CoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels)
{ {
@@ -1563,6 +1605,11 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
if (bNeedRebuild) if (bNeedRebuild)
{ {
if (VoxelDensityProfile::IsEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::SdfCacheBuild);
}
// Center the search box on this query's chunk. Search area = chunk XY extent // Center the search box on this query's chunk. Search area = chunk XY extent
// + CaveWarpStrength margin (covers warp displacement) + gradient sampling. // + CaveWarpStrength margin (covers warp displacement) + gradient sampling.
// MaxInfluence (room/tunnel reach) is added internally by BuildChunkCache. // 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) const int32 OctaveBias = (LODOctaveDrop > 0 && Step > 1)
? LODOctaveDrop * (int32)FMath::FloorLog2((uint32)Step) : 0; ? LODOctaveDrop * (int32)FMath::FloorLog2((uint32)Step) : 0;
TGuardValue<int32> OctaveBiasGuard(VoxelGenLOD::OctaveBias, OctaveBias); 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). // World-cm origin of the tile's min corner (positions are built relative to this).
const FVector ChunkWorldPos = FVector(OriginVoxels) * VOXEL_SIZE; 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. // gradient) sont échantillonnées — le reste du tampon reste non initialisé et non lu.
for (int32 gz = GzLo; gz <= GzHi; gz++) for (int32 gz = GzLo; gz <= GzHi; gz++)
{ {
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 gy = -1; gy <= GridDim; gy++) for (int32 gy = -1; gy <= GridDim; gy++)
{ {
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 gx = -1; gx <= GridDim; gx++) for (int32 gx = -1; gx <= GridDim; gx++)
{ {
// World voxel = tile origin + grid offset scaled by the cell size (Step). // World voxel = tile origin + grid offset scaled by the cell size (Step).
@@ -289,8 +292,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
CapTris.Reset(); CapTris.Reset();
for (int32 cz = CzLo; cz <= CzHi; cz++) // bande de strate : cf. CzLo/CzHi plus haut 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++) for (int32 cy = 0; cy < CellsPerAxis; cy++)
{ {
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 cx = 0; cx < CellsPerAxis; cx++) for (int32 cx = 0; cx < CellsPerAxis; cx++)
{ {
// PASSE 1 : densités aux 8 coins + index de cas MC SEULEMENT. // 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); SheetCols.SetNumUninitialized(MDim * MDim);
for (int32 gy = -1; gy <= GridDim; ++gy) for (int32 gy = -1; gy <= GridDim; ++gy)
{ {
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 gx = -1; gx <= GridDim; ++gx) for (int32 gx = -1; gx <= GridDim; ++gx)
{ {
float Tz = 0.0f, Cz = 0.0f; float Tz = 0.0f, Cz = 0.0f;
@@ -572,6 +578,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateSheetMesh(FIntVector OriginVox
for (int32 cy = 0; cy < CellsXY; ++cy) for (int32 cy = 0; cy < CellsXY; ++cy)
{ {
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 cx = 0; cx < CellsXY; ++cx) for (int32 cx = 0; cx < CellsXY; ++cx)
{ {
if (CellInHole(cx, cy)) continue; // couverte par les coquilles MC proches 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. // (pas dans le trou XY) — sinon mur flottant sans surface.
for (int32 c = 0; c < CellsXY; ++c) for (int32 c = 0; c < CellsXY; ++c)
{ {
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 Pass = 0; Pass < 2; ++Pass) for (int32 Pass = 0; Pass < 2; ++Pass)
{ {
const bool bCap = (Pass == 1); const bool bCap = (Pass == 1);
@@ -8,6 +8,7 @@
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord #include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
#include "VoxelCaveMorphology.h" // For VoxelSDF and VoxelHash #include "VoxelCaveMorphology.h" // For VoxelSDF and VoxelHash
#include "VoxelDensityPrimitives.h" // Shared passage carve polarity/strength #include "VoxelDensityPrimitives.h" // Shared passage carve polarity/strength
#include "VoxelDensityProfile.h" // Opt-in targeted per-voxel attribution
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo #include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten) #include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
#include "UObject/UObjectGlobals.h" #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, // 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. // landing, and floor paths. No source-fit stencil or topology search is allowed below it.
const TArray<int32>& Nearby = VF_GetNearbyPassages(this, ChunkCoord); 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 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 // capsule chain unconditionally — the dominant lag source once passages became
// 12-segment worms. Now far passages cost a single squared-distance compare. // 12-segment worms. Now far passages cost a single squared-distance compare.
if (FVector::DistSquared(Pos, P.BoundCenter) > P.BoundRadiusSq) continue; if (FVector::DistSquared(Pos, P.BoundCenter) > P.BoundRadiusSq) continue;
if (VoxelDensityProfile::IsEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::PassageEvaluated);
}
if (P.ControlPoints.Num() >= 2) if (P.ControlPoints.Num() >= 2)
{ {
@@ -1748,6 +1760,8 @@ void UVoxelStrateManager::ApplyPassageModifier(
float& Density, float WorldX, float WorldY, float WorldZ, float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const float BaseDensity, float SealThickness) const
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageModifier);
const float ModSDF = EvaluateModifierSDF(WorldX, WorldY, WorldZ); const float ModSDF = EvaluateModifierSDF(WorldX, WorldY, WorldZ);
VF_ApplyPassageCarving(Density, ModSDF, BaseDensity, SealThickness); VF_ApplyPassageCarving(Density, ModSDF, BaseDensity, SealThickness);
ApplyPassageLandingAir(Density, WorldX, WorldY, WorldZ, BaseDensity, SealThickness); ApplyPassageLandingAir(Density, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
@@ -1781,10 +1795,22 @@ void UVoxelStrateManager::ApplyPassageModifier(
ApplyPassageTunnelAir(Density, WorldX, WorldY, WorldZ, BaseDensity, SealThickness); 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( void UVoxelStrateManager::ApplyPassageLandingAir(
float& Density, float WorldX, float WorldY, float WorldZ, float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const float BaseDensity, float SealThickness) const
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingAir);
const FIntVector ChunkCoord( const FIntVector ChunkCoord(
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE), FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldY / (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& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const float BaseDensity, float SealThickness) const
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageTunnelAir);
const FVector Position(WorldX, WorldY, WorldZ); const FVector Position(WorldX, WorldY, WorldZ);
int32 PassageIndex = INDEX_NONE; int32 PassageIndex = INDEX_NONE;
float FloorZ = 0.0f; float FloorZ = 0.0f;
@@ -1874,6 +1902,8 @@ void UVoxelStrateManager::ApplyPassageTunnelAirMC(
void UVoxelStrateManager::ApplyPassageLandingFloorMC( void UVoxelStrateManager::ApplyPassageLandingFloorMC(
float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity) const float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity) const
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingFloor);
const FIntVector ChunkCoord( const FIntVector ChunkCoord(
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE), FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldY / (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); <= 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( void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC(
float& Density, float WorldX, float WorldY, float WorldZ, float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity) const float BaseDensity) const
{ {
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::PassageLandingRoomFloor);
const FIntVector ChunkCoord( const FIntVector ChunkCoord(
FMath::FloorToInt(WorldX / (float)CHUNK_SIZE), FMath::FloorToInt(WorldX / (float)CHUNK_SIZE),
FMath::FloorToInt(WorldY / (float)CHUNK_SIZE), FMath::FloorToInt(WorldY / (float)CHUNK_SIZE),
+77 -1
View File
@@ -31,6 +31,7 @@
#include "Modules/ModuleManager.h" #include "Modules/ModuleManager.h"
#include "Misc/FileHelper.h" #include "Misc/FileHelper.h"
#include "Misc/Paths.h" #include "Misc/Paths.h"
#include "HAL/IConsoleManager.h"
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0) #include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
#if WITH_EDITOR #if WITH_EDITOR
@@ -38,6 +39,15 @@
#include "VoxelStrateMeasure.h" #include "VoxelStrateMeasure.h"
#endif #endif
namespace
{
int32 GVoxelForgeProfileTileGeneration = 0;
FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration(
TEXT("voxel.ProfileTileGeneration"),
GVoxelForgeProfileTileGeneration,
TEXT("Log worker tile generation time with level/step/cell count."));
}
AVoxelWorld::AVoxelWorld() AVoxelWorld::AVoxelWorld()
{ {
PrimaryActorTick.bCanEverTick = true; PrimaryActorTick.bCanEverTick = true;
@@ -855,7 +865,10 @@ void AVoxelWorld::OnObjectModifiedInEditor(UObject* ModifiedObject)
void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason) void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
{ {
// Signal all async tasks to bail out ASAP // Signal all async tasks to bail out ASAP
const double EndPlayStartSeconds = FPlatformTime::Seconds();
bShuttingDown.store(true, std::memory_order_release); 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 // 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 // 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 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 // Drain any queued results
FChunkResult Discard; FChunkResult Discard;
@@ -965,6 +980,7 @@ void AVoxelWorld::BeginPlay()
Generator->InitializeSettings(Settings); Generator->InitializeSettings(Settings);
Mesher->SetGenerator(Generator); Mesher->SetGenerator(Generator);
Mesher->SetShutdownFlag(&bShuttingDown);
Mesher->bGenerateSkirts = Settings->bGenerateSkirts; Mesher->bGenerateSkirts = Settings->bGenerateSkirts;
Mesher->SkirtCells = Settings->SkirtCells; Mesher->SkirtCells = Settings->SkirtCells;
Mesher->LODOctaveDrop = Settings->LODOctaveDrop; // T2.b — 0 = off Mesher->LODOctaveDrop = Settings->LODOctaveDrop; // T2.b — 0 = off
@@ -1161,6 +1177,13 @@ void AVoxelWorld::ProcessPendingChunks()
{ {
PendingTiles.Remove(DequeuedChunk.Tile); 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. // 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. // Only a real (visible) upload counts against the per-frame budget — stale/empty drain free.
if (ApplyTileResult(DequeuedChunk)) if (ApplyTileResult(DequeuedChunk))
@@ -1179,7 +1202,7 @@ void AVoxelWorld::ProcessPendingChunks()
bool AVoxelWorld::ApplyTileResult(FChunkResult& Result) bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
{ {
// Discard results from a previous generation epoch (stale). // Discard results from a previous generation epoch (stale).
if (Result.Epoch != GenerationEpoch) if (Result.bAborted || Result.Epoch != GenerationEpoch)
{ {
return false; return false;
} }
@@ -2078,11 +2101,44 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY, int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
FChunkResult& Result) 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.Tile = Tile;
Result.Epoch = Epoch; 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.BandChunkLo = BandChunkLo; // strate content cut (MIN/MAX = uncut)
Result.BandChunkHi = BandChunkHi; 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 / // 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 // 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 // (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); TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile);
INC_DWORD_STAT(STAT_VoxelForgeTilesClassified); INC_DWORD_STAT(STAT_VoxelForgeTilesClassified);
const EVoxelTileClass Verdict = Generator->ClassifyTile(OriginVoxels, Step, Cells); const EVoxelTileClass Verdict = Generator->ClassifyTile(OriginVoxels, Step, Cells);
if (ShouldAbortWork())
{
AbortResult();
EmitTileProfile();
return;
}
if (Verdict == EVoxelTileClass::AllSolid) if (Verdict == EVoxelTileClass::AllSolid)
{ {
INC_DWORD_STAT(STAT_VoxelForgeTilesSkippedAllSolid); INC_DWORD_STAT(STAT_VoxelForgeTilesSkippedAllSolid);
@@ -2123,6 +2185,12 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
bWantCapture ? &Result.CaptureGrid : nullptr, bWantCapture ? &Result.CaptureGrid : nullptr,
BandVoxLo, BandVoxHi); BandVoxLo, BandVoxHi);
INC_DWORD_STAT(STAT_VoxelForgeTilesMeshed); 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 // 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); TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_BuildStreams);
Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>(); Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>();
BuildTileStreamSet(*Result.Streams, MeshData); BuildTileStreamSet(*Result.Streams, MeshData);
if (ShouldAbortWork())
{
AbortResult();
EmitTileProfile();
return;
}
Result.bEmpty = false; Result.bEmpty = false;
// F17 — the mesher classified every triangle semantically (sky-cap vs ground, per // 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.bHasCeilingTris = MeshData.NumCeilingTriangles > 0;
Result.bHasGroundTris = NumTris > MeshData.NumCeilingTriangles; Result.bHasGroundTris = NumTris > MeshData.NumCeilingTriangles;
} }
EmitTileProfile();
} }
// Section-group key shared by every tile component ("the" tile geometry group). // Section-group key shared by every tile component ("the" tile geometry group).
@@ -669,6 +669,13 @@ struct FChunkSDFCache
TArray<FCachedColumn> Columns; 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 // CAVE MORPHOLOGY EVALUATOR
//============================================================================= //=============================================================================
@@ -757,6 +764,11 @@ namespace VoxelCaveMorphology
const FChunkSDFCache& Cache 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 // 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 // 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. // the rounded capsule's bottom from becoming an unsupported point.
@@ -35,6 +35,7 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "VoxelDensityOp.h" #include "VoxelDensityOp.h"
#include "VoxelDensityProfile.h"
#include "VoxelStrateTypes.h" // FMazeGenerationParams #include "VoxelStrateTypes.h" // FMazeGenerationParams
#include "VoxelHeightOp.h" // IVoxelBiomeField — BuildSurfaceStack takes ownership of one #include "VoxelHeightOp.h" // IVoxelBiomeField — BuildSurfaceStack takes ownership of one
@@ -85,6 +86,7 @@ private:
bool bAdditive = false; bool bAdditive = false;
EVoxelOpResourceMask RequiredResources = VoxelOpResources::None; EVoxelOpResourceMask RequiredResources = VoxelOpResources::None;
EVoxelOpResourceMask ProvidedResources = VoxelOpResources::None; EVoxelOpResourceMask ProvidedResources = VoxelOpResources::None;
const TCHAR* ProfileName = nullptr;
}; };
public: public:
@@ -121,6 +123,7 @@ public:
Entry.bAdditive = Entry.Op->IsAdditive(); Entry.bAdditive = Entry.Op->IsAdditive();
Entry.RequiredResources = Entry.Op->RequiredResources(); Entry.RequiredResources = Entry.Op->RequiredResources();
Entry.ProvidedResources = Entry.Op->ProvidedResources(); Entry.ProvidedResources = Entry.Op->ProvidedResources();
Entry.ProfileName = Entry.Op->DebugName();
} }
Ops.Add(MoveTemp(Entry)); Ops.Add(MoveTemp(Entry));
} }
@@ -153,7 +156,11 @@ public:
FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const
{ {
FVoxelOpSample S; 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; 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. // (generator + mesher) are inside this module, so no export is needed anyway.
extern thread_local int32 OctaveBias; 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. // Effective octave count for a per-voxel noise call site.
// At least 1 octave always survives (the coarse base shape). // At least 1 octave always survives (the coarse base shape).
FORCEINLINE int32 Eff(int32 Octaves) { return FMath::Max(1, Octaves - OctaveBias); } FORCEINLINE int32 Eff(int32 Octaves) { return FMath::Max(1, Octaves - OctaveBias); }
@@ -12,6 +12,7 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include <atomic>
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc. #include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
#include "VoxelGenerator.h" #include "VoxelGenerator.h"
#include "VoxelMarchingCubesMesher.generated.h" #include "VoxelMarchingCubesMesher.generated.h"
@@ -88,6 +89,20 @@ public:
void SetGenerator(const UVoxelGenerator* InGenerator) { Generator = InGenerator; } 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 // SETTINGS
//========================================================================= //=========================================================================
@@ -378,6 +378,10 @@ public:
void ApplyPassageModifier(float& Density, float WorldX, float WorldY, float WorldZ, void ApplyPassageModifier(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const; 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). */ /** Reapply the landing-only air carve in internal density (used after visual disturbances). */
void ApplyPassageLandingAir(float& Density, float WorldX, float WorldY, float WorldZ, void ApplyPassageLandingAir(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const; float BaseDensity, float SealThickness) const;
@@ -394,6 +398,10 @@ public:
void ApplyPassageTunnelAirMC(float& Density, float WorldX, float WorldY, float WorldZ, void ApplyPassageTunnelAirMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const; 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. */ /** 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, void ApplyPassageLandingFloorMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity) const; float BaseDensity) const;
+1
View File
@@ -84,6 +84,7 @@ struct FChunkResult
// Null ⇒ empty/all-air tile (no component). // Null ⇒ empty/all-air tile (no component).
TSharedPtr<RealtimeMesh::FRealtimeMeshStreamSet> Streams; TSharedPtr<RealtimeMesh::FRealtimeMeshStreamSet> Streams;
uint32 Epoch = 0; // Generation epoch — discard if stale uint32 Epoch = 0; // Generation epoch — discard if stale
bool bAborted = false; // Worker observed shutdown; never mark this tile loaded
bool bEmpty = true; // true ⇒ all-air tile (Streams null); still marked loaded so we don't re-submit bool bEmpty = true; // true ⇒ all-air tile (Streams null); still marked loaded so we don't re-submit
// F17 — the mesher classifies every triangle semantically (sky-cap = down-facing near the // F17 — the mesher classifies every triangle semantically (sky-cap = down-facing near the
// column's CeilSurf; overhangs/cave roofs stay ground) and packs them as two contiguous runs // 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/StrongObjectPtr.h"
#include "UObject/UObjectGlobals.h" #include "UObject/UObjectGlobals.h"
#include "VoxelDiffLayer.h" #include "VoxelDiffLayer.h"
#include "VoxelDensityProfile.h"
#include "VoxelGenerator.h" #include "VoxelGenerator.h"
#include "VoxelMarchingCubesMesher.h" #include "VoxelMarchingCubesMesher.h"
#include "VoxelNoise.h" #include "VoxelNoise.h"
@@ -172,6 +173,8 @@ struct FExploreArguments
bool bRender = true; bool bRender = true;
bool bWalk = true; bool bWalk = true;
bool bExport = true; bool bExport = true;
bool bProfileDensity = false;
bool bProfileLod = false;
bool bFailureFocusRender = false; bool bFailureFocusRender = false;
FString OutDirectory; FString OutDirectory;
@@ -209,6 +212,8 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
FParse::Value(*Params, TEXT("archetype="), ArchetypeText); FParse::Value(*Params, TEXT("archetype="), ArchetypeText);
FParse::Value(*Params, TEXT("slot="), OutArguments.Slot); FParse::Value(*Params, TEXT("slot="), OutArguments.Slot);
FParse::Value(*Params, TEXT("modes="), ModesText); 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("out="), OutText);
FParse::Value(*Params, TEXT("renderwidth="), OutArguments.RenderWidth); FParse::Value(*Params, TEXT("renderwidth="), OutArguments.RenderWidth);
FParse::Value(*Params, TEXT("renderheight="), OutArguments.RenderHeight); 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); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] %s"), *Error);
return 1; return 1;
} }
if (Arguments.bProfileDensity)
{
VoxelDensityProfile::Reset();
VoxelDensityProfile::SetEnabled(true);
}
if (!IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true)) if (!IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true))
{ {
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeExplore] Could not create output directory '%s'."), 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; 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; FExploreRunOutput Output;
if (Budget.ShouldStop(TEXT("setup"))) if (Budget.ShouldStop(TEXT("setup")))
{ {
@@ -4044,5 +4110,48 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
FPlatformTime::Seconds() - MainStartSeconds, FPlatformTime::Seconds() - MainStartSeconds,
*ReportPath, *ReportPath,
(bPayloadRepeatEqual && bFinalRepeatEqual) ? TEXT("yes") : TEXT("no")); (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; return bRequestedModeFailed ? 2 : 0;
} }