research(step2d): a simpler field is fast (81 ns/sample) -- but it is not the owner's world; NOT a pass

Editor-only, default off (voxel.ResearchRedefinedDensityField). The redefined field is a seeded,
periodic cross-corridor pattern per strate plus the authored passages, landings and a shaft tree,
with four value-noise detail bands at every near-surface 25 cm sample and sound 8^3 brick proofs.
259^3 ring, six workers: 308 ms median (467 ms without skips) vs 8.9 s for the live field.
Detail, boundary-artefact, determinism (1 vs 6 workers, two runs) and bound-audit checks pass.

Why it is not a pass: the slices show a mostly-solid field with a few straight corridors, where the
owner's world is mostly open space; rooms, the tunnel network, composer recipes and archetypes are
gone. Much of the speed comes from 60% of bricks being proven solid in that simpler world.
What it does show: detail at full resolution plus brick skipping costs ~80-130 ns/sample; the
cost of the live field is how rooms/tunnels/passages are represented, not the detail or the lattice.

The two walkability tests were redirected to the research field in the working tree; that change
is NOT committed (saved as scratchpad step2d-test-redirect.patch) so they keep testing the live world.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-09-30 09:06:25 +02:00
1 parent d6e021a80b
commit 78641da0bb
15 files changed
+3077 -6

No files matched your search

