measure(step2a): candidate bins alone are slower -- the per-sample GetDensityAt call must go
Editor-only ablation (explore commandlet, -regioneval, default off): 8^3 bins for rooms, room-floor joins and tunnel chains, but every point still goes through GetDensityAt. 259^3 ring, seed 1337, six workers: live : 6.776 s wall, 29.16 s worker CPU, 1.679 us/sample, 21.2 candidate visits/sample binned: 19.54 s wall, 50.02 s worker CPU, 2.879 us/sample, 16.4 candidate visits/sample Correctness: zero Q16 (x16, ties away from zero) disagreements on the full ring and ~284k probes on 4 more seeds (composer recipes, warped rooms, passages, landings, strate edges, edits); shrinking room reach to 0.5 -> 18,107 Q16 / 735 sign differences (the check bites). Not the planned region program: the step-2 question is still open. Baseline for the next attempt: 1.68 us CPU per sample on the live path. 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
3b4d6855fe
commit
41a9265ebd
8 files changed
+1693
-31
No files matched your search
@@ -1,5 +1,6 @@
|
||||
#include "VoxelForgeExploreCommandlet.h"
|
||||
#include "VoxelStackOfflineSymbolizer.h"
|
||||
#include "VoxelForgeRegionEvalExperiment.h"
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Async/ParallelFor.h"
|
||||
@@ -40,6 +41,7 @@
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelStrateManager.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
#include "VoxelStrateComposer.h"
|
||||
#include "VoxelTypes.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -1456,6 +1458,737 @@ struct FExploreWorld
|
||||
}
|
||||
};
|
||||
|
||||
struct FRegionEvalPass
|
||||
{
|
||||
VoxelForgeRegionEval::FTiming Timing;
|
||||
VoxelForgeRegionEval::FCandidateVisits Visits;
|
||||
TArray<float> Densities;
|
||||
TArray<int64> SampleIndices;
|
||||
FIntVector Origin = FIntVector::ZeroValue;
|
||||
int32 RecipeIndex = INDEX_NONE;
|
||||
uint32 RecipeHash = 0;
|
||||
int32 PassageCount = 0;
|
||||
int32 EditChunkCount = 0;
|
||||
int32 TargetedSampleCount = 0;
|
||||
double WorldPrepareSeconds = 0.0;
|
||||
FString StartedUtc;
|
||||
FString EndedUtc;
|
||||
};
|
||||
|
||||
int32 FindRoomGraphRecipeIndex(int32 Seed)
|
||||
{
|
||||
for (int32 Index = 0; Index < 64; ++Index)
|
||||
{
|
||||
const FVoxelOpStackRecipe Recipe = VF_RollStrateStructure(Seed, Index);
|
||||
if (Recipe.ShapeSource.OpClass == EVoxelStrateOpClass::RoomGraphSource)
|
||||
{
|
||||
return Index;
|
||||
}
|
||||
}
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
bool PrepareRegionEvalWorld(
|
||||
FExploreWorld& World,
|
||||
const FExploreArguments& BaseArguments,
|
||||
int32 Seed,
|
||||
int32& OutRecipeIndex,
|
||||
uint32& OutRecipeHash,
|
||||
int32& OutEditChunkCount,
|
||||
FString& OutError)
|
||||
{
|
||||
FExploreArguments Arguments = BaseArguments;
|
||||
Arguments.Seed = Seed;
|
||||
Arguments.bSeedOverride = true;
|
||||
Arguments.bRender = false;
|
||||
Arguments.bWalk = false;
|
||||
Arguments.bExport = false;
|
||||
Arguments.bBlockEarlyOut = false;
|
||||
if (!World.Build(Arguments, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!World.Definitions.IsValidIndex(Arguments.Slot))
|
||||
{
|
||||
OutError = TEXT("Region evaluator target definition is missing.");
|
||||
return false;
|
||||
}
|
||||
UVoxelStrateDefinition* Definition = World.Definitions[Arguments.Slot].Get();
|
||||
if (Definition == nullptr || Definition->GeneratorType != ECaveGeneratorType::TunnelNetwork)
|
||||
{
|
||||
OutError = TEXT("Region evaluator requires the owner TunnelNetwork target slot.");
|
||||
return false;
|
||||
}
|
||||
|
||||
OutRecipeIndex = FindRoomGraphRecipeIndex(Seed);
|
||||
if (OutRecipeIndex == INDEX_NONE)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("No RoomGraphSource recipe found in the 64-entry structure roll for seed %d."), Seed);
|
||||
return false;
|
||||
}
|
||||
const FVoxelOpStackRecipe Recipe = VF_RollStrateStructure(Seed, OutRecipeIndex);
|
||||
OutRecipeHash = VF_HashStrateStructureRecipe(Recipe);
|
||||
|
||||
// Make the selected RoomGraph recipe exercise the authored warp path. The definition is a
|
||||
// transient duplicate of DA_Strate3, and both the live and prepared passes receive the same
|
||||
// operator parameters and recipe.
|
||||
FVoxelStrateArchetypeParams NativeParams;
|
||||
NativeParams.TunnelNetworkParams = Definition->GenerationParams;
|
||||
NativeParams.TunnelNetworkParams.TunnelWarpStrength = FMath::Max(
|
||||
NativeParams.TunnelNetworkParams.TunnelWarpStrength, 1.25f);
|
||||
NativeParams.SlabParams = Definition->SlabParams;
|
||||
NativeParams.MazeParams = Definition->MazeParams;
|
||||
NativeParams.SurfaceParams = Definition->SurfaceParams;
|
||||
NativeParams.VerticalShaftParams = Definition->VerticalShaftParams;
|
||||
NativeParams.FloatingIslandParams = Definition->FloatingIslandParams;
|
||||
FString ComposerError;
|
||||
if (!World.Manager->SetComposerOverrideForStrate(
|
||||
Arguments.Slot, Seed, Definition->GeneratorType,
|
||||
NativeParams, true, &Recipe, ComposerError))
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Could not install rolled composer recipe for seed %d: %s"),
|
||||
Seed, *ComposerError);
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 CenterZ = (World.TargetTopWorldZ + World.TargetBottomWorldZ) / 2;
|
||||
FVoxelModification Carve;
|
||||
Carve.Center = FVector(8.0f, 4.0f, static_cast<float>(CenterZ));
|
||||
Carve.Radius = 6.0f;
|
||||
Carve.Strength = -12.0f;
|
||||
Carve.Shape = EVoxelBrushShape::Sphere;
|
||||
const TArray<FIntVector> CarvedChunks = World.DiffLayer->ApplyModification(Carve);
|
||||
FVoxelModification Fill;
|
||||
Fill.Center = FVector(-12.0f, 6.0f, static_cast<float>(CenterZ - 10));
|
||||
Fill.Radius = 4.0f;
|
||||
Fill.Strength = 12.0f;
|
||||
Fill.Shape = EVoxelBrushShape::Sphere;
|
||||
const TArray<FIntVector> FilledChunks = World.DiffLayer->ApplyModification(Fill);
|
||||
if (CarvedChunks.IsEmpty() || FilledChunks.IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Transient player carve/fill edits did not enter the diff layer.");
|
||||
return false;
|
||||
}
|
||||
OutEditChunkCount = CarvedChunks.Num() + FilledChunks.Num();
|
||||
|
||||
int32 RecipeSeed = 0;
|
||||
ECaveGeneratorType RecipeArchetype = ECaveGeneratorType::TunnelNetwork;
|
||||
FVoxelStrateArchetypeParams ResolvedParams;
|
||||
FVoxelOpStackRecipe ResolvedRecipe;
|
||||
const int32 ChunkZ = FMath::FloorToInt(static_cast<float>(CenterZ) / CHUNK_SIZE);
|
||||
if (!World.Manager->GetRecipeForChunk(
|
||||
FIntVector(0, 0, ChunkZ), RecipeSeed, RecipeArchetype,
|
||||
ResolvedParams, ResolvedRecipe)
|
||||
|| !VF_AreStrateStructureRecipesIdentical(Recipe, ResolvedRecipe))
|
||||
{
|
||||
OutError = TEXT("Composer recipe was not active on the target density lattice.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AddRegionSampleIndex(
|
||||
TSet<int64>& Unique,
|
||||
int32 X, int32 Y, int32 Z,
|
||||
FIntVector Origin, int32 Dimension)
|
||||
{
|
||||
const int32 LX = X - Origin.X;
|
||||
const int32 LY = Y - Origin.Y;
|
||||
const int32 LZ = Z - Origin.Z;
|
||||
if (LX >= 0 && LX < Dimension && LY >= 0 && LY < Dimension
|
||||
&& LZ >= 0 && LZ < Dimension)
|
||||
{
|
||||
Unique.Add(VoxelForgeRegionEval::LinearIndex(LX, LY, LZ, Dimension));
|
||||
}
|
||||
}
|
||||
|
||||
TArray<int64> BuildRegionEvalSampleIndices(
|
||||
const FExploreWorld& World,
|
||||
FIntVector Origin,
|
||||
int32 Dimension,
|
||||
int32 RequestedRandomSamples,
|
||||
int32 Seed,
|
||||
int32& OutTargetedSamples)
|
||||
{
|
||||
const int64 Total = static_cast<int64>(Dimension) * Dimension * Dimension;
|
||||
const int32 RandomCount = FMath::Clamp(RequestedRandomSamples, 1, static_cast<int32>(Total));
|
||||
TArray<int64> Indices;
|
||||
Indices.Reserve(RandomCount + 50000);
|
||||
|
||||
auto Gcd = [](int64 A, int64 B)
|
||||
{
|
||||
while (B != 0)
|
||||
{
|
||||
const int64 R = A % B;
|
||||
A = B;
|
||||
B = R;
|
||||
}
|
||||
return A;
|
||||
};
|
||||
int64 Multiplier = 104729;
|
||||
while (Gcd(Multiplier, Total) != 1)
|
||||
{
|
||||
Multiplier += 2;
|
||||
}
|
||||
const int64 Offset = (static_cast<int64>(Seed) * 48271ll) % Total;
|
||||
for (int32 Sample = 0; Sample < RandomCount; ++Sample)
|
||||
{
|
||||
Indices.Add((static_cast<int64>(Sample) * Multiplier + Offset) % Total);
|
||||
}
|
||||
|
||||
TSet<int64> Unique;
|
||||
Unique.Reserve(50000);
|
||||
for (const int64 Index : Indices)
|
||||
{
|
||||
Unique.Add(Index);
|
||||
}
|
||||
const int32 RandomUniqueCount = Unique.Num();
|
||||
|
||||
// Dense probes around both edited volumes.
|
||||
const int32 CenterZ = (World.TargetTopWorldZ + World.TargetBottomWorldZ) / 2;
|
||||
const FVector EditCenters[] = {
|
||||
FVector(8.0f, 4.0f, static_cast<float>(CenterZ)),
|
||||
FVector(-12.0f, 6.0f, static_cast<float>(CenterZ - 10)),
|
||||
};
|
||||
for (const FVector& Center : EditCenters)
|
||||
{
|
||||
const int32 CX = FMath::FloorToInt(Center.X);
|
||||
const int32 CY = FMath::FloorToInt(Center.Y);
|
||||
const int32 CZ = FMath::FloorToInt(Center.Z);
|
||||
for (int32 Z = CZ - 8; Z <= CZ + 8; ++Z)
|
||||
{
|
||||
for (int32 Y = CY - 8; Y <= CY + 8; ++Y)
|
||||
{
|
||||
for (int32 X = CX - 8; X <= CX + 8; ++X)
|
||||
{
|
||||
AddRegionSampleIndex(Unique, X, Y, Z, Origin, Dimension);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Include lattice sites around each authored route tube, landing room, and floor anchor.
|
||||
for (const FVoxelPassage& Passage : World.Manager->GetPassages())
|
||||
{
|
||||
TArray<FVector> Anchors;
|
||||
Anchors.Add(Passage.UpperPoint);
|
||||
Anchors.Add(Passage.LowerPoint);
|
||||
Anchors.Add(Passage.UpperLanding.StandingPoint);
|
||||
Anchors.Add(Passage.UpperLanding.DoorPoint);
|
||||
Anchors.Add(Passage.LowerLanding.StandingPoint);
|
||||
Anchors.Add(Passage.LowerLanding.DoorPoint);
|
||||
Anchors.Append(Passage.ControlPoints);
|
||||
for (const FVector& Anchor : Anchors)
|
||||
{
|
||||
const int32 AX = FMath::FloorToInt(Anchor.X);
|
||||
const int32 AY = FMath::FloorToInt(Anchor.Y);
|
||||
const int32 AZ = FMath::FloorToInt(Anchor.Z);
|
||||
for (int32 Z = AZ - 3; Z <= AZ + 3; ++Z)
|
||||
{
|
||||
for (int32 Y = AY - 3; Y <= AY + 3; ++Y)
|
||||
{
|
||||
for (int32 X = AX - 3; X <= AX + 3; ++X)
|
||||
{
|
||||
AddRegionSampleIndex(Unique, X, Y, Z, Origin, Dimension);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Both strate boundaries are swept across the horizontal region, including ±3 lattice rows.
|
||||
const int32 Boundaries[] = { World.TargetTopWorldZ, World.TargetBottomWorldZ };
|
||||
for (const int32 BoundaryZ : Boundaries)
|
||||
{
|
||||
for (int32 Dz = -3; Dz <= 3; ++Dz)
|
||||
{
|
||||
for (int32 Y = -128; Y <= 128; Y += 8)
|
||||
{
|
||||
for (int32 X = -128; X <= 128; X += 8)
|
||||
{
|
||||
AddRegionSampleIndex(Unique, X, Y, BoundaryZ + Dz, Origin, Dimension);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Indices = Unique.Array();
|
||||
Indices.Sort();
|
||||
OutTargetedSamples = FMath::Max(0, Indices.Num() - RandomUniqueCount);
|
||||
return Indices;
|
||||
}
|
||||
|
||||
bool RunRegionEvalPass(
|
||||
const FExploreArguments& BaseArguments,
|
||||
int32 Seed,
|
||||
bool bPrepared,
|
||||
float ReachScale,
|
||||
bool bFullRing,
|
||||
const TArray<int64>* RequestedIndices,
|
||||
int32 RequestedSamples,
|
||||
bool bCountCandidates,
|
||||
bool bKeepDensities,
|
||||
FRegionEvalPass& OutPass,
|
||||
FString& OutError)
|
||||
{
|
||||
OutPass = FRegionEvalPass();
|
||||
FExploreWorld World;
|
||||
const double PrepareStartSeconds = FPlatformTime::Seconds();
|
||||
if (!PrepareRegionEvalWorld(
|
||||
World, BaseArguments, Seed, OutPass.RecipeIndex,
|
||||
OutPass.RecipeHash, OutPass.EditChunkCount, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutPass.WorldPrepareSeconds = FPlatformTime::Seconds() - PrepareStartSeconds;
|
||||
OutPass.PassageCount = World.Manager->GetPassages().Num();
|
||||
const int32 CenterZ = (World.TargetTopWorldZ + World.TargetBottomWorldZ) / 2;
|
||||
OutPass.Origin = FIntVector(-129, -129, CenterZ - 129);
|
||||
if (OutPass.PassageCount <= 0)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Seed %d generated no inter-strate route passages."), Seed);
|
||||
return false;
|
||||
}
|
||||
if (RequestedIndices != nullptr)
|
||||
{
|
||||
OutPass.SampleIndices = *RequestedIndices;
|
||||
}
|
||||
else
|
||||
{
|
||||
OutPass.SampleIndices = BuildRegionEvalSampleIndices(
|
||||
World, OutPass.Origin, VoxelForgeRegionEval::RingDimension,
|
||||
RequestedSamples, Seed, OutPass.TargetedSampleCount);
|
||||
}
|
||||
if (OutPass.SampleIndices.IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Region evaluator produced an empty correctness sample set.");
|
||||
return false;
|
||||
}
|
||||
|
||||
OutPass.StartedUtc = FDateTime::UtcNow().ToIso8601();
|
||||
const bool bEvaluated = VoxelForgeRegionEval::Evaluate(
|
||||
*World.Generator, OutPass.Origin, VoxelForgeRegionEval::RingDimension,
|
||||
bFullRing ? nullptr : &OutPass.SampleIndices,
|
||||
bFullRing, bPrepared, ReachScale,
|
||||
bKeepDensities ? &OutPass.Densities : nullptr,
|
||||
OutPass.Timing);
|
||||
OutPass.EndedUtc = FDateTime::UtcNow().ToIso8601();
|
||||
if (!bEvaluated)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Density evaluation failed or emitted a non-finite value (seed %d)."), Seed);
|
||||
return false;
|
||||
}
|
||||
if (bCountCandidates)
|
||||
{
|
||||
OutPass.Visits = VoxelForgeRegionEval::CountCandidates(
|
||||
*World.Generator, OutPass.Origin,
|
||||
VoxelForgeRegionEval::RingDimension,
|
||||
OutPass.SampleIndices, bPrepared, ReachScale);
|
||||
if (OutPass.Visits.SampleCount != OutPass.SampleIndices.Num())
|
||||
{
|
||||
OutError = TEXT("Candidate counter sweep did not consume the full fixed sample set.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> RegionEvalTimingJson(const FRegionEvalPass& Pass, bool bPrepared)
|
||||
{
|
||||
TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>();
|
||||
Json->SetStringField(TEXT("path"), bPrepared ? TEXT("prepared_8cube_bins") : TEXT("live_GetDensityAt"));
|
||||
Json->SetStringField(TEXT("started_utc"), Pass.StartedUtc);
|
||||
Json->SetStringField(TEXT("ended_utc"), Pass.EndedUtc);
|
||||
Json->SetNumberField(TEXT("worker_count"), Pass.Timing.Workers);
|
||||
Json->SetNumberField(TEXT("observed_worker_threads"), Pass.Timing.ObservedWorkerThreads);
|
||||
Json->SetNumberField(TEXT("samples"), static_cast<double>(Pass.Timing.SampleCount));
|
||||
Json->SetNumberField(TEXT("world_setup_seconds_before_density_ring"), Pass.WorldPrepareSeconds);
|
||||
Json->SetNumberField(TEXT("wall_seconds"), Pass.Timing.WallSeconds);
|
||||
Json->SetNumberField(TEXT("worker_cpu_seconds"), Pass.Timing.WorkerCpuSeconds);
|
||||
Json->SetNumberField(TEXT("worker_cpu_ns_per_sample"), Pass.Timing.NanosecondsPerSample);
|
||||
Json->SetStringField(TEXT("worker_cpu_source"), Pass.Timing.bWorkerCpuFromOsThreadTimes
|
||||
? TEXT("summed Windows GetThreadTimes user+kernel CPU across six fixed work partitions")
|
||||
: TEXT("summed worker elapsed fallback; OS thread CPU counters unavailable"));
|
||||
Json->SetStringField(TEXT("quantized_hash_fnv1a64"),
|
||||
FString::Printf(TEXT("%016llx"), static_cast<unsigned long long>(Pass.Timing.QuantizedHash)));
|
||||
Json->SetStringField(TEXT("raw_float_hash_fnv1a64"),
|
||||
FString::Printf(TEXT("%016llx"), static_cast<unsigned long long>(Pass.Timing.RawFloatHash)));
|
||||
Json->SetNumberField(TEXT("saturated_samples"), static_cast<double>(Pass.Timing.SaturatedSamples));
|
||||
Json->SetNumberField(TEXT("non_finite_samples"), static_cast<double>(Pass.Timing.NonFiniteSamples));
|
||||
if (Pass.Visits.SampleCount > 0)
|
||||
{
|
||||
Json->SetNumberField(TEXT("candidate_visits_per_sample"), Pass.Visits.PerSample);
|
||||
Json->SetNumberField(TEXT("room_candidate_visits"), static_cast<double>(Pass.Visits.Room));
|
||||
Json->SetNumberField(TEXT("tunnel_candidate_visits"), static_cast<double>(Pass.Visits.Tunnel));
|
||||
Json->SetNumberField(TEXT("tunnel_core_candidate_visits"), static_cast<double>(Pass.Visits.TunnelCore));
|
||||
Json->SetNumberField(TEXT("passage_candidate_visits"), static_cast<double>(Pass.Visits.Passage));
|
||||
Json->SetNumberField(TEXT("tunnel_cache_lookups"), static_cast<double>(Pass.Visits.TunnelCacheLookups));
|
||||
Json->SetNumberField(TEXT("tunnel_cache_hits"), static_cast<double>(Pass.Visits.TunnelCacheHits));
|
||||
Json->SetNumberField(TEXT("tunnel_cache_misses"), static_cast<double>(Pass.Visits.TunnelCacheMisses));
|
||||
Json->SetNumberField(TEXT("sdf_cache_builds"), static_cast<double>(Pass.Visits.SdfCacheBuilds));
|
||||
Json->SetNumberField(TEXT("candidate_counter_samples"), static_cast<double>(Pass.Visits.SampleCount));
|
||||
Json->SetNumberField(TEXT("attribution_sample_interval"), Pass.Visits.AttributionSampleInterval);
|
||||
Json->SetStringField(TEXT("attribution_basis"), TEXT("sampled VoxelDensityProfile phases; diagnostic only, excluded from clean timing"));
|
||||
Json->SetNumberField(TEXT("get_density_at_attributed_ns_per_sample"), Pass.Visits.GetDensityAtAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_prologue_attributed_ns_per_sample"), Pass.Visits.DensityPrologueAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_core_attributed_ns_per_sample"), Pass.Visits.DensityCoreAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_disturbances_attributed_ns_per_sample"), Pass.Visits.DensityDisturbancesAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_structural_posts_attributed_ns_per_sample"), Pass.Visits.DensityStructuralPostsAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_boundary_seal_attributed_ns_per_sample"), Pass.Visits.DensityBoundarySealAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_diff_layer_attributed_ns_per_sample"), Pass.Visits.DensityDiffLayerAttributedNsPerSample);
|
||||
Json->SetNumberField(TEXT("density_tail_attributed_ns_per_sample"), Pass.Visits.DensityTailAttributedNsPerSample);
|
||||
}
|
||||
return Json;
|
||||
}
|
||||
|
||||
struct FQuantizedComparison
|
||||
{
|
||||
int64 Disagreements = 0;
|
||||
int64 SignDisagreements = 0;
|
||||
double MaxAbsoluteFloatDelta = 0.0;
|
||||
int64 FirstIndex = INDEX_NONE;
|
||||
};
|
||||
|
||||
FQuantizedComparison CompareQuantizedArrays(
|
||||
const TArray<float>& A, const TArray<float>& B,
|
||||
const TArray<int64>* LatticeIndices = nullptr)
|
||||
{
|
||||
FQuantizedComparison Result;
|
||||
if (A.Num() != B.Num())
|
||||
{
|
||||
Result.Disagreements = FMath::Max(A.Num(), B.Num());
|
||||
return Result;
|
||||
}
|
||||
for (int32 Index = 0; Index < A.Num(); ++Index)
|
||||
{
|
||||
bool bSaturatedA = false, bFiniteA = false;
|
||||
bool bSaturatedB = false, bFiniteB = false;
|
||||
const int16 QA = VoxelForgeRegionEval::QuantizeDensity(A[Index], bSaturatedA, bFiniteA);
|
||||
const int16 QB = VoxelForgeRegionEval::QuantizeDensity(B[Index], bSaturatedB, bFiniteB);
|
||||
Result.MaxAbsoluteFloatDelta = FMath::Max(
|
||||
Result.MaxAbsoluteFloatDelta,
|
||||
FMath::Abs(static_cast<double>(A[Index]) - static_cast<double>(B[Index])));
|
||||
if (!bFiniteA || !bFiniteB || bSaturatedA || bSaturatedB || QA != QB)
|
||||
{
|
||||
++Result.Disagreements;
|
||||
if (Result.FirstIndex == INDEX_NONE)
|
||||
{
|
||||
Result.FirstIndex = LatticeIndices != nullptr
|
||||
? (*LatticeIndices)[Index] : static_cast<int64>(Index);
|
||||
}
|
||||
}
|
||||
if ((QA < 0) != (QB < 0))
|
||||
{
|
||||
++Result.SignDisagreements;
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> RegionEvalComparisonJson(const FQuantizedComparison& Comparison)
|
||||
{
|
||||
TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>();
|
||||
Json->SetNumberField(TEXT("quantized_disagreements"), static_cast<double>(Comparison.Disagreements));
|
||||
Json->SetNumberField(TEXT("quantized_sign_disagreements"), static_cast<double>(Comparison.SignDisagreements));
|
||||
Json->SetNumberField(TEXT("max_abs_float_delta"), Comparison.MaxAbsoluteFloatDelta);
|
||||
Json->SetNumberField(TEXT("first_disagreement_linear_index"), static_cast<double>(Comparison.FirstIndex));
|
||||
return Json;
|
||||
}
|
||||
|
||||
int32 RunRegionEval(const FString& Params)
|
||||
{
|
||||
FExploreArguments Arguments;
|
||||
FString Error;
|
||||
if (!ParseArguments(Params, Arguments, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] argument error: %s"), *Error);
|
||||
return 2;
|
||||
}
|
||||
int32 RequestedSamples = 262144;
|
||||
FParse::Value(*Params, TEXT("regionsamples="), RequestedSamples);
|
||||
RequestedSamples = FMath::Clamp(RequestedSamples, 32768, 1000000);
|
||||
IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true);
|
||||
|
||||
const int32 Seeds[] = { 1337, 7331, 19427, 30241, 41011 };
|
||||
TArray<TSharedPtr<FJsonValue>> SeedReports;
|
||||
TArray<TSharedPtr<FJsonValue>> OldTimingReports;
|
||||
TArray<TSharedPtr<FJsonValue>> PreparedTimingReports;
|
||||
TArray<float> OldFullDensities;
|
||||
TArray<float> PreparedFullDensities;
|
||||
TArray<int64> PrimarySampleIndices;
|
||||
FQuantizedComparison FullComparison;
|
||||
uint64 OldFullRawHash = 0;
|
||||
uint64 PreparedFullRawHash = 0;
|
||||
uint64 OldFullSaturatedSamples = 0;
|
||||
uint64 PreparedFullSaturatedSamples = 0;
|
||||
bool bAllSeedComparisonsPass = true;
|
||||
bool bFullDeterminismPass = true;
|
||||
int32 PrimaryTargetedSampleCount = 0;
|
||||
int32 PrimaryPassageCount = 0;
|
||||
int32 PrimaryEditChunkCount = 0;
|
||||
int32 PrimaryRecipeIndex = INDEX_NONE;
|
||||
uint32 PrimaryRecipeHash = 0;
|
||||
|
||||
// Cold full-ring timings use independent transient worlds for every pass. Caches cannot be
|
||||
// warmed by the comparison leg, and the seed/layout/composer/edit payload remains identical.
|
||||
for (int32 Trial = 0; Trial < 2; ++Trial)
|
||||
{
|
||||
FRegionEvalPass Pass;
|
||||
const bool bOk = RunRegionEvalPass(
|
||||
Arguments, Seeds[0], false, 1.0f, true, nullptr,
|
||||
RequestedSamples, Trial == 0, true, Pass, Error);
|
||||
if (!bOk)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] live full-ring trial failed: %s"), *Error);
|
||||
return 2;
|
||||
}
|
||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||
if (Trial == 0)
|
||||
{
|
||||
OldFullDensities = MoveTemp(Pass.Densities);
|
||||
OldFullRawHash = Pass.Timing.RawFloatHash;
|
||||
OldFullSaturatedSamples = Pass.Timing.SaturatedSamples;
|
||||
PrimarySampleIndices = Pass.SampleIndices;
|
||||
PrimaryTargetedSampleCount = Pass.TargetedSampleCount;
|
||||
PrimaryPassageCount = Pass.PassageCount;
|
||||
PrimaryEditChunkCount = Pass.EditChunkCount;
|
||||
PrimaryRecipeIndex = Pass.RecipeIndex;
|
||||
PrimaryRecipeHash = Pass.RecipeHash;
|
||||
}
|
||||
else
|
||||
{
|
||||
bFullDeterminismPass &= Pass.Timing.RawFloatHash == OldFullRawHash;
|
||||
}
|
||||
OldTimingReports.Add(MakeShared<FJsonValueObject>(RegionEvalTimingJson(Pass, false)));
|
||||
}
|
||||
if (PrimarySampleIndices.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] primary statistical sample set is empty."));
|
||||
return 2;
|
||||
}
|
||||
|
||||
for (int32 Trial = 0; Trial < 2; ++Trial)
|
||||
{
|
||||
FRegionEvalPass Pass;
|
||||
const bool bOk = RunRegionEvalPass(
|
||||
Arguments, Seeds[0], true, 1.0f, true, nullptr,
|
||||
RequestedSamples, Trial == 0, true, Pass, Error);
|
||||
if (!bOk)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] prepared full-ring trial failed: %s"), *Error);
|
||||
return 2;
|
||||
}
|
||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||
if (Trial == 0)
|
||||
{
|
||||
PreparedFullDensities = MoveTemp(Pass.Densities);
|
||||
PreparedFullRawHash = Pass.Timing.RawFloatHash;
|
||||
PreparedFullSaturatedSamples = Pass.Timing.SaturatedSamples;
|
||||
FullComparison = CompareQuantizedArrays(OldFullDensities, PreparedFullDensities);
|
||||
}
|
||||
else
|
||||
{
|
||||
bFullDeterminismPass &= Pass.Timing.RawFloatHash == PreparedFullRawHash;
|
||||
}
|
||||
PreparedTimingReports.Add(MakeShared<FJsonValueObject>(RegionEvalTimingJson(Pass, true)));
|
||||
}
|
||||
bAllSeedComparisonsPass &= FullComparison.Disagreements == 0
|
||||
&& FullComparison.SignDisagreements == 0
|
||||
&& OldFullSaturatedSamples == 0
|
||||
&& PreparedFullSaturatedSamples == 0;
|
||||
|
||||
// Statistical fixed-lattice comparisons across four additional independent seeds. The sample
|
||||
// set is a deterministic affine permutation of the entire 259^3 ring plus dense strata,
|
||||
// passage-floor, landing, carve, and fill probes.
|
||||
for (int32 SeedIndex = 1; SeedIndex < UE_ARRAY_COUNT(Seeds); ++SeedIndex)
|
||||
{
|
||||
FRegionEvalPass OldPass;
|
||||
if (!RunRegionEvalPass(
|
||||
Arguments, Seeds[SeedIndex], false, 1.0f, false, nullptr,
|
||||
RequestedSamples, true, true, OldPass, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] seed %d live sample failed: %s"),
|
||||
Seeds[SeedIndex], *Error);
|
||||
return 2;
|
||||
}
|
||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||
FRegionEvalPass NewPass;
|
||||
if (!RunRegionEvalPass(
|
||||
Arguments, Seeds[SeedIndex], true, 1.0f, false, &OldPass.SampleIndices,
|
||||
RequestedSamples, true, true, NewPass, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] seed %d prepared sample failed: %s"),
|
||||
Seeds[SeedIndex], *Error);
|
||||
return 2;
|
||||
}
|
||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||
const FQuantizedComparison Comparison = CompareQuantizedArrays(
|
||||
OldPass.Densities, NewPass.Densities, &OldPass.SampleIndices);
|
||||
bAllSeedComparisonsPass &= Comparison.Disagreements == 0
|
||||
&& Comparison.SignDisagreements == 0
|
||||
&& OldPass.Timing.SaturatedSamples == 0
|
||||
&& NewPass.Timing.SaturatedSamples == 0;
|
||||
|
||||
TSharedRef<FJsonObject> SeedJson = MakeShared<FJsonObject>();
|
||||
SeedJson->SetNumberField(TEXT("seed"), Seeds[SeedIndex]);
|
||||
SeedJson->SetNumberField(TEXT("samples"), OldPass.Timing.SampleCount);
|
||||
SeedJson->SetNumberField(TEXT("targeted_route_edit_boundary_samples"), OldPass.TargetedSampleCount);
|
||||
SeedJson->SetNumberField(TEXT("composer_recipe_index"), OldPass.RecipeIndex);
|
||||
SeedJson->SetStringField(TEXT("composer_recipe_hash"), FString::Printf(TEXT("%08x"), OldPass.RecipeHash));
|
||||
SeedJson->SetNumberField(TEXT("passage_count"), OldPass.PassageCount);
|
||||
SeedJson->SetNumberField(TEXT("edit_chunk_count"), OldPass.EditChunkCount);
|
||||
SeedJson->SetObjectField(TEXT("comparison"), RegionEvalComparisonJson(Comparison));
|
||||
SeedJson->SetObjectField(TEXT("live_candidate_visits"), RegionEvalTimingJson(OldPass, false));
|
||||
SeedJson->SetObjectField(TEXT("prepared_candidate_visits"), RegionEvalTimingJson(NewPass, true));
|
||||
SeedReports.Add(MakeShared<FJsonValueObject>(SeedJson));
|
||||
}
|
||||
|
||||
// Deliberately shrink one reach, evaluate the same statistical lattice points, and require
|
||||
// the reference comparison to catch omitted room influence. The scale is scoped to this one
|
||||
// commandlet pass and every worker resets it to 1.0 afterward.
|
||||
FRegionEvalPass ShrunkPass;
|
||||
if (!RunRegionEvalPass(
|
||||
Arguments, Seeds[0], true, 0.50f, false, &PrimarySampleIndices,
|
||||
RequestedSamples, false, true, ShrunkPass, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] deliberate reach-shrink run failed: %s"), *Error);
|
||||
return 2;
|
||||
}
|
||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||
FQuantizedComparison ShrinkComparison;
|
||||
ShrinkComparison.FirstIndex = INDEX_NONE;
|
||||
for (int32 Sample = 0; Sample < ShrunkPass.Densities.Num(); ++Sample)
|
||||
{
|
||||
const int64 LatticeIndex = PrimarySampleIndices[Sample];
|
||||
bool bSaturatedOld = false, bFiniteOld = false;
|
||||
bool bSaturatedShrink = false, bFiniteShrink = false;
|
||||
const int16 QOld = VoxelForgeRegionEval::QuantizeDensity(
|
||||
OldFullDensities[static_cast<int32>(LatticeIndex)], bSaturatedOld, bFiniteOld);
|
||||
const int16 QShrink = VoxelForgeRegionEval::QuantizeDensity(
|
||||
ShrunkPass.Densities[Sample], bSaturatedShrink, bFiniteShrink);
|
||||
ShrinkComparison.MaxAbsoluteFloatDelta = FMath::Max(
|
||||
ShrinkComparison.MaxAbsoluteFloatDelta,
|
||||
FMath::Abs(static_cast<double>(OldFullDensities[static_cast<int32>(LatticeIndex)])
|
||||
- static_cast<double>(ShrunkPass.Densities[Sample])));
|
||||
if (!bFiniteOld || !bFiniteShrink || bSaturatedOld || bSaturatedShrink || QOld != QShrink)
|
||||
{
|
||||
++ShrinkComparison.Disagreements;
|
||||
if (ShrinkComparison.FirstIndex == INDEX_NONE) ShrinkComparison.FirstIndex = LatticeIndex;
|
||||
}
|
||||
if ((QOld < 0) != (QShrink < 0)) ++ShrinkComparison.SignDisagreements;
|
||||
}
|
||||
|
||||
double PreparedWallSum = 0.0;
|
||||
for (const TSharedPtr<FJsonValue>& Value : PreparedTimingReports)
|
||||
{
|
||||
const TSharedPtr<FJsonObject> Json = Value->AsObject();
|
||||
PreparedWallSum += Json->GetNumberField(TEXT("wall_seconds"));
|
||||
}
|
||||
const double PreparedMedianWall = PreparedWallSum / 2.0;
|
||||
double OldWallSum = 0.0;
|
||||
for (const TSharedPtr<FJsonValue>& Value : OldTimingReports)
|
||||
{
|
||||
OldWallSum += Value->AsObject()->GetNumberField(TEXT("wall_seconds"));
|
||||
}
|
||||
const double OldMedianWall = OldWallSum / 2.0;
|
||||
bool bSixWorkersObserved = true;
|
||||
for (const TSharedPtr<FJsonValue>& Value : OldTimingReports)
|
||||
{
|
||||
bSixWorkersObserved &= Value->AsObject()->GetNumberField(TEXT("observed_worker_threads"))
|
||||
>= VoxelForgeRegionEval::WorkerCount;
|
||||
}
|
||||
for (const TSharedPtr<FJsonValue>& Value : PreparedTimingReports)
|
||||
{
|
||||
bSixWorkersObserved &= Value->AsObject()->GetNumberField(TEXT("observed_worker_threads"))
|
||||
>= VoxelForgeRegionEval::WorkerCount;
|
||||
}
|
||||
const bool bSpeedPass = PreparedMedianWall <= 0.600 && bSixWorkersObserved;
|
||||
|
||||
TSharedRef<FJsonObject> Report = MakeShared<FJsonObject>();
|
||||
Report->SetNumberField(TEXT("schema_version"), 1);
|
||||
Report->SetStringField(TEXT("route"), TEXT("editor-only explore commandlet; required Unreal composer, manager, and generator types"));
|
||||
Report->SetStringField(TEXT("evaluator"), TEXT("live GetDensityAt final field with editor-only 8^3 SDF room/join/tunnel candidate bins"));
|
||||
Report->SetStringField(TEXT("comparison_quantization"), TEXT("signed int16; round ties away from zero; multiply density by 16; saturate outside [-2048, 2047.9375] density voxels"));
|
||||
Report->SetNumberField(TEXT("worker_count"), VoxelForgeRegionEval::WorkerCount);
|
||||
Report->SetNumberField(TEXT("ring_dimension"), VoxelForgeRegionEval::RingDimension);
|
||||
Report->SetNumberField(TEXT("ring_lattice_samples"), static_cast<double>(VoxelForgeRegionEval::RingDimension) * VoxelForgeRegionEval::RingDimension * VoxelForgeRegionEval::RingDimension);
|
||||
Report->SetNumberField(TEXT("ring_interval_span_voxels"), VoxelForgeRegionEval::RingDimension - 1);
|
||||
Report->SetNumberField(TEXT("ring_wall_p50_live_seconds"), OldMedianWall);
|
||||
Report->SetNumberField(TEXT("ring_wall_p50_prepared_seconds"), PreparedMedianWall);
|
||||
Report->SetBoolField(TEXT("speed_pass_600ms"), bSpeedPass);
|
||||
Report->SetBoolField(TEXT("all_full_ring_trials_used_six_threads"), bSixWorkersObserved);
|
||||
Report->SetBoolField(TEXT("five_seed_correctness_pass"), bAllSeedComparisonsPass);
|
||||
Report->SetBoolField(TEXT("two_run_determinism_pass"), bFullDeterminismPass);
|
||||
Report->SetStringField(TEXT("determinism_basis"), TEXT("bitwise float32 lattice hash on two independent cold worlds for live and prepared paths"));
|
||||
Report->SetObjectField(TEXT("full_ring_comparison"), RegionEvalComparisonJson(FullComparison));
|
||||
Report->SetArrayField(TEXT("live_full_ring_trials"), OldTimingReports);
|
||||
Report->SetArrayField(TEXT("prepared_full_ring_trials"), PreparedTimingReports);
|
||||
Report->SetArrayField(TEXT("additional_seed_comparisons"), SeedReports);
|
||||
|
||||
TSharedRef<FJsonObject> Coverage = MakeShared<FJsonObject>();
|
||||
Coverage->SetStringField(TEXT("owner_settings"), Arguments.SettingsReference);
|
||||
Coverage->SetStringField(TEXT("owner_strate"), Arguments.StrateReference);
|
||||
Coverage->SetNumberField(TEXT("primary_seed"), Seeds[0]);
|
||||
Coverage->SetNumberField(TEXT("primary_targeted_route_edit_boundary_samples"), PrimaryTargetedSampleCount);
|
||||
Coverage->SetNumberField(TEXT("target_passages"), PrimaryPassageCount);
|
||||
Coverage->SetNumberField(TEXT("transient_edit_chunks"), PrimaryEditChunkCount);
|
||||
Coverage->SetNumberField(TEXT("composer_recipe_index"), PrimaryRecipeIndex);
|
||||
Coverage->SetStringField(TEXT("composer_recipe_hash"), FString::Printf(TEXT("%08x"), PrimaryRecipeHash));
|
||||
Coverage->SetStringField(TEXT("composer_recipe_family"), TEXT("RoomGraphSource with TunnelNetwork params; rolled by VF_RollStrateStructure"));
|
||||
Coverage->SetNumberField(TEXT("room_warp_strength_lower_bound"), 1.25);
|
||||
Coverage->SetStringField(TEXT("room_bin_reach"), TEXT("cached CullRadiusSq: shape extent + floor-relief envelope + 3x SDF blend + conservative terrain-op reach; mouth-rise reach uses center-to-mouth distance + blend"));
|
||||
Coverage->SetStringField(TEXT("tunnel_bin_reach"), TEXT("union of finalized warped-SDF and world-chain centerline bounds expanded by their influence radii, plus both conservative bounding spheres"));
|
||||
Coverage->SetStringField(TEXT("warp_coordinates"), TEXT("room bins are queried after the exact live cave warp in the same coordinate domain; tunnel bins union warped-SDF and unwarped world-chain reach"));
|
||||
Coverage->SetStringField(TEXT("tunnel_bin_unit"), TEXT("whole cached tunnel chain; per-chain segment loops remain in the exact evaluator"));
|
||||
Coverage->SetStringField(TEXT("unbinned_writers"), TEXT("passage/landing posts, disturbances, and player diff edits stay in canonical GetDensityAt tail; this is the candidate-bin ablation, not the full region compiler"));
|
||||
Coverage->SetBoolField(TEXT("route_landing_floor_probes"), PrimaryPassageCount > 0);
|
||||
Coverage->SetBoolField(TEXT("strata_top_and_bottom_boundary_sweep"), true);
|
||||
Coverage->SetBoolField(TEXT("player_carve_and_fill_edits"), true);
|
||||
Report->SetObjectField(TEXT("content_coverage"), Coverage);
|
||||
|
||||
TSharedRef<FJsonObject> Shrink = MakeShared<FJsonObject>();
|
||||
Shrink->SetNumberField(TEXT("room_reach_scale"), 0.50);
|
||||
Shrink->SetBoolField(TEXT("deliberately_shrunk_bin_was_caught"), ShrinkComparison.Disagreements > 0);
|
||||
Shrink->SetNumberField(TEXT("saturated_samples"), static_cast<double>(ShrunkPass.Timing.SaturatedSamples));
|
||||
Shrink->SetNumberField(TEXT("quantized_disagreements"), static_cast<double>(ShrinkComparison.Disagreements));
|
||||
Shrink->SetNumberField(TEXT("sign_disagreements"), static_cast<double>(ShrinkComparison.SignDisagreements));
|
||||
Shrink->SetNumberField(TEXT("max_abs_float_delta"), ShrinkComparison.MaxAbsoluteFloatDelta);
|
||||
Shrink->SetNumberField(TEXT("first_disagreement_linear_index"), static_cast<double>(ShrinkComparison.FirstIndex));
|
||||
Report->SetObjectField(TEXT("deliberate_reach_shrink"), Shrink);
|
||||
|
||||
const bool bPassed = bAllSeedComparisonsPass && bFullDeterminismPass
|
||||
&& ShrinkComparison.Disagreements > 0
|
||||
&& ShrunkPass.Timing.SaturatedSamples == 0;
|
||||
Report->SetStringField(TEXT("status"), bPassed ? TEXT("correctness_pass_speed_measured") : TEXT("correctness_failed"));
|
||||
Report->SetBoolField(TEXT("pass"), bPassed);
|
||||
Report->SetStringField(TEXT("speed_verdict"), bSpeedPass
|
||||
? TEXT("meets 600 ms") : TEXT("misses 600 ms; candidate bins are an ablation, not a full region compiler"));
|
||||
FString JsonText;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonText);
|
||||
if (!FJsonSerializer::Serialize(Report, Writer)
|
||||
|| !FFileHelper::SaveStringToFile(
|
||||
JsonText, *FPaths::Combine(Arguments.OutDirectory, TEXT("region_eval.json")),
|
||||
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] could not write region_eval.json."));
|
||||
return 2;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeRegionEval] status=%s speed600=%s live_ms=%.3f prepared_ms=%.3f "
|
||||
"full_q16_diff=%lld five_seed=%s deterministic=%s shrink_q16_diff=%lld report=%s"),
|
||||
bPassed ? TEXT("pass") : TEXT("fail"), bSpeedPass ? TEXT("yes") : TEXT("no"),
|
||||
OldMedianWall * 1000.0, PreparedMedianWall * 1000.0,
|
||||
static_cast<long long>(FullComparison.Disagreements),
|
||||
bAllSeedComparisonsPass ? TEXT("pass") : TEXT("fail"),
|
||||
bFullDeterminismPass ? TEXT("pass") : TEXT("fail"),
|
||||
static_cast<long long>(ShrinkComparison.Disagreements),
|
||||
*FPaths::Combine(Arguments.OutDirectory, TEXT("region_eval.json")));
|
||||
return bPassed ? 0 : 2;
|
||||
}
|
||||
|
||||
class FGeneratorDensitySampler final : public IVoxelStrateDensitySampler
|
||||
{
|
||||
public:
|
||||
@@ -8593,6 +9326,14 @@ int32 RunBatch(const FString& Params)
|
||||
|
||||
int32 UVoxelForgeExploreCommandlet::Main(const FString& Params)
|
||||
{
|
||||
const bool bRegionEvalSwitch = FParse::Param(*Params, TEXT("regioneval"));
|
||||
int32 RegionEvalFlag = 0;
|
||||
const bool bRegionEvalValue = FParse::Value(*Params, TEXT("regioneval="), RegionEvalFlag);
|
||||
if (bRegionEvalSwitch || (bRegionEvalValue && RegionEvalFlag != 0))
|
||||
{
|
||||
return RunRegionEval(Params);
|
||||
}
|
||||
|
||||
FString SymbolizeCsvPath;
|
||||
if (FParse::Value(*Params, TEXT("symbolizestacks="), SymbolizeCsvPath))
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user