perf(gen): 5x fewer candidates, 16x cheaper floor -- and 0% end-to-end. Read the last section.

Owner, after playing 0576e80: "slightly faster, still not on par... I can outrun the
generation and fall." Also, unprompted: "the tunnels seem forced, with a forced
flat -- it technically looks good, but could be a reason." He was right that it
was a reason; it was not THE reason.

WHAT LANDED, all real and all measured

Conservative spatial index (immutable per-chunk uniform bins, baked conservative
bounds, squared rejects, deterministic ordering):

  candidates scanned/sample   25.140 -> 5.192
  candidates evaluated/sample 13.879 -> 2.992
    cave tunnels  11.228/6.069 -> 1.525/0.723
    tunnel core   11.228/5.831 -> 1.216/0.567

The earlier flat AABB attempt failed because loose bounds culled almost nothing
while adding a pass. Technique from GPU Gems 3 ch.32 broad-phase.

The forced-flat floor, which the owner spotted by eye: a floor decision is
inherently per-COLUMN and was being recomputed for every Z sample in that column.
Now resolved once per integer XY column and reused.

  TunnelCorePosts   2.29 us/sample -> 0.138 us/sample   (16x)

The floor is unchanged in behaviour: still applied only after a tunnel passes its
world bounds and walkable projection, still walkable, look untouched.

FIRST MEASUREMENT OF OUR MEMORY FOOTPRINT
The owner: "it shouldn't be pressured on such system, even less when not running
anything." He is right, and it is ours:

  total resident worker cache   431,162,116 B  =  411 MiB
  15 workers                    27.41 MiB each
  average valid entry           278,704 B
  largest entry                 541,344 B
  static slot storage             2.4 MiB   (dynamic: 428.6 MB)

Cutting 4096 -> 128 slots only saved static storage. The weight is 272 KB PER
CACHED CHUNK STATE, ~102 live per worker. Nobody has yet asked why one cached
chunk state needs 272 KB. That is the open question, not the slot count.

⚠️ AND THE PART THAT MATTERS MOST: THIS BOUGHT NOTHING END-TO-END

  128^3 TunnelNetwork, seed 1337 slot 1, same region, unprofiled:
    before this commit   2.623 s
    after, 3 runs        2.705 / 2.678 / 2.682 s

Flat. A 5x candidate reduction and a 16x cheaper floor moved real generation time
by zero.

The reason is visible in the numbers and I should have caught it earlier: the
PROFILED path reports 14.74 us/sample while the real unprofiled cost is
1.28 us/voxel. **The instrument inflates by ~11x.** Every bucket we have been
ranking and optimising for two rounds was measured under that inflation, so the
ranking does not reflect where real time goes. We optimised what the profiler
exaggerated.

⇒ Before any further optimisation: make the profiler cheap enough that profiled
and unprofiled totals agree within a small factor, or switch to sampling. An
alarming number is the instrument until proven otherwise -- three for three in
this project, and now four.

Still true: ~21x slower than the pre-op-stack build, and generation still loses
to a walking player. Geometry changed by 40 triangles (153,692 -> 153,732), which
is allowed; two full exports hash identically (37E00BEE..0A7DF). Build 42/42.
No CPU-feature dispatch. Nothing outside Plugins/VoxelForge was touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-09 03:05:40 +02:00
1 parent 0576e800c4
commit ee89c4d8c2
11 files changed
+2380 -388

No files matched your search