@@ -5,9 +5,12 @@
#include "VoxelCaveMorphology.h"
#include "VoxelDensityProfile.h"
#include "VoxelGenerator.h"
#include "VoxelRedefinedDensityField.h"
#include "VoxelStackSampler.h"
#include "HAL/PlatformTLS.h"
#include <atomic>
#if PLATFORM_WINDOWS
#include "Windows/AllowWindowsPlatformTypes.h"
THIRD_PARTY_INCLUDES_START
@@ -92,6 +95,14 @@ namespace VoxelForgeRegionEval
int32 LatticeZ = 0;
TArray<FRegionChunkSample> RequestedSamples;
};
struct FResearchBrick
{
FIntVector Origin = FIntVector::ZeroValue;
FIntVector Size = FIntVector::ZeroValue;
FIntVector Lattice = FIntVector::ZeroValue;
int32 SampleCount = 0;
};
}
int64 LinearIndex(int32 X, int32 Y, int32 Z, int32 Dimension)
@@ -138,6 +149,245 @@ namespace VoxelForgeRegionEval
return Hash;
}
bool EvaluateRedefined(
const UVoxelGenerator& Generator,
FIntVector OriginVoxels,
int32 Dimension,
int32 RequestedWorkerCount,
float DetailScale,
TArray<float>& OutDensities,
FTiming& OutTiming,
bool bEnableBrickSkips)
{
OutTiming = FTiming();
OutDensities.Reset();
if (Dimension <= 0 || Dimension > 1024 || !FMath::IsFinite(DetailScale))
{
return false;
}
const int32 NumWorkers = FMath::Clamp(RequestedWorkerCount, 1, 32);
const int64 TotalSamples = LatticeCount(Dimension);
if (TotalSamples > MAX_int32)
{
return false;
}
const double StartSeconds = FPlatformTime::Seconds();
OutTiming.Workers = NumWorkers;
OutTiming.SampleCount = TotalSamples;
OutDensities.SetNumUninitialized(static_cast<int32>(TotalSamples));
TArray<FResearchBrick> Bricks;
const FIntVector Last = OriginVoxels
+ FIntVector(Dimension - 1, Dimension - 1, Dimension - 1);
const FIntVector FirstBrick(
FMath::FloorToInt(static_cast<float>(OriginVoxels.X) / 8.0f),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Y) / 8.0f),
FMath::FloorToInt(static_cast<float>(OriginVoxels.Z) / 8.0f));
const FIntVector LastBrick(
FMath::FloorToInt(static_cast<float>(Last.X) / 8.0f),
FMath::FloorToInt(static_cast<float>(Last.Y) / 8.0f),
FMath::FloorToInt(static_cast<float>(Last.Z) / 8.0f));
Bricks.Reserve((LastBrick.X - FirstBrick.X + 1)
* (LastBrick.Y - FirstBrick.Y + 1) * (LastBrick.Z - FirstBrick.Z + 1));
for (int32 BZ = FirstBrick.Z; BZ <= LastBrick.Z; ++BZ)
for (int32 BY = FirstBrick.Y; BY <= LastBrick.Y; ++BY)
for (int32 BX = FirstBrick.X; BX <= LastBrick.X; ++BX)
{
const FIntVector BrickWorldMin(BX * 8, BY * 8, BZ * 8);
FResearchBrick& Brick = Bricks.AddDefaulted_GetRef();
Brick.Origin = FIntVector(
FMath::Max(OriginVoxels.X, BrickWorldMin.X),
FMath::Max(OriginVoxels.Y, BrickWorldMin.Y),
FMath::Max(OriginVoxels.Z, BrickWorldMin.Z));
const FIntVector BrickEnd = BrickWorldMin + FIntVector(8, 8, 8);
const FIntVector RingEnd = OriginVoxels
+ FIntVector(Dimension, Dimension, Dimension);
const FIntVector End(
FMath::Min(BrickEnd.X, RingEnd.X),
FMath::Min(BrickEnd.Y, RingEnd.Y),
FMath::Min(BrickEnd.Z, RingEnd.Z));
Brick.Size = End - Brick.Origin;
Brick.Lattice = Brick.Origin - OriginVoxels;
Brick.SampleCount = Brick.Size.X * Brick.Size.Y * Brick.Size.Z;
}
TArray<double> WorkerWall;
TArray<double> WorkerCpu;
TArray<double> WorkerClassify;
TArray<double> WorkerEvaluate;
TArray<double> WorkerFill;
TArray<uint32> WorkerThreadIds;
TArray<uint64> WorkerEvaluated;
TArray<uint64> WorkerSkippedBricks;
TArray<uint64> WorkerSkippedSamples;
TArray<uint64> WorkerMacroChecks;
TArray<uint64> WorkerMacroCentreEvaluations;
TArray<uint64> WorkerFallbackReasons;
WorkerWall.SetNumZeroed(NumWorkers);
WorkerCpu.SetNumZeroed(NumWorkers);
WorkerClassify.SetNumZeroed(NumWorkers);
WorkerEvaluate.SetNumZeroed(NumWorkers);
WorkerFill.SetNumZeroed(NumWorkers);
WorkerThreadIds.SetNumZeroed(NumWorkers);
WorkerEvaluated.SetNumZeroed(NumWorkers);
WorkerSkippedBricks.SetNumZeroed(NumWorkers);
WorkerSkippedSamples.SetNumZeroed(NumWorkers);
WorkerMacroChecks.SetNumZeroed(NumWorkers);
WorkerMacroCentreEvaluations.SetNumZeroed(NumWorkers);
WorkerFallbackReasons.SetNumZeroed(NumWorkers * 5);
// Surface bricks carry more work than uniform interior bricks. Equal brick counts do
// not balance that work, so claim small contiguous batches dynamically. Each brick
// writes a disjoint output range and still evaluates the same exact field.
std::atomic<int32> NextBrickIndex{0};
constexpr int32 BrickBatchSize = 16;
ParallelFor(NumWorkers, [&](int32 WorkerIndex)
{
WorkerThreadIds[WorkerIndex] = FPlatformTLS::GetCurrentThreadId();
const double WorkerStart = FPlatformTime::Seconds();
double CpuStart = 0.0;
const bool bHaveCpuStart = GetCurrentThreadCpuSeconds(CpuStart);
TArray<float> BrickDetail;
VoxelForgeRedefinedDensityField::FBrickEvaluationContext BrickContext;
for (;;)
{
const int32 BatchBegin = NextBrickIndex.fetch_add(
BrickBatchSize, std::memory_order_relaxed);
if (BatchBegin >= Bricks.Num())
{
break;
}
const int32 BatchEnd = FMath::Min(BatchBegin + BrickBatchSize, Bricks.Num());
for (int32 BrickIndex = BatchBegin; BrickIndex < BatchEnd; ++BrickIndex)
{
const FResearchBrick& Brick = Bricks[BrickIndex];
bool bUniformAir = false;
bool bEvaluatedMacroCentre = false;
VoxelForgeRedefinedDensityField::EBrickFallbackReason FallbackReason =
VoxelForgeRedefinedDensityField::EBrickFallbackReason::UnsupportedState;
++WorkerMacroChecks[WorkerIndex];
const double ClassifyStart = FPlatformTime::Seconds();
const bool bCanSkip = bEnableBrickSkips
&& VoxelForgeRedefinedDensityField::TryClassifyBrick(
Generator, Brick.Origin, Brick.Size, bUniformAir, DetailScale,
&bEvaluatedMacroCentre, &FallbackReason);
WorkerClassify[WorkerIndex] += FPlatformTime::Seconds() - ClassifyStart;
WorkerMacroCentreEvaluations[WorkerIndex] += bEvaluatedMacroCentre ? 1u : 0u;
if (bEnableBrickSkips && !bCanSkip)
{
const int32 ReasonIndex = static_cast<uint8>(FallbackReason);
if (ReasonIndex < 5)
{
++WorkerFallbackReasons[WorkerIndex * 5 + ReasonIndex];
}
}
if (bCanSkip)
{
++WorkerSkippedBricks[WorkerIndex];
WorkerSkippedSamples[WorkerIndex] += static_cast<uint64>(Brick.SampleCount);
const float UniformDensity = bUniformAir ? 64.0f : -64.0f;
const double FillStart = FPlatformTime::Seconds();
for (int32 Z = 0; Z < Brick.Size.Z; ++Z)
for (int32 Y = 0; Y < Brick.Size.Y; ++Y)
for (int32 X = 0; X < Brick.Size.X; ++X)
{
const int64 LatticeIndex = LinearIndex(
Brick.Lattice.X + X, Brick.Lattice.Y + Y,
Brick.Lattice.Z + Z, Dimension);
OutDensities[static_cast<int32>(LatticeIndex)] = UniformDensity;
}
WorkerFill[WorkerIndex] += FPlatformTime::Seconds() - FillStart;
continue;
}
const double EvaluationStart = FPlatformTime::Seconds();
VoxelForgeRedefinedDensityField::BuildDetailBrick(
Generator, Brick.Origin, Brick.Size, DetailScale, BrickDetail,
&BrickContext);
for (int32 Z = 0; Z < Brick.Size.Z; ++Z)
for (int32 Y = 0; Y < Brick.Size.Y; ++Y)
for (int32 X = 0; X < Brick.Size.X; ++X)
{
const int32 WorldX = Brick.Origin.X + X;
const int32 WorldY = Brick.Origin.Y + Y;
const int32 WorldZ = Brick.Origin.Z + Z;
const int64 LatticeIndex = LinearIndex(
Brick.Lattice.X + X, Brick.Lattice.Y + Y,
Brick.Lattice.Z + Z, Dimension);
OutDensities[static_cast<int32>(LatticeIndex)] =
VoxelForgeRedefinedDensityField::EvaluateWithDetailValue(
Generator, static_cast<float>(WorldX), static_cast<float>(WorldY),
static_cast<float>(WorldZ),
BrickDetail[(Z * Brick.Size.Y + Y) * Brick.Size.X + X],
nullptr, DetailScale, &BrickContext);
++WorkerEvaluated[WorkerIndex];
}
WorkerEvaluate[WorkerIndex] += FPlatformTime::Seconds() - EvaluationStart;
}
}
WorkerWall[WorkerIndex] = FPlatformTime::Seconds() - WorkerStart;
double CpuEnd = 0.0;
if (bHaveCpuStart && GetCurrentThreadCpuSeconds(CpuEnd))
{
WorkerCpu[WorkerIndex] = FMath::Max(0.0, CpuEnd - CpuStart);
}
else
{
WorkerCpu[WorkerIndex] = WorkerWall[WorkerIndex];
}
}, EParallelForFlags::None);
OutTiming.WallSeconds = FPlatformTime::Seconds() - StartSeconds;
OutTiming.bRegionProgram = true;
OutTiming.BrickCount = static_cast<uint64>(Bricks.Num());
OutTiming.ObservedWorkerThreads = 0;
for (int32 Worker = 0; Worker < NumWorkers; ++Worker)
{
OutTiming.WorkerCpuSeconds += WorkerCpu[Worker];
OutTiming.BrickClassificationSeconds += WorkerClassify[Worker];
OutTiming.DensityEvaluationSeconds += WorkerEvaluate[Worker];
OutTiming.UniformFillSeconds += WorkerFill[Worker];
OutTiming.LongestWorkerWallSeconds = FMath::Max(
OutTiming.LongestWorkerWallSeconds, WorkerWall[Worker]);
OutTiming.EvaluatedSamples += static_cast<int64>(WorkerEvaluated[Worker]);
OutTiming.SkippedBricks += WorkerSkippedBricks[Worker];
OutTiming.SkippedSamples += WorkerSkippedSamples[Worker];
OutTiming.MacroBrickChecks += WorkerMacroChecks[Worker];
OutTiming.MacroCentreEvaluations += WorkerMacroCentreEvaluations[Worker];
OutTiming.UnsupportedBrickProofs += WorkerFallbackReasons[Worker * 5
+ static_cast<uint8>(VoxelForgeRedefinedDensityField::EBrickFallbackReason::UnsupportedState)];
OutTiming.ChunkBoundaryBrickProofs += WorkerFallbackReasons[Worker * 5
+ static_cast<uint8>(VoxelForgeRedefinedDensityField::EBrickFallbackReason::ChunkOrStrateBoundary)];
OutTiming.PassageBoundBrickProofs += WorkerFallbackReasons[Worker * 5
+ static_cast<uint8>(VoxelForgeRedefinedDensityField::EBrickFallbackReason::PassageBounds)];
OutTiming.MixedMacroBrickProofs += WorkerFallbackReasons[Worker * 5
+ static_cast<uint8>(VoxelForgeRedefinedDensityField::EBrickFallbackReason::MixedMacroInterval)];
bool bPreviouslyObserved = false;
for (int32 Previous = 0; Previous < Worker; ++Previous)
{
bPreviouslyObserved |= WorkerThreadIds[Previous] == WorkerThreadIds[Worker];
}
OutTiming.ObservedWorkerThreads += bPreviouslyObserved ? 0 : 1;
}
OutTiming.NanosecondsPerSample = TotalSamples > 0
? OutTiming.WorkerCpuSeconds * 1.0e9 / static_cast<double>(TotalSamples)
: 0.0;
OutTiming.CandidateEvaluationsPerSample = TotalSamples > 0
? static_cast<double>(OutTiming.EvaluatedSamples
+ static_cast<int64>(OutTiming.MacroCentreEvaluations))
/ static_cast<double>(TotalSamples)
: 0.0;
for (const float Density : OutDensities)
{
OutTiming.NonFiniteSamples += FMath::IsFinite(Density) ? 0u : 1u;
}
OutTiming.QuantizedHash = HashQuantized(OutDensities);
OutTiming.RawFloatHash = HashRawFloats(OutDensities);
return OutTiming.EvaluatedSamples + static_cast<int64>(OutTiming.SkippedSamples)
== TotalSamples && OutTiming.NonFiniteSamples == 0;
}
bool Evaluate(
const UVoxelGenerator& Generator,
FIntVector OriginVoxels,