perf(mesher): adjacent tiles were re-evaluating 18% of the field -- 2.69s to 1.49s
Overnight task 2. Every claim here is end-to-end wall clock on a real 128^3 export,
not a profiled bucket, because the last two rounds shipped large bucket wins worth
0% in reality. Measurement floor is ~17% run-to-run; only Part 2 cleared it.
PART 2 -- KEPT. Density-grid reuse.
The mesher samples an expanded grid (-1..Cells+1), so adjacent tiles re-evaluated
their shared boundary planes. Measured: 2,744,000 requested samples contained
495,909 duplicates -- 18.07% of the field generated twice. A run-scoped read-only
lattice (FVoxelSharedDensityGrid) owns each world sample once and hands it to both
adjacent tile grids. Duplicates now 0.
128^3 TunnelNetwork seed 1337 slot 1, freshly built binary:
before 2.603 / 2.693 / 2.697 s
after 1.488 / 1.499 / 1.486 s -44.6%, 1.80x
Geometry SHA-256 98eaad61..d3c4f IDENTICAL to the pre-change binary's output.
153,740 triangles either way. Pure win, no field change.
The lattice is deliberately not a UObject and not a cache with eviction: ownership,
lifetime and iteration order stay explicit. A malformed or incomplete lattice is
ignored and falls back to the canonical generator path rather than altering geometry.
PART 1 -- REVERTED. Block-level empty-space early-out.
**0 of 4,096 blocks were safely classifiable**, and the attempt made it 85% slower
(3.632 -> 6.735 s). No unsafe bound was retained.
⇒ This is a finding, not just a failure: our conservative bounds are too loose to
prove ANY 8^3 block uniform. Hierarchical evaluation of any kind is blocked until
we know which operator has effectively unbounded reach. Next task.
PART 3 -- REVERTED. Task granularity.
15 workers, 64 tile jobs, mean job 0.594 s, launch fringe ~14.1 us. MinBatchSize=4
was 1.09% SLOWER end-to-end despite a better bucket time -- exactly the trap this
task's rule exists to catch. Default stays 1. ParallelForWithExistingTaskContext
not adopted.
PART 4 -- NO CHANGE. Renderer audited over 3 runs (8.736 s total, 1.483 s raster).
Mesh and BVH are already reused across viewpoints; no per-pixel heap allocation, no
redundant remeshing. Nothing gross to fix; it benefits from the above automatically.
STILL OPEN: the spatial index from ee89c4d has never earned its keep in wall clock
and accounts for ~214 MB of measured allocations. Codex proposes an on/off ablation
then removal if confirmed. Not done unilaterally.
⚠️ PROCESS NOTE: the agent reported "build passed" but the runtime DLL was still
from 03:07 while sources were edited at 05:22/05:42. My first three verification
runs measured the OLD binary and showed no change. Always check binary timestamps
against source timestamps before believing a measurement.
Cumulative on this region: 342.2 s -> 16.2 s -> 2.62 s -> 1.49 s (230x).
Determinism: identical hashes across all repeat runs. Build clean. Nothing outside
Plugins/VoxelForge touched; RealtimeMeshComponent untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
e63f016835
commit
2f33870a84
6 files changed
+488
-8
No files matched your search
@@ -178,6 +178,26 @@ static thread_local FSurfaceColumnCache GSurfColCache;
|
||||
thread_local int32 VoxelGenLOD::OctaveBias = 0;
|
||||
thread_local int32 VoxelGenLOD::SampleStep = 1;
|
||||
|
||||
int32 VoxelGenLOD::GetThreadOctaveBias()
|
||||
{
|
||||
return OctaveBias;
|
||||
}
|
||||
|
||||
void VoxelGenLOD::SetThreadOctaveBias(int32 Value)
|
||||
{
|
||||
OctaveBias = Value;
|
||||
}
|
||||
|
||||
int32 VoxelGenLOD::GetThreadSampleStep()
|
||||
{
|
||||
return SampleStep;
|
||||
}
|
||||
|
||||
void VoxelGenLOD::SetThreadSampleStep(int32 Value)
|
||||
{
|
||||
SampleStep = Value;
|
||||
}
|
||||
|
||||
// NOTE (T2.a): the fBm/Ridged bodies moved to VoxelNoise.h, where octaves are evaluated
|
||||
// 4-wide via SSE (Perlin3D_x4). These thin wrappers keep every call site unchanged. They
|
||||
// sample a DIFFERENT (float hash-gradient) noise field than the old FMath::PerlinNoise3D,
|
||||
|
||||
@@ -231,6 +231,26 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
// capacité, donc plus de malloc/free de ~170 Ko (35³ floats) par tuile.
|
||||
static thread_local TArray<float> DensityGrid;
|
||||
DensityGrid.SetNumUninitialized(MDim * MDim * MDim);
|
||||
const FVoxelSharedDensityGrid* ReuseGrid = SharedDensityGrid;
|
||||
const int32 SharedDeltaX = ReuseGrid
|
||||
? (OriginVoxels.X - ReuseGrid->OriginVoxels.X) / Step : 0;
|
||||
const int32 SharedDeltaY = ReuseGrid
|
||||
? (OriginVoxels.Y - ReuseGrid->OriginVoxels.Y) / Step : 0;
|
||||
const int32 SharedDeltaZ = ReuseGrid
|
||||
? (OriginVoxels.Z - ReuseGrid->OriginVoxels.Z) / Step : 0;
|
||||
const bool bUseSharedDensityGrid = ReuseGrid
|
||||
&& ReuseGrid->Step == Step
|
||||
&& ReuseGrid->Dim > 0
|
||||
&& ReuseGrid->Samples.Num() == ReuseGrid->Dim * ReuseGrid->Dim * ReuseGrid->Dim
|
||||
&& (OriginVoxels.X - ReuseGrid->OriginVoxels.X) % Step == 0
|
||||
&& (OriginVoxels.Y - ReuseGrid->OriginVoxels.Y) % Step == 0
|
||||
&& (OriginVoxels.Z - ReuseGrid->OriginVoxels.Z) % Step == 0
|
||||
&& SharedDeltaX - 1 >= 0
|
||||
&& SharedDeltaY - 1 >= 0
|
||||
&& SharedDeltaZ - 1 >= 0
|
||||
&& SharedDeltaX + GridDim < ReuseGrid->Dim
|
||||
&& SharedDeltaY + GridDim < ReuseGrid->Dim
|
||||
&& SharedDeltaZ + GridDim < ReuseGrid->Dim;
|
||||
// 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(
|
||||
@@ -243,12 +263,23 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
if (ShouldAbortWork()) return FVoxelMeshData();
|
||||
for (int32 gx = -1; gx <= GridDim; gx++)
|
||||
{
|
||||
// World voxel = tile origin + grid offset scaled by the cell size (Step).
|
||||
DensityGrid[((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1)] =
|
||||
Generator->GetDensityAt(
|
||||
const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1);
|
||||
if (bUseSharedDensityGrid)
|
||||
{
|
||||
const int32 SharedX = SharedDeltaX + gx;
|
||||
const int32 SharedY = SharedDeltaY + gy;
|
||||
const int32 SharedZ = SharedDeltaZ + gz;
|
||||
DensityGrid[LocalIndex] = ReuseGrid->Samples[
|
||||
(SharedZ * ReuseGrid->Dim + SharedY) * ReuseGrid->Dim + SharedX];
|
||||
}
|
||||
else
|
||||
{
|
||||
// World voxel = tile origin + grid offset scaled by the cell size (Step).
|
||||
DensityGrid[LocalIndex] = Generator->GetDensityAt(
|
||||
OriginVoxels.X + gx * Step,
|
||||
OriginVoxels.Y + gy * Step,
|
||||
OriginVoxels.Z + gz * Step);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,9 @@ class UVoxelBiomeDefinition;
|
||||
// the same thread, and biome/climate must stay LOD-independent.
|
||||
namespace VoxelGenLOD
|
||||
{
|
||||
// NOT VOXELFORGE_API: MSVC forbids dll-interface on thread_local (C2492). Both users
|
||||
// (generator + mesher) are inside this module, so no export is needed anyway.
|
||||
// The TLS variables themselves stay module-local because MSVC forbids exporting thread_local
|
||||
// data (C2492). These tiny accessors let the editor explorer build a shared LOD grid without
|
||||
// linking directly to the storage in the game module.
|
||||
extern thread_local int32 OctaveBias;
|
||||
|
||||
// Sampling step of the tile currently being meshed on this worker. GetDensityAt itself has
|
||||
@@ -50,6 +51,11 @@ namespace VoxelGenLOD
|
||||
// read-only hint for caches that may safely widen their spatial window for coarse tiles.
|
||||
extern thread_local int32 SampleStep;
|
||||
|
||||
VOXELFORGE_API int32 GetThreadOctaveBias();
|
||||
VOXELFORGE_API void SetThreadOctaveBias(int32 Value);
|
||||
VOXELFORGE_API int32 GetThreadSampleStep();
|
||||
VOXELFORGE_API void SetThreadSampleStep(int32 Value);
|
||||
|
||||
// Effective octave count for a per-voxel noise call site.
|
||||
// At least 1 octave always survives (the coarse base shape).
|
||||
FORCEINLINE int32 Eff(int32 Octaves) { return FMath::Max(1, Octaves - OctaveBias); }
|
||||
|
||||
@@ -17,6 +17,27 @@
|
||||
#include "VoxelGenerator.h"
|
||||
#include "VoxelMarchingCubesMesher.generated.h"
|
||||
|
||||
// A run-scoped, read-only density lattice. The explorer owns the storage and installs the
|
||||
// pointer only while its canonical tiles are being meshed. The lattice includes the one-point
|
||||
// halo needed by every tile, so each world sample is generated once and copied into both adjacent
|
||||
// tile scratch grids. It is deliberately not a UObject or a cache with eviction: ownership,
|
||||
// lifetime, and iteration order remain explicit and deterministic.
|
||||
struct FVoxelSharedDensityGrid
|
||||
{
|
||||
FIntVector OriginVoxels = FIntVector::ZeroValue;
|
||||
int32 Step = 1;
|
||||
int32 Dim = 0;
|
||||
TArray<float> Samples;
|
||||
|
||||
void Reset()
|
||||
{
|
||||
OriginVoxels = FIntVector::ZeroValue;
|
||||
Step = 1;
|
||||
Dim = 0;
|
||||
Samples.Reset();
|
||||
}
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject
|
||||
{
|
||||
@@ -93,6 +114,11 @@ public:
|
||||
// This is intentionally read-only: workers may observe shutdown, but never mutate world state.
|
||||
void SetShutdownFlag(const std::atomic<bool>* InShutdownFlag) { ShutdownFlag = InShutdownFlag; }
|
||||
|
||||
// Optional run-scoped shared lattice. The caller must keep it alive and immutable until all
|
||||
// GenerateMesh calls finish. A malformed/incomplete lattice is ignored and falls back to
|
||||
// the canonical generator path rather than changing geometry.
|
||||
void SetSharedDensityGrid(const FVoxelSharedDensityGrid* InGrid) { SharedDensityGrid = InGrid; }
|
||||
|
||||
private:
|
||||
FORCEINLINE bool ShouldAbortWork() const
|
||||
{
|
||||
@@ -100,6 +126,7 @@ private:
|
||||
}
|
||||
|
||||
const std::atomic<bool>* ShutdownFlag = nullptr;
|
||||
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
|
||||
|
||||
public:
|
||||
|
||||
|
||||
Reference in new issue
Block a user