File diff suppressed because it is too large. Load diff
+267 -133
View File
@@ -90,21 +90,20 @@ namespace
static constexpr float VF_PerlinAbsBound = 1.5f;
/** La même enveloppe que `FractalNoise3D` de VoxelGenerator.cpp (qui y est `static`, donc
* invisible ici). Le détour par `FVector` est délibéré — voir l'en-tête de ce fichier. */
FORCEINLINE float HFractal3D(const FVector& Position, int32 Octaves = 4,
* invisible ici). Les coordonnées de bruit restent en float : FVector est double dans UE5
* et son aller-retour ne servait qu'à conserver l'ancien arrondi, exigence abandonnée. */
FORCEINLINE float HFractal3D(const FVector3f& Position, int32 Octaves = 4,
float Lacunarity = 2.0f, float Persistence = 0.5f)
{
return VoxelNoise::FBM((float)Position.X, (float)Position.Y, (float)Position.Z,
return VoxelNoise::FBM(Position.X, Position.Y, Position.Z,
Octaves, Lacunarity, Persistence);
}
/** Idem pour `RidgedNoise3D` (également `static` dans VoxelGenerator.cpp). Le bruit cellulaire,
* lui, n'a pas besoin d'enveloppe : son corps a migré dans VoxelCaveMorphology.h et s'appelle
* `VoxelNoise::Cellular3D`, avec la MÊME signature `const FVector&` que l'original. */
FORCEINLINE float HRidged3D(const FVector& Position, int32 Octaves = 4,
/** Idem pour `RidgedNoise3D` (également `static` dans VoxelGenerator.cpp). */
FORCEINLINE float HRidged3D(const FVector3f& Position, int32 Octaves = 4,
float Lacunarity = 2.0f, float Persistence = 0.5f)
{
return VoxelNoise::Ridged((float)Position.X, (float)Position.Y, (float)Position.Z,
return VoxelNoise::Ridged(Position.X, Position.Y, Position.Z,
Octaves, Lacunarity, Persistence);
}
@@ -285,12 +284,12 @@ namespace
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{
const float Frequency = 1.0f / CellSize;
const FVector NoisePos(
const FVector3f NoisePos(
WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 3.17f),
WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 7.31f),
WorldZ * Frequency + VoxelHash::SeedOffset(SeedU, 11.47f));
const float Ribbon = FMath::Clamp(
FMath::Abs(VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z,
FMath::Abs(VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z,
VoxelGenLOD::Eff(3), 2.0f, 0.5f))
* VOXEL_NOISE_SCALE,
0.0f, 2.0f);
@@ -343,12 +342,12 @@ namespace
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{
if (!(Strength > 0.0f)) { return; }
const FVector NoisePos(
const FVector3f NoisePos(
WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 13.2f),
WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 17.8f),
WorldZ * Frequency + VoxelHash::SeedOffset(SeedU, 23.4f));
const float Noise01 = FMath::Clamp(
VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z,
VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z,
VoxelGenLOD::Eff(Octaves), 2.0f, 0.5f) * 0.5f + 0.5f,
0.0f, 1.0f);
const float Delta = Strength * Noise01;
@@ -693,17 +692,16 @@ namespace
}
private:
// Les deux surfaces, transcrites au caractère près depuis GetSlabDensity — y compris le
// détour par FVector, qui est le même piège d'arrondi que dans FSdfRoughnessMod
// (float → double → float sous /fp:fast). Ne pas « simplifier ».
// The surface coordinates stay in float; the old FVector round-trip only preserved
// legacy rounding and is no longer part of the output contract.
float SurfaceFloor(float WorldX, float WorldY) const
{
if (FloorRoughness <= 0.0f) { return FloorZ; }
const float FF = FloorFrequency;
const FVector NoisePos(WorldX * FF + VoxelHash::SeedOffset(SeedU, 7.3f),
const FVector3f NoisePos(WorldX * FF + VoxelHash::SeedOffset(SeedU, 7.3f),
WorldY * FF + VoxelHash::SeedOffset(SeedU, 11.1f),
0.0f);
const float N = VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z,
const float N = VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z,
VoxelGenLOD::Eff(3), 2.0f, 0.5f)
* VOXEL_NOISE_SCALE * FloorRoughness;
return FloorZ + N;
@@ -715,10 +713,10 @@ namespace
if (CeilRoughness > 0.0f)
{
const float CF = CeilFrequency;
const FVector NoisePos(WorldX * CF + VoxelHash::SeedOffset(SeedU, 17.3f) + 1000.0f,
const FVector3f NoisePos(WorldX * CF + VoxelHash::SeedOffset(SeedU, 17.3f) + 1000.0f,
WorldY * CF + VoxelHash::SeedOffset(SeedU, 19.7f) + 2000.0f,
3000.0f);
const float Raw = VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z,
const float Raw = VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z,
VoxelGenLOD::Eff(3), 2.0f, 0.5f)
* VOXEL_NOISE_SCALE;
// abs() ⇒ les formations ne pendent QUE vers le bas.
@@ -1199,7 +1197,7 @@ namespace
const float f = P.OverhangFrequency;
// Bruit de forme d'étagère [0,1] ; le terme en Z fait onduler la portée avec la hauteur
// (déchiqueté, pas une lèvre lisse).
const float Ns = HFractal3D(FVector(
const float Ns = HFractal3D(FVector3f(
WorldX * f + VoxelHash::SeedOffset(SeedU, 17.3f),
WorldY * f + VoxelHash::SeedOffset(SeedU, 23.9f),
WorldZ * f * P.OverhangZScale + VoxelHash::SeedOffset(SeedU, 5.1f)), 3) * 0.5f + 0.5f; // [0,1]
@@ -1259,24 +1257,10 @@ namespace
{
if (Strength <= 0.0f || InOut.Sdf >= ApplyWithin) { return; }
// ⚠️ LE DÉTOUR PAR FVector EST DÉLIBÉRÉ — ne pas « simplifier ».
// L'original écrit `FractalNoise3D(FVector(WorldX * 0.12f, ...), Eff(3))`, et
// FractalNoise3D fait `VoxelNoise::FBM((float)Position.X, ...)`. FVector étant en
// DOUBLE (UE5), le produit flottant y transite par un double avant d'être re-arrondi
// en float. Passer directement des floats saute cet aller-retour, et sous /fp:fast
// les deux chemins ne s'arrondissent pas au même endroit : ~1 ULP d'écart sur le SDF,
// qui ressort en 1 ULP sur la densité finale. Reproduire le détour, c'est reproduire
// l'arrondi. VERIFIE par MazeEquivalence : le détour conserve l'identité binaire avec
// GetMazeDensity (0 écart sur 20,000 échantillons). Si cela change, le candidat suivant
// est une contraction FMA entre unités de compilation.
//
// THE FVector ROUND-TRIP IS DELIBERATE — do not "simplify" it. MazeEquivalence
// verifies the resulting path against GetMazeDensity (0 differences / 20,000 samples).
// The original goes float -> double (FVector is double in UE5) -> float; going straight
// through floats skips a rounding step, and under /fp:fast the two paths round in
// different places. Reproducing the detour reproduces the rounding.
const FVector NoisePos(WorldX * Frequency, WorldY * Frequency, WorldZ * Frequency);
InOut.Sdf += VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z,
// Keep hot noise coordinates in float. The old FVector round-trip only preserved
// legacy rounding; output compatibility with that field is intentionally abandoned.
const FVector3f NoisePos(WorldX * Frequency, WorldY * Frequency, WorldZ * Frequency);
InOut.Sdf += VoxelNoise::FBM(NoisePos.X, NoisePos.Y, NoisePos.Z,
VoxelGenLOD::Eff(BaseOctaves), 2.0f, 0.5f)
* VOXEL_NOISE_SCALE * Strength;
}
@@ -1300,9 +1284,8 @@ namespace
}
if (Strength <= 0.0f) { return; }
// The noise input is float after the deliberate FVector round-trip in Eval. If an
// authored frequency/box product overflows, Eval can publish a non-finite SDF and no
// finite interval is a proof. Unknown costs the skip and protects the geometry.
// If an authored frequency/box product overflows, Eval can publish a non-finite SDF
// and no finite interval is a proof. Unknown costs the skip and protects the geometry.
const float MaxAbsCoord = FMath::Max3(
FMath::Max(FMath::Abs((float)VoxelBox.Min.X), FMath::Abs((float)VoxelBox.Max.X)),
FMath::Max(FMath::Abs((float)VoxelBox.Min.Y), FMath::Abs((float)VoxelBox.Max.Y)),
@@ -2546,10 +2529,10 @@ namespace
// partagé par toutes les îles proches — chacune échantillonne une autre partie du champ,
// d'où des silhouettes distinctes.
const float WarpAmp = (P.IslandMinRadius + P.IslandMaxRadius) * 0.5f * 0.35f;
const float WX = WorldX + HFractal3D(FVector(WorldX * 0.04f + VoxelHash::SeedOffset(Salt, 0.0007f),
const float WX = WorldX + HFractal3D(FVector3f(WorldX * 0.04f + VoxelHash::SeedOffset(Salt, 0.0007f),
WorldY * 0.04f, WorldZ * 0.012f), VoxelGenLOD::Eff(3))
* VOXEL_NOISE_SCALE * WarpAmp;
const float WY = WorldY + HFractal3D(FVector(WorldX * 0.04f + 31.0f, WorldY * 0.04f + 7.0f,
const float WY = WorldY + HFractal3D(FVector3f(WorldX * 0.04f + 31.0f, WorldY * 0.04f + 7.0f,
WorldZ * 0.012f), VoxelGenLOD::Eff(3))
* VOXEL_NOISE_SCALE * WarpAmp;
@@ -2931,8 +2914,112 @@ namespace
/** The warped SDF-space position belonging to the current voxel. */
FVector LastWarpedPosition = FVector::ZeroVector;
/** The unwarped position belonging to the current voxel. */
FVector LastWorldPosition = FVector::ZeroVector;
/** The unwarped position belonging to the current voxel. */
FVector LastWorldPosition = FVector::ZeroVector;
// Support-floor projection depends only on XY for one immutable SDF cache. The
// mesher traverses Z outside X/Y, so keep a small worker-local LRU of XY boxes rather
// than only the immediately previous column; each integer column is then projected
// once and both structural posts reuse its interval for every Z sample.
struct FSupportColumnBox
{
static constexpr int32 Halo = CHUNK_SIZE + 8;
static constexpr int32 Dim = 2 * Halo + 1;
const FChunkSDFCache* Cache = nullptr;
int32 BaseX = 0;
int32 BaseY = 0;
uint32 LastUse = 0;
bool bValid = false;
TArray<int32> SlotToEntry;
TArray<FTunnelSupportFloorColumn> Columns;
void Clear()
{
Cache = nullptr;
bValid = false;
LastUse = 0;
SlotToEntry.Reset();
Columns.Reset();
}
FTunnelSupportFloorColumn* AcquireColumn(
int32 X, int32 Y, const FChunkSDFCache* InCache,
uint32 InUse, bool& bOutNew)
{
bOutNew = false;
if (!bValid || Cache != InCache
|| X < BaseX || X >= BaseX + Dim
|| Y < BaseY || Y >= BaseY + Dim)
{
Cache = InCache;
BaseX = X - Halo;
BaseY = Y - Halo;
LastUse = InUse;
bValid = true;
SlotToEntry.Init(INDEX_NONE, Dim * Dim);
Columns.Reset();
}
else
{
LastUse = InUse;
}
const int32 Slot = (Y - BaseY) * Dim + (X - BaseX);
int32& EntryIndex = SlotToEntry[Slot];
if (EntryIndex == INDEX_NONE)
{
EntryIndex = Columns.AddDefaulted();
bOutNew = true;
}
return &Columns[EntryIndex];
}
};
struct FSupportColumnCache
{
static constexpr int32 NumBoxes = 4;
FSupportColumnBox Boxes[NumBoxes];
uint32 Clock = 0;
void Invalidate()
{
for (FSupportColumnBox& Box : Boxes)
{
Box.Clear();
}
Clock = 0;
}
FSupportColumnBox& AcquireBox(
int32 X, int32 Y, const FChunkSDFCache* Cache)
{
++Clock;
for (FSupportColumnBox& Box : Boxes)
{
if (Box.bValid && Box.Cache == Cache
&& X >= Box.BaseX && X < Box.BaseX + FSupportColumnBox::Dim
&& Y >= Box.BaseY && Y < Box.BaseY + FSupportColumnBox::Dim)
{
Box.LastUse = Clock;
return Box;
}
}
FSupportColumnBox* Victim = &Boxes[0];
for (FSupportColumnBox& Box : Boxes)
{
if (!Box.bValid || Box.LastUse < Victim->LastUse)
{
Victim = &Box;
}
}
Victim->Clear();
return *Victim;
}
} SupportColumns;
FTunnelSupportFloorColumn FractionalSupportColumn;
/** ÉTAPE C1 — les params de la strate avec l'op de CETTE salle appliqué par-dessus.
* Mémo par voxel : invalidé au début de chaque `Eval`, calculé au PREMIER modificateur
@@ -3081,6 +3168,7 @@ namespace
{
S.Cache = FChunkSDFCache();
S.ActiveCache = &S.Cache;
S.SupportColumns.Invalidate();
InOut.Sdf = FLT_MAX;
return;
}
@@ -3088,6 +3176,7 @@ namespace
if (!(P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f))
{
S.ActiveCache = &S.Cache;
S.SupportColumns.Invalidate();
return; // Sdf reste FLT_MAX
}
@@ -3106,15 +3195,15 @@ namespace
{
const float WF = P.CaveWarpFrequency;
const float WS = P.CaveWarpStrength;
WarpedX += VoxelNoise::Perlin3D(FVector(
WarpedX += VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.37f),
WorldY * WF + 1.3f,
EffectiveZ * WF + 5.7f)) * VOXEL_NOISE_SCALE * WS;
WarpedY += VoxelNoise::Perlin3D(FVector(
WarpedY += VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + 7.1f,
WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.59f),
EffectiveZ * WF + 2.3f)) * VOXEL_NOISE_SCALE * WS;
WarpedZ += VoxelNoise::Perlin3D(FVector(
WarpedZ += VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + 11.3f,
WorldY * WF + 9.7f,
EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 0.41f))) * VOXEL_NOISE_SCALE * WS;
@@ -3219,7 +3308,8 @@ namespace
SharedCache->RegionMinX = RegionMinX;
SharedCache->RegionMinY = RegionMinY;
SharedCache->RegionSize = RegionSize;
const float Expansion = P.CaveWarpStrength + 2.0f;
const float Expansion = FMath::Abs(P.CaveWarpStrength)
* VOXEL_NOISE_SCALE * VF_PerlinAbsBound + 2.0f;
VoxelCaveMorphology::BuildChunkCache(
SharedCache->Cache,
(float)RegionMinX - Expansion,
@@ -3228,6 +3318,8 @@ namespace
(float)(RegionMinY + RegionSize) + Expansion,
P, SeedU, StrateIdx, TerrainOps);
SharedCache->bValid = true;
VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint();
S.SupportColumns.Invalidate();
}
S.ActiveCache = &SharedCache->Cache;
@@ -3749,9 +3841,48 @@ namespace
bool IsTunnelSupportFloor() const
{
const FVector& Position = State().LastWorldPosition;
return VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
Position.X, Position.Y, Position.Z, GetCache());
FState& S = State();
const FVector& Position = S.LastWorldPosition;
const FChunkSDFCache& Cache = GetCache();
if (VoxelDensityProfile::IsEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportFloorQueries);
}
// Density-grid samples are integer XY. Keep fractional callers exact by using a
// scratch column; they are uncommon gradient/debug queries and must not alias a
// neighbouring integer column.
const bool bIntegerXY =
Position.X == FMath::FloorToFloat(static_cast<float>(Position.X))
&& Position.Y == FMath::FloorToFloat(static_cast<float>(Position.Y));
if (bIntegerXY)
{
const int32 IX = FMath::FloorToInt(static_cast<float>(Position.X));
const int32 IY = FMath::FloorToInt(static_cast<float>(Position.Y));
FState::FSupportColumnBox& Box = S.SupportColumns.AcquireBox(IX, IY, &Cache);
bool bNewColumn = false;
FTunnelSupportFloorColumn* Column = Box.AcquireColumn(
IX, IY, &Cache, S.SupportColumns.Clock, bNewColumn);
if (bNewColumn)
{
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
static_cast<float>(Position.X),
static_cast<float>(Position.Y),
Cache,
*Column);
}
return VoxelCaveMorphology::IsTunnelSupportFloorColumnZ(
static_cast<float>(Position.Z), *Column);
}
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
static_cast<float>(Position.X),
static_cast<float>(Position.Y),
Cache,
S.FractionalSupportColumn);
return VoxelCaveMorphology::IsTunnelSupportFloorColumnZ(
static_cast<float>(Position.Z), S.FractionalSupportColumn);
}
private:
@@ -3947,15 +4078,14 @@ namespace
const float RF = P.RoughnessFrequency;
// ⚠️ LE DÉTOUR PAR FVector EST DÉLIBÉRÉ (même raison que dans FSdfRoughnessMod) :
// FVector est en DOUBLE, donc chaque produit transite par un double avant d'être
// re-arrondi en float à l'appel du bruit. Sauter l'aller-retour change l'arrondi.
FVector MainPos(
// Keep both hot noise-coordinate chains in float. The old FVector round-trip only
// preserved legacy rounding; output compatibility with that field is abandoned.
FVector3f MainPos(
WorldX * RF + VoxelHash::SeedOffset(SeedU, 11.3f),
WorldY * RF + VoxelHash::SeedOffset(SeedU, 13.7f),
EffectiveZ * RF + VoxelHash::SeedOffset(SeedU, 17.1f)
);
FVector FinePos(
FVector3f FinePos(
WorldX * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 19.1f) + 2000.0f,
WorldY * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 23.7f) + 2500.0f,
EffectiveZ * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 29.3f) + 3000.0f
@@ -3969,25 +4099,25 @@ namespace
const float WF = P.DomainWarpFrequency;
const float WS = P.DomainWarpStrength;
const float WarpX = VoxelNoise::Perlin3D(FVector(
const float WarpX = VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + VoxelHash::SeedOffset(SeedU, 5.2f),
WorldY * WF + VoxelHash::SeedOffset(SeedU, 1.3f),
EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 9.7f)
)) * VOXEL_NOISE_SCALE * WS;
const float WarpY = VoxelNoise::Perlin3D(FVector(
const float WarpY = VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + 100.0f + VoxelHash::SeedOffset(SeedU, 7.7f),
WorldY * WF + 200.0f + VoxelHash::SeedOffset(SeedU, 3.1f),
EffectiveZ * WF + 300.0f
)) * VOXEL_NOISE_SCALE * WS;
const float WarpZ = VoxelNoise::Perlin3D(FVector(
const float WarpZ = VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + 400.0f,
WorldY * WF + 500.0f + VoxelHash::SeedOffset(SeedU, 11.9f),
EffectiveZ * WF + 600.0f + VoxelHash::SeedOffset(SeedU, 13.3f)
)) * VOXEL_NOISE_SCALE * WS;
const FVector WarpOffset(WarpX, WarpY, WarpZ);
const FVector3f WarpOffset(WarpX, WarpY, WarpZ);
MainPos += WarpOffset;
FinePos += WarpOffset;
}
@@ -4161,7 +4291,7 @@ namespace
float NoisedZ = WorldZ;
if (LP.TerraceNoiseDisplacement > 0.0f)
{
const float DispNoise = HFractal3D(FVector(
const float DispNoise = HFractal3D(FVector3f(
WorldX * 0.04f + VoxelHash::SeedOffset(SeedU, 31.1f),
WorldY * 0.04f + VoxelHash::SeedOffset(SeedU, 37.3f),
WorldZ * 0.02f + VoxelHash::SeedOffset(SeedU, 41.7f)
@@ -4432,7 +4562,7 @@ namespace
? (WorldZ / P.VerticalScale) : WorldZ;
// Fréquence en Z à 0.15× celle de XY ⇒ les motifs s'étirent horizontalement.
const float OverhangNoise = HFractal3D(FVector(
const float OverhangNoise = HFractal3D(FVector3f(
WorldX * LP.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 53.1f),
WorldY * LP.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 59.3f),
EffectiveZ * LP.OverhangFrequency * 0.15f + VoxelHash::SeedOffset(SeedU, 61.7f)
@@ -4533,7 +4663,7 @@ namespace
const float EffectiveZ = (P.VerticalScale != 1.0f && P.VerticalScale > 0.0f)
? (WorldZ / P.VerticalScale) : WorldZ;
const float VertGrad = VoxelNoise::Perlin3D(FVector(
const float VertGrad = VoxelNoise::Perlin3D(FVector3f(
WorldX * 0.05f + VoxelHash::SeedOffset(SeedU, 71.3f),
WorldY * 0.05f + VoxelHash::SeedOffset(SeedU, 73.7f),
EffectiveZ * 0.15f + VoxelHash::SeedOffset(SeedU, 79.1f) // 3× plus vite en Z
@@ -4610,7 +4740,7 @@ namespace
? (WorldZ / P.VerticalScale) : WorldZ;
const float SF = LP.ScallopFrequency;
const float ScallopNoise = VoxelNoise::Cellular3D(FVector(
const float ScallopNoise = VoxelNoise::Cellular3D(FVector3f(
WorldX * SF + VoxelHash::SeedOffset(SeedU, 83.1f),
WorldY * SF + VoxelHash::SeedOffset(SeedU, 89.3f),
EffectiveZ * SF + VoxelHash::SeedOffset(SeedU, 97.7f)
@@ -4710,45 +4840,21 @@ namespace
if (!Cache.Rooms.IsValidIndex(NearestRoomIdx)) { return; }
const FCachedRoom& Room = Cache.Rooms[NearestRoomIdx];
const int32 MaxArches = 3;
const FVector VoxPos(WorldX, WorldY, WorldZ);
for (int32 i = 0; i < MaxArches; i++)
for (int32 i = 0; i < 3; i++)
{
const uint32 AH = VoxelHash::Mix(Room.Hash ^ (0xA4C400u + (uint32)i * 7369u));
if (VoxelHash::ToFloat01(AH) > LP.ArchDensity) { continue; }
const uint32 AH2 = VoxelHash::Mix(AH ^ 0xA4C4u);
const float ArcCX = Room.Center.X + VoxelHash::ToFloatSigned(AH2) * Room.RadiusXY * 0.3f;
const float ArcCY = Room.Center.Y
+ VoxelHash::ToFloatSigned(VoxelHash::Mix(AH2)) * Room.RadiusXY * 0.3f;
const uint32 AH3 = VoxelHash::Mix(AH2 ^ 0xB41Du);
const float ArcCZ = Room.Center.Z + VoxelHash::ToFloatSigned(AH3) * Room.RadiusZ * 0.4f;
const uint32 AH4 = VoxelHash::Mix(AH3 ^ 0xCAFEu);
const float Angle = VoxelHash::ToFloat01(AH4) * PI;
const float HalfSpan = Room.RadiusXY
* (0.5f + VoxelHash::ToFloat01(VoxelHash::Mix(AH4)) * 0.35f);
const float CosA = FMath::Cos(Angle);
const float SinA = FMath::Sin(Angle);
const FVector ArchA(ArcCX - CosA * HalfSpan, ArcCY - SinA * HalfSpan, ArcCZ);
const FVector ArchB(ArcCX + CosA * HalfSpan, ArcCY + SinA * HalfSpan, ArcCZ);
const uint32 AH5 = VoxelHash::Mix(AH4 ^ 0xF00Du);
const float ArchRadius = FMath::Lerp(LP.ArchMinRadius, LP.ArchMaxRadius,
VoxelHash::ToFloat01(AH5));
const float ArchSDF = VoxelSDF::Capsule(VoxPos, ArchA, ArchB, ArchRadius);
const FCachedArch& Arch = Room.Arches[i];
if (!Arch.bActive) { continue; }
const float ArchSDF = VoxelSDF::Capsule(
VoxPos, Arch.EndpointA, Arch.EndpointB, Arch.Radius);
const float ArchBlend = 2.0f;
if (ArchSDF < ArchBlend)
{
float Fill = FMath::Clamp((ArchBlend - ArchSDF) / (ArchBlend * 2.0f), 0.0f, 1.0f);
Fill = SmoothStep01(Fill);
InOut.Density += Fill * LP.BaseDensity * 1.5f;
InOut.Density += Fill * Arch.BaseDensity * 1.5f;
}
}
}
@@ -4997,45 +5103,22 @@ namespace
if (!Cache.Rooms.IsValidIndex(NearestRoomIdx)) { return; } // cf. FCaveArchMod
const FCachedRoom& Room = Cache.Rooms[NearestRoomIdx];
const float Spread = 0.85f;
const int32 MaxPinches = 3;
for (int32 i = 0; i < MaxPinches; i++)
for (int32 i = 0; i < 3; i++)
{
const uint32 PnH = VoxelHash::Mix(Room.Hash ^ (0xF1C400u + (uint32)i * 5417u));
const FCachedPinch& Pinch = Room.Pinches[i];
if (!Pinch.bActive) { continue; }
const float DXPn = WorldX - Pinch.CenterX;
const float DYPn = WorldY - Pinch.CenterY;
const float DZPn = WorldZ - Pinch.CenterZ;
if (VoxelHash::ToFloat01(PnH) > LP.PinchDensity) { continue; }
if (FMath::Abs(DXPn) + FMath::Abs(DYPn) + FMath::Abs(DZPn) > Pinch.MaxExtent) { continue; }
const uint32 PnH2 = VoxelHash::Mix(PnH ^ 0xF1C4u);
const float PnX = Room.Center.X + VoxelHash::ToFloatSigned(PnH2) * Room.RadiusXY * Spread;
const float PnY = Room.Center.Y
+ VoxelHash::ToFloatSigned(VoxelHash::Mix(PnH2)) * Room.RadiusXY * Spread;
const float Along = DXPn * Pinch.CosAngle + DYPn * Pinch.SinAngle;
const float Across = -DXPn * Pinch.SinAngle + DYPn * Pinch.CosAngle;
const uint32 PnH3 = VoxelHash::Mix(PnH2 ^ 0x5432u);
const float PnZ = Room.Center.Z + VoxelHash::ToFloatSigned(PnH3) * Room.RadiusZ * 0.5f;
const uint32 PnH4 = VoxelHash::Mix(PnH3 ^ 0x9A3Bu);
const float PnAngle = VoxelHash::ToFloat01(PnH4) * PI;
const float CosPN = FMath::Cos(PnAngle);
const float SinPN = FMath::Sin(PnAngle);
const float DXPn = WorldX - PnX;
const float DYPn = WorldY - PnY;
const float DZPn = WorldZ - PnZ;
const float MaxExtent = FMath::Max(LP.PinchLength, LP.PinchStrength) + 5.0f;
if (FMath::Abs(DXPn) + FMath::Abs(DYPn) + FMath::Abs(DZPn) > MaxExtent) { continue; }
const float Along = DXPn * CosPN + DYPn * SinPN;
const float Across = -DXPn * SinPN + DYPn * CosPN;
const float HalfLength = LP.PinchLength * 0.5f;
const float HalfNarrow = LP.PinchStrength;
const float HalfVertical = LP.PinchStrength * 1.5f;
const float NAlong = Along / HalfLength;
const float NAcross = Across / HalfNarrow;
const float NUp = DZPn / HalfVertical;
const float NAlong = Along / Pinch.HalfLength;
const float NAcross = Across / Pinch.HalfNarrow;
const float NUp = DZPn / Pinch.HalfVertical;
const float EllipDist = NAlong * NAlong + NAcross * NAcross + NUp * NUp;
if (EllipDist < 1.0f)
@@ -5044,7 +5127,7 @@ namespace
Fill = SmoothStep01(Fill);
const float AxisDist = FMath::Sqrt(NAcross * NAcross + NUp * NUp);
const float SideFactor = FMath::Clamp(AxisDist * 2.0f, 0.0f, 1.0f);
InOut.Density += Fill * SideFactor * LP.BaseDensity * 1.5f;
InOut.Density += Fill * SideFactor * Pinch.BaseDensity * 1.5f;
}
}
}
@@ -5192,7 +5275,7 @@ namespace
const float WormZFreq = P.WormFrequency * P.WormHorizontalBias;
const float N1 = FMath::Abs(VoxelNoise::Perlin3D(FVector(
const float N1 = FMath::Abs(VoxelNoise::Perlin3D(FVector3f(
WorldX * P.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f),
WorldY * P.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f),
EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f)
@@ -5202,7 +5285,7 @@ namespace
// second Perlin (le cas courant ; sortie bit-identique). Transcrit tel quel.
if (N1 >= P.WormThreshold) { return; }
const float N2 = FMath::Abs(VoxelNoise::Perlin3D(FVector(
const float N2 = FMath::Abs(VoxelNoise::Perlin3D(FVector3f(
WorldX * P.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f) + 137.0f,
WorldY * P.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f) + 259.0f,
EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f) + 431.0f
@@ -5440,10 +5523,61 @@ VoxelDensityOps::FRoomBoxDiagnostic VoxelDensityOps::GetLastRoomBoxDiagnostic()
return D;
}
void VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint()
{
if (!VoxelDensityProfile::IsEnabled())
{
return;
}
uint64 DynamicBytes = 0;
uint64 EntryBytes = 0;
uint64 LargestEntryBytes = 0;
uint64 ValidEntries = 0;
for (int32 Index = 0; Index < RoomGraphCacheSlotCount; ++Index)
{
const FRoomGraphCacheEntry& Entry = GRoomGraphCache[Index];
if (!Entry.bValid)
{
continue;
}
++ValidEntries;
const uint64 EntryDynamicBytes = static_cast<uint64>(Entry.Cache.GetAllocatedSize());
const uint64 FullEntryBytes = static_cast<uint64>(sizeof(FRoomGraphCacheEntry))
+ EntryDynamicBytes;
DynamicBytes += EntryDynamicBytes;
EntryBytes += FullEntryBytes;
LargestEntryBytes = FMath::Max(LargestEntryBytes, FullEntryBytes);
}
VoxelDensityProfile::SetWorkerRoomGraphCacheFootprint(
RoomGraphCacheSlotCount,
ValidEntries,
static_cast<uint64>(sizeof(GRoomGraphCache)),
DynamicBytes,
EntryBytes,
LargestEntryBytes,
ValidEntries);
}
//=============================================================================
// FVoxelOpStack
//=============================================================================
SIZE_T FVoxelOpStack::GetAllocatedSize() const
{
SIZE_T Bytes = static_cast<SIZE_T>(Ops.GetAllocatedSize());
for (const FOpEntry& Entry : Ops)
{
if (Entry.Op.IsValid())
{
Bytes += FMemory::GetAllocSize(const_cast<IVoxelDensityOp*>(Entry.Op.Get()));
}
}
return Bytes;
}
bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
{
if (OutError != nullptr) { OutError->Reset(); }
+197 -22
View File
@@ -5,9 +5,57 @@ namespace VoxelDensityProfile
namespace
{
std::atomic<bool> GEnabled(false);
std::atomic<uint64> GCalls[BucketCount];
std::atomic<uint64> GCycles[BucketCount];
std::atomic<uint64> GCounters[CounterCount];
// The old implementation performed a contended atomic fetch_add for every timer and
// counter. A TunnelNetwork sample can close twenty-plus ranges and add several
// candidate counters, so profiling changed the workload it was supposed to measure.
// Keep one accumulator per worker and only aggregate at Snapshot(). Commandlet resets
// happen between completed jobs, so the deliberately light-weight list is sufficient.
struct FThreadState
{
uint64 Calls[BucketCount]{};
uint64 Cycles[BucketCount]{};
uint64 Counters[CounterCount]{};
uint64 TunnelCacheCapacityEntries = 0;
uint64 TunnelCacheValidEntries = 0;
uint64 TunnelCacheStaticBytes = 0;
uint64 TunnelCacheDynamicBytes = 0;
uint64 TunnelCacheEntryBytes = 0;
uint64 TunnelCacheLargestEntryBytes = 0;
uint64 TunnelCacheLargestWorkerValidEntries = 0;
uint64 RoomGraphCacheCapacityEntries = 0;
uint64 RoomGraphCacheValidEntries = 0;
uint64 RoomGraphCacheStaticBytes = 0;
uint64 RoomGraphCacheDynamicBytes = 0;
uint64 RoomGraphCacheEntryBytes = 0;
uint64 RoomGraphCacheLargestEntryBytes = 0;
uint64 RoomGraphCacheLargestWorkerValidEntries = 0;
std::atomic<FThreadState*> Next{nullptr};
};
std::atomic<FThreadState*> GThreadStates{nullptr};
thread_local FThreadState* GThreadState = nullptr;
FThreadState& GetThreadState()
{
if (GThreadState == nullptr)
{
GThreadState = new FThreadState();
FThreadState* Head = GThreadStates.load(std::memory_order_relaxed);
do
{
GThreadState->Next.store(Head, std::memory_order_relaxed);
}
while (!GThreadStates.compare_exchange_weak(
Head, GThreadState,
std::memory_order_release,
std::memory_order_relaxed));
}
return *GThreadState;
}
int32 ToIndex(EBucket Bucket)
{
@@ -30,28 +78,78 @@ namespace VoxelDensityProfile
void Reset()
{
for (int32 Index = 0; Index < BucketCount; ++Index)
for (FThreadState* State = GThreadStates.load(std::memory_order_acquire);
State != nullptr;
State = State->Next.load(std::memory_order_acquire))
{
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);
for (int32 Index = 0; Index < BucketCount; ++Index)
{
State->Calls[Index] = 0;
State->Cycles[Index] = 0;
}
for (int32 Index = 0; Index < CounterCount; ++Index)
{
State->Counters[Index] = 0;
}
}
}
FSnapshot Snapshot()
{
FSnapshot Result;
for (int32 Index = 0; Index < BucketCount; ++Index)
for (FThreadState* State = GThreadStates.load(std::memory_order_acquire);
State != nullptr;
State = State->Next.load(std::memory_order_acquire))
{
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);
for (int32 Index = 0; Index < BucketCount; ++Index)
{
Result.Calls[Index] += State->Calls[Index];
Result.Cycles[Index] += State->Cycles[Index];
}
for (int32 Index = 0; Index < CounterCount; ++Index)
{
Result.Counters[Index] += State->Counters[Index];
}
const uint64 TunnelWorkerBytes = State->TunnelCacheStaticBytes
+ State->TunnelCacheDynamicBytes;
if (State->TunnelCacheCapacityEntries > 0)
{
++Result.TunnelCacheWorkers;
Result.TunnelCacheCapacityEntries += State->TunnelCacheCapacityEntries;
Result.TunnelCacheValidEntries += State->TunnelCacheValidEntries;
Result.TunnelCacheStaticBytes += State->TunnelCacheStaticBytes;
Result.TunnelCacheDynamicBytes += State->TunnelCacheDynamicBytes;
Result.TunnelCacheEntryBytes += State->TunnelCacheEntryBytes;
Result.TunnelCacheLargestEntryBytes = FMath::Max(
Result.TunnelCacheLargestEntryBytes,
State->TunnelCacheLargestEntryBytes);
Result.TunnelCacheLargestWorkerValidEntries = FMath::Max(
Result.TunnelCacheLargestWorkerValidEntries,
State->TunnelCacheValidEntries);
Result.TunnelCacheLargestWorkerBytes = FMath::Max(
Result.TunnelCacheLargestWorkerBytes, TunnelWorkerBytes);
}
const uint64 RoomGraphWorkerBytes = State->RoomGraphCacheStaticBytes
+ State->RoomGraphCacheDynamicBytes;
if (State->RoomGraphCacheCapacityEntries > 0)
{
++Result.RoomGraphCacheWorkers;
Result.RoomGraphCacheCapacityEntries += State->RoomGraphCacheCapacityEntries;
Result.RoomGraphCacheValidEntries += State->RoomGraphCacheValidEntries;
Result.RoomGraphCacheStaticBytes += State->RoomGraphCacheStaticBytes;
Result.RoomGraphCacheDynamicBytes += State->RoomGraphCacheDynamicBytes;
Result.RoomGraphCacheEntryBytes += State->RoomGraphCacheEntryBytes;
Result.RoomGraphCacheLargestEntryBytes = FMath::Max(
Result.RoomGraphCacheLargestEntryBytes,
State->RoomGraphCacheLargestEntryBytes);
Result.RoomGraphCacheLargestWorkerValidEntries = FMath::Max(
Result.RoomGraphCacheLargestWorkerValidEntries,
State->RoomGraphCacheValidEntries);
Result.RoomGraphCacheLargestWorkerBytes = FMath::Max(
Result.RoomGraphCacheLargestWorkerBytes, RoomGraphWorkerBytes);
}
}
return Result;
}
@@ -61,10 +159,52 @@ namespace VoxelDensityProfile
const int32 Index = static_cast<int32>(Counter);
if (Index >= 0 && Index < CounterCount)
{
GCounters[Index].fetch_add(Amount, std::memory_order_relaxed);
GetThreadState().Counters[Index] += Amount;
}
}
void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls)
{
const int32 Index = ToIndex(Bucket);
FThreadState& State = GetThreadState();
State.Calls[Index] += Calls;
State.Cycles[Index] += Cycles;
}
void SetWorkerTunnelCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries)
{
if (!IsEnabled()) { return; }
FThreadState& State = GetThreadState();
State.TunnelCacheCapacityEntries = CapacityEntries;
State.TunnelCacheValidEntries = ValidEntries;
State.TunnelCacheStaticBytes = StaticBytes;
State.TunnelCacheDynamicBytes = DynamicBytes;
State.TunnelCacheEntryBytes = EntryBytes;
State.TunnelCacheLargestEntryBytes = LargestEntryBytes;
State.TunnelCacheLargestWorkerValidEntries = LargestWorkerValidEntries;
}
void SetWorkerRoomGraphCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries)
{
if (!IsEnabled()) { return; }
FThreadState& State = GetThreadState();
State.RoomGraphCacheCapacityEntries = CapacityEntries;
State.RoomGraphCacheValidEntries = ValidEntries;
State.RoomGraphCacheStaticBytes = StaticBytes;
State.RoomGraphCacheDynamicBytes = DynamicBytes;
State.RoomGraphCacheEntryBytes = EntryBytes;
State.RoomGraphCacheLargestEntryBytes = LargestEntryBytes;
State.RoomGraphCacheLargestWorkerValidEntries = LargestWorkerValidEntries;
}
const TCHAR* CounterName(ECounter Counter)
{
switch (Counter)
@@ -79,6 +219,10 @@ namespace VoxelDensityProfile
case ECounter::CaveTunnelEvaluated:return TEXT("CaveTunnelEvaluated");
case ECounter::TunnelCoreCandidates:return TEXT("TunnelCoreCandidates");
case ECounter::TunnelCoreEvaluated:return TEXT("TunnelCoreEvaluated");
case ECounter::TunnelSupportColumnBuilds:return TEXT("TunnelSupportColumnBuilds");
case ECounter::TunnelSupportColumnCandidates:return TEXT("TunnelSupportColumnCandidates");
case ECounter::TunnelSupportFloorQueries:return TEXT("TunnelSupportFloorQueries");
case ECounter::TunnelSupportFloorChecks:return TEXT("TunnelSupportFloorChecks");
case ECounter::PassageCandidates: return TEXT("PassageCandidates");
case ECounter::PassageEvaluated: return TEXT("PassageEvaluated");
case ECounter::Count: break;
@@ -130,6 +274,20 @@ namespace VoxelDensityProfile
{ TEXT("CaveTunnelFloorOp"), EBucket::CaveTunnelFloorOp },
{ TEXT("CaveTunnelAirOp"), EBucket::CaveTunnelAirOp },
{ TEXT("XYEdgeSealOp"), EBucket::XYEdgeSealOp },
{ TEXT("DensityPrologue"), EBucket::DensityPrologue },
{ TEXT("DensityCore"), EBucket::DensityCore },
{ TEXT("DensityDisturbances"), EBucket::DensityDisturbances },
{ TEXT("DensityStructuralPosts"), EBucket::DensityStructuralPosts },
{ TEXT("DensityBoundarySeal"), EBucket::DensityBoundarySeal },
{ TEXT("DensityDiffLayer"), EBucket::DensityDiffLayer },
{ TEXT("DensityTail"), EBucket::DensityTail },
{ TEXT("MesherGenerateMesh"), EBucket::MesherGenerateMesh },
{ TEXT("MesherDensityGrid"), EBucket::MesherDensityGrid },
{ TEXT("MesherCellClassification"), EBucket::MesherCellClassification },
{ TEXT("MesherGradientNormals"), EBucket::MesherGradientNormals },
{ TEXT("MesherVertexInterpolation"), EBucket::MesherVertexInterpolation },
{ TEXT("MesherStreamBuilding"), EBucket::MesherStreamBuilding },
{ TEXT("MesherOther"), EBucket::MesherOther },
};
for (const FNameBucket& Entry : Names)
{
@@ -179,17 +337,34 @@ namespace VoxelDensityProfile
case EBucket::CaveTunnelAirOp: return TEXT("CaveTunnelAirOp");
case EBucket::XYEdgeSealOp: return TEXT("XYEdgeSealOp");
case EBucket::OtherOp: return TEXT("OtherOp");
case EBucket::DensityPrologue: return TEXT("DensityPrologue");
case EBucket::DensityCore: return TEXT("DensityCore");
case EBucket::DensityDisturbances:return TEXT("DensityDisturbances");
case EBucket::DensityStructuralPosts:return TEXT("DensityStructuralPosts");
case EBucket::DensityBoundarySeal:return TEXT("DensityBoundarySeal");
case EBucket::DensityDiffLayer: return TEXT("DensityDiffLayer");
case EBucket::DensityTail: return TEXT("DensityTail");
case EBucket::MesherGenerateMesh:return TEXT("MesherGenerateMesh");
case EBucket::MesherDensityGrid:return TEXT("MesherDensityGrid");
case EBucket::MesherCellClassification:return TEXT("MesherCellClassification");
case EBucket::MesherGradientNormals:return TEXT("MesherGradientNormals");
case EBucket::MesherVertexInterpolation:return TEXT("MesherVertexInterpolation");
case EBucket::MesherStreamBuilding:return TEXT("MesherStreamBuilding");
case EBucket::MesherOther: return TEXT("MesherOther");
case EBucket::Count: break;
}
return TEXT("Unknown");
}
FScopedTimer::~FScopedTimer()
{
End();
}
void FScopedTimer::End()
{
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);
AddMeasurement(Bucket, FPlatformTime::Cycles64() - StartCycles, 1);
bActive = false;
}
}
+286 -162
View File
@@ -181,10 +181,10 @@ thread_local int32 VoxelGenLOD::SampleStep = 1;
// 4-wide via SSE (Perlin3D_x4). These thin wrappers keep every call site unchanged. They
// sample a DIFFERENT (float hash-gradient) noise field than the old FMath::PerlinNoise3D,
// so worlds re-tune once — but the fBm/Ridged math/contracts ([-1,1]) are identical.
static float FractalNoise3D(const FVector& Position, int32 Octaves = 4,
float Lacunarity = 2.0f, float Persistence = 0.5f)
static float FractalNoise3D(const FVector3f& Position, int32 Octaves = 4,
float Lacunarity = 2.0f, float Persistence = 0.5f)
{
return VoxelNoise::FBM((float)Position.X, (float)Position.Y, (float)Position.Z,
return VoxelNoise::FBM(Position.X, Position.Y, Position.Z,
Octaves, Lacunarity, Persistence);
}
@@ -199,10 +199,10 @@ static float FractalNoise3D(const FVector& Position, int32 Octaves = 4,
// Returns approximately [-1, 1] to match FractalNoise3D's range.
// Character: craggy cliffs, natural erosion patterns, sharp corridors.
static float RidgedNoise3D(const FVector& Position, int32 Octaves = 4,
float Lacunarity = 2.0f, float Persistence = 0.5f)
static float RidgedNoise3D(const FVector3f& Position, int32 Octaves = 4,
float Lacunarity = 2.0f, float Persistence = 0.5f)
{
return VoxelNoise::Ridged((float)Position.X, (float)Position.Y, (float)Position.Z,
return VoxelNoise::Ridged(Position.X, Position.Y, Position.Z,
Octaves, Lacunarity, Persistence);
}
@@ -225,9 +225,9 @@ static float RidgedNoise3D(const FVector& Position, int32 Octaves = 4,
// qui voit déjà `VoxelHash` : la pile d'opérateurs a besoin exactement du même bruit pour la
// rugosité (4b, type Cellular) et pour les festons (4f), et deux copies d'une fonction pure finissent
// par diverger — c'est littéralement `AUDIT §C1`. Ce forwarder garde les ~3 sites d'appel ci-dessous
// inchangés, comme l'ont fait FractalNoise3D et RidgedNoise3D lors de T2.a. Aucun changement de
// comportement : corps identique, mêmes doubles de `FVector`, même ordre d'opérations.
static float CellularNoise3D(const FVector& Position)
// inchangés, comme l'ont fait FractalNoise3D et RidgedNoise3D lors de T2.a. Le chemin chaud garde
// maintenant les coordonnées en float : la compatibilité d'arrondi historique est abandonnée.
static float CellularNoise3D(const FVector3f& Position)
{
return VoxelNoise::Cellular3D(Position);
}
@@ -483,6 +483,16 @@ namespace
static thread_local FTunnelCoreCacheState GTunnelCoreCache;
static void BuildTunnelSupportColumnsForChunk(
FChunkSDFCache& Cache,
const FIntVector& ChunkCoord);
static const FTunnelSupportFloorColumn* FindTunnelSupportColumn(
const FChunkSDFCache& Cache,
int32 X,
int32 Y,
const FTunnelSupportFloorColumn& EmptyColumn);
// A mesher tile samples an expanded grid (-1..Cells+1), so one worker visits several exact
// chunk coordinates even while it is generating one tile. The old CP_* cache retained only
// the last coordinate and consequently rebuilt the whole TunnelNetwork stack whenever the
@@ -491,12 +501,11 @@ namespace
// result. Native TunnelNetwork/Underwater states are immutable after construction; the active
// pointer below lets the hot sample path read the stored stack and tunnel-core cache without
// moving them on every voxel.
// 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;
// The cache is open-addressed and bounded. The 128-slot budget is evidence-sized: the
// 128³ production export measured at most 102 live tunnel entries on one worker, leaving
// 26 slots of headroom; the old 4,096-slot experiment reserved 3.34 MiB per worker before
// one operator or SDF allocation existed. Collisions only cause a safe deterministic rebuild.
constexpr int32 TunnelDensityCacheSlotCount = 128;
struct FTunnelNetworkDensityCacheEntry
{
@@ -514,6 +523,49 @@ namespace
FTunnelCoreCacheState TunnelCore;
};
void ReportTunnelDensityCacheFootprint(
const FTunnelNetworkDensityCacheEntry* Entries)
{
if (!VoxelDensityProfile::IsEnabled() || Entries == nullptr)
{
return;
}
uint64 DynamicBytes = 0;
uint64 EntryBytes = 0;
uint64 LargestEntryBytes = 0;
uint64 ValidEntries = 0;
for (int32 Index = 0; Index < TunnelDensityCacheSlotCount; ++Index)
{
const FTunnelNetworkDensityCacheEntry& Entry = Entries[Index];
if (!Entry.bValid)
{
continue;
}
++ValidEntries;
const uint64 StackBytes = static_cast<uint64>(Entry.OpStack.GetAllocatedSize());
const uint64 TunnelCoreBytes = static_cast<uint64>(
Entry.TunnelCore.Cache.GetAllocatedSize());
const uint64 EntryDynamicBytes = StackBytes + TunnelCoreBytes;
const uint64 FullEntryBytes = static_cast<uint64>(
sizeof(FTunnelNetworkDensityCacheEntry)) + EntryDynamicBytes;
DynamicBytes += EntryDynamicBytes;
EntryBytes += FullEntryBytes;
LargestEntryBytes = FMath::Max(LargestEntryBytes, FullEntryBytes);
}
VoxelDensityProfile::SetWorkerTunnelCacheFootprint(
TunnelDensityCacheSlotCount,
ValidEntries,
static_cast<uint64>(sizeof(FTunnelNetworkDensityCacheEntry))
* TunnelDensityCacheSlotCount,
DynamicBytes,
EntryBytes,
LargestEntryBytes,
ValidEntries);
}
FORCEINLINE int32 TunnelDensityCacheSlot(const FIntVector& Chunk)
{
uint32 Hash = static_cast<uint32>(Chunk.X) * 0x9E3779B9u;
@@ -547,7 +599,8 @@ namespace
const float ChunkMinY = static_cast<float>(ChunkCoord.Y * CHUNK_SIZE);
const float ChunkMaxX = ChunkMinX + static_cast<float>(CHUNK_SIZE);
const float ChunkMaxY = ChunkMinY + static_cast<float>(CHUNK_SIZE);
const float Expansion = FMath::Max(Params.CaveWarpStrength, 0.0f) + 2.0f;
const float Expansion = FMath::Abs(Params.CaveWarpStrength)
* VOXEL_NOISE_SCALE * 1.5f + 2.0f;
const int32 StrateIndex = Manager.GetStrateIndex(
(static_cast<float>(ChunkCoord.Z) + 0.5f)
* static_cast<float>(CHUNK_SIZE) * VOXEL_SIZE);
@@ -558,6 +611,7 @@ namespace
ChunkMaxX + Expansion, ChunkMaxY + Expansion,
Params, static_cast<uint32>(Manager.GetWorldSeed()),
StrateIndex, nullptr);
BuildTunnelSupportColumnsForChunk(OutState.Cache, ChunkCoord);
OutState.OwnerId = OwnerId;
OutState.ManagerLifetimeId = ManagerLifetimeId;
OutState.Chunk = ChunkCoord;
@@ -566,6 +620,67 @@ namespace
OutState.bValid = true;
}
static void BuildTunnelSupportColumnsForChunk(
FChunkSDFCache& Cache,
const FIntVector& ChunkCoord)
{
const int32 MinX = ChunkCoord.X * CHUNK_SIZE - 1;
const int32 MinY = ChunkCoord.Y * CHUNK_SIZE - 1;
const int32 Cells = CHUNK_SIZE + 2;
Cache.SupportColumnMinX = MinX;
Cache.SupportColumnMinY = MinY;
Cache.SupportColumnCellsX = Cells;
Cache.SupportColumnCellsY = Cells;
Cache.SupportColumnEntries.Init(INDEX_NONE, Cells * Cells);
Cache.SupportColumns.Reset();
// Empty columns are represented only by INDEX_NONE in the slot map, so
// each stored column retains the inline bands for actual graph tubes.
for (int32 Y = 0; Y < Cells; ++Y)
{
for (int32 X = 0; X < Cells; ++X)
{
FTunnelSupportFloorColumn Column;
VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
static_cast<float>(MinX + X),
static_cast<float>(MinY + Y),
Cache,
Column);
if (Column.Intervals.Num() == 0)
{
continue;
}
const int32 EntryIndex = Cache.SupportColumns.Add(MoveTemp(Column));
Cache.SupportColumnEntries[Y * Cells + X] = EntryIndex;
}
}
}
static const FTunnelSupportFloorColumn* FindTunnelSupportColumn(
const FChunkSDFCache& Cache,
int32 X,
int32 Y,
const FTunnelSupportFloorColumn& EmptyColumn)
{
if (Cache.SupportColumnCellsX <= 0
|| X < Cache.SupportColumnMinX
|| X >= Cache.SupportColumnMinX + Cache.SupportColumnCellsX
|| Y < Cache.SupportColumnMinY
|| Y >= Cache.SupportColumnMinY + Cache.SupportColumnCellsY)
{
return nullptr;
}
const int32 Slot =
(Y - Cache.SupportColumnMinY) * Cache.SupportColumnCellsX
+ (X - Cache.SupportColumnMinX);
const int32 EntryIndex = Cache.SupportColumnEntries[Slot];
return EntryIndex == INDEX_NONE
? &EmptyColumn
: &Cache.SupportColumns[EntryIndex];
}
struct FVoxelStackParamRefs
{
const FSlabGenerationParams* Slab = nullptr;
@@ -739,6 +854,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
{
VoxelDensityProfile::FScopedTimer DensityProfileTimer(
VoxelDensityProfile::EBucket::GetDensityAt);
VoxelDensityProfile::FScopedTimer DensityPrologueTimer(
VoxelDensityProfile::EBucket::DensityPrologue);
// ── STRATE SYSTEM ──
// Query per-chunk params from the manager so each strate has different caves.
float Result;
@@ -762,6 +879,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// to reach the next layer. No caves, no spine, no vertical seal — just rock + passage
// tube/landing;
// the global XY edge seal is applied below for bounded worlds.
DensityPrologueTimer.End();
VoxelDensityProfile::FScopedTimer DensityCoreTimer(
VoxelDensityProfile::EBucket::DensityCore);
float Density = 8.0f; // bedrock solidity (positive = solid)
StrateManager->ApplyPassageModifier(
Density, WorldX, WorldY, WorldZ, 8.0f, 0.0f);
@@ -1161,6 +1281,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_ActiveTunnelDensityCache = &CachedEntry;
ActiveOpStack = &CachedEntry.OpStack;
ActiveTunnelCoreCache = &CachedEntry.TunnelCore;
ReportTunnelDensityCacheFootprint(CP_TunnelDensityCache);
}
else if (!bLoadedTunnelDensityCache)
{
@@ -1170,6 +1291,10 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
}
DensityPrologueTimer.End();
VoxelDensityProfile::FScopedTimer DensityCoreTimer(
VoxelDensityProfile::EBucket::DensityCore);
// Le seul point d'entrée de la pile dans le chemin de production. Elle rend la convention
// MC (négatif = solide) comme les fonctions d'archétype, donc les disturbances et la couche
// de diff qui suivent ne voient aucune différence.
@@ -1226,21 +1351,28 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
default:
// Underwater shares tunnel rock (water table is a render-side overlay).
Result = GetDensityWithParams(WorldX, WorldY, WorldZ, CP_Tunnel,
CP_TunnelFP, LayoutVersion); break;
CP_TunnelFP, LayoutVersion,
/*bApplyLegacyStructuralPosts=*/false); break;
}
DensityCoreTimer.End();
// Disturbance layer (the "wow" post-process) — cached params, MC convention. The
// vertical shaft tree opens a walkable route before this shared pass, so a bridge/ridge
// must not refill it. Both native and operator-stack paths use the same marker helper.
VoxelDensityProfile::FScopedTimer DensityDisturbancesTimer(
VoxelDensityProfile::EBucket::DensityDisturbances);
const bool bProtectVerticalShaftAir =
VoxelPassageGeometry::VerticalShaftConnectorAirMarker();
ApplyDisturbances(Result, WorldX, WorldY, WorldZ, CP_Dist, (uint32)Seed,
bProtectVerticalShaftAir);
DensityDisturbancesTimer.End();
// A disturbance is allowed to add visual detail, but it must not refill the landing's
// measured air volume or turn its support slab back into a hole. Reassert the landing
// air first, then the structural floor, in MC space before the final XY seal. The two
// calls use the same landing geometry already evaluated by the legacy and op-stack post.
VoxelDensityProfile::FScopedTimer DensityStructuralPostsTimer(
VoxelDensityProfile::EBucket::DensityStructuralPosts);
float LandingBaseDensity = CP_Dist.BaseDensity;
#if WITH_EDITOR
// A composer parent can deliberately use a different structural base than the authored
@@ -1326,12 +1458,26 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// never once per voxel.
if (ActiveTunnelCoreCache->bValid)
{
const FTunnelSupportFloorColumn* SupportColumn = nullptr;
FTunnelSupportFloorColumn EmptySupportColumn;
const bool bIntegerXY =
WorldX == FMath::FloorToFloat(WorldX)
&& WorldY == FMath::FloorToFloat(WorldY);
if (bIntegerXY)
{
const int32 IX = FMath::FloorToInt(WorldX);
const int32 IY = FMath::FloorToInt(WorldY);
SupportColumn = FindTunnelSupportColumn(
ActiveTunnelCoreCache->Cache, IX, IY, EmptySupportColumn);
}
FTunnelCoreWorldEvaluation TunnelCore;
{
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCorePosts);
TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld(
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache);
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache,
SupportColumn);
}
const bool bTunnelSupportFloor = TunnelCore.bSupportFloor;
if (bTunnelSupportFloor)
@@ -1362,6 +1508,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
StrateManager->ApplyPassageLandingRoomFloorMC(
Result, WorldX, WorldY, WorldZ, LandingBaseDensity);
}
DensityStructuralPostsTimer.End();
}
else
{
@@ -1371,16 +1518,23 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// aussi. La calculer une fois évite un CRC par voxel sur un chemin qui n'en a aucun besoin.
// `LayoutVersion = 0` : sans `StrateManager` il n'y a pas de layout, donc rien qui puisse
// périmer — et l'empreinte constante suffit à distinguer ce cache de tous les autres.
DensityPrologueTimer.End();
VoxelDensityProfile::FScopedTimer DensityCoreTimer(
VoxelDensityProfile::EBucket::DensityCore);
static const FStrateGenerationParams FallbackParams;
static const uint32 FallbackFP = FCrc::MemCrc32(&FallbackParams, sizeof(FallbackParams));
Result = GetDensityWithParams(WorldX, WorldY, WorldZ, FallbackParams, FallbackFP, 0);
DensityCoreTimer.End();
}
// The edge is the final generated structural invariant. Passage carving is ordered before it
// in every archetype/stack, and this MC-facing pass also covers out-of-layout air plus any
// disturbance that might otherwise carve back into the shell. Player edits remain separate
// below and are deliberately still the user override path.
VoxelDensityProfile::FScopedTimer DensityBoundarySealTimer(
VoxelDensityProfile::EBucket::DensityBoundarySeal);
VF_ApplyXYEdgeSealMC(Result, WorldX, WorldY, WorldRadiusVoxels, EdgeSealThickness, 8.0f);
DensityBoundarySealTimer.End();
//=========================================================================
// PLAYER MODIFICATIONS (diff layer)
@@ -1391,6 +1545,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
// we subtract the diff offset:
// Carve (diff < 0) → Result -= negative → Result increases → more air ✓
// Fill (diff > 0) → Result -= positive → Result decreases → more solid ✓
VoxelDensityProfile::FScopedTimer DensityDiffLayerTimer(
VoxelDensityProfile::EBucket::DensityDiffLayer);
if (DiffLayer && DiffLayer->HasAnyMods())
{
// ── PER-CHUNK MOD SNAPSHOT ──
@@ -1422,14 +1578,18 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
Result -= UVoxelDiffLayer::EvaluateMods(Slot.Mods, WorldX, WorldY, WorldZ);
}
}
DensityDiffLayerTimer.End();
VoxelDensityProfile::FScopedTimer DensityTailTimer(
VoxelDensityProfile::EBucket::DensityTail);
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
return Result;
}
float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
const FStrateGenerationParams& Params,
uint32 ParamsFingerprint, uint32 LayoutVersion) const
uint32 ParamsFingerprint, uint32 LayoutVersion,
bool bApplyLegacyStructuralPosts) const
{
//=========================================================================
// STRATE DENSITY FUNCTION (Morphology Pipeline)
@@ -1505,15 +1665,15 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
// Three independent Perlin fields offset by irrational-ish numbers
// so the X/Y/Z warp channels don't correlate with each other.
// Single octave to keep per-voxel cost low (3 Perlin calls total).
WarpedX += VoxelNoise::Perlin3D(FVector(
WarpedX += VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.37f),
WorldY * WF + 1.3f,
EffectiveZ * WF + 5.7f)) * VOXEL_NOISE_SCALE * WS;
WarpedY += VoxelNoise::Perlin3D(FVector(
WarpedY += VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + 7.1f,
WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.59f),
EffectiveZ * WF + 2.3f)) * VOXEL_NOISE_SCALE * WS;
WarpedZ += VoxelNoise::Perlin3D(FVector(
WarpedZ += VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + 11.3f,
WorldY * WF + 9.7f,
EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 0.41f))) * VOXEL_NOISE_SCALE * WS;
@@ -1619,7 +1779,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
const float ChunkMinY = CacheChunkY * (float)CHUNK_SIZE;
const float ChunkMaxX = ChunkMinX + (float)CHUNK_SIZE;
const float ChunkMaxY = ChunkMinY + (float)CHUNK_SIZE;
const float Expansion = Params.CaveWarpStrength + 2.0f;
const float Expansion = FMath::Abs(Params.CaveWarpStrength)
* VOXEL_NOISE_SCALE * 1.5f + 2.0f;
const float SMinX = ChunkMinX - Expansion;
const float SMinY = ChunkMinY - Expansion;
@@ -1786,12 +1947,12 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
float RF = Params.RoughnessFrequency;
// Base noise input positions (with seed offsets for uniqueness)
FVector MainPos(
FVector3f MainPos(
WorldX * RF + VoxelHash::SeedOffset(SeedU, 11.3f),
WorldY * RF + VoxelHash::SeedOffset(SeedU, 13.7f),
EffectiveZ * RF + VoxelHash::SeedOffset(SeedU, 17.1f)
);
FVector FinePos(
FVector3f FinePos(
WorldX * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 19.1f) + 2000.0f,
WorldY * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 23.7f) + 2500.0f,
EffectiveZ * RF * 3.0f + VoxelHash::SeedOffset(SeedU, 29.3f) + 3000.0f
@@ -1807,26 +1968,26 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
float WS = Params.DomainWarpStrength;
// Sample three independent noise fields for X, Y, Z warp
float WarpX = VoxelNoise::Perlin3D(FVector(
float WarpX = VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + VoxelHash::SeedOffset(SeedU, 5.2f),
WorldY * WF + VoxelHash::SeedOffset(SeedU, 1.3f),
EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 9.7f)
)) * VOXEL_NOISE_SCALE * WS;
float WarpY = VoxelNoise::Perlin3D(FVector(
float WarpY = VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + 100.0f + VoxelHash::SeedOffset(SeedU, 7.7f),
WorldY * WF + 200.0f + VoxelHash::SeedOffset(SeedU, 3.1f),
EffectiveZ * WF + 300.0f
)) * VOXEL_NOISE_SCALE * WS;
float WarpZ = VoxelNoise::Perlin3D(FVector(
float WarpZ = VoxelNoise::Perlin3D(FVector3f(
WorldX * WF + 400.0f,
WorldY * WF + 500.0f + VoxelHash::SeedOffset(SeedU, 11.9f),
EffectiveZ * WF + 600.0f + VoxelHash::SeedOffset(SeedU, 13.3f)
)) * VOXEL_NOISE_SCALE * WS;
// Apply warp to both noise positions
FVector WarpOffset(WarpX, WarpY, WarpZ);
FVector3f WarpOffset(WarpX, WarpY, WarpZ);
MainPos += WarpOffset;
FinePos += WarpOffset;
}
@@ -1978,7 +2139,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
float NoisedZ = WorldZ;
if (Params.TerraceNoiseDisplacement > 0.0f)
{
float DispNoise = FractalNoise3D(FVector(
float DispNoise = FractalNoise3D(FVector3f(
WorldX * 0.04f + VoxelHash::SeedOffset(SeedU, 31.1f),
WorldY * 0.04f + VoxelHash::SeedOffset(SeedU, 37.3f),
WorldZ * 0.02f + VoxelHash::SeedOffset(SeedU, 41.7f)
@@ -2104,7 +2265,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
if (DistFromSurface < OverhangRange)
{
// Low Z frequency (0.15x of XY) → features extend horizontally
float OverhangNoise = FractalNoise3D(FVector(
float OverhangNoise = FractalNoise3D(FVector3f(
WorldX * Params.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 53.1f),
WorldY * Params.OverhangFrequency + VoxelHash::SeedOffset(SeedU, 59.3f),
EffectiveZ * Params.OverhangFrequency * 0.15f + VoxelHash::SeedOffset(SeedU, 61.7f)
@@ -2144,7 +2305,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
// Near the surface (CaveSDF ≈ 0), the sign of CaveSDF tells us which
// side we're on: negative = inside cave, positive = solid rock.
// We use a noise-modulated vertical gradient to detect steep faces.
float VertGrad = VoxelNoise::Perlin3D(FVector(
float VertGrad = VoxelNoise::Perlin3D(FVector3f(
WorldX * 0.05f + VoxelHash::SeedOffset(SeedU, 71.3f),
WorldY * 0.05f + VoxelHash::SeedOffset(SeedU, 73.7f),
EffectiveZ * 0.15f + VoxelHash::SeedOffset(SeedU, 79.1f) // 3x faster in Z → detects vertical features
@@ -2185,7 +2346,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
{
// Cellular noise: returns ~[-1, 1] where positive = cell interior (bowl)
float SF = Params.ScallopFrequency;
float ScallopNoise = CellularNoise3D(FVector(
float ScallopNoise = CellularNoise3D(FVector3f(
WorldX * SF + VoxelHash::SeedOffset(SeedU, 83.1f),
WorldY * SF + VoxelHash::SeedOffset(SeedU, 89.3f),
EffectiveZ * SF + VoxelHash::SeedOffset(SeedU, 97.7f)
@@ -2212,47 +2373,21 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
&& NearestRoomIdx >= 0)
{
const FCachedRoom& Room = SDFCache.Rooms[NearestRoomIdx];
const int32 MaxArches = 3;
const FVector VoxPos(WorldX, WorldY, WorldZ);
for (int32 i = 0; i < MaxArches; i++)
for (int32 i = 0; i < 3; i++)
{
uint32 AH = VoxelHash::Mix(Room.Hash ^ (0xA4C400u + (uint32)i * 7369u));
if (VoxelHash::ToFloat01(AH) > Params.ArchDensity) continue;
// Arch center XY: small offset from room center
uint32 AH2 = VoxelHash::Mix(AH ^ 0xA4C4u);
float ArcCX = Room.Center.X + VoxelHash::ToFloatSigned(AH2) * Room.RadiusXY * 0.3f;
float ArcCY = Room.Center.Y + VoxelHash::ToFloatSigned(VoxelHash::Mix(AH2)) * Room.RadiusXY * 0.3f;
// Arch height: mid-room Z (bridging the open space)
uint32 AH3 = VoxelHash::Mix(AH2 ^ 0xB41Du);
float ArcCZ = Room.Center.Z + VoxelHash::ToFloatSigned(AH3) * Room.RadiusZ * 0.4f;
// Arch direction and span: stretch across most of the room
uint32 AH4 = VoxelHash::Mix(AH3 ^ 0xCAFEu);
float Angle = VoxelHash::ToFloat01(AH4) * PI;
float HalfSpan = Room.RadiusXY * (0.5f + VoxelHash::ToFloat01(VoxelHash::Mix(AH4)) * 0.35f);
float CosA = FMath::Cos(Angle);
float SinA = FMath::Sin(Angle);
FVector ArchA(ArcCX - CosA * HalfSpan, ArcCY - SinA * HalfSpan, ArcCZ);
FVector ArchB(ArcCX + CosA * HalfSpan, ArcCY + SinA * HalfSpan, ArcCZ);
// Arch thickness
uint32 AH5 = VoxelHash::Mix(AH4 ^ 0xF00Du);
float ArchRadius = FMath::Lerp(Params.ArchMinRadius, Params.ArchMaxRadius,
VoxelHash::ToFloat01(AH5));
float ArchSDF = VoxelSDF::Capsule(VoxPos, ArchA, ArchB, ArchRadius);
const FCachedArch& Arch = Room.Arches[i];
if (!Arch.bActive) { continue; }
const float ArchSDF = VoxelSDF::Capsule(
VoxPos, Arch.EndpointA, Arch.EndpointB, Arch.Radius);
const float ArchBlend = 2.0f;
if (ArchSDF < ArchBlend)
{
float Fill = FMath::Clamp((ArchBlend - ArchSDF) / (ArchBlend * 2.0f), 0.0f, 1.0f);
Fill = SmoothStep01(Fill);
Density += Fill * Params.BaseDensity * 1.5f;
Density += Fill * Arch.BaseDensity * 1.5f;
}
}
}
@@ -2356,58 +2491,32 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
&& NearestRoomIdx >= 0)
{
const FCachedRoom& Room = SDFCache.Rooms[NearestRoomIdx];
// Pinches on the perimeter of the room (near tunnel entry points)
const float Spread = 0.85f;
const int32 MaxPinches = 3;
for (int32 i = 0; i < MaxPinches; i++)
for (int32 i = 0; i < 3; i++)
{
uint32 PnH = VoxelHash::Mix(Room.Hash ^ (0xF1C400u + (uint32)i * 5417u));
if (VoxelHash::ToFloat01(PnH) > Params.PinchDensity) continue;
// XY: near room perimeter (where tunnels meet the room)
uint32 PnH2 = VoxelHash::Mix(PnH ^ 0xF1C4u);
float PnX = Room.Center.X + VoxelHash::ToFloatSigned(PnH2) * Room.RadiusXY * Spread;
float PnY = Room.Center.Y + VoxelHash::ToFloatSigned(VoxelHash::Mix(PnH2)) * Room.RadiusXY * Spread;
// Z: mid-height within the room
uint32 PnH3 = VoxelHash::Mix(PnH2 ^ 0x5432u);
float PnZ = Room.Center.Z + VoxelHash::ToFloatSigned(PnH3) * Room.RadiusZ * 0.5f;
// Pinch direction aligned toward room center (squeezes inward)
uint32 PnH4 = VoxelHash::Mix(PnH3 ^ 0x9A3Bu);
float PnAngle = VoxelHash::ToFloat01(PnH4) * PI;
float CosPN = FMath::Cos(PnAngle);
float SinPN = FMath::Sin(PnAngle);
float DXPn = WorldX - PnX;
float DYPn = WorldY - PnY;
float DZPn = WorldZ - PnZ;
const FCachedPinch& Pinch = Room.Pinches[i];
if (!Pinch.bActive) { continue; }
const float DXPn = WorldX - Pinch.CenterX;
const float DYPn = WorldY - Pinch.CenterY;
const float DZPn = WorldZ - Pinch.CenterZ;
// Quick reject
float MaxExtent = FMath::Max(Params.PinchLength, Params.PinchStrength) + 5.0f;
if (FMath::Abs(DXPn) + FMath::Abs(DYPn) + FMath::Abs(DZPn) > MaxExtent) continue;
if (FMath::Abs(DXPn) + FMath::Abs(DYPn) + FMath::Abs(DZPn) > Pinch.MaxExtent) continue;
float Along = DXPn * CosPN + DYPn * SinPN;
float Across = -DXPn * SinPN + DYPn * CosPN;
const float Along = DXPn * Pinch.CosAngle + DYPn * Pinch.SinAngle;
const float Across = -DXPn * Pinch.SinAngle + DYPn * Pinch.CosAngle;
float HalfLength = Params.PinchLength * 0.5f;
float HalfNarrow = Params.PinchStrength;
float HalfVertical = Params.PinchStrength * 1.5f;
float NAlong = Along / HalfLength;
float NAcross = Across / HalfNarrow;
float NUp = DZPn / HalfVertical;
float EllipDist = NAlong * NAlong + NAcross * NAcross + NUp * NUp;
const float NAlong = Along / Pinch.HalfLength;
const float NAcross = Across / Pinch.HalfNarrow;
const float NUp = DZPn / Pinch.HalfVertical;
const float EllipDist = NAlong * NAlong + NAcross * NAcross + NUp * NUp;
if (EllipDist < 1.0f)
{
float Fill = 1.0f - EllipDist;
Fill = SmoothStep01(Fill);
float AxisDist = FMath::Sqrt(NAcross * NAcross + NUp * NUp);
float SideFactor = FMath::Clamp(AxisDist * 2.0f, 0.0f, 1.0f);
Density += Fill * SideFactor * Params.BaseDensity * 1.5f;
const float AxisDist = FMath::Sqrt(NAcross * NAcross + NUp * NUp);
const float SideFactor = FMath::Clamp(AxisDist * 2.0f, 0.0f, 1.0f);
Density += Fill * SideFactor * Pinch.BaseDensity * 1.5f;
}
}
}
@@ -2480,7 +2589,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
{
float WormZFreq = Params.WormFrequency * Params.WormHorizontalBias;
float N1 = FMath::Abs(VoxelNoise::Perlin3D(FVector(
float N1 = FMath::Abs(VoxelNoise::Perlin3D(FVector3f(
WorldX * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f),
WorldY * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f),
EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f)
@@ -2490,7 +2599,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
// skip the second Perlin entirely (most voxels; bit-identical output).
if (N1 < Params.WormThreshold)
{
float N2 = FMath::Abs(VoxelNoise::Perlin3D(FVector(
float N2 = FMath::Abs(VoxelNoise::Perlin3D(FVector3f(
WorldX * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f) + 137.0f,
WorldY * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f) + 259.0f,
EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f) + 431.0f
@@ -2523,39 +2632,54 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
//=========================================================================
if (StrateManager)
{
StrateManager->ApplyPassageModifier(
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
if (bApplyLegacyStructuralPosts)
{
StrateManager->ApplyPassageModifier(
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
else
{
// GetDensityAt owns one common MC-space structural tail after either the legacy or
// operator-stack branch. Keep the actual tube carve here, but do not repeat its
// landing air/floor/tunnel-air posts before the shared disturbance/post sequence.
StrateManager->ApplyPassageCarvingOnly(
Density, WorldX, WorldY, WorldZ,
Params.BaseDensity, Params.BoundarySealThickness);
}
}
// Terrain operations and passage support floors are allowed to write solid density, but the
// graph's own tunnel floor/core are structural route geometry. Close the rounded bottom first,
// then reopen only the core above its half-voxel air clearance. Both predicates are shared
// with the operator stack and are evaluated from the per-chunk cache, never per-voxel rebuilt.
const bool bTunnelSupportFloor =
VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
WorldX, WorldY, WorldZ, SDFCache);
if (bTunnelSupportFloor)
if (bApplyLegacyStructuralPosts)
{
Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
}
const float TunnelCoreSDF = (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(WorldX, WorldY, WorldZ, SDFCache)
: FLT_MAX;
if (!bTunnelSupportFloor
&& TunnelCoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels)
{
Density = FMath::Min(
Density,
-FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
// Terrain operations and passage support floors are allowed to write solid density, but
// the graph's own tunnel floor/core are structural route geometry. Direct callers of
// GetDensityWithParams retain this historical tail; GetDensityAt skips it and applies the
// same predicates once in its common MC-space post section below both generation paths.
const bool bTunnelSupportFloor =
VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
WorldX, WorldY, WorldZ, SDFCache);
if (bTunnelSupportFloor)
{
Density = FMath::Max(Density, FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
}
const float TunnelCoreSDF = (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
? VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(WorldX, WorldY, WorldZ, SDFCache)
: FLT_MAX;
if (!bTunnelSupportFloor
&& TunnelCoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels)
{
Density = FMath::Min(
Density,
-FMath::Max(Params.BaseDensity * 2.0f, 1.0f));
}
ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius);
// Fourth structural post: the XY edge wins over a passage near the rim.
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
// Fourth structural post: the XY edge wins over a passage near the rim.
VF_ApplyXYEdgeSeal(Density, WorldX, WorldY,
WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity);
}
// Convention MC: négatif = solide, positif = air.
// La logique interne utilise positif = solide (plus lisible), donc on négate.
@@ -2609,7 +2733,7 @@ float UVoxelGenerator::GetSlabDensity(float WorldX, float WorldY, float WorldZ,
if (Params.FloorRoughness > 0.0f)
{
float FF = Params.FloorRoughnessFrequency;
FloorNoise = FractalNoise3D(FVector(
FloorNoise = FractalNoise3D(FVector3f(
WorldX * FF + VoxelHash::SeedOffset(SeedU, 7.3f),
WorldY * FF + VoxelHash::SeedOffset(SeedU, 11.1f),
0.0f // XY-pur : plus aucune dépendance en Z / no Z dependence
@@ -2640,7 +2764,7 @@ float UVoxelGenerator::GetSlabDensity(float WorldX, float WorldY, float WorldZ,
if (Params.CeilingRoughness > 0.0f)
{
float CF = Params.CeilingRoughnessFrequency;
float RawNoise = FractalNoise3D(FVector(
float RawNoise = FractalNoise3D(FVector3f(
WorldX * CF + VoxelHash::SeedOffset(SeedU, 17.3f) + 1000.0f,
WorldY * CF + VoxelHash::SeedOffset(SeedU, 19.7f) + 2000.0f,
3000.0f // XY-pur : décalage de décorrélation seul / offset only
@@ -2872,7 +2996,7 @@ float UVoxelGenerator::GetMazeDensity(float WorldX, float WorldY, float WorldZ,
// Wall roughness: perturb the corridor surface.
if (Params.SurfaceRoughness > 0.0f && MazeSDF < R + Params.SurfaceRoughness + 2.0f)
{
MazeSDF += FractalNoise3D(FVector(WorldX * 0.12f, WorldY * 0.12f, WorldZ * 0.12f), VoxelGenLOD::Eff(3))
MazeSDF += FractalNoise3D(FVector3f(WorldX * 0.12f, WorldY * 0.12f, WorldZ * 0.12f), VoxelGenLOD::Eff(3))
* VOXEL_NOISE_SCALE * Params.SurfaceRoughness;
}
@@ -2933,8 +3057,8 @@ float UVoxelGenerator::SampleSurfaceStructuralZ(float WorldX, float WorldY,
if (Params.HeightWarpStrength > 0.0f)
{
const float WF = Params.HeightWarpFrequency;
const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.31f), WorldY * WF + 4.2f, VoxelHash::SeedOffset(SeedU, 1.7f)));
const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 8.6f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.53f), VoxelHash::SeedOffset(SeedU, 2.9f)));
const float wx = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.31f), WorldY * WF + 4.2f, VoxelHash::SeedOffset(SeedU, 1.7f)));
const float wy = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + 8.6f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.53f), VoxelHash::SeedOffset(SeedU, 2.9f)));
QX += wx * VOXEL_NOISE_SCALE * Params.HeightWarpStrength;
QY += wy * VOXEL_NOISE_SCALE * Params.HeightWarpStrength;
}
@@ -2944,12 +3068,12 @@ float UVoxelGenerator::SampleSurfaceStructuralZ(float WorldX, float WorldY,
const float Relief = SampleRelief(WorldX, WorldY, Params.ReliefFrequency, Params.ReliefContrast);
const float M = FMath::Lerp(1.0f, Relief, Params.ReliefStrength);
float Cont = FractalNoise3D(FVector(
float Cont = FractalNoise3D(FVector3f(
QX * Params.ContinentFrequency + VoxelHash::SeedOffset(SeedU, 3.1f),
QY * Params.ContinentFrequency + VoxelHash::SeedOffset(SeedU, 5.7f),
VoxelHash::SeedOffset(SeedU, 0.7f)), 4); // [-1,1]
float Detail = FractalNoise3D(FVector(
float Detail = FractalNoise3D(FVector3f(
WorldX * Params.DetailFrequency + 11.0f,
WorldY * Params.DetailFrequency + 22.0f,
VoxelHash::SeedOffset(SeedU, 1.3f)), 3); // [-1,1]
@@ -2957,7 +3081,7 @@ float UVoxelGenerator::SampleSurfaceStructuralZ(float WorldX, float WorldY,
float Mountain = 0.0f;
if (Params.MountainStrength > 0.0f)
{
float Ridge = RidgedNoise3D(FVector(
float Ridge = RidgedNoise3D(FVector3f(
QX * Params.MountainFrequency + 99.0f,
QY * Params.MountainFrequency + 77.0f,
VoxelHash::SeedOffset(SeedU, 0.9f)), 4); // [-1,1]
@@ -3072,8 +3196,8 @@ float UVoxelGenerator::ComputeSurfaceCeiling(float WorldX, float WorldY,
if (Params.CeilingWarpStrength > 0.0f)
{
const float WF = Params.CeilingWarpFrequency;
const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.71f), WorldY * WF + 2.3f, VoxelHash::SeedOffset(SeedU, 3.3f)));
const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 6.1f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.19f), VoxelHash::SeedOffset(SeedU, 4.7f)));
const float wx = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.71f), WorldY * WF + 2.3f, VoxelHash::SeedOffset(SeedU, 3.3f)));
const float wy = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + 6.1f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.19f), VoxelHash::SeedOffset(SeedU, 4.7f)));
QX += wx * VOXEL_NOISE_SCALE * Params.CeilingWarpStrength;
QY += wy * VOXEL_NOISE_SCALE * Params.CeilingWarpStrength;
}
@@ -3081,7 +3205,7 @@ float UVoxelGenerator::ComputeSurfaceCeiling(float WorldX, float WorldY,
// Broad SIGNED swell: raises/lowers the whole cap → big inverted hills and valleys.
if (Params.CeilingUndulation > 0.0f)
{
const float Swell = FractalNoise3D(FVector(
const float Swell = FractalNoise3D(FVector3f(
QX * Params.CeilingUndulationFrequency + VoxelHash::SeedOffset(SeedU, 1.9f),
QY * Params.CeilingUndulationFrequency + 13.0f,
VoxelHash::SeedOffset(SeedU, 0.5f)), 3); // [-1,1]
@@ -3093,14 +3217,14 @@ float UVoxelGenerator::ComputeSurfaceCeiling(float WorldX, float WorldY,
float Hang = 0.0f;
if (Params.CeilingRoughness > 0.0f)
{
Hang += FMath::Abs(FractalNoise3D(FVector(
Hang += FMath::Abs(FractalNoise3D(FVector3f(
WorldX * Params.CeilingRoughnessFrequency + 5.0f,
WorldY * Params.CeilingRoughnessFrequency + 6.0f,
VoxelHash::SeedOffset(SeedU, 2.1f)), 3)) * VOXEL_NOISE_SCALE * Params.CeilingRoughness;
}
if (Params.CeilingRidgeStrength > 0.0f)
{
float Ridge = RidgedNoise3D(FVector(
float Ridge = RidgedNoise3D(FVector3f(
QX * Params.CeilingRidgeFrequency + 31.0f,
QY * Params.CeilingRidgeFrequency + 47.0f,
VoxelHash::SeedOffset(SeedU, 1.1f)), 4); // [-1,1]
@@ -3132,7 +3256,7 @@ float UVoxelGenerator::SurfaceDensityFromColumn(float WorldX, float WorldY, floa
const uint32 SeedU = (uint32)Seed;
const float f = S.OverhangFrequency;
// Shelf-shape noise [0,1]; the Z term makes the reach fold/curl with height (ragged, not a lip).
const float Ns = FractalNoise3D(FVector(
const float Ns = FractalNoise3D(FVector3f(
WorldX * f + VoxelHash::SeedOffset(SeedU, 17.3f),
WorldY * f + VoxelHash::SeedOffset(SeedU, 23.9f),
WorldZ * f * S.OverhangZScale + VoxelHash::SeedOffset(SeedU, 5.1f)), 3) * 0.5f + 0.5f; // [0,1]
@@ -4146,7 +4270,7 @@ float UVoxelGenerator::SampleRelief(float WorldX, float WorldY, float Frequency,
const uint32 SeedU = (uint32)Seed;
// Same offsets/octaves as the original SurfaceWorld relief so existing worlds are
// unchanged (this is the function that code path now calls).
float R = FractalNoise3D(FVector(
float R = FractalNoise3D(FVector3f(
WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 7.3f),
WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 2.1f),
VoxelHash::SeedOffset(SeedU, 0.5f)), 2) * 0.5f + 0.5f; // [0,1]
@@ -4157,7 +4281,7 @@ float UVoxelGenerator::SampleRelief(float WorldX, float WorldY, float Frequency,
float UVoxelGenerator::SampleMoisture(float WorldX, float WorldY, float Frequency) const
{
const uint32 SeedU = (uint32)Seed;
const float N = FractalNoise3D(FVector(
const float N = FractalNoise3D(FVector3f(
WorldX * Frequency + VoxelHash::SeedOffset(SeedU, 4.7f),
WorldY * Frequency + VoxelHash::SeedOffset(SeedU, 8.9f),
VoxelHash::SeedOffset(SeedU, 1.3f)), 2) * 0.5f + 0.5f; // [0,1]
@@ -4241,8 +4365,8 @@ FBiomeSample UVoxelGenerator::SampleBiomeAt(float WorldX, float WorldY, const FB
{
const uint32 SeedU = (uint32)Seed;
const float WF = MP.WarpFrequency;
const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.27f), WorldY * WF + 3.1f, VoxelHash::SeedOffset(SeedU, 1.1f)));
const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 7.7f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.61f), VoxelHash::SeedOffset(SeedU, 2.3f)));
const float wx = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.27f), WorldY * WF + 3.1f, VoxelHash::SeedOffset(SeedU, 1.1f)));
const float wy = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + 7.7f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.61f), VoxelHash::SeedOffset(SeedU, 2.3f)));
QX += wx * VOXEL_NOISE_SCALE * MP.WarpStrength;
QY += wy * VOXEL_NOISE_SCALE * MP.WarpStrength;
}
@@ -4373,8 +4497,8 @@ FBiomeSample UVoxelGenerator::ResolveBiomeSampleAt(float WorldX, float WorldY, i
{
const uint32 SeedU = (uint32)Seed;
const float WF = MP.WarpFrequency;
const float wx = VoxelNoise::Perlin3D(FVector(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.27f), WorldY * WF + 3.1f, VoxelHash::SeedOffset(SeedU, 1.1f)));
const float wy = VoxelNoise::Perlin3D(FVector(WorldX * WF + 7.7f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.61f), VoxelHash::SeedOffset(SeedU, 2.3f)));
const float wx = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + VoxelHash::SeedOffset(SeedU, 0.27f), WorldY * WF + 3.1f, VoxelHash::SeedOffset(SeedU, 1.1f)));
const float wy = VoxelNoise::Perlin3D(FVector3f(WorldX * WF + 7.7f, WorldY * WF + VoxelHash::SeedOffset(SeedU, 0.61f), VoxelHash::SeedOffset(SeedU, 2.3f)));
QX += wx * VOXEL_NOISE_SCALE * MP.WarpStrength;
QY += wy * VOXEL_NOISE_SCALE * MP.WarpStrength;
}
@@ -4861,7 +4985,7 @@ float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float
// Wall roughness.
if (Params.SurfaceRoughness > 0.0f && CaveSDF < Params.SurfaceRoughness + 4.0f)
{
CaveSDF += FractalNoise3D(FVector(WorldX * 0.1f, WorldY * 0.1f, WorldZ * 0.1f), VoxelGenLOD::Eff(3))
CaveSDF += FractalNoise3D(FVector3f(WorldX * 0.1f, WorldY * 0.1f, WorldZ * 0.1f), VoxelGenLOD::Eff(3))
* VOXEL_NOISE_SCALE * Params.SurfaceRoughness;
}
@@ -4986,9 +5110,9 @@ float UVoxelGenerator::GetFloatingIslandDensity(float WorldX, float WorldY, floa
// NOTE : `SeedOffset` quantifie la clé de site par ×100, donc 0.0007 → site 0. Unique aujourd'hui
// (toutes les autres clés du plugin sont ≥ 0.19) ; la prochaine clé sous 0.005 devra en choisir
// une autre plutôt que de collisionner en silence.
const float WX = WorldX + FractalNoise3D(FVector(WorldX * 0.04f + VoxelHash::SeedOffset(S, 0.0007f), WorldY * 0.04f, WorldZ * 0.012f), VoxelGenLOD::Eff(3))
const float WX = WorldX + FractalNoise3D(FVector3f(WorldX * 0.04f + VoxelHash::SeedOffset(S, 0.0007f), WorldY * 0.04f, WorldZ * 0.012f), VoxelGenLOD::Eff(3))
* VOXEL_NOISE_SCALE * WarpAmp;
const float WY = WorldY + FractalNoise3D(FVector(WorldX * 0.04f + 31.0f, WorldY * 0.04f + 7.0f, WorldZ * 0.012f), VoxelGenLOD::Eff(3))
const float WY = WorldY + FractalNoise3D(FVector3f(WorldX * 0.04f + 31.0f, WorldY * 0.04f + 7.0f, WorldZ * 0.012f), VoxelGenLOD::Eff(3))
* VOXEL_NOISE_SCALE * WarpAmp;
// Per-island constants (existence roll, jitter, radius, Z anchor, taper) are pure functions
@@ -5075,7 +5199,7 @@ float UVoxelGenerator::GetFloatingIslandDensity(float WorldX, float WorldY, floa
// Craggy shells.
if (Params.SurfaceRoughness > 0.0f && IslandSDF < Params.SurfaceRoughness + BlendK + 2.0f)
{
IslandSDF += FractalNoise3D(FVector(WorldX * 0.08f, WorldY * 0.08f, WorldZ * 0.08f), VoxelGenLOD::Eff(4))
IslandSDF += FractalNoise3D(FVector3f(WorldX * 0.08f, WorldY * 0.08f, WorldZ * 0.08f), VoxelGenLOD::Eff(4))
* VOXEL_NOISE_SCALE * Params.SurfaceRoughness;
}
@@ -3,6 +3,7 @@
#include "VoxelMarchingCubesMesher.h"
#include "MarchingCubesTables.h"
#include "VoxelDensityProfile.h"
//=============================================================================
// MAIN ALGORITHM
@@ -17,6 +18,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
FVoxelMeshData MeshData;
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
if (!Generator) return MeshData;
VoxelDensityProfile::FScopedTimer MesherProfileTimer(
VoxelDensityProfile::EBucket::MesherGenerateMesh);
VoxelDensityProfile::FScopedTimer MesherSetupTimer(
VoxelDensityProfile::EBucket::MesherOther);
// Cell size in voxels. No upper clamp: coarse clipmap levels use bigger steps (the EXTENT
// grows). Coarse tiles also use FEWER cells (InCellsPerAxis) for cheaper gen.
@@ -218,12 +223,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const int32 GzLo = CzLo - 1; // coins CzLo..CzHi+1, gradients ±1
const int32 GzHi = CzHi + 2; // (== -1..GridDim sans bande)
MesherSetupTimer.End();
// Réutilise le tampon entre tuiles (thread_local) : SetNumUninitialized garde la
// capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile.
static thread_local TArray<float> DensityGrid;
DensityGrid.SetNumUninitialized(MDim * MDim * MDim);
// Bande de strate : seules les rangées Z réellement lues (cellules CzLo..CzHi + marges de
// gradient) sont échantillonnées — le reste du tampon reste non initialisé et non lu.
VoxelDensityProfile::FScopedTimer MesherDensityGridTimer(
VoxelDensityProfile::EBucket::MesherDensityGrid);
for (int32 gz = GzLo; gz <= GzHi; gz++)
{
if (ShouldAbortWork()) return FVoxelMeshData();
@@ -241,6 +250,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
}
}
}
MesherDensityGridTimer.End();
// ── CAPTURE-DURING-MESHING ──
// Si demandé et que la tuile est pleine résolution (CellsPerAxis==CHUNK_SIZE ⇒ Step==1<<Level,
@@ -290,6 +300,13 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
static thread_local TArray<int32> CapTris;
GroundTris.Reset();
CapTris.Reset();
const bool bProfileMesher = VoxelDensityProfile::IsEnabled();
uint64 CellClassificationCycles = 0;
uint64 GradientCycles = 0;
uint64 VertexInterpolationCycles = 0;
uint64 StreamBuildingCycles = 0;
uint64 CellClassificationCalls = 0;
uint64 SurfaceCellCalls = 0;
for (int32 cz = CzLo; cz <= CzHi; cz++) // bande de strate : cf. CzLo/CzHi plus haut
{
if (ShouldAbortWork()) return FVoxelMeshData();
@@ -298,6 +315,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 cx = 0; cx < CellsPerAxis; cx++)
{
const uint64 ClassificationStart = bProfileMesher
? FPlatformTime::Cycles64() : 0;
// PASSE 1 : densités aux 8 coins + index de cas MC SEULEMENT.
// ~70% des cellules d'un chunk sont tout-roc ou tout-air (aucune surface) ;
// on les rejette ICI, AVANT de payer les 8 positions + 8 gradients (48 lectures
@@ -314,9 +333,18 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
if (D >= IsoLevel) CaseIndex |= (1 << i);
}
if (MCEdgeTable[CaseIndex] == 0) continue; // Pas de surface ici → skip
const bool bHasSurface = MCEdgeTable[CaseIndex] != 0;
if (bProfileMesher)
{
CellClassificationCycles += FPlatformTime::Cycles64() - ClassificationStart;
++CellClassificationCalls;
}
if (!bHasSurface) continue; // Pas de surface ici → skip
if (bProfileMesher) { ++SurfaceCellCalls; }
// PASSE 2 : positions + gradients aux 8 coins (uniquement si surface présente).
const uint64 GradientStart = bProfileMesher
? FPlatformTime::Cycles64() : 0;
FVector Positions[8];
FVector Gradients[8];
for (int32 i = 0; i < 8; i++)
@@ -328,10 +356,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
+ FVector(GX * Step, GY * Step, GZ * Step) * VOXEL_SIZE;
Gradients[i] = GradAt(GX, GY, GZ);
}
if (bProfileMesher)
{
GradientCycles += FPlatformTime::Cycles64() - GradientStart;
}
// Interpolation des positions + normales sur les arêtes traversées. t = point de
// traversée de l'iso entre les deux coins (clampé, milieu si densités quasi-égales) ;
// la normale interpole les gradients de coin par le même t.
const uint64 VertexInterpolationStart = bProfileMesher
? FPlatformTime::Cycles64() : 0;
FVector EdgeVertices[12];
FVector EdgeNormals[12];
for (int32 i = 0; i < 12; i++)
@@ -348,9 +382,16 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
EdgeNormals[i] = Gradients[A] + T * (Gradients[B] - Gradients[A]);
}
}
if (bProfileMesher)
{
VertexInterpolationCycles +=
FPlatformTime::Cycles64() - VertexInterpolationStart;
}
// Génère les triangles avec vertices dédupliqués.
// Ordre 0, 2, 1 (pas 0, 1, 2) pour le winding attendu par RealtimeMesh.
const uint64 StreamBuildingStart = bProfileMesher
? FPlatformTime::Cycles64() : 0;
for (int32 i = 0; MCTriTable[CaseIndex][i] != -1; i += 3)
{
const int32 E0 = MCTriTable[CaseIndex][i];
@@ -369,10 +410,33 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
Dst.Add(Idx2);
Dst.Add(Idx1);
}
if (bProfileMesher)
{
StreamBuildingCycles += FPlatformTime::Cycles64() - StreamBuildingStart;
}
}
}
}
if (bProfileMesher)
{
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::MesherCellClassification,
CellClassificationCycles, CellClassificationCalls);
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::MesherGradientNormals,
GradientCycles, SurfaceCellCalls);
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::MesherVertexInterpolation,
VertexInterpolationCycles, SurfaceCellCalls);
VoxelDensityProfile::AddMeasurement(
VoxelDensityProfile::EBucket::MesherStreamBuilding,
StreamBuildingCycles, SurfaceCellCalls);
}
VoxelDensityProfile::FScopedTimer MesherFinalisationTimer(
VoxelDensityProfile::EBucket::MesherOther);
//=========================================================================
// SKIRTS — boucheurs de fissures aux coutures de LOD (clipmap)
//=========================================================================
+188 -5
View File
@@ -439,16 +439,16 @@ namespace VoxelMazeTopology
// point le plus bas qui voit déjà le hash, et le générateur comme la pile d'opérateurs l'appellent.
//
// Ce corps était `static float CellularNoise3D(const FVector&)` dans VoxelGenerator.cpp, invisible
// à la pile d'opérateurs. Déplacement LITTÉRAL : mêmes opérations, même ordre, même passage par
// `FVector` (donc par des doubles) — l'égalité binaire du portage TunnelNetwork en dépend.
// `UVoxelGenerator`'s copy is now a one-line forwarder; the body moved verbatim.
// à la pile d'opérateurs. Il vit ici pour que les deux chemins partagent la même implémentation.
// The hot overload uses FVector3f: noise coordinates are floats, so no double round-trip is paid
// before the hash/lattice arithmetic. Output compatibility with the old field is not a contract.
//
// Algorithme : distance au point-feature le plus proche dans une grille hachée.
// 1. cellule entière du point 2. voisinage 3×3×3 3. rendre (F2 − F1), normalisé ~[-1, 1]
// F2−F1 donne des frontières de cellules lisses avec des arêtes entre elles.
namespace VoxelNoise
{
FORCEINLINE float Cellular3D(const FVector& Position)
FORCEINLINE float Cellular3D(const FVector3f& Position)
{
// Integer cell coordinates
int32 CellX = FMath::FloorToInt(Position.X);
@@ -508,6 +508,11 @@ namespace VoxelNoise
// Result is in [0, ~1.0]. Map to [-1, 1] for compatibility with other noise types.
return Result * 2.0f - 1.0f;
}
FORCEINLINE float Cellular3D(const FVector& Position)
{
return Cellular3D(FVector3f((float)Position.X, (float)Position.Y, (float)Position.Z));
}
}
//=============================================================================
@@ -524,6 +529,32 @@ namespace VoxelNoise
// With caching: build once, evaluate 32K times with just SDF math.
// This is the single biggest CPU performance win for cave generation.
// Room-relative detail geometry whose hash/trigonometry is constant for a cached room.
// These are built once with the room's terrain-op parameters and then only evaluated per voxel.
struct FCachedArch
{
bool bActive = false;
FVector EndpointA = FVector::ZeroVector;
FVector EndpointB = FVector::ZeroVector;
float Radius = 0.0f;
float BaseDensity = 0.0f;
};
struct FCachedPinch
{
bool bActive = false;
float CenterX = 0.0f;
float CenterY = 0.0f;
float CenterZ = 0.0f;
float CosAngle = 1.0f;
float SinAngle = 0.0f;
float MaxExtent = 0.0f;
float HalfLength = 0.0f;
float HalfNarrow = 0.0f;
float HalfVertical = 0.0f;
float BaseDensity = 0.0f;
};
// A room in the cache — everything needed for per-voxel SDF evaluation.
// Internal details (CellX, CellY) used during cache building are NOT stored here.
struct FCachedRoom
@@ -568,6 +599,11 @@ struct FCachedRoom
FVector ShapeA = FVector::ZeroVector;
FVector ShapeB = FVector::ZeroVector;
float ShapeR = 0.0f;
// Hash-derived room details. The old evaluator rebuilt these hashes and angles for every
// surface sample; the cache build is the only place where their geometry can change.
FCachedArch Arches[3];
FCachedPinch Pinches[3];
};
// A local flat-floor bridge over the overlap of two room bodies. It is not a new graph edge: it
@@ -605,11 +641,112 @@ struct FCachedTunnel
TArray<float> WorldControlRadii;
FVector WorldBoundCenter = FVector::ZeroVector;
float WorldBoundRadiusSq = 0.0f;
// Centerline AABBs and scalar influence radii used by the immutable broad phase. The
// AABB test is a cheap lower bound on distance to the whole chain; the existing squared
// sphere test remains the exact conservative second-stage reject.
FVector SDFCenterlineMin = FVector::ZeroVector;
FVector SDFCenterlineMax = FVector::ZeroVector;
float SDFInfluenceRadius = 0.0f;
FVector WorldCenterlineMin = FVector::ZeroVector;
FVector WorldCenterlineMax = FVector::ZeroVector;
float WorldInfluenceRadius = 0.0f;
// Bounding sphere for quick per-voxel rejection
FVector BoundCenter; // Center of the bounding sphere
float BoundRadiusSq; // Squared radius — if voxel is further, skip this tunnel
};
// A deterministic uniform-grid broad phase. Build() is called while a chunk cache is being
// assembled; after that the arrays are immutable and workers only read a bin range. Entries are
// supplied in candidate order, so each bin is already sorted by candidate ID and no runtime sort
// or allocation is needed. A failed/overflowing build leaves bValid=false and callers fall back
// to the exact legacy scan rather than risking a false negative.
struct FCaveSpatialIndexEntry
{
int32 CandidateIndex = INDEX_NONE;
FVector Min = FVector::ZeroVector;
FVector Max = FVector::ZeroVector;
};
struct FCaveSpatialIndexBin
{
int32 First = 0;
int32 Count = 0;
};
struct FCaveSpatialIndex
{
static constexpr int32 BinSizeVoxels = 16;
bool bValid = false;
int32 MinBinX = 0;
int32 MinBinY = 0;
int32 MinBinZ = 0;
int32 CellsX = 0;
int32 CellsY = 0;
int32 CellsZ = 0;
TArray<FCaveSpatialIndexBin> Bins;
TArray<int32> CandidateIndices;
void Reset();
void Build(const TArray<FCaveSpatialIndexEntry>& Entries);
// Returns false only when the index is invalid. A valid query outside its extent returns
// true with an empty range, which is the normal cheap reject for that sample.
bool GetBin(float X, float Y, float Z, int32& OutFirst, int32& OutCount) const;
// Build-time/column-query helper. Coordinates are already integer cell coordinates.
bool GetBinAt(int32 CellX, int32 CellY, int32 CellZ,
int32& OutFirst, int32& OutCount) const;
// A support-floor column is Z-dependent but not Z-search-dependent. Gather the unique
// tunnel IDs from every height bin crossing one XY cell once, then reuse the resulting floor
// intervals for all samples in that column.
template <typename AllocatorType>
bool GetColumnCandidates(float X, float Y, TArray<int32, AllocatorType>& OutCandidates) const;
};
struct FTunnelSupportFloorInterval
{
int32 TunnelIndex = INDEX_NONE;
float FloorZ = 0.0f;
float MinZ = 0.0f;
float MaxZ = 0.0f;
};
struct FTunnelSupportFloorColumn
{
TArray<FTunnelSupportFloorInterval, TInlineAllocator<16>> Intervals;
FTunnelSupportFloorColumn() = default;
FTunnelSupportFloorColumn(const FTunnelSupportFloorColumn& Other)
: Intervals(Other.Intervals)
{
}
FTunnelSupportFloorColumn& operator=(const FTunnelSupportFloorColumn& Other)
{
Intervals = Other.Intervals;
return *this;
}
FTunnelSupportFloorColumn(FTunnelSupportFloorColumn&& Other)
: Intervals(MoveTemp(Other.Intervals))
{
}
FTunnelSupportFloorColumn& operator=(FTunnelSupportFloorColumn&& Other)
{
Intervals = MoveTemp(Other.Intervals);
return *this;
}
void Reset()
{
Intervals.Reset();
}
};
// A pre-baked pit shaft — position and dimensions resolved during BuildChunkCache.
//
// Pits are included in the main SDF evaluation (SmoothMin alongside rooms and
@@ -667,6 +804,27 @@ struct FChunkSDFCache
TArray<FCachedPit> Pits;
TArray<FCachedChimney> Chimneys;
TArray<FCachedColumn> Columns;
// Immutable broad-phase indices. The world index is also the source for per-column support
// intervals; its Z bins are traversed only when a column is first built.
FCaveSpatialIndex RoomIndex;
FCaveSpatialIndex RoomFloorJoinIndex;
FCaveSpatialIndex TunnelIndex;
FCaveSpatialIndex TunnelWorldIndex;
// Sparse immutable support-column table. SupportColumnEntries is a dense
// integer-XY slot map whose values index only non-empty SupportColumns.
int32 SupportColumnMinX = 0;
int32 SupportColumnMinY = 0;
int32 SupportColumnCellsX = 0;
int32 SupportColumnCellsY = 0;
TArray<int32> SupportColumnEntries;
TArray<FTunnelSupportFloorColumn> SupportColumns;
// Allocator-backed bytes owned by this cache, including the nested tunnel chains and index
// arrays. The returned value excludes sizeof(FChunkSDFCache) itself so callers can add the
// enclosing entry's inline storage exactly once.
SIZE_T GetAllocatedSize() const;
};
/** Combined world-space tunnel post query; one cache scan supplies both support and air tests. */
@@ -766,7 +924,8 @@ namespace VoxelCaveMorphology
VOXELFORGE_API FTunnelCoreWorldEvaluation EvaluateTunnelCoreWorld(
float WorldX, float WorldY, float WorldZ,
const FChunkSDFCache& Cache
const FChunkSDFCache& Cache,
const FTunnelSupportFloorColumn* SupportColumn = nullptr
);
// True for the finite support slab beneath a world-space graph tunnel. The native density
@@ -777,6 +936,30 @@ namespace VoxelCaveMorphology
const FChunkSDFCache& Cache
);
// Build/query the same support-floor predicate at column granularity. This changes only
// where the deterministic projection is computed; it does not change the floor interval.
VOXELFORGE_API void BuildTunnelSupportFloorColumn(
float WorldX, float WorldY,
const FChunkSDFCache& Cache,
FTunnelSupportFloorColumn& OutColumn
);
VOXELFORGE_API bool IsTunnelSupportFloorColumnZ(
float WorldZ,
const FTunnelSupportFloorColumn& Column
);
// Returns the exact projected floor band for one tunnel at this XY column.
// The world evaluator uses FloorZ for its per-tunnel air ownership rule and
// Min/Max for the support slab itself.
VOXELFORGE_API bool GetTunnelSupportFloorColumnBand(
int32 TunnelIndex,
const FTunnelSupportFloorColumn& Column,
float& OutFloorZ,
float& OutMinZ,
float& OutMaxZ
);
// CONVENIENCE WRAPPER: builds a temporary cache and evaluates in one call.
// Use this for one-off queries (debug visualization, single-point sampling).
// For chunk generation, use BuildChunkCache + EvaluateSDFCached instead.
@@ -130,6 +130,10 @@ public:
int32 Num() const { return Ops.Num(); }
// Allocator-backed bytes owned by the stack, including each concrete operator allocation.
// The enclosing cache entry adds sizeof(FVoxelOpStack) separately.
SIZE_T GetAllocatedSize() const;
/** Hoist chunk-constant work for every op. Une fois par chunk et par worker.
* Non-const : ça MUTE l'état par-chunk des opérateurs, et le prétendre const serait un
* mensonge utile qui finirait par masquer une course. */
@@ -449,6 +453,9 @@ namespace VoxelDensityOps
VOXELFORGE_API FRoomBoxDiagnostic GetLastRoomBoxDiagnostic();
/** Current worker-local room-graph cache footprint; profiling/diagnostic use only. */
VOXELFORGE_API void ReportWorkerRoomGraphCacheFootprint();
/**
* FloatingIslands — 8 ops, et **la pile tourne à l'ENVERS** :
* ConstantVoid → IslandBlob → SdfRoughness → SdfFill → [structural post ×4]
@@ -47,6 +47,25 @@ namespace VoxelDensityProfile
CaveTunnelAirOp,
XYEdgeSealOp,
OtherOp,
// Non-overlapping top-level phases. The named operation buckets above are intentionally
// nested diagnostics; these phase buckets are the accounting ledger that must add up to
// GetDensityAt without double-counting a stack op inside its enclosing phase.
DensityPrologue,
DensityCore,
DensityDisturbances,
DensityStructuralPosts,
DensityBoundarySeal,
DensityDiffLayer,
DensityTail,
// Mesher-side accounting. GenerateMesh is the inclusive total; the following buckets
// cover its regular-grid work and its non-grid setup/finalisation.
MesherGenerateMesh,
MesherDensityGrid,
MesherCellClassification,
MesherGradientNormals,
MesherVertexInterpolation,
MesherStreamBuilding,
MesherOther,
Count
};
@@ -64,6 +83,10 @@ namespace VoxelDensityProfile
CaveTunnelEvaluated,
TunnelCoreCandidates,
TunnelCoreEvaluated,
TunnelSupportColumnBuilds,
TunnelSupportColumnCandidates,
TunnelSupportFloorQueries,
TunnelSupportFloorChecks,
PassageCandidates,
PassageEvaluated,
Count
@@ -76,6 +99,29 @@ namespace VoxelDensityProfile
uint64 Calls[BucketCount]{};
uint64 Cycles[BucketCount]{};
uint64 Counters[CounterCount]{};
// Opt-in worker-cache footprint snapshot. These are current per-thread values (not
// allocation-event counters), so Snapshot() reports the resident cache footprint at the
// end of a profiling run rather than multiplying bytes by cache churn.
uint64 TunnelCacheWorkers = 0;
uint64 TunnelCacheCapacityEntries = 0;
uint64 TunnelCacheValidEntries = 0;
uint64 TunnelCacheStaticBytes = 0;
uint64 TunnelCacheDynamicBytes = 0;
uint64 TunnelCacheEntryBytes = 0;
uint64 TunnelCacheLargestEntryBytes = 0;
uint64 TunnelCacheLargestWorkerValidEntries = 0;
uint64 TunnelCacheLargestWorkerBytes = 0;
uint64 RoomGraphCacheWorkers = 0;
uint64 RoomGraphCacheCapacityEntries = 0;
uint64 RoomGraphCacheValidEntries = 0;
uint64 RoomGraphCacheStaticBytes = 0;
uint64 RoomGraphCacheDynamicBytes = 0;
uint64 RoomGraphCacheEntryBytes = 0;
uint64 RoomGraphCacheLargestEntryBytes = 0;
uint64 RoomGraphCacheLargestWorkerValidEntries = 0;
uint64 RoomGraphCacheLargestWorkerBytes = 0;
};
VOXELFORGE_API void SetEnabled(bool bEnabled);
@@ -83,6 +129,18 @@ namespace VoxelDensityProfile
VOXELFORGE_API void Reset();
VOXELFORGE_API FSnapshot Snapshot();
VOXELFORGE_API void AddCounter(ECounter Counter, uint64 Amount = 1);
VOXELFORGE_API void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls = 1);
VOXELFORGE_API void SetWorkerTunnelCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries);
VOXELFORGE_API void SetWorkerRoomGraphCacheFootprint(
uint64 CapacityEntries, uint64 ValidEntries,
uint64 StaticBytes, uint64 DynamicBytes,
uint64 EntryBytes, uint64 LargestEntryBytes,
uint64 LargestWorkerValidEntries);
VOXELFORGE_API const TCHAR* CounterName(ECounter Counter);
VOXELFORGE_API EBucket BucketFromName(const TCHAR* Name);
VOXELFORGE_API const TCHAR* BucketName(EBucket Bucket);
@@ -111,6 +169,10 @@ namespace VoxelDensityProfile
~FScopedTimer();
// End a range before its lexical scope ends. This lets GetDensityAt split its enclosing
// total into disjoint prologue/core/post phases without moving the large cache block.
void End();
FScopedTimer(const FScopedTimer&) = delete;
FScopedTimer& operator=(const FScopedTimer&) = delete;
+2 -1
View File
@@ -160,7 +160,8 @@ public:
*/
float GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
const FStrateGenerationParams& Params,
uint32 ParamsFingerprint, uint32 LayoutVersion) const;
uint32 ParamsFingerprint, uint32 LayoutVersion,
bool bApplyLegacyStructuralPosts = true) const;
/**
* Densité pour une strate Slab (FlatPlain / CrystalChamber).
+9
View File
@@ -132,6 +132,15 @@ FORCEINLINE float Perlin3D(const FVector& P)
return Perlin3D((float)P.X, (float)P.Y, (float)P.Z);
}
// Hot density callers already own float voxel coordinates. Keeping this overload separate from
// FVector avoids constructing UE5's double-precision FVector merely to round the coordinates back
// to float at the noise entry point. It is the same scalar formula and remains deterministic on
// every build of the module.
FORCEINLINE float Perlin3D(const FVector3f& P)
{
return Perlin3D(P.X, P.Y, P.Z);
}
//=============================================================================
// 4-WIDE BATCH — the SIMD multiplier. Computes 4 independent Perlin samples.
// Inputs are 4-element arrays; unused lanes must be zero-filled by the caller
@@ -173,6 +173,7 @@ struct FExploreArguments
bool bRender = true;
bool bWalk = true;
bool bExport = true;
bool bUseOperatorStack = true;
bool bProfileDensity = false;
bool bProfileLod = false;
bool bFailureFocusRender = false;
@@ -212,6 +213,9 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
FParse::Value(*Params, TEXT("archetype="), ArchetypeText);
FParse::Value(*Params, TEXT("slot="), OutArguments.Slot);
FParse::Value(*Params, TEXT("modes="), ModesText);
int32 UseOperatorStack = 1;
FParse::Value(*Params, TEXT("opstack="), UseOperatorStack);
OutArguments.bUseOperatorStack = UseOperatorStack != 0;
OutArguments.bProfileDensity = FParse::Param(*Params, TEXT("profiledensity"));
OutArguments.bProfileLod = FParse::Param(*Params, TEXT("profilelod"));
FParse::Value(*Params, TEXT("out="), OutText);
@@ -528,9 +532,9 @@ struct FExploreWorld
Definition->GeneratorType = Index == Arguments.Slot
? Arguments.Archetype
: ECaveGeneratorType::TunnelNetwork;
// Exercise the production operator-stack opt-in where it is implemented (Maze),
// while the runtime switch safely falls back for other archetypes.
Definition->bUseOperatorStack = true;
// Explicitly select the production switch so the same synthetic layout, seed and
// mesher invocation can be measured through both density branches.
Definition->bUseOperatorStack = Arguments.bUseOperatorStack;
const TSoftObjectPtr<UVoxelStrateDefinition> SoftDefinition(Definition);
Settings->FixedStrates.Add(Index, SoftDefinition);
@@ -3270,6 +3274,7 @@ FString BuildManifestJson(
Writer->WriteValue(TEXT("seed"), Arguments.Seed);
Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype));
Writer->WriteValue(TEXT("slot"), Arguments.Slot);
Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack);
Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride);
if (Arguments.bSurfaceRoughnessOverride)
{
@@ -3478,6 +3483,7 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("seed"), Arguments.Seed);
Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype));
Writer->WriteValue(TEXT("slot"), Arguments.Slot);
Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack);
Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride);
if (Arguments.bSurfaceRoughnessOverride)
{
@@ -3494,10 +3500,11 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory);
Writer->WriteValue(TEXT("canonical_invocation"), FString::Printf(
TEXT("UnrealEditor-Cmd VoxelM.uproject -run=VoxelForgeExplore -seed=%d -archetype=%s "
"-slot=%d -modes=%s -out=Saved/VoxelForge/Explore/<runid>"),
"-slot=%d -opstack=%d -modes=%s -out=Saved/VoxelForge/Explore/<runid>"),
Arguments.Seed,
ArchetypeName(Arguments.Archetype),
Arguments.Slot,
Arguments.bUseOperatorStack ? 1 : 0,
*Arguments.CanonicalModes()));
Writer->WriteObjectEnd();
@@ -4123,6 +4130,102 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
static_cast<unsigned long long>(DensityCalls), DensityTotalUs,
DensityCalls > 0 ? DensityTotalUs / static_cast<double>(DensityCalls) : 0.0);
const uint64 CacheTotalStaticBytes = Profile.TunnelCacheStaticBytes
+ Profile.RoomGraphCacheStaticBytes;
const uint64 CacheTotalDynamicBytes = Profile.TunnelCacheDynamicBytes
+ Profile.RoomGraphCacheDynamicBytes;
const uint64 CacheTotalBytes = CacheTotalStaticBytes + CacheTotalDynamicBytes;
const uint64 CacheWorkers = FMath::Max(
Profile.TunnelCacheWorkers, Profile.RoomGraphCacheWorkers);
const uint64 CacheValidEntries = Profile.TunnelCacheValidEntries
+ Profile.RoomGraphCacheValidEntries;
const uint64 CacheEntryBytes = Profile.TunnelCacheEntryBytes
+ Profile.RoomGraphCacheEntryBytes;
const uint64 CacheCapacityEntries = Profile.TunnelCacheCapacityEntries
+ Profile.RoomGraphCacheCapacityEntries;
const uint64 CacheLargestEntryBytes = FMath::Max(
Profile.TunnelCacheLargestEntryBytes,
Profile.RoomGraphCacheLargestEntryBytes);
const uint64 CacheLargestWorkerBytes = FMath::Max(
Profile.TunnelCacheLargestWorkerBytes,
Profile.RoomGraphCacheLargestWorkerBytes);
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCacheMemory] workers=%llu capacity_entries=%llu valid_entries=%llu "
"static_bytes=%llu dynamic_bytes=%llu total_bytes=%llu "
"avg_valid_entry_bytes=%.1f largest_entry_bytes=%llu "
"avg_worker_bytes=%.1f largest_worker_bytes=%llu "
"tunnel_avg_entry_bytes=%.1f tunnel_largest_entry_bytes=%llu "
"tunnel_largest_worker_valid=%llu "
"roomgraph_avg_entry_bytes=%.1f roomgraph_largest_entry_bytes=%llu "
"roomgraph_largest_worker_valid=%llu "
"tunnel_workers=%llu tunnel_capacity=%llu tunnel_valid=%llu "
"tunnel_static=%llu tunnel_dynamic=%llu roomgraph_workers=%llu "
"roomgraph_capacity=%llu roomgraph_valid=%llu roomgraph_static=%llu "
"roomgraph_dynamic=%llu"),
static_cast<unsigned long long>(CacheWorkers),
static_cast<unsigned long long>(CacheCapacityEntries),
static_cast<unsigned long long>(CacheValidEntries),
static_cast<unsigned long long>(CacheTotalStaticBytes),
static_cast<unsigned long long>(CacheTotalDynamicBytes),
static_cast<unsigned long long>(CacheTotalBytes),
CacheValidEntries > 0
? static_cast<double>(CacheEntryBytes) / static_cast<double>(CacheValidEntries)
: 0.0,
static_cast<unsigned long long>(CacheLargestEntryBytes),
CacheWorkers > 0
? static_cast<double>(CacheTotalBytes) / static_cast<double>(CacheWorkers)
: 0.0,
static_cast<unsigned long long>(CacheLargestWorkerBytes),
Profile.TunnelCacheValidEntries > 0
? static_cast<double>(Profile.TunnelCacheEntryBytes)
/ static_cast<double>(Profile.TunnelCacheValidEntries)
: 0.0,
static_cast<unsigned long long>(Profile.TunnelCacheLargestEntryBytes),
static_cast<unsigned long long>(Profile.TunnelCacheLargestWorkerValidEntries),
Profile.RoomGraphCacheValidEntries > 0
? static_cast<double>(Profile.RoomGraphCacheEntryBytes)
/ static_cast<double>(Profile.RoomGraphCacheValidEntries)
: 0.0,
static_cast<unsigned long long>(Profile.RoomGraphCacheLargestEntryBytes),
static_cast<unsigned long long>(Profile.RoomGraphCacheLargestWorkerValidEntries),
static_cast<unsigned long long>(Profile.TunnelCacheWorkers),
static_cast<unsigned long long>(Profile.TunnelCacheCapacityEntries),
static_cast<unsigned long long>(Profile.TunnelCacheValidEntries),
static_cast<unsigned long long>(Profile.TunnelCacheStaticBytes),
static_cast<unsigned long long>(Profile.TunnelCacheDynamicBytes),
static_cast<unsigned long long>(Profile.RoomGraphCacheWorkers),
static_cast<unsigned long long>(Profile.RoomGraphCacheCapacityEntries),
static_cast<unsigned long long>(Profile.RoomGraphCacheValidEntries),
static_cast<unsigned long long>(Profile.RoomGraphCacheStaticBytes),
static_cast<unsigned long long>(Profile.RoomGraphCacheDynamicBytes));
const VoxelDensityProfile::EBucket DensityPhases[] = {
VoxelDensityProfile::EBucket::DensityPrologue,
VoxelDensityProfile::EBucket::DensityCore,
VoxelDensityProfile::EBucket::DensityDisturbances,
VoxelDensityProfile::EBucket::DensityStructuralPosts,
VoxelDensityProfile::EBucket::DensityBoundarySeal,
VoxelDensityProfile::EBucket::DensityDiffLayer,
VoxelDensityProfile::EBucket::DensityTail,
};
uint64 DensityAttributionCycles = 0;
uint64 DensityAttributionCalls = 0;
for (const VoxelDensityProfile::EBucket Phase : DensityPhases)
{
const int32 PhaseIndex = static_cast<int32>(Phase);
DensityAttributionCycles += Profile.Cycles[PhaseIndex];
DensityAttributionCalls += Profile.Calls[PhaseIndex];
}
const double DensityAttributionUs =
static_cast<double>(DensityAttributionCycles) * CycleToMicroseconds;
const double DensityUnattributedUs = DensityTotalUs - DensityAttributionUs;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeDensityProfile] attribution_sum_us=%.3f unattributed_us=%.3f "
"coverage=%.2f%% phase_calls=%llu"),
DensityAttributionUs, DensityUnattributedUs,
DensityTotalUs > 0.0 ? DensityAttributionUs * 100.0 / DensityTotalUs : 0.0,
static_cast<unsigned long long>(DensityAttributionCalls));
for (int32 Index = 0; Index < VoxelDensityProfile::CounterCount; ++Index)
{
const VoxelDensityProfile::ECounter Counter =
@@ -4151,6 +4254,37 @@ int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
static_cast<unsigned long long>(Calls), TotalUs,
TotalUs / static_cast<double>(Calls));
}
const VoxelDensityProfile::EBucket MesherBuckets[] = {
VoxelDensityProfile::EBucket::MesherDensityGrid,
VoxelDensityProfile::EBucket::MesherCellClassification,
VoxelDensityProfile::EBucket::MesherGradientNormals,
VoxelDensityProfile::EBucket::MesherVertexInterpolation,
VoxelDensityProfile::EBucket::MesherStreamBuilding,
VoxelDensityProfile::EBucket::MesherOther,
};
uint64 MesherAttributionCycles = 0;
uint64 MesherAttributionCalls = 0;
for (const VoxelDensityProfile::EBucket Bucket : MesherBuckets)
{
const int32 BucketIndex = static_cast<int32>(Bucket);
MesherAttributionCycles += Profile.Cycles[BucketIndex];
MesherAttributionCalls += Profile.Calls[BucketIndex];
}
const int32 MesherTotalIndex = static_cast<int32>(
VoxelDensityProfile::EBucket::MesherGenerateMesh);
const double MesherTotalUs = static_cast<double>(
Profile.Cycles[MesherTotalIndex]) * CycleToMicroseconds;
const double MesherAttributionUs = static_cast<double>(
MesherAttributionCycles) * CycleToMicroseconds;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeMesherProfile] total_calls=%llu total_us=%.3f "
"attribution_sum_us=%.3f unattributed_us=%.3f coverage=%.2f%% "
"phase_calls=%llu"),
static_cast<unsigned long long>(Profile.Calls[MesherTotalIndex]),
MesherTotalUs, MesherAttributionUs, MesherTotalUs - MesherAttributionUs,
MesherTotalUs > 0.0 ? MesherAttributionUs * 100.0 / MesherTotalUs : 0.0,
static_cast<unsigned long long>(MesherAttributionCalls));
VoxelDensityProfile::SetEnabled(false);
}
return bRequestedModeFailed ? 2 : 0;