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
@@ -0,0 +1,146 @@
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "VoxelForgeTestFixture.h"
|
||||
#include "VoxelRedefinedDensityField.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeRedefinedDensityFieldTraversalSeedsTest,
|
||||
"VoxelForge.Research.RedefinedFieldTraversalSeeds",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FVoxelForgeRedefinedDensityFieldTraversalSeedsTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
(void)Parameters;
|
||||
struct FRestoreResearchField
|
||||
{
|
||||
bool bWasEnabled = VoxelForgeRedefinedDensityField::IsEnabled();
|
||||
float PreviousScale = VoxelForgeRedefinedDensityField::GetDetailScale();
|
||||
~FRestoreResearchField()
|
||||
{
|
||||
VoxelForgeRedefinedDensityField::SetDetailScale(PreviousScale);
|
||||
VoxelForgeRedefinedDensityField::SetEnabled(bWasEnabled);
|
||||
}
|
||||
} Restore;
|
||||
VoxelForgeRedefinedDensityField::SetEnabled(true);
|
||||
VoxelForgeRedefinedDensityField::SetDetailScale(1.0f);
|
||||
|
||||
constexpr int32 Seeds[] = {1337, 12345, 20260930};
|
||||
int32 CheckedLandings = 0;
|
||||
int32 CheckedRoutes = 0;
|
||||
int32 RoutesWithResolvedEndpoints = 0;
|
||||
for (const int32 Seed : Seeds)
|
||||
{
|
||||
VoxelForgeTest::FTestWorld World;
|
||||
World.Build(Seed, /*InGapChunks=*/2, /*InStrateHeightInChunks=*/4);
|
||||
if (!World.IsValid())
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("Seed %d: test world did not build: %s"), Seed, *World.WhyInvalid()));
|
||||
return false;
|
||||
}
|
||||
|
||||
const TArray<FStrateSlot>& Layout = World.StrateManager->GetLayout();
|
||||
const TArray<FVoxelPassage>& Passages = World.StrateManager->GetPassages();
|
||||
for (const FVoxelPassage& Passage : Passages)
|
||||
{
|
||||
if (Passage.UpperStrateIndex == Passage.LowerStrateIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FVector UpperAir = Passage.UpperLanding.StandingPoint;
|
||||
const FVector LowerAir = Passage.LowerLanding.StandingPoint;
|
||||
const float UpperDensity = World.Generator->GetDensityAt(
|
||||
UpperAir.X, UpperAir.Y, UpperAir.Z);
|
||||
const float UpperFooting = World.Generator->GetDensityAt(
|
||||
UpperAir.X, UpperAir.Y, Passage.UpperLanding.FloorZ - 0.75f);
|
||||
const float LowerDensity = World.Generator->GetDensityAt(
|
||||
LowerAir.X, LowerAir.Y, LowerAir.Z);
|
||||
const float LowerFooting = World.Generator->GetDensityAt(
|
||||
LowerAir.X, LowerAir.Y, Passage.LowerLanding.FloorZ - 0.75f);
|
||||
++CheckedLandings;
|
||||
if (!FMath::IsFinite(UpperDensity) || !FMath::IsFinite(UpperFooting)
|
||||
|| !FMath::IsFinite(LowerDensity) || !FMath::IsFinite(LowerFooting)
|
||||
|| UpperDensity <= 0.0f || LowerDensity <= 0.0f
|
||||
|| UpperFooting >= 0.0f || LowerFooting >= 0.0f)
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("Seed %d: passage %d landing failed the final-field air/floor bracket "
|
||||
"(upper %.4f/%.4f, lower %.4f/%.4f)."),
|
||||
Seed, CheckedLandings - 1, UpperDensity, UpperFooting,
|
||||
LowerDensity, LowerFooting));
|
||||
}
|
||||
}
|
||||
|
||||
for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex)
|
||||
{
|
||||
FVector Arrival = FVector::ZeroVector;
|
||||
FVector Departure = FVector::ZeroVector;
|
||||
bool bHasArrival = false;
|
||||
bool bHasDeparture = false;
|
||||
for (const FVoxelPassage& Passage : Passages)
|
||||
{
|
||||
if (Passage.UpperStrateIndex == Passage.LowerStrateIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (Passage.LowerStrateIndex == StrateIndex && !bHasArrival)
|
||||
{
|
||||
Arrival = Passage.LowerLanding.StandingPoint;
|
||||
bHasArrival = true;
|
||||
}
|
||||
if (Passage.UpperStrateIndex == StrateIndex && !bHasDeparture)
|
||||
{
|
||||
Departure = Passage.UpperLanding.StandingPoint;
|
||||
bHasDeparture = true;
|
||||
}
|
||||
}
|
||||
if (!bHasArrival || !bHasDeparture)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FVoxelStrateMeasureSettings Settings;
|
||||
Settings.SampleStep = 4;
|
||||
Settings.RadiusInVoxels = 192;
|
||||
Settings.CenterXY = FVector2D(
|
||||
(Arrival.X + Departure.X) * 0.5f,
|
||||
(Arrival.Y + Departure.Y) * 0.5f);
|
||||
Settings.MaxCells = 4000000;
|
||||
Settings.HeadroomCells = 2;
|
||||
const FVoxelConnectivityDiagnostics Route = VF_DiagnoseConnectivity(
|
||||
*World.Generator, *World.StrateManager, StrateIndex,
|
||||
Arrival, Departure, Settings);
|
||||
++CheckedRoutes;
|
||||
RoutesWithResolvedEndpoints += Route.bStartSnapped || Route.bGoalSnapped;
|
||||
if (!Route.bValid || Route.Result != EVoxelConnectivityResult::Connected)
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("Seed %d strate %d (%s): arrival-to-exit walking route was %s "
|
||||
"(result=%d, valid=%s, start resolved=%s, goal resolved=%s)."),
|
||||
Seed, StrateIndex,
|
||||
Layout[StrateIndex].Definition
|
||||
? *Layout[StrateIndex].Definition->GetName() : TEXT("no definition"),
|
||||
Route.Result == EVoxelConnectivityResult::Connected
|
||||
? TEXT("connected") : TEXT("not connected"),
|
||||
static_cast<int32>(Route.Result),
|
||||
Route.bValid ? TEXT("yes") : TEXT("no"),
|
||||
Route.bStartSnapped ? TEXT("yes") : TEXT("no"),
|
||||
Route.bGoalSnapped ? TEXT("yes") : TEXT("no")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestTrue(TEXT("Several seeds exercised passage landings"), CheckedLandings >= 6);
|
||||
TestTrue(TEXT("Several seeds exercised arrival-to-exit walking"), CheckedRoutes >= 6);
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Redefined field functional sweep: %d authored passage landings bracket open air "
|
||||
"over %d seeds; %d arrival-to-exit walking routes checked; %d used a resolved "
|
||||
"coarse endpoint after the full-density segment check."),
|
||||
CheckedLandings, UE_ARRAY_COUNT(Seeds), CheckedRoutes, RoutesWithResolvedEndpoints));
|
||||
return CheckedLandings >= 6 && CheckedRoutes >= 6;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "VoxelDensityAblation.h" // fingerprinted development-only stage measurements
|
||||
#include "VoxelDensityPrimitives.h" // spine / seals / passage — shared with the operator stack
|
||||
#include "VoxelDensityOpStack.h" // the per-strate operator stack
|
||||
#include "VoxelRedefinedDensityField.h" // default-off editor research evaluator
|
||||
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
|
||||
#include "VoxelStats.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
@@ -2815,6 +2816,19 @@ bool UVoxelGenerator::EvaluateDensityChunk(
|
||||
float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
|
||||
float* OutPreEditDensity) const
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
// Step 2d is an editor-only, default-off side path. It does not alter the live evaluator's
|
||||
// state or caches; the field is selected only for an explicit research/test run.
|
||||
if (VoxelForgeRedefinedDensityField::IsEnabled())
|
||||
{
|
||||
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
|
||||
const float ResearchDensity = VoxelForgeRedefinedDensityField::Evaluate(
|
||||
*this, WorldX, WorldY, WorldZ, OutPreEditDensity,
|
||||
VoxelForgeRedefinedDensityField::GetDetailScale());
|
||||
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
|
||||
return ResearchDensity;
|
||||
}
|
||||
#endif
|
||||
// Soundness mode: for a sample in a block whose core or passage-carving reach bit is clear,
|
||||
// evaluate the complete canonical field once with the bypass bit set. The normal call then
|
||||
// takes the fast path below and compares the final MC field after the boundary seal and diff
|
||||
@@ -6298,6 +6312,16 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
VoxelDensityProfile::AddCounter(
|
||||
VoxelDensityProfile::ECounter::TileClassifyCalls);
|
||||
|
||||
#if WITH_EDITOR
|
||||
// The ordinary tile classifier proves the live field's interval bounds. While the separate
|
||||
// research field is selected, decline those proofs until its own complete final-field interval
|
||||
// contract is wired into streaming; a conservative Mixed verdict keeps the test mesh complete.
|
||||
if (VoxelForgeRedefinedDensityField::IsEnabled())
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
#endif
|
||||
|
||||
const EVoxelTileClass Verdict = ClassifyTileUncached(
|
||||
OriginVoxels, NormalizedStep, NormalizedCellsPerAxis, OutStats);
|
||||
if (Verdict == EVoxelTileClass::AllSolid)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
class UVoxelGenerator;
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Step 2d editor research field. It is intentionally separate from the gameplay evaluator and
|
||||
* is selected only by the default-off voxel.ResearchRedefinedDensityField console variable.
|
||||
*
|
||||
* The field is a connected, archetype-sized cave lattice with an authored walkable floor at each
|
||||
* passage landing. Its macro shape is evaluated directly in world space; bounded, continuous
|
||||
* hash-value noise is added at every full-resolution sample. The brick classifier is permitted to
|
||||
* skip only when a Lipschitz bound on the macro field plus the exact maximum detail amplitude
|
||||
* proves that the final sign is uniform throughout the brick and its marching-cubes halo.
|
||||
*/
|
||||
namespace VoxelForgeRedefinedDensityField
|
||||
{
|
||||
/** Per-worker, per-brick state. The opaque plan pointer is valid until the layout changes. */
|
||||
struct FBrickEvaluationContext
|
||||
{
|
||||
const void* PlanToken = nullptr;
|
||||
bool bPlanResolved = false;
|
||||
bool bMayContainPassage = true;
|
||||
bool bChunkResolved = false;
|
||||
bool bIsGapChunk = false;
|
||||
bool bHasDiffMods = false;
|
||||
bool bMacroXYPrecomputed = false;
|
||||
FIntVector BrickOrigin = FIntVector::ZeroValue;
|
||||
FIntVector BrickSize = FIntVector::ZeroValue;
|
||||
FIntVector Chunk = FIntVector::ZeroValue;
|
||||
TArray<float> FloorByXY;
|
||||
TArray<float> GridAirByXY;
|
||||
TArray<float> DetailFadeByXY;
|
||||
TArray<float> LandingHorizontalByXY;
|
||||
TArray<float> LandingApproachByXY;
|
||||
TArray<float> LandingBridgeHorizontalByXY;
|
||||
TArray<float> LandingBridgeFloorByXY;
|
||||
TArray<int32> PassageCandidates;
|
||||
};
|
||||
|
||||
enum class EBrickFallbackReason : uint8
|
||||
{
|
||||
None,
|
||||
UnsupportedState,
|
||||
ChunkOrStrateBoundary,
|
||||
PassageBounds,
|
||||
MixedMacroInterval
|
||||
};
|
||||
|
||||
struct FBrickCounters
|
||||
{
|
||||
uint64 Tested = 0;
|
||||
uint64 Skipped = 0;
|
||||
uint64 EvaluatedSamples = 0;
|
||||
};
|
||||
|
||||
VOXELFORGE_API bool IsEnabled();
|
||||
VOXELFORGE_API void SetEnabled(bool bEnabled);
|
||||
VOXELFORGE_API float GetDetailScale();
|
||||
VOXELFORGE_API void SetDetailScale(float Scale);
|
||||
|
||||
/** Evaluate the new field, including existing passage carving and player diffs. */
|
||||
VOXELFORGE_API float Evaluate(const UVoxelGenerator& Generator, float WorldX, float WorldY,
|
||||
float WorldZ, float* OutPreEditDensity = nullptr,
|
||||
float DetailScale = 1.0f);
|
||||
|
||||
/** Build mathematically identical multi-scale noise for one integer-lattice brick. */
|
||||
VOXELFORGE_API void BuildDetailBrick(const UVoxelGenerator& Generator,
|
||||
FIntVector OriginVoxels, FIntVector Size, float DetailScale,
|
||||
TArray<float>& OutDetail, FBrickEvaluationContext* OutContext = nullptr);
|
||||
|
||||
/** Evaluate with a detail value produced by BuildDetailBrick. */
|
||||
VOXELFORGE_API float EvaluateWithDetailValue(const UVoxelGenerator& Generator,
|
||||
float WorldX, float WorldY, float WorldZ, float DetailValue,
|
||||
float* OutPreEditDensity = nullptr, float DetailScale = 1.0f,
|
||||
const FBrickEvaluationContext* Context = nullptr);
|
||||
|
||||
/** Prove a world-aligned box has one final density sign; false means evaluate every point. */
|
||||
VOXELFORGE_API bool TryClassifyBrick(
|
||||
const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size,
|
||||
bool& bOutAir, float DetailScale = 1.0f,
|
||||
bool* bOutEvaluatedMacroCentre = nullptr,
|
||||
EBrickFallbackReason* OutFallbackReason = nullptr);
|
||||
|
||||
/** Reset this thread's immutable plan cache; used by deterministic editor experiments. */
|
||||
VOXELFORGE_API void ResetThreadCache();
|
||||
}
|
||||
#endif
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "VoxelGenerator.h"
|
||||
#include "VoxelGeometryHash.h"
|
||||
#include "VoxelMarchingCubesMesher.h"
|
||||
#include "VoxelRedefinedDensityField.h"
|
||||
#include "VoxelNoise.h"
|
||||
#include "VoxelPassageGeometry.h"
|
||||
#include "VoxelSettings.h"
|
||||
@@ -1509,6 +1510,9 @@ struct FRegionEvalStackSamplerSession
|
||||
}
|
||||
};
|
||||
|
||||
bool SavePng(const FString& FilePath, const TArray<FColor>& Pixels,
|
||||
int32 Width, int32 Height, FString& OutError);
|
||||
|
||||
int32 FindRoomGraphRecipeIndex(int32 Seed)
|
||||
{
|
||||
for (int32 Index = 0; Index < 64; ++Index)
|
||||
@@ -2511,6 +2515,829 @@ int32 RunRegionEval(const FString& Params)
|
||||
return bPassed ? 0 : 2;
|
||||
}
|
||||
|
||||
struct FStep2dSurfaceSpectrum
|
||||
{
|
||||
uint64 SurfaceBandSamples = 0;
|
||||
double RMSLag1 = 0.0;
|
||||
double RMSLag2 = 0.0;
|
||||
double RMSLag4 = 0.0;
|
||||
};
|
||||
|
||||
struct FStep2dBoundaryStats
|
||||
{
|
||||
uint64 BrickBoundarySamples = 0;
|
||||
uint64 InteriorSamples = 0;
|
||||
uint64 ChunkBoundarySamples = 0;
|
||||
uint64 FeatureCellBoundarySamples = 0;
|
||||
uint64 FeatureCellInteriorSamples = 0;
|
||||
double BrickJumpRms = 0.0;
|
||||
double InteriorJumpRms = 0.0;
|
||||
double ChunkBoundaryJumpRms = 0.0;
|
||||
double FeatureCellJumpRms = 0.0;
|
||||
double FeatureCellInteriorJumpRms = 0.0;
|
||||
double BrickJumpMax = 0.0;
|
||||
double ChunkBoundaryJumpMax = 0.0;
|
||||
double FeatureCellJumpMax = 0.0;
|
||||
};
|
||||
|
||||
struct FStep2dBrickProofAudit
|
||||
{
|
||||
uint64 CorrectlySkippedBricksAudited = 0;
|
||||
uint64 CorrectProofSamplesChecked = 0;
|
||||
uint64 CorrectProofViolations = 0;
|
||||
uint64 DeliberatelyUndersizedBoundClaimsChecked = 0;
|
||||
uint64 DeliberateBoundDisagreements = 0;
|
||||
};
|
||||
|
||||
FStep2dBrickProofAudit AuditStep2dBrickProofs(
|
||||
const UVoxelGenerator& Generator,
|
||||
FIntVector Origin,
|
||||
int32 Dimension)
|
||||
{
|
||||
FStep2dBrickProofAudit Result;
|
||||
const FIntVector Last = Origin + FIntVector(Dimension - 1, Dimension - 1, Dimension - 1);
|
||||
const FIntVector FirstBrick(
|
||||
FMath::FloorToInt(static_cast<float>(Origin.X) / 8.0f),
|
||||
FMath::FloorToInt(static_cast<float>(Origin.Y) / 8.0f),
|
||||
FMath::FloorToInt(static_cast<float>(Origin.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));
|
||||
uint64 CorrectSkipOrdinal = 0;
|
||||
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);
|
||||
const FIntVector BrickOrigin(
|
||||
FMath::Max(Origin.X, BrickWorldMin.X),
|
||||
FMath::Max(Origin.Y, BrickWorldMin.Y),
|
||||
FMath::Max(Origin.Z, BrickWorldMin.Z));
|
||||
const FIntVector End(
|
||||
FMath::Min(BrickWorldMin.X + 8, Origin.X + Dimension),
|
||||
FMath::Min(BrickWorldMin.Y + 8, Origin.Y + Dimension),
|
||||
FMath::Min(BrickWorldMin.Z + 8, Origin.Z + Dimension));
|
||||
const FIntVector Size = End - BrickOrigin;
|
||||
bool bProvenAir = false;
|
||||
if (VoxelForgeRedefinedDensityField::TryClassifyBrick(
|
||||
Generator, BrickOrigin, Size, bProvenAir, 1.0f))
|
||||
{
|
||||
const bool bAuditThisSkip = (CorrectSkipOrdinal++ % 128u) == 0u;
|
||||
if (bAuditThisSkip)
|
||||
{
|
||||
++Result.CorrectlySkippedBricksAudited;
|
||||
for (int32 Z = -1; Z <= Size.Z; ++Z)
|
||||
for (int32 Y = -1; Y <= Size.Y; ++Y)
|
||||
for (int32 X = -1; X <= Size.X; ++X)
|
||||
{
|
||||
const float Density = VoxelForgeRedefinedDensityField::Evaluate(
|
||||
Generator,
|
||||
static_cast<float>(BrickOrigin.X + X),
|
||||
static_cast<float>(BrickOrigin.Y + Y),
|
||||
static_cast<float>(BrickOrigin.Z + Z), nullptr, 1.0f);
|
||||
++Result.CorrectProofSamplesChecked;
|
||||
if ((Density > 0.0f) != bProvenAir)
|
||||
{
|
||||
++Result.CorrectProofViolations;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Negative control: classify while omitting all detail, then check the supported
|
||||
// maximum detail amplitude. A valid supremum must reject at least some of these
|
||||
// sign claims; otherwise this audit would not demonstrate that it can catch a bad bound.
|
||||
bool bDeliberatelyClaimedAir = false;
|
||||
if (VoxelForgeRedefinedDensityField::TryClassifyBrick(
|
||||
Generator, BrickOrigin, Size, bDeliberatelyClaimedAir, 0.0f))
|
||||
{
|
||||
++Result.DeliberatelyUndersizedBoundClaimsChecked;
|
||||
for (int32 Z = 0; Z < Size.Z; ++Z)
|
||||
for (int32 Y = 0; Y < Size.Y; ++Y)
|
||||
for (int32 X = 0; X < Size.X; ++X)
|
||||
{
|
||||
const float MaximumDetailDensity = VoxelForgeRedefinedDensityField::Evaluate(
|
||||
Generator,
|
||||
static_cast<float>(BrickOrigin.X + X),
|
||||
static_cast<float>(BrickOrigin.Y + Y),
|
||||
static_cast<float>(BrickOrigin.Z + Z),
|
||||
nullptr,
|
||||
4.0f);
|
||||
if ((MaximumDetailDensity > 0.0f) != bDeliberatelyClaimedAir)
|
||||
{
|
||||
++Result.DeliberateBoundDisagreements;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
FStep2dSurfaceSpectrum ComputeStep2dSpectrum(const TArray<float>& Density, int32 Dimension)
|
||||
{
|
||||
FStep2dSurfaceSpectrum Result;
|
||||
double SumSquares[3] = {0.0, 0.0, 0.0};
|
||||
uint64 Counts[3] = {0, 0, 0};
|
||||
const int32 Lags[] = {1, 2, 4};
|
||||
for (int32 Z = 4; Z < Dimension - 4; ++Z)
|
||||
for (int32 Y = 4; Y < Dimension - 4; ++Y)
|
||||
for (int32 X = 4; X < Dimension - 4; ++X)
|
||||
{
|
||||
const int32 CenterIndex = (Z * Dimension + Y) * Dimension + X;
|
||||
const float Center = Density[CenterIndex];
|
||||
if (!FMath::IsFinite(Center) || FMath::Abs(Center) > 12.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
++Result.SurfaceBandSamples;
|
||||
for (int32 LagIndex = 0; LagIndex < UE_ARRAY_COUNT(Lags); ++LagIndex)
|
||||
{
|
||||
const int32 Lag = Lags[LagIndex];
|
||||
const float Laplacian =
|
||||
Density[CenterIndex - Lag] + Density[CenterIndex + Lag]
|
||||
+ Density[CenterIndex - Lag * Dimension]
|
||||
+ Density[CenterIndex + Lag * Dimension]
|
||||
+ Density[CenterIndex - Lag * Dimension * Dimension]
|
||||
+ Density[CenterIndex + Lag * Dimension * Dimension]
|
||||
- 6.0f * Center;
|
||||
SumSquares[LagIndex] += static_cast<double>(Laplacian) * Laplacian;
|
||||
++Counts[LagIndex];
|
||||
}
|
||||
}
|
||||
Result.RMSLag1 = Counts[0] > 0 ? FMath::Sqrt(SumSquares[0] / Counts[0]) : 0.0;
|
||||
Result.RMSLag2 = Counts[1] > 0 ? FMath::Sqrt(SumSquares[1] / Counts[1]) : 0.0;
|
||||
Result.RMSLag4 = Counts[2] > 0 ? FMath::Sqrt(SumSquares[2] / Counts[2]) : 0.0;
|
||||
return Result;
|
||||
}
|
||||
|
||||
FIntVector SelectStep2dMeshOrigin(
|
||||
const TArray<float>& OldDensity,
|
||||
const TArray<float>& NewDensity,
|
||||
FIntVector RingOrigin,
|
||||
int32 Dimension,
|
||||
bool& bOutContainsBothIsoBands)
|
||||
{
|
||||
bOutContainsBothIsoBands = false;
|
||||
float BestCombinedBand = FLT_MAX;
|
||||
FIntVector BestSample(Dimension / 2, Dimension / 2, Dimension / 2);
|
||||
for (int32 Z = 32; Z < Dimension - 32; ++Z)
|
||||
for (int32 Y = 32; Y < Dimension - 32; ++Y)
|
||||
for (int32 X = 32; X < Dimension - 32; ++X)
|
||||
{
|
||||
const int32 Index = (Z * Dimension + Y) * Dimension + X;
|
||||
const float OldAbs = FMath::Abs(OldDensity[Index]);
|
||||
const float NewAbs = FMath::Abs(NewDensity[Index]);
|
||||
if (OldAbs <= 8.0f && NewAbs <= 8.0f)
|
||||
{
|
||||
const float Combined = OldAbs + NewAbs;
|
||||
if (Combined < BestCombinedBand)
|
||||
{
|
||||
BestCombinedBand = Combined;
|
||||
BestSample = FIntVector(X, Y, Z);
|
||||
bOutContainsBothIsoBands = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!bOutContainsBothIsoBands)
|
||||
{
|
||||
float BestNewBand = FLT_MAX;
|
||||
for (int32 Z = 32; Z < Dimension - 32; ++Z)
|
||||
for (int32 Y = 32; Y < Dimension - 32; ++Y)
|
||||
for (int32 X = 32; X < Dimension - 32; ++X)
|
||||
{
|
||||
const int32 Index = (Z * Dimension + Y) * Dimension + X;
|
||||
const float NewAbs = FMath::Abs(NewDensity[Index]);
|
||||
if (NewAbs < BestNewBand)
|
||||
{
|
||||
BestNewBand = NewAbs;
|
||||
BestSample = FIntVector(X, Y, Z);
|
||||
}
|
||||
}
|
||||
}
|
||||
return RingOrigin + BestSample - FIntVector(32, 32, 32);
|
||||
}
|
||||
|
||||
FStep2dBoundaryStats ComputeStep2dBoundaryStats(
|
||||
const TArray<float>& Density, int32 Dimension, FIntVector Origin, int32 FeatureCellSpacing)
|
||||
{
|
||||
FStep2dBoundaryStats Result;
|
||||
double BrickSquares = 0.0;
|
||||
double InteriorSquares = 0.0;
|
||||
double ChunkSquares = 0.0;
|
||||
double FeatureSquares = 0.0;
|
||||
double FeatureInteriorSquares = 0.0;
|
||||
const int32 CellSpacing = FMath::Max(FeatureCellSpacing, 1);
|
||||
for (int32 Z = 1; Z < Dimension - 1; ++Z)
|
||||
for (int32 Y = 1; Y < Dimension - 1; ++Y)
|
||||
for (int32 X = 1; X < Dimension - 1; ++X)
|
||||
{
|
||||
const int32 CenterIndex = (Z * Dimension + Y) * Dimension + X;
|
||||
const int32 Stride[3] = {1, Dimension, Dimension * Dimension};
|
||||
const int32 World[3] = {Origin.X + X, Origin.Y + Y, Origin.Z + Z};
|
||||
for (int32 Axis = 0; Axis < 3; ++Axis)
|
||||
{
|
||||
const double LeftGradient = Density[CenterIndex] - Density[CenterIndex - Stride[Axis]];
|
||||
const double RightGradient = Density[CenterIndex + Stride[Axis]] - Density[CenterIndex];
|
||||
const double Jump = FMath::Abs(RightGradient - LeftGradient);
|
||||
const bool bBrickBoundary = (World[Axis] % 8) == 0;
|
||||
const bool bChunkBoundary = (World[Axis] % CHUNK_SIZE) == 0 && Axis < 2;
|
||||
const bool bFeatureBoundary = ((World[Axis] + CellSpacing / 2) % CellSpacing) == 0;
|
||||
if (FMath::Abs(Density[CenterIndex]) <= 12.0f)
|
||||
{
|
||||
if (bBrickBoundary)
|
||||
{
|
||||
++Result.BrickBoundarySamples;
|
||||
BrickSquares += Jump * Jump;
|
||||
Result.BrickJumpMax = FMath::Max(Result.BrickJumpMax, Jump);
|
||||
}
|
||||
else
|
||||
{
|
||||
++Result.InteriorSamples;
|
||||
InteriorSquares += Jump * Jump;
|
||||
}
|
||||
if (bChunkBoundary)
|
||||
{
|
||||
++Result.ChunkBoundarySamples;
|
||||
ChunkSquares += Jump * Jump;
|
||||
Result.ChunkBoundaryJumpMax = FMath::Max(Result.ChunkBoundaryJumpMax, Jump);
|
||||
}
|
||||
}
|
||||
if (bFeatureBoundary)
|
||||
{
|
||||
++Result.FeatureCellBoundarySamples;
|
||||
FeatureSquares += Jump * Jump;
|
||||
}
|
||||
else
|
||||
{
|
||||
++Result.FeatureCellInteriorSamples;
|
||||
FeatureInteriorSquares += Jump * Jump;
|
||||
}
|
||||
if (bFeatureBoundary)
|
||||
Result.FeatureCellJumpMax = FMath::Max(Result.FeatureCellJumpMax, Jump);
|
||||
}
|
||||
}
|
||||
Result.BrickJumpRms = Result.BrickBoundarySamples > 0
|
||||
? FMath::Sqrt(BrickSquares / Result.BrickBoundarySamples) : 0.0;
|
||||
Result.InteriorJumpRms = Result.InteriorSamples > 0
|
||||
? FMath::Sqrt(InteriorSquares / Result.InteriorSamples) : 0.0;
|
||||
Result.ChunkBoundaryJumpRms = Result.ChunkBoundarySamples > 0
|
||||
? FMath::Sqrt(ChunkSquares / Result.ChunkBoundarySamples) : 0.0;
|
||||
Result.FeatureCellJumpRms = Result.FeatureCellBoundarySamples > 0
|
||||
? FMath::Sqrt(FeatureSquares / Result.FeatureCellBoundarySamples) : 0.0;
|
||||
Result.FeatureCellInteriorJumpRms = Result.FeatureCellInteriorSamples > 0
|
||||
? FMath::Sqrt(FeatureInteriorSquares / Result.FeatureCellInteriorSamples) : 0.0;
|
||||
return Result;
|
||||
}
|
||||
|
||||
FColor Step2dDensityColor(float Density)
|
||||
{
|
||||
if (FMath::Abs(Density) < 1.0f)
|
||||
{
|
||||
return FColor(56, 184, 206, 255);
|
||||
}
|
||||
return Density >= 0.0f
|
||||
? FColor(226, 220, 202, 255)
|
||||
: FColor(22, 30, 45, 255);
|
||||
}
|
||||
|
||||
void SaveStep2dSideBySideSlice(
|
||||
const FString& Path,
|
||||
const TArray<float>& OldDensity,
|
||||
const TArray<float>& NewDensity,
|
||||
int32 Dimension,
|
||||
TCHAR Plane,
|
||||
int32 FixedIndex,
|
||||
FString& OutError)
|
||||
{
|
||||
const int32 Width = Dimension * 2;
|
||||
const int32 Height = Dimension;
|
||||
TArray<FColor> Pixels;
|
||||
Pixels.SetNumUninitialized(Width * Height);
|
||||
for (int32 Row = 0; Row < Height; ++Row)
|
||||
for (int32 Column = 0; Column < Dimension; ++Column)
|
||||
{
|
||||
int32 OldIndex = 0;
|
||||
int32 NewIndex = 0;
|
||||
if (Plane == TEXT('h'))
|
||||
{
|
||||
const int32 X = Column;
|
||||
const int32 Y = Dimension - 1 - Row;
|
||||
const int32 Z = FixedIndex;
|
||||
OldIndex = (Z * Dimension + Y) * Dimension + X;
|
||||
}
|
||||
else if (Plane == TEXT('x'))
|
||||
{
|
||||
const int32 X = Column;
|
||||
const int32 Y = FixedIndex;
|
||||
const int32 Z = Dimension - 1 - Row;
|
||||
OldIndex = (Z * Dimension + Y) * Dimension + X;
|
||||
}
|
||||
else
|
||||
{
|
||||
const int32 X = FixedIndex;
|
||||
const int32 Y = Column;
|
||||
const int32 Z = Dimension - 1 - Row;
|
||||
OldIndex = (Z * Dimension + Y) * Dimension + X;
|
||||
}
|
||||
NewIndex = OldIndex;
|
||||
Pixels[Row * Width + Column] = Step2dDensityColor(OldDensity[OldIndex]);
|
||||
Pixels[Row * Width + Dimension + Column] = Step2dDensityColor(NewDensity[NewIndex]);
|
||||
}
|
||||
SavePng(Path, Pixels, Width, Height, OutError);
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Step2dTimingJson(const VoxelForgeRegionEval::FTiming& Timing)
|
||||
{
|
||||
TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>();
|
||||
Json->SetNumberField(TEXT("workers_requested"), Timing.Workers);
|
||||
Json->SetNumberField(TEXT("workers_observed"), Timing.ObservedWorkerThreads);
|
||||
Json->SetNumberField(TEXT("lattice_samples"), static_cast<double>(Timing.SampleCount));
|
||||
Json->SetNumberField(TEXT("density_samples_evaluated"), static_cast<double>(Timing.EvaluatedSamples));
|
||||
Json->SetNumberField(TEXT("samples_skipped_by_brick_proof"), static_cast<double>(Timing.SkippedSamples));
|
||||
Json->SetNumberField(TEXT("bricks_tested"), static_cast<double>(Timing.BrickCount));
|
||||
Json->SetNumberField(TEXT("bricks_skipped"), static_cast<double>(Timing.SkippedBricks));
|
||||
Json->SetNumberField(TEXT("share_of_bricks_skipped"), Timing.BrickCount > 0
|
||||
? static_cast<double>(Timing.SkippedBricks) / static_cast<double>(Timing.BrickCount) : 0.0);
|
||||
Json->SetNumberField(TEXT("share_of_lattice_samples_skipped"), Timing.SampleCount > 0
|
||||
? static_cast<double>(Timing.SkippedSamples) / static_cast<double>(Timing.SampleCount) : 0.0);
|
||||
Json->SetNumberField(TEXT("wall_ms"), Timing.WallSeconds * 1000.0);
|
||||
Json->SetNumberField(TEXT("worker_cpu_seconds"), Timing.WorkerCpuSeconds);
|
||||
Json->SetNumberField(TEXT("worker_cpu_ns_per_lattice_sample"), Timing.NanosecondsPerSample);
|
||||
Json->SetNumberField(TEXT("brick_classification_worker_seconds"), Timing.BrickClassificationSeconds);
|
||||
Json->SetNumberField(TEXT("density_evaluation_worker_seconds"), Timing.DensityEvaluationSeconds);
|
||||
Json->SetNumberField(TEXT("uniform_fill_worker_seconds"), Timing.UniformFillSeconds);
|
||||
Json->SetNumberField(TEXT("longest_worker_wall_ms"), Timing.LongestWorkerWallSeconds * 1000.0);
|
||||
Json->SetNumberField(TEXT("parallel_overhead_wall_ms"), FMath::Max(
|
||||
0.0, Timing.WallSeconds - Timing.LongestWorkerWallSeconds) * 1000.0);
|
||||
Json->SetNumberField(TEXT("brick_classification_calls"), static_cast<double>(Timing.MacroBrickChecks));
|
||||
Json->SetNumberField(TEXT("macro_center_evaluations"), static_cast<double>(Timing.MacroCentreEvaluations));
|
||||
Json->SetNumberField(TEXT("unsupported_brick_proofs"), static_cast<double>(Timing.UnsupportedBrickProofs));
|
||||
Json->SetNumberField(TEXT("chunk_or_strate_boundary_proofs"),
|
||||
static_cast<double>(Timing.ChunkBoundaryBrickProofs));
|
||||
Json->SetNumberField(TEXT("passage_bound_proofs"), static_cast<double>(Timing.PassageBoundBrickProofs));
|
||||
Json->SetNumberField(TEXT("mixed_macro_interval_proofs"), static_cast<double>(Timing.MixedMacroBrickProofs));
|
||||
Json->SetNumberField(TEXT("room_sdf_evaluations"), static_cast<double>(Timing.RoomEvaluations));
|
||||
Json->SetNumberField(TEXT("tunnel_sdf_evaluations"), static_cast<double>(Timing.TunnelEvaluations));
|
||||
Json->SetNumberField(TEXT("tunnel_core_evaluations"), static_cast<double>(Timing.TunnelCoreEvaluations));
|
||||
Json->SetNumberField(TEXT("passage_sdf_evaluations"), static_cast<double>(Timing.PassageEvaluations));
|
||||
Json->SetNumberField(TEXT("geometry_evaluations_per_lattice_sample"), Timing.CandidateEvaluationsPerSample);
|
||||
Json->SetNumberField(TEXT("quantized_hash"), static_cast<double>(Timing.QuantizedHash));
|
||||
Json->SetNumberField(TEXT("raw_float_hash"), static_cast<double>(Timing.RawFloatHash));
|
||||
return Json;
|
||||
}
|
||||
|
||||
int32 RunStep2d(const FString& Params)
|
||||
{
|
||||
FExploreArguments Arguments;
|
||||
FString Error;
|
||||
if (!ParseArguments(Params, Arguments, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] argument error: %s"), *Error);
|
||||
return 2;
|
||||
}
|
||||
int32 Seed = 1337;
|
||||
FParse::Value(*Params, TEXT("seed="), Seed);
|
||||
int32 Workers = VoxelForgeRegionEval::WorkerCount;
|
||||
FParse::Value(*Params, TEXT("regionworkers="), Workers);
|
||||
Workers = FMath::Clamp(Workers, 1, 32);
|
||||
FString ResearchOut;
|
||||
FParse::Value(*Params, TEXT("researchout="), ResearchOut);
|
||||
if (ResearchOut.IsEmpty())
|
||||
{
|
||||
ResearchOut = FPaths::Combine(Arguments.OutDirectory, TEXT("Research/step2d"));
|
||||
}
|
||||
ResearchOut = FPaths::ConvertRelativePathToFull(ResearchOut);
|
||||
IFileManager::Get().MakeDirectory(*ResearchOut, true);
|
||||
IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true);
|
||||
|
||||
FExploreWorld World;
|
||||
int32 RecipeIndex = INDEX_NONE;
|
||||
uint32 RecipeHash = 0;
|
||||
int32 EditChunkCount = 0;
|
||||
const double PrepareStartSeconds = FPlatformTime::Seconds();
|
||||
if (!PrepareRegionEvalWorld(World, Arguments, Seed,
|
||||
RecipeIndex, RecipeHash, EditChunkCount, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] world preparation failed: %s"), *Error);
|
||||
return 2;
|
||||
}
|
||||
const double WorldPrepareSeconds = FPlatformTime::Seconds() - PrepareStartSeconds;
|
||||
if (World.DiffLayer != nullptr)
|
||||
{
|
||||
World.DiffLayer->Clear();
|
||||
}
|
||||
|
||||
const int32 Dimension = VoxelForgeRegionEval::RingDimension;
|
||||
const int32 CenterZ = (World.TargetTopWorldZ + World.TargetBottomWorldZ) / 2;
|
||||
const FIntVector Origin(-129, -129, CenterZ - 129);
|
||||
TArray<VoxelForgeRegionEval::FTiming> OldTrials;
|
||||
TArray<VoxelForgeRegionEval::FTiming> NewTrials;
|
||||
TArray<float> OldDensities;
|
||||
TArray<float> NewDensities;
|
||||
TArray<float> NewRepeatDensities;
|
||||
TArray<float> NewSkippedDensities;
|
||||
TArray<float> SmoothDensities;
|
||||
TArray<float> SingleWorkerDensities;
|
||||
VoxelForgeRedefinedDensityField::SetEnabled(false);
|
||||
VoxelForgeRedefinedDensityField::SetDetailScale(1.0f);
|
||||
|
||||
for (int32 Trial = 0; Trial < 3; ++Trial)
|
||||
{
|
||||
VoxelForgeRegionEval::FTiming Timing;
|
||||
if (!VoxelForgeRegionEval::Evaluate(
|
||||
*World.Generator, Origin, Dimension, nullptr, true, false, 1.0f,
|
||||
&OldDensities, Timing, false, Workers, false))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] old field timing pass %d failed."), Trial + 1);
|
||||
return 2;
|
||||
}
|
||||
OldTrials.Add(Timing);
|
||||
}
|
||||
|
||||
// This fourth old-field pass carries counters; its wall time is deliberately excluded from
|
||||
// the performance row so instrumentation does not contaminate the measured baseline.
|
||||
VoxelForgeRegionEval::FTiming OldCounters;
|
||||
if (!VoxelForgeRegionEval::Evaluate(
|
||||
*World.Generator, Origin, Dimension, nullptr, true, false, 1.0f,
|
||||
nullptr, OldCounters, false, Workers, true))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] old field counter pass failed."));
|
||||
return 2;
|
||||
}
|
||||
OldCounters.WallSeconds = OldTrials.Last().WallSeconds;
|
||||
OldCounters.WorkerCpuSeconds = OldTrials.Last().WorkerCpuSeconds;
|
||||
OldCounters.NanosecondsPerSample = OldTrials.Last().NanosecondsPerSample;
|
||||
|
||||
for (int32 Trial = 0; Trial < 3; ++Trial)
|
||||
{
|
||||
VoxelForgeRegionEval::FTiming Timing;
|
||||
if (!VoxelForgeRegionEval::EvaluateRedefined(
|
||||
*World.Generator, Origin, Dimension, Workers, 1.0f,
|
||||
NewSkippedDensities, Timing, true))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] redefined 6-worker pass %d failed."), Trial + 1);
|
||||
return 2;
|
||||
}
|
||||
NewTrials.Add(Timing);
|
||||
}
|
||||
VoxelForgeRegionEval::FTiming OneWorkerTiming;
|
||||
if (!VoxelForgeRegionEval::EvaluateRedefined(
|
||||
*World.Generator, Origin, Dimension, 1, 1.0f,
|
||||
SingleWorkerDensities, OneWorkerTiming, false))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] 1-worker determinism pass failed."));
|
||||
return 2;
|
||||
}
|
||||
VoxelForgeRegionEval::FTiming NewFullSixWorkerTiming;
|
||||
if (!VoxelForgeRegionEval::EvaluateRedefined(
|
||||
*World.Generator, Origin, Dimension, Workers, 1.0f,
|
||||
NewDensities, NewFullSixWorkerTiming, false))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] full-detail six-worker evidence pass failed."));
|
||||
return 2;
|
||||
}
|
||||
VoxelForgeRegionEval::FTiming NewRepeatTiming;
|
||||
if (!VoxelForgeRegionEval::EvaluateRedefined(
|
||||
*World.Generator, Origin, Dimension, Workers, 1.0f,
|
||||
NewRepeatDensities, NewRepeatTiming, false))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] repeated full-detail hash pass failed."));
|
||||
return 2;
|
||||
}
|
||||
VoxelForgeRegionEval::FTiming SmoothTiming;
|
||||
if (!VoxelForgeRegionEval::EvaluateRedefined(
|
||||
*World.Generator, Origin, Dimension, Workers, 0.0f,
|
||||
SmoothDensities, SmoothTiming, false))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] deliberate smoothing control failed."));
|
||||
return 2;
|
||||
}
|
||||
|
||||
int64 DetailBatchParitySamples = 0;
|
||||
double DetailBatchMaxAbsoluteError = 0.0;
|
||||
const int64 TotalLatticeSamples = static_cast<int64>(Dimension) * Dimension * Dimension;
|
||||
for (int64 Index = 0; Index < TotalLatticeSamples; Index += 2047)
|
||||
{
|
||||
const int32 X = static_cast<int32>(Index % Dimension);
|
||||
const int32 Y = static_cast<int32>((Index / Dimension) % Dimension);
|
||||
const int32 Z = static_cast<int32>(Index / (static_cast<int64>(Dimension) * Dimension));
|
||||
const float RawDetailField = VoxelForgeRedefinedDensityField::Evaluate(
|
||||
*World.Generator, static_cast<float>(Origin.X + X),
|
||||
static_cast<float>(Origin.Y + Y), static_cast<float>(Origin.Z + Z), nullptr, 1.0f);
|
||||
DetailBatchMaxAbsoluteError = FMath::Max(
|
||||
DetailBatchMaxAbsoluteError,
|
||||
static_cast<double>(FMath::Abs(RawDetailField - NewDensities[static_cast<int32>(Index)])));
|
||||
++DetailBatchParitySamples;
|
||||
}
|
||||
const bool bDetailBatchMatchesRaw = DetailBatchMaxAbsoluteError <= 1.0e-5;
|
||||
|
||||
const FStep2dSurfaceSpectrum OldSpectrum = ComputeStep2dSpectrum(OldDensities, Dimension);
|
||||
const FStep2dSurfaceSpectrum NewSpectrum = ComputeStep2dSpectrum(NewDensities, Dimension);
|
||||
const FStep2dSurfaceSpectrum SmoothSpectrum = ComputeStep2dSpectrum(SmoothDensities, Dimension);
|
||||
const FStep2dBrickProofAudit BrickProofAudit = AuditStep2dBrickProofs(
|
||||
*World.Generator, Origin, Dimension);
|
||||
const int32 FeatureCellSpacing = World.Definitions.IsValidIndex(Arguments.Slot)
|
||||
&& World.Definitions[Arguments.Slot].IsValid()
|
||||
&& World.Definitions[Arguments.Slot]->GeneratorType == ECaveGeneratorType::Maze
|
||||
? 96 : 128;
|
||||
const FStep2dBoundaryStats BoundaryStats = ComputeStep2dBoundaryStats(
|
||||
NewDensities, Dimension, Origin, FeatureCellSpacing);
|
||||
|
||||
const bool bHashesAgree = OldDensities.Num() == Dimension * Dimension * Dimension
|
||||
&& VoxelForgeRegionEval::HashQuantized(NewDensities)
|
||||
== VoxelForgeRegionEval::HashQuantized(SingleWorkerDensities)
|
||||
&& VoxelForgeRegionEval::HashQuantized(NewDensities)
|
||||
== VoxelForgeRegionEval::HashQuantized(NewRepeatDensities)
|
||||
&& NewFullSixWorkerTiming.RawFloatHash == OneWorkerTiming.RawFloatHash
|
||||
&& NewFullSixWorkerTiming.RawFloatHash == NewRepeatTiming.RawFloatHash;
|
||||
|
||||
VoxelForgeRedefinedDensityField::SetEnabled(false);
|
||||
VoxelForgeRedefinedDensityField::SetDetailScale(1.0f);
|
||||
bool bMeshContainsBothIsoBands = false;
|
||||
const FIntVector MeshOrigin = SelectStep2dMeshOrigin(
|
||||
OldDensities, NewDensities, Origin, Dimension, bMeshContainsBothIsoBands);
|
||||
const FVoxelMeshData OldMesh = World.Mesher->GenerateMesh(MeshOrigin, 1, 64);
|
||||
const int32 OldTriangleCount = FMath::Max(0, OldMesh.Triangles.Num() / 3 - OldMesh.NumSkirtTriangles);
|
||||
VoxelForgeRedefinedDensityField::SetEnabled(true);
|
||||
const FVoxelMeshData NewMesh = World.Mesher->GenerateMesh(MeshOrigin, 1, 64);
|
||||
const int32 NewTriangleCount = FMath::Max(0, NewMesh.Triangles.Num() / 3 - NewMesh.NumSkirtTriangles);
|
||||
VoxelForgeRedefinedDensityField::SetDetailScale(0.0f);
|
||||
const FVoxelMeshData SmoothMesh = World.Mesher->GenerateMesh(MeshOrigin, 1, 64);
|
||||
const int32 SmoothTriangleCount = FMath::Max(0, SmoothMesh.Triangles.Num() / 3 - SmoothMesh.NumSkirtTriangles);
|
||||
VoxelForgeRedefinedDensityField::SetDetailScale(1.0f);
|
||||
VoxelForgeRedefinedDensityField::SetEnabled(false);
|
||||
|
||||
const FString ImageNames[] = {
|
||||
TEXT("slice_xy_center.png"), TEXT("slice_xz_y0.png"),
|
||||
TEXT("slice_yz_x0.png"), TEXT("slice_xz_passage.png")};
|
||||
FString SliceError;
|
||||
SaveStep2dSideBySideSlice(
|
||||
FPaths::Combine(ResearchOut, ImageNames[0]), OldDensities, NewDensities,
|
||||
Dimension, TEXT('h'), Dimension / 2, SliceError);
|
||||
SaveStep2dSideBySideSlice(
|
||||
FPaths::Combine(ResearchOut, ImageNames[1]), OldDensities, NewDensities,
|
||||
Dimension, TEXT('x'), Dimension / 2, SliceError);
|
||||
SaveStep2dSideBySideSlice(
|
||||
FPaths::Combine(ResearchOut, ImageNames[2]), OldDensities, NewDensities,
|
||||
Dimension, TEXT('y'), Dimension / 2, SliceError);
|
||||
int32 PassageIndex = Dimension / 2;
|
||||
if (World.Manager != nullptr && !World.Manager->GetPassages().IsEmpty())
|
||||
{
|
||||
const FVoxelPassage& Passage = World.Manager->GetPassages()[0];
|
||||
PassageIndex = FMath::Clamp(FMath::RoundToInt(Passage.LowerLanding.StandingPoint.Y)
|
||||
- Origin.Y, 0, Dimension - 1);
|
||||
}
|
||||
SaveStep2dSideBySideSlice(
|
||||
FPaths::Combine(ResearchOut, ImageNames[3]), OldDensities, NewDensities,
|
||||
Dimension, TEXT('x'), PassageIndex, SliceError);
|
||||
if (!SliceError.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] slice image write failed: %s"), *SliceError);
|
||||
return 2;
|
||||
}
|
||||
|
||||
TArray<double> OldWallTrials;
|
||||
TArray<double> NewWallTrials;
|
||||
for (const auto& Timing : OldTrials) OldWallTrials.Add(Timing.WallSeconds);
|
||||
for (const auto& Timing : NewTrials) NewWallTrials.Add(Timing.WallSeconds);
|
||||
OldWallTrials.Sort();
|
||||
NewWallTrials.Sort();
|
||||
const double OldMedianWall = OldWallTrials[OldWallTrials.Num() / 2];
|
||||
const double NewMedianWall = NewWallTrials[NewWallTrials.Num() / 2];
|
||||
const double OldGeometryEvaluations = static_cast<double>(OldCounters.RoomEvaluations
|
||||
+ OldCounters.TunnelEvaluations + OldCounters.TunnelCoreEvaluations
|
||||
+ OldCounters.PassageEvaluations);
|
||||
const double OldGeometryPerSample = OldCounters.SampleCount > 0
|
||||
? OldGeometryEvaluations / static_cast<double>(OldCounters.SampleCount) : 0.0;
|
||||
const double NewMacroPerSample = NewTrials.Last().SampleCount > 0
|
||||
? static_cast<double>(NewTrials.Last().MacroCentreEvaluations
|
||||
+ NewTrials.Last().EvaluatedSamples)
|
||||
/ static_cast<double>(NewTrials.Last().SampleCount) : 0.0;
|
||||
const bool bDetailAtLeastOld = NewSpectrum.RMSLag1 >= OldSpectrum.RMSLag1
|
||||
&& NewSpectrum.RMSLag2 >= OldSpectrum.RMSLag2;
|
||||
const bool bSmoothControlDetected = SmoothSpectrum.RMSLag1 < NewSpectrum.RMSLag1 * 0.80
|
||||
|| SmoothSpectrum.RMSLag2 < NewSpectrum.RMSLag2 * 0.80;
|
||||
const bool bBoundaryStatsSound = BoundaryStats.BrickBoundarySamples > 0
|
||||
&& BoundaryStats.InteriorSamples > 0
|
||||
&& BoundaryStats.ChunkBoundarySamples > 0
|
||||
&& BoundaryStats.FeatureCellBoundarySamples > 0
|
||||
&& BoundaryStats.BrickJumpRms <= BoundaryStats.InteriorJumpRms * 2.0 + 0.25
|
||||
&& BoundaryStats.ChunkBoundaryJumpRms <= BoundaryStats.InteriorJumpRms * 2.0 + 0.25
|
||||
&& BoundaryStats.FeatureCellJumpRms
|
||||
<= BoundaryStats.FeatureCellInteriorJumpRms * 2.0 + 0.25;
|
||||
const bool bCorrectProofsSound = BrickProofAudit.CorrectlySkippedBricksAudited > 0
|
||||
&& BrickProofAudit.CorrectProofSamplesChecked > 0
|
||||
&& BrickProofAudit.CorrectProofViolations == 0;
|
||||
const bool bUndersizedBoundCaught = BrickProofAudit.DeliberateBoundDisagreements > 0;
|
||||
const bool bSmoothWouldFallBelowOldSpectrum = SmoothSpectrum.RMSLag1 < OldSpectrum.RMSLag1
|
||||
|| SmoothSpectrum.RMSLag2 < OldSpectrum.RMSLag2;
|
||||
const bool bSpeedPass = NewMedianWall <= 0.600;
|
||||
const bool bPassed = bSpeedPass && bHashesAgree && bDetailAtLeastOld
|
||||
&& bSmoothControlDetected
|
||||
&& bBoundaryStatsSound && bCorrectProofsSound
|
||||
&& bUndersizedBoundCaught
|
||||
&& bDetailBatchMatchesRaw
|
||||
&& NewTrials.Last().NonFiniteSamples == 0;
|
||||
|
||||
TSharedRef<FJsonObject> Report = MakeShared<FJsonObject>();
|
||||
Report->SetStringField(TEXT("status"), bPassed ? TEXT("pass") : TEXT("fail"));
|
||||
Report->SetBoolField(TEXT("pass"), bPassed);
|
||||
Report->SetBoolField(TEXT("speed_pass_600ms"), bSpeedPass);
|
||||
Report->SetNumberField(TEXT("seed"), Seed);
|
||||
Report->SetNumberField(TEXT("ring_dimension"), Dimension);
|
||||
Report->SetNumberField(TEXT("ring_samples"), static_cast<double>(Dimension) * Dimension * Dimension);
|
||||
Report->SetNumberField(TEXT("workers_requested"), Workers);
|
||||
Report->SetNumberField(TEXT("world_prepare_ms_excluded"), WorldPrepareSeconds * 1000.0);
|
||||
Report->SetNumberField(TEXT("world_prepare_recipe_index"), RecipeIndex);
|
||||
Report->SetNumberField(TEXT("world_prepare_recipe_hash"), RecipeHash);
|
||||
Report->SetNumberField(TEXT("cleared_player_edit_chunks"), EditChunkCount);
|
||||
TSharedRef<FJsonObject> Window = MakeShared<FJsonObject>();
|
||||
Window->SetNumberField(TEXT("origin_x_voxels"), Origin.X);
|
||||
Window->SetNumberField(TEXT("origin_y_voxels"), Origin.Y);
|
||||
Window->SetNumberField(TEXT("origin_z_voxels"), Origin.Z);
|
||||
Window->SetNumberField(TEXT("samples_per_axis"), Dimension);
|
||||
Window->SetNumberField(TEXT("sample_stride_voxels"), 1);
|
||||
Window->SetNumberField(TEXT("sample_spacing_cm"), 25.0);
|
||||
Report->SetObjectField(TEXT("full_resolution_window"), Window);
|
||||
Report->SetStringField(TEXT("field_definition"),
|
||||
TEXT("A seed-offset periodic cross-corridor signed field per strate, joined with rounded "
|
||||
"landing chambers and existing authored passage carving. Four continuous hash-value "
|
||||
"noise bands (8, 4, 2, and 1 lattice point) with total bounded amplitude 5.25 are "
|
||||
"evaluated at every full-resolution sample that can affect the zero set."));
|
||||
Report->SetStringField(TEXT("resolution_contract"),
|
||||
TEXT("The lattice remains one sample per 25 cm. No coarse density result is interpolated "
|
||||
"onto it: every potentially surface-affecting point evaluates all four detail bands. "
|
||||
"Brick skips require a final-field sign proof including the full noise supremum, a "
|
||||
"floating-point safety margin, and the one-sample marching-cubes halo."));
|
||||
Report->SetStringField(TEXT("finer_lattice_scaling"),
|
||||
TEXT("At 12.5 cm, scale macro lengths and the broad/medium noise cell sizes in lattice "
|
||||
"units by 2 to preserve their world-space shapes; keep or add a 1-2 sample fine octave "
|
||||
"to retain and extend detail. With fixed 8^3 sample bricks, samples grow 8x but "
|
||||
"surface-intersecting bricks grow approximately with area (about 4x); the rest are "
|
||||
"sign-proven and require only density writes. Full-resolution detail remains per "
|
||||
"potentially surface-affecting point."));
|
||||
Report->SetNumberField(TEXT("old_macro_geometry_evaluations_per_point"), OldGeometryPerSample);
|
||||
Report->SetNumberField(TEXT("new_macro_evaluations_per_point_including_brick_proofs"), NewMacroPerSample);
|
||||
Report->SetNumberField(TEXT("new_noise_value_octaves_per_evaluated_point"), 4.0);
|
||||
Report->SetNumberField(TEXT("new_noise_octave_evaluations_per_full_ring_point"),
|
||||
static_cast<double>(NewTrials.Last().EvaluatedSamples) * 4.0
|
||||
/ static_cast<double>(NewTrials.Last().SampleCount));
|
||||
Report->SetNumberField(TEXT("old_reported_noise_share_percent_from_step2c"), 2.6);
|
||||
Report->SetNumberField(TEXT("old_six_worker_median_ms"), OldMedianWall * 1000.0);
|
||||
Report->SetNumberField(TEXT("new_six_worker_median_ms"), NewMedianWall * 1000.0);
|
||||
const auto WorkerRemainderSeconds = [](const VoxelForgeRegionEval::FTiming& Timing)
|
||||
{
|
||||
return FMath::Max(0.0, Timing.WorkerCpuSeconds
|
||||
- Timing.BrickClassificationSeconds - Timing.DensityEvaluationSeconds
|
||||
- Timing.UniformFillSeconds);
|
||||
};
|
||||
Report->SetNumberField(TEXT("new_other_worker_cpu_seconds"),
|
||||
WorkerRemainderSeconds(NewTrials.Last()));
|
||||
Report->SetNumberField(TEXT("new_other_worker_cpu_ns_per_point"),
|
||||
WorkerRemainderSeconds(NewTrials.Last()) * 1.0e9
|
||||
/ static_cast<double>(NewTrials.Last().SampleCount));
|
||||
Report->SetNumberField(TEXT("new_speedup_vs_old"), NewMedianWall > 0.0
|
||||
? OldMedianWall / NewMedianWall : 0.0);
|
||||
TArray<TSharedPtr<FJsonValue>> OldRows;
|
||||
TArray<TSharedPtr<FJsonValue>> NewRows;
|
||||
for (const auto& Timing : OldTrials)
|
||||
{
|
||||
OldRows.Add(MakeShared<FJsonValueObject>(Step2dTimingJson(Timing)));
|
||||
}
|
||||
for (const auto& Timing : NewTrials)
|
||||
{
|
||||
NewRows.Add(MakeShared<FJsonValueObject>(Step2dTimingJson(Timing)));
|
||||
}
|
||||
Report->SetArrayField(TEXT("old_field_trials"), OldRows);
|
||||
Report->SetArrayField(TEXT("new_field_trials"), NewRows);
|
||||
Report->SetObjectField(TEXT("old_full_ring_counters"), Step2dTimingJson(OldCounters));
|
||||
Report->SetObjectField(TEXT("new_one_worker_determinism_pass"), Step2dTimingJson(OneWorkerTiming));
|
||||
Report->SetObjectField(TEXT("new_full_detail_six_worker_evidence_pass"),
|
||||
Step2dTimingJson(NewFullSixWorkerTiming));
|
||||
Report->SetObjectField(TEXT("new_repeated_full_detail_six_worker_hash_pass"),
|
||||
Step2dTimingJson(NewRepeatTiming));
|
||||
Report->SetObjectField(TEXT("smooth_control_full_detail_pass"), Step2dTimingJson(SmoothTiming));
|
||||
|
||||
TSharedRef<FJsonObject> Hashes = MakeShared<FJsonObject>();
|
||||
Hashes->SetStringField(TEXT("six_worker_run_1_quantized"), FString::Printf(
|
||||
TEXT("%016llx"), static_cast<unsigned long long>(NewFullSixWorkerTiming.QuantizedHash)));
|
||||
Hashes->SetStringField(TEXT("six_worker_run_2_quantized"), FString::Printf(
|
||||
TEXT("%016llx"), static_cast<unsigned long long>(NewRepeatTiming.QuantizedHash)));
|
||||
Hashes->SetStringField(TEXT("one_worker_quantized"), FString::Printf(
|
||||
TEXT("%016llx"), static_cast<unsigned long long>(OneWorkerTiming.QuantizedHash)));
|
||||
Hashes->SetStringField(TEXT("six_worker_run_1_raw_float"), FString::Printf(
|
||||
TEXT("%016llx"), static_cast<unsigned long long>(NewFullSixWorkerTiming.RawFloatHash)));
|
||||
Hashes->SetStringField(TEXT("six_worker_run_2_raw_float"), FString::Printf(
|
||||
TEXT("%016llx"), static_cast<unsigned long long>(NewRepeatTiming.RawFloatHash)));
|
||||
Hashes->SetStringField(TEXT("one_worker_raw_float"), FString::Printf(
|
||||
TEXT("%016llx"), static_cast<unsigned long long>(OneWorkerTiming.RawFloatHash)));
|
||||
Hashes->SetBoolField(TEXT("same_across_two_runs_and_1_vs_6_workers"), bHashesAgree);
|
||||
Report->SetObjectField(TEXT("determinism"), Hashes);
|
||||
|
||||
TSharedRef<FJsonObject> Detail = MakeShared<FJsonObject>();
|
||||
Detail->SetNumberField(TEXT("old_surface_band_samples"), static_cast<double>(OldSpectrum.SurfaceBandSamples));
|
||||
Detail->SetNumberField(TEXT("new_surface_band_samples"), static_cast<double>(NewSpectrum.SurfaceBandSamples));
|
||||
Detail->SetNumberField(TEXT("old_curvature_rms_lag1"), OldSpectrum.RMSLag1);
|
||||
Detail->SetNumberField(TEXT("new_curvature_rms_lag1"), NewSpectrum.RMSLag1);
|
||||
Detail->SetNumberField(TEXT("smoothed_control_curvature_rms_lag1"), SmoothSpectrum.RMSLag1);
|
||||
Detail->SetNumberField(TEXT("old_curvature_rms_lag2"), OldSpectrum.RMSLag2);
|
||||
Detail->SetNumberField(TEXT("new_curvature_rms_lag2"), NewSpectrum.RMSLag2);
|
||||
Detail->SetNumberField(TEXT("smoothed_control_curvature_rms_lag2"), SmoothSpectrum.RMSLag2);
|
||||
Detail->SetNumberField(TEXT("new_curvature_rms_lag4"), NewSpectrum.RMSLag4);
|
||||
Detail->SetNumberField(TEXT("smoothed_control_curvature_rms_lag4"), SmoothSpectrum.RMSLag4);
|
||||
Detail->SetNumberField(TEXT("old_surface_triangles_64cubed"), OldTriangleCount);
|
||||
Detail->SetNumberField(TEXT("new_surface_triangles_64cubed"), NewTriangleCount);
|
||||
Detail->SetNumberField(TEXT("smoothed_control_surface_triangles_64cubed"), SmoothTriangleCount);
|
||||
Detail->SetNumberField(TEXT("mesh_region_origin_x_voxels"), MeshOrigin.X);
|
||||
Detail->SetNumberField(TEXT("mesh_region_origin_y_voxels"), MeshOrigin.Y);
|
||||
Detail->SetNumberField(TEXT("mesh_region_origin_z_voxels"), MeshOrigin.Z);
|
||||
Detail->SetBoolField(TEXT("mesh_region_contains_old_and_new_iso_bands"),
|
||||
bMeshContainsBothIsoBands);
|
||||
Detail->SetBoolField(TEXT("new_detail_at_least_old_spectrum"), bDetailAtLeastOld);
|
||||
Detail->SetNumberField(TEXT("detail_batch_raw_parity_samples"), DetailBatchParitySamples);
|
||||
Detail->SetNumberField(TEXT("detail_batch_max_absolute_error"), DetailBatchMaxAbsoluteError);
|
||||
Detail->SetBoolField(TEXT("detail_batch_matches_direct_field"), bDetailBatchMatchesRaw);
|
||||
Detail->SetBoolField(TEXT("deliberate_smoothing_control_detected"), bSmoothControlDetected);
|
||||
Detail->SetBoolField(TEXT("smoothed_control_below_old_spectrum"),
|
||||
bSmoothWouldFallBelowOldSpectrum);
|
||||
Report->SetObjectField(TEXT("detail_preservation"), Detail);
|
||||
|
||||
TSharedRef<FJsonObject> Boundaries = MakeShared<FJsonObject>();
|
||||
Boundaries->SetNumberField(TEXT("brick_size_voxels"), 8);
|
||||
Boundaries->SetNumberField(TEXT("feature_cell_spacing_voxels"), FeatureCellSpacing);
|
||||
Boundaries->SetNumberField(TEXT("chunk_boundary_gradient_jump_rms_xy_near_surface"),
|
||||
BoundaryStats.ChunkBoundaryJumpRms);
|
||||
Boundaries->SetNumberField(TEXT("chunk_boundary_gradient_jump_max_xy_near_surface"),
|
||||
BoundaryStats.ChunkBoundaryJumpMax);
|
||||
Boundaries->SetNumberField(TEXT("brick_boundary_gradient_jump_rms"), BoundaryStats.BrickJumpRms);
|
||||
Boundaries->SetNumberField(TEXT("interior_gradient_jump_rms"), BoundaryStats.InteriorJumpRms);
|
||||
Boundaries->SetNumberField(TEXT("feature_cell_boundary_gradient_jump_rms"), BoundaryStats.FeatureCellJumpRms);
|
||||
Boundaries->SetNumberField(TEXT("feature_cell_interior_gradient_jump_rms"),
|
||||
BoundaryStats.FeatureCellInteriorJumpRms);
|
||||
Boundaries->SetNumberField(TEXT("brick_boundary_samples_near_surface"),
|
||||
static_cast<double>(BoundaryStats.BrickBoundarySamples));
|
||||
Boundaries->SetNumberField(TEXT("chunk_boundary_samples_near_surface_xy"),
|
||||
static_cast<double>(BoundaryStats.ChunkBoundarySamples));
|
||||
Boundaries->SetNumberField(TEXT("macro_period_boundary_samples"),
|
||||
static_cast<double>(BoundaryStats.FeatureCellBoundarySamples));
|
||||
Boundaries->SetNumberField(TEXT("brick_boundary_gradient_jump_max"), BoundaryStats.BrickJumpMax);
|
||||
Boundaries->SetNumberField(TEXT("feature_cell_boundary_gradient_jump_max"), BoundaryStats.FeatureCellJumpMax);
|
||||
Boundaries->SetBoolField(TEXT("boundary_check_pass"), bBoundaryStatsSound);
|
||||
Report->SetObjectField(TEXT("boundary_artifact_check"), Boundaries);
|
||||
|
||||
TSharedRef<FJsonObject> Proofs = MakeShared<FJsonObject>();
|
||||
Proofs->SetNumberField(TEXT("correctly_skipped_bricks_audited"),
|
||||
static_cast<double>(BrickProofAudit.CorrectlySkippedBricksAudited));
|
||||
Proofs->SetNumberField(TEXT("final_field_samples_checked_including_halo"),
|
||||
static_cast<double>(BrickProofAudit.CorrectProofSamplesChecked));
|
||||
Proofs->SetNumberField(TEXT("correct_proof_sign_violations"),
|
||||
static_cast<double>(BrickProofAudit.CorrectProofViolations));
|
||||
Proofs->SetNumberField(TEXT("deliberately_undersized_detail_bound_claims_checked"),
|
||||
static_cast<double>(BrickProofAudit.DeliberatelyUndersizedBoundClaimsChecked));
|
||||
Proofs->SetNumberField(TEXT("wrong_sign_samples_from_undersized_bound"),
|
||||
static_cast<double>(BrickProofAudit.DeliberateBoundDisagreements));
|
||||
Proofs->SetNumberField(TEXT("deliberate_control_classifier_detail_scale"), 0.0);
|
||||
Proofs->SetNumberField(TEXT("deliberate_control_actual_detail_scale"), 4.0);
|
||||
Proofs->SetBoolField(TEXT("sound_bound_audit_pass"), bCorrectProofsSound);
|
||||
Proofs->SetBoolField(TEXT("deliberate_undersized_bound_control_caught"), bUndersizedBoundCaught);
|
||||
Report->SetObjectField(TEXT("brick_bound_audit"), Proofs);
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Slices;
|
||||
for (const FString& Name : ImageNames)
|
||||
{
|
||||
TSharedRef<FJsonObject> Slice = MakeShared<FJsonObject>();
|
||||
Slice->SetStringField(TEXT("file"), FPaths::Combine(ResearchOut, Name));
|
||||
Slice->SetStringField(TEXT("layout"), TEXT("old field on left; redefined field on right; light=air, dark=solid, cyan=near zero"));
|
||||
Slices.Add(MakeShared<FJsonValueObject>(Slice));
|
||||
}
|
||||
Report->SetArrayField(TEXT("slices"), Slices);
|
||||
FString JsonText;
|
||||
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonText);
|
||||
const FString ReportPath = FPaths::Combine(ResearchOut, TEXT("step2d_report.json"));
|
||||
if (!FJsonSerializer::Serialize(Report, Writer)
|
||||
|| !FFileHelper::SaveStringToFile(
|
||||
JsonText, *ReportPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)
|
||||
|| !FFileHelper::SaveStringToFile(
|
||||
JsonText, *FPaths::Combine(Arguments.OutDirectory, TEXT("step2d_report.json")),
|
||||
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] could not write report '%s'."), *ReportPath);
|
||||
return 2;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStep2d] pass=%s speed600=%s old_ms=%.3f new_ms=%.3f skip=%.1f%% "
|
||||
"roughness(old/new/smooth)=%.4f/%.4f/%.4f hashes=%s report=%s"),
|
||||
bPassed ? TEXT("yes") : TEXT("no"), bSpeedPass ? TEXT("yes") : TEXT("no"),
|
||||
OldMedianWall * 1000.0, NewMedianWall * 1000.0,
|
||||
NewTrials.Last().SampleCount > 0
|
||||
? 100.0 * static_cast<double>(NewTrials.Last().SkippedSamples)
|
||||
/ static_cast<double>(NewTrials.Last().SampleCount) : 0.0,
|
||||
OldSpectrum.RMSLag1, NewSpectrum.RMSLag1, SmoothSpectrum.RMSLag1,
|
||||
bHashesAgree ? TEXT("same") : TEXT("DIFFERENT"), *ReportPath);
|
||||
return bPassed ? 0 : 2;
|
||||
}
|
||||
|
||||
class FGeneratorDensitySampler final : public IVoxelStrateDensitySampler
|
||||
{
|
||||
public:
|
||||
@@ -9648,6 +10475,10 @@ int32 RunBatch(const FString& Params)
|
||||
|
||||
int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
||||
{
|
||||
if (FParse::Param(*Params, TEXT("step2d")))
|
||||
{
|
||||
return RunStep2d(Params);
|
||||
}
|
||||
const bool bRegionEvalSwitch = FParse::Param(*Params, TEXT("regioneval"));
|
||||
int32 RegionEvalFlag = 0;
|
||||
const bool bRegionEvalValue = FParse::Value(*Params, TEXT("regioneval="), RegionEvalFlag);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -26,6 +26,9 @@ namespace VoxelForgeRegionEval
|
||||
double GeneralPathEntriesPerSample = 0.0;
|
||||
double WallSeconds = 0.0;
|
||||
double WorkerCpuSeconds = 0.0;
|
||||
double BrickClassificationSeconds = 0.0;
|
||||
double DensityEvaluationSeconds = 0.0;
|
||||
double UniformFillSeconds = 0.0;
|
||||
double NanosecondsPerSample = 0.0;
|
||||
bool bWorkerCpuFromOsThreadTimes = false;
|
||||
bool bRegionProgram = false;
|
||||
@@ -34,6 +37,16 @@ namespace VoxelForgeRegionEval
|
||||
uint64 RawFloatHash = 0;
|
||||
uint64 SaturatedSamples = 0;
|
||||
uint64 NonFiniteSamples = 0;
|
||||
uint64 BrickCount = 0;
|
||||
uint64 SkippedBricks = 0;
|
||||
uint64 SkippedSamples = 0;
|
||||
uint64 MacroBrickChecks = 0;
|
||||
uint64 MacroCentreEvaluations = 0;
|
||||
uint64 UnsupportedBrickProofs = 0;
|
||||
uint64 ChunkBoundaryBrickProofs = 0;
|
||||
uint64 PassageBoundBrickProofs = 0;
|
||||
uint64 MixedMacroBrickProofs = 0;
|
||||
double LongestWorkerWallSeconds = 0.0;
|
||||
uint64 SdfCacheBuilds = 0;
|
||||
uint64 ActiveOpListBuilds = 0;
|
||||
uint64 BroadPhaseIndexBuilds = 0;
|
||||
@@ -122,6 +135,17 @@ namespace VoxelForgeRegionEval
|
||||
int32 RequestedWorkerCount = WorkerCount,
|
||||
bool bCollectWholeRunCounters = false);
|
||||
|
||||
/** Full-resolution step 2d field pass with sound world-aligned 8^3 brick proofs. */
|
||||
bool EvaluateRedefined(
|
||||
const UVoxelGenerator& Generator,
|
||||
FIntVector OriginVoxels,
|
||||
int32 Dimension,
|
||||
int32 RequestedWorkerCount,
|
||||
float DetailScale,
|
||||
TArray<float>& OutDensities,
|
||||
FTiming& OutTiming,
|
||||
bool bEnableBrickSkips = true);
|
||||
|
||||
/** Count exact room/tunnel/core/passage candidate visits with instrumentation enabled. */
|
||||
FCandidateVisits CountCandidates(
|
||||
const UVoxelGenerator& Generator,
|
||||
|
||||
Reference in new issue
Block a user