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

@@ -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)
//=========================================================================