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:
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,
|
||||
|
||||
Reference in new issue
Block a user