measure(step2b): per-brick setup rebuilds the chunk caches -- 10x slower; live cost attributed
Editor-only brick API (<= 8^3 points per call, TunnelNetwork/Underwater op-stack routes, 1.16% scalar fallback). Exact: 0 Q16/sign disagreements on the full 259^3 ring and the five-seed probe set, same hash on 1 and 6 workers, reach shrink still caught (18,107 Q16 / 735 sign). Speed: 69.19 s wall, 17.6 us/sample vs live 7.04 s, 1.65 us/sample. Stack sampler (inclusive): BuildChunkCache 93.5%, PropagateSdfOverBox 92.5%, BuildActiveOpList 92.6% -- each brick rebuilds the cache windows and active-op lists; EvalBlock itself is 3.6%. Live path attribution (inclusive, 68,934 stacks): op-stack RoomGraph EvalBlock 54.4%, SDF and spatial candidate scans ~28%, tunnel core 18.4%, passage floor segment 15.0%, structural posts 14.6%, noise octaves 2.6%, disturbances 0.6%, diff layer 0.3%. 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
41a9265ebd
commit
fd9f809c1c
7 files changed
+749
-97
No files matched your search
@@ -1475,6 +1475,40 @@ struct FRegionEvalPass
|
||||
FString EndedUtc;
|
||||
};
|
||||
|
||||
struct FRegionEvalStackSamplerSession
|
||||
{
|
||||
bool bStarted = false;
|
||||
|
||||
~FRegionEvalStackSamplerSession()
|
||||
{
|
||||
if (bStarted)
|
||||
{
|
||||
FVoxelStackSampler::Get().StopAndWrite();
|
||||
}
|
||||
}
|
||||
|
||||
bool Start(uint32 IntervalUs, const FString& OutputDirectory,
|
||||
const TCHAR* Scenario = TEXT("region_eval_live_ring"))
|
||||
{
|
||||
bStarted = FVoxelStackSampler::Get().Start(
|
||||
IntervalUs, OutputDirectory, Scenario);
|
||||
return bStarted;
|
||||
}
|
||||
|
||||
bool Stop(FVoxelStackSampler::FSummary& OutSummary)
|
||||
{
|
||||
if (!bStarted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OutSummary = FVoxelStackSampler::Get().StopAndWrite();
|
||||
bStarted = false;
|
||||
return OutSummary.bOutputWritten
|
||||
&& OutSummary.RegisteredThreadSamples > 0
|
||||
&& OutSummary.RetainedSamples > 0;
|
||||
}
|
||||
};
|
||||
|
||||
int32 FindRoomGraphRecipeIndex(int32 Seed)
|
||||
{
|
||||
for (int32 Index = 0; Index < 64; ++Index)
|
||||
@@ -1724,6 +1758,7 @@ bool RunRegionEvalPass(
|
||||
const FExploreArguments& BaseArguments,
|
||||
int32 Seed,
|
||||
bool bPrepared,
|
||||
bool bBrickProgram,
|
||||
float ReachScale,
|
||||
bool bFullRing,
|
||||
const TArray<int64>* RequestedIndices,
|
||||
@@ -1731,7 +1766,8 @@ bool RunRegionEvalPass(
|
||||
bool bCountCandidates,
|
||||
bool bKeepDensities,
|
||||
FRegionEvalPass& OutPass,
|
||||
FString& OutError)
|
||||
FString& OutError,
|
||||
int32 RequestedWorkerCount = VoxelForgeRegionEval::WorkerCount)
|
||||
{
|
||||
OutPass = FRegionEvalPass();
|
||||
FExploreWorld World;
|
||||
@@ -1773,7 +1809,7 @@ bool RunRegionEvalPass(
|
||||
bFullRing ? nullptr : &OutPass.SampleIndices,
|
||||
bFullRing, bPrepared, ReachScale,
|
||||
bKeepDensities ? &OutPass.Densities : nullptr,
|
||||
OutPass.Timing);
|
||||
OutPass.Timing, bBrickProgram, RequestedWorkerCount);
|
||||
OutPass.EndedUtc = FDateTime::UtcNow().ToIso8601();
|
||||
if (!bEvaluated)
|
||||
{
|
||||
@@ -1799,19 +1835,27 @@ bool RunRegionEvalPass(
|
||||
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("path"), Pass.Timing.bBrickProgram
|
||||
? TEXT("editor_region_brick_program")
|
||||
: (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("evaluated_brick_points"), static_cast<double>(Pass.Timing.EvaluatedSamples));
|
||||
Json->SetNumberField(TEXT("brick_count"), static_cast<double>(Pass.Timing.BrickCount));
|
||||
Json->SetNumberField(TEXT("fallback_points"), static_cast<double>(Pass.Timing.FallbackSamples));
|
||||
Json->SetNumberField(TEXT("fallback_share"), Pass.Timing.FallbackShare);
|
||||
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"));
|
||||
const FString WorkerCpuSource = Pass.Timing.bWorkerCpuFromOsThreadTimes
|
||||
? FString::Printf(TEXT("summed Windows GetThreadTimes user+kernel CPU across %d fixed work partitions"),
|
||||
Pass.Timing.Workers)
|
||||
: TEXT("summed worker elapsed fallback; OS thread CPU counters unavailable");
|
||||
Json->SetStringField(TEXT("worker_cpu_source"), WorkerCpuSource);
|
||||
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"),
|
||||
@@ -1910,8 +1954,112 @@ int32 RunRegionEval(const FString& Params)
|
||||
int32 RequestedSamples = 262144;
|
||||
FParse::Value(*Params, TEXT("regionsamples="), RequestedSamples);
|
||||
RequestedSamples = FMath::Clamp(RequestedSamples, 32768, 1000000);
|
||||
int32 RequestedWorkerCount = VoxelForgeRegionEval::WorkerCount;
|
||||
FParse::Value(*Params, TEXT("regionworkers="), RequestedWorkerCount);
|
||||
RequestedWorkerCount = FMath::Clamp(RequestedWorkerCount, 1, 32);
|
||||
IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true);
|
||||
|
||||
if (FParse::Param(*Params, TEXT("regionhashprobe")))
|
||||
{
|
||||
FRegionEvalStackSamplerSession WorkerHashStackSampler;
|
||||
if (Arguments.bSampleStacks
|
||||
&& !WorkerHashStackSampler.Start(
|
||||
Arguments.SampleStacksIntervalUs, Arguments.OutDirectory,
|
||||
TEXT("region_eval_brick_worker_hash")))
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelForgeRegionEval] worker hash stack sampler could not be started."));
|
||||
return 2;
|
||||
}
|
||||
|
||||
FRegionEvalPass WorkerHashPass;
|
||||
const bool bHashPassSucceeded = RunRegionEvalPass(
|
||||
Arguments, 1337, true, true, 1.0f, true, nullptr,
|
||||
RequestedSamples, false, true, WorkerHashPass, Error,
|
||||
RequestedWorkerCount);
|
||||
bool bStackCaptureSucceeded = true;
|
||||
if (Arguments.bSampleStacks)
|
||||
{
|
||||
FVoxelStackSampler::FSummary StackSummary;
|
||||
bStackCaptureSucceeded = WorkerHashStackSampler.Stop(StackSummary);
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeRegionEval] worker-hash stack capture registered=%llu retained=%llu "
|
||||
"raw=%s summary=%s"),
|
||||
static_cast<unsigned long long>(StackSummary.RegisteredThreadSamples),
|
||||
static_cast<unsigned long long>(StackSummary.RetainedSamples),
|
||||
*StackSummary.RawSamplesPath, *StackSummary.SummaryPath);
|
||||
}
|
||||
if (!bHashPassSucceeded || !bStackCaptureSucceeded)
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelForgeRegionEval] worker hash probe failed: %s"),
|
||||
Error.IsEmpty() ? TEXT("stack capture retained no worker samples") : *Error);
|
||||
return 2;
|
||||
}
|
||||
|
||||
FString ReferenceRawHash;
|
||||
FParse::Value(*Params, TEXT("regionreferencehash="), ReferenceRawHash);
|
||||
const FString ActualRawHash = FString::Printf(TEXT("%016llx"),
|
||||
static_cast<unsigned long long>(WorkerHashPass.Timing.RawFloatHash));
|
||||
const bool bHasReferenceHash = !ReferenceRawHash.IsEmpty();
|
||||
const bool bMatchesReference = bHasReferenceHash
|
||||
&& ActualRawHash.Equals(ReferenceRawHash, ESearchCase::IgnoreCase);
|
||||
const bool bProbePassed = bMatchesReference
|
||||
&& WorkerHashPass.Timing.SampleCount
|
||||
== static_cast<int64>(VoxelForgeRegionEval::RingDimension)
|
||||
* VoxelForgeRegionEval::RingDimension * VoxelForgeRegionEval::RingDimension
|
||||
&& WorkerHashPass.Timing.SaturatedSamples == 0
|
||||
&& WorkerHashPass.Timing.NonFiniteSamples == 0;
|
||||
|
||||
TSharedRef<FJsonObject> ProbeReport = MakeShared<FJsonObject>();
|
||||
ProbeReport->SetBoolField(TEXT("pass"), bProbePassed);
|
||||
ProbeReport->SetBoolField(TEXT("worker_count_hash_probe"), true);
|
||||
ProbeReport->SetNumberField(TEXT("worker_count"), WorkerHashPass.Timing.Workers);
|
||||
ProbeReport->SetNumberField(TEXT("observed_worker_threads"),
|
||||
WorkerHashPass.Timing.ObservedWorkerThreads);
|
||||
ProbeReport->SetNumberField(TEXT("samples"), WorkerHashPass.Timing.SampleCount);
|
||||
ProbeReport->SetNumberField(TEXT("brick_count"), WorkerHashPass.Timing.BrickCount);
|
||||
ProbeReport->SetNumberField(TEXT("fallback_points"), WorkerHashPass.Timing.FallbackSamples);
|
||||
ProbeReport->SetNumberField(TEXT("fallback_share"), WorkerHashPass.Timing.FallbackShare);
|
||||
ProbeReport->SetNumberField(TEXT("wall_seconds"), WorkerHashPass.Timing.WallSeconds);
|
||||
ProbeReport->SetNumberField(TEXT("worker_cpu_seconds"), WorkerHashPass.Timing.WorkerCpuSeconds);
|
||||
ProbeReport->SetStringField(TEXT("reference_raw_float_hash_fnv1a64"), ReferenceRawHash);
|
||||
ProbeReport->SetStringField(TEXT("raw_float_hash_fnv1a64"), ActualRawHash);
|
||||
ProbeReport->SetBoolField(TEXT("matches_reference"), bMatchesReference);
|
||||
|
||||
FString ProbeJsonText;
|
||||
TSharedRef<TJsonWriter<>> ProbeWriter = TJsonWriterFactory<>::Create(&ProbeJsonText);
|
||||
if (!FJsonSerializer::Serialize(ProbeReport, ProbeWriter)
|
||||
|| !FFileHelper::SaveStringToFile(
|
||||
ProbeJsonText, *FPaths::Combine(Arguments.OutDirectory, TEXT("region_eval.json")),
|
||||
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelForgeRegionEval] could not write worker hash probe report."));
|
||||
return 2;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeRegionEval] worker hash probe workers=%d observed=%d samples=%lld "
|
||||
"raw=%s reference_match=%s report=%s"),
|
||||
WorkerHashPass.Timing.Workers, WorkerHashPass.Timing.ObservedWorkerThreads,
|
||||
static_cast<long long>(WorkerHashPass.Timing.SampleCount), *ActualRawHash,
|
||||
bMatchesReference ? TEXT("yes") : TEXT("no"),
|
||||
*FPaths::Combine(Arguments.OutDirectory, TEXT("region_eval.json")));
|
||||
return bProbePassed ? 0 : 2;
|
||||
}
|
||||
|
||||
// Region-eval returns before the normal Explore sampler setup. Capture only its two cold live
|
||||
// ring trials so prepared-path and multi-seed work cannot pollute the baseline attribution.
|
||||
FRegionEvalStackSamplerSession StackSamplerSession;
|
||||
if (Arguments.bSampleStacks
|
||||
&& !StackSamplerSession.Start(
|
||||
Arguments.SampleStacksIntervalUs, Arguments.OutDirectory))
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelForgeRegionEval] requested stack sampler could not be started."));
|
||||
return 2;
|
||||
}
|
||||
|
||||
const int32 Seeds[] = { 1337, 7331, 19427, 30241, 41011 };
|
||||
TArray<TSharedPtr<FJsonValue>> SeedReports;
|
||||
TArray<TSharedPtr<FJsonValue>> OldTimingReports;
|
||||
@@ -1938,8 +2086,8 @@ int32 RunRegionEval(const FString& Params)
|
||||
{
|
||||
FRegionEvalPass Pass;
|
||||
const bool bOk = RunRegionEvalPass(
|
||||
Arguments, Seeds[0], false, 1.0f, true, nullptr,
|
||||
RequestedSamples, Trial == 0, true, Pass, Error);
|
||||
Arguments, Seeds[0], false, false, 1.0f, true, nullptr,
|
||||
RequestedSamples, Trial == 0 && !Arguments.bSampleStacks, true, Pass, Error);
|
||||
if (!bOk)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] live full-ring trial failed: %s"), *Error);
|
||||
@@ -1964,6 +2112,22 @@ int32 RunRegionEval(const FString& Params)
|
||||
}
|
||||
OldTimingReports.Add(MakeShared<FJsonValueObject>(RegionEvalTimingJson(Pass, false)));
|
||||
}
|
||||
if (Arguments.bSampleStacks)
|
||||
{
|
||||
FVoxelStackSampler::FSummary StackSummary;
|
||||
if (!StackSamplerSession.Stop(StackSummary))
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelForgeRegionEval] stack capture retained no live-ring worker samples."));
|
||||
return 2;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeRegionEval] live-ring stack capture registered=%llu retained=%llu "
|
||||
"raw=%s summary=%s"),
|
||||
static_cast<unsigned long long>(StackSummary.RegisteredThreadSamples),
|
||||
static_cast<unsigned long long>(StackSummary.RetainedSamples),
|
||||
*StackSummary.RawSamplesPath, *StackSummary.SummaryPath);
|
||||
}
|
||||
if (PrimarySampleIndices.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] primary statistical sample set is empty."));
|
||||
@@ -1974,7 +2138,7 @@ int32 RunRegionEval(const FString& Params)
|
||||
{
|
||||
FRegionEvalPass Pass;
|
||||
const bool bOk = RunRegionEvalPass(
|
||||
Arguments, Seeds[0], true, 1.0f, true, nullptr,
|
||||
Arguments, Seeds[0], true, true, 1.0f, true, nullptr,
|
||||
RequestedSamples, Trial == 0, true, Pass, Error);
|
||||
if (!bOk)
|
||||
{
|
||||
@@ -2003,11 +2167,46 @@ int32 RunRegionEval(const FString& Params)
|
||||
// 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.
|
||||
if (Arguments.bSampleStacks)
|
||||
{
|
||||
FRegionEvalStackSamplerSession BrickStackSamplerSession;
|
||||
if (!BrickStackSamplerSession.Start(
|
||||
Arguments.SampleStacksIntervalUs, Arguments.OutDirectory,
|
||||
TEXT("region_eval_brick_ring")))
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelForgeRegionEval] brick stack sampler could not be started."));
|
||||
return 2;
|
||||
}
|
||||
FRegionEvalPass ProfiledBrickPass;
|
||||
if (!RunRegionEvalPass(
|
||||
Arguments, Seeds[0], true, true, 1.0f, true, nullptr,
|
||||
RequestedSamples, false, false, ProfiledBrickPass, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelForgeRegionEval] brick attribution run failed: %s"), *Error);
|
||||
return 2;
|
||||
}
|
||||
FVoxelStackSampler::FSummary BrickStackSummary;
|
||||
if (!BrickStackSamplerSession.Stop(BrickStackSummary))
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelForgeRegionEval] brick stack capture retained no worker samples."));
|
||||
return 2;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeRegionEval] brick stack capture registered=%llu retained=%llu "
|
||||
"raw=%s summary=%s"),
|
||||
static_cast<unsigned long long>(BrickStackSummary.RegisteredThreadSamples),
|
||||
static_cast<unsigned long long>(BrickStackSummary.RetainedSamples),
|
||||
*BrickStackSummary.RawSamplesPath, *BrickStackSummary.SummaryPath);
|
||||
}
|
||||
|
||||
for (int32 SeedIndex = 1; SeedIndex < UE_ARRAY_COUNT(Seeds); ++SeedIndex)
|
||||
{
|
||||
FRegionEvalPass OldPass;
|
||||
if (!RunRegionEvalPass(
|
||||
Arguments, Seeds[SeedIndex], false, 1.0f, false, nullptr,
|
||||
Arguments, Seeds[SeedIndex], false, false, 1.0f, false, nullptr,
|
||||
RequestedSamples, true, true, OldPass, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] seed %d live sample failed: %s"),
|
||||
@@ -2017,7 +2216,7 @@ int32 RunRegionEval(const FString& Params)
|
||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||
FRegionEvalPass NewPass;
|
||||
if (!RunRegionEvalPass(
|
||||
Arguments, Seeds[SeedIndex], true, 1.0f, false, &OldPass.SampleIndices,
|
||||
Arguments, Seeds[SeedIndex], true, true, 1.0f, false, &OldPass.SampleIndices,
|
||||
RequestedSamples, true, true, NewPass, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] seed %d prepared sample failed: %s"),
|
||||
@@ -2051,7 +2250,7 @@ int32 RunRegionEval(const FString& Params)
|
||||
// commandlet pass and every worker resets it to 1.0 afterward.
|
||||
FRegionEvalPass ShrunkPass;
|
||||
if (!RunRegionEvalPass(
|
||||
Arguments, Seeds[0], true, 0.50f, false, &PrimarySampleIndices,
|
||||
Arguments, Seeds[0], true, true, 0.50f, false, &PrimarySampleIndices,
|
||||
RequestedSamples, false, true, ShrunkPass, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeRegionEval] deliberate reach-shrink run failed: %s"), *Error);
|
||||
@@ -2110,7 +2309,7 @@ int32 RunRegionEval(const FString& Params)
|
||||
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("evaluator"), TEXT("editor-only 8^3 region brick program; block-evaluated composer operator core and canonical per-point final-field writers; unsupported routes use counted live fallbacks"));
|
||||
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);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "VoxelCaveMorphology.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
#include "VoxelGenerator.h"
|
||||
#include "VoxelStackSampler.h"
|
||||
#include "HAL/PlatformTLS.h"
|
||||
|
||||
#if PLATFORM_WINDOWS
|
||||
@@ -73,6 +74,24 @@ namespace VoxelForgeRegionEval
|
||||
{
|
||||
return static_cast<int64>(Dimension) * Dimension * Dimension;
|
||||
}
|
||||
|
||||
struct FRegionBrickSample
|
||||
{
|
||||
int32 LocalIndex = 0;
|
||||
int32 OutputIndex = 0;
|
||||
};
|
||||
|
||||
struct FRegionBrick
|
||||
{
|
||||
FIntVector Origin = FIntVector::ZeroValue;
|
||||
int32 SizeX = 0;
|
||||
int32 SizeY = 0;
|
||||
int32 SizeZ = 0;
|
||||
int32 LatticeX = 0;
|
||||
int32 LatticeY = 0;
|
||||
int32 LatticeZ = 0;
|
||||
TArray<FRegionBrickSample> RequestedSamples;
|
||||
};
|
||||
}
|
||||
|
||||
int64 LinearIndex(int32 X, int32 Y, int32 Z, int32 Dimension)
|
||||
@@ -128,9 +147,13 @@ namespace VoxelForgeRegionEval
|
||||
bool bPrepared,
|
||||
float ReachScale,
|
||||
TArray<float>* OutDensities,
|
||||
FTiming& OutTiming)
|
||||
FTiming& OutTiming,
|
||||
bool bBrickProgram,
|
||||
int32 RequestedWorkerCount)
|
||||
{
|
||||
OutTiming = FTiming();
|
||||
const int32 NumWorkers = FMath::Clamp(RequestedWorkerCount, 1, 32);
|
||||
OutTiming.Workers = NumWorkers;
|
||||
if (Dimension <= 0 || Dimension > 4096
|
||||
|| (bFullRing && SampleIndices != nullptr)
|
||||
|| (!bFullRing && SampleIndices == nullptr))
|
||||
@@ -145,6 +168,94 @@ namespace VoxelForgeRegionEval
|
||||
return false;
|
||||
}
|
||||
const double StartSeconds = FPlatformTime::Seconds();
|
||||
TArray<FRegionBrick> Bricks;
|
||||
TArray<int32> WorkBricks;
|
||||
if (bBrickProgram)
|
||||
{
|
||||
const int32 LastX = OriginVoxels.X + Dimension - 1;
|
||||
const int32 LastY = OriginVoxels.Y + Dimension - 1;
|
||||
const int32 LastZ = OriginVoxels.Z + Dimension - 1;
|
||||
const int32 FirstCellX = FMath::FloorToInt(static_cast<float>(OriginVoxels.X) / 8.0f);
|
||||
const int32 FirstCellY = FMath::FloorToInt(static_cast<float>(OriginVoxels.Y) / 8.0f);
|
||||
const int32 FirstCellZ = FMath::FloorToInt(static_cast<float>(OriginVoxels.Z) / 8.0f);
|
||||
const int32 LastCellX = FMath::FloorToInt(static_cast<float>(LastX) / 8.0f);
|
||||
const int32 LastCellY = FMath::FloorToInt(static_cast<float>(LastY) / 8.0f);
|
||||
const int32 LastCellZ = FMath::FloorToInt(static_cast<float>(LastZ) / 8.0f);
|
||||
const int32 CellsX = LastCellX - FirstCellX + 1;
|
||||
const int32 CellsY = LastCellY - FirstCellY + 1;
|
||||
const int32 CellsZ = LastCellZ - FirstCellZ + 1;
|
||||
Bricks.Reserve(CellsX * CellsY * CellsZ);
|
||||
for (int32 CellZ = FirstCellZ; CellZ <= LastCellZ; ++CellZ)
|
||||
for (int32 CellY = FirstCellY; CellY <= LastCellY; ++CellY)
|
||||
for (int32 CellX = FirstCellX; CellX <= LastCellX; ++CellX)
|
||||
{
|
||||
FRegionBrick& Brick = Bricks.AddDefaulted_GetRef();
|
||||
const int32 CellMinX = CellX * 8;
|
||||
const int32 CellMinY = CellY * 8;
|
||||
const int32 CellMinZ = CellZ * 8;
|
||||
Brick.Origin = FIntVector(
|
||||
FMath::Max(CellMinX, OriginVoxels.X),
|
||||
FMath::Max(CellMinY, OriginVoxels.Y),
|
||||
FMath::Max(CellMinZ, OriginVoxels.Z));
|
||||
const int32 EndX = FMath::Min(CellMinX + 8, LastX + 1);
|
||||
const int32 EndY = FMath::Min(CellMinY + 8, LastY + 1);
|
||||
const int32 EndZ = FMath::Min(CellMinZ + 8, LastZ + 1);
|
||||
Brick.SizeX = EndX - Brick.Origin.X;
|
||||
Brick.SizeY = EndY - Brick.Origin.Y;
|
||||
Brick.SizeZ = EndZ - Brick.Origin.Z;
|
||||
Brick.LatticeX = Brick.Origin.X - OriginVoxels.X;
|
||||
Brick.LatticeY = Brick.Origin.Y - OriginVoxels.Y;
|
||||
Brick.LatticeZ = Brick.Origin.Z - OriginVoxels.Z;
|
||||
}
|
||||
|
||||
if (bFullRing)
|
||||
{
|
||||
WorkBricks.Reserve(Bricks.Num());
|
||||
for (int32 BrickIndex = 0; BrickIndex < Bricks.Num(); ++BrickIndex)
|
||||
{
|
||||
WorkBricks.Add(BrickIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WorkBricks.Reserve(Bricks.Num());
|
||||
for (int32 OutputIndex = 0; OutputIndex < SampleIndices->Num(); ++OutputIndex)
|
||||
{
|
||||
const int64 LatticeIndex = (*SampleIndices)[OutputIndex];
|
||||
if (LatticeIndex < 0 || LatticeIndex >= TotalLatticeSamples)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int32 X = static_cast<int32>(LatticeIndex % Dimension);
|
||||
const int32 Y = static_cast<int32>((LatticeIndex / Dimension) % Dimension);
|
||||
const int32 Z = static_cast<int32>(LatticeIndex
|
||||
/ (static_cast<int64>(Dimension) * Dimension));
|
||||
const int32 WorldX = OriginVoxels.X + X;
|
||||
const int32 WorldY = OriginVoxels.Y + Y;
|
||||
const int32 WorldZ = OriginVoxels.Z + Z;
|
||||
const int32 CellX = FMath::FloorToInt(static_cast<float>(WorldX) / 8.0f);
|
||||
const int32 CellY = FMath::FloorToInt(static_cast<float>(WorldY) / 8.0f);
|
||||
const int32 CellZ = FMath::FloorToInt(static_cast<float>(WorldZ) / 8.0f);
|
||||
const int32 BrickIndex = ((CellZ - FirstCellZ) * CellsY
|
||||
+ (CellY - FirstCellY)) * CellsX + (CellX - FirstCellX);
|
||||
FRegionBrick& Brick = Bricks[BrickIndex];
|
||||
const int32 LocalX = WorldX - Brick.Origin.X;
|
||||
const int32 LocalY = WorldY - Brick.Origin.Y;
|
||||
const int32 LocalZ = WorldZ - Brick.Origin.Z;
|
||||
FRegionBrickSample& BrickSample = Brick.RequestedSamples.AddDefaulted_GetRef();
|
||||
BrickSample.LocalIndex =
|
||||
(LocalZ * Brick.SizeY + LocalY) * Brick.SizeX + LocalX;
|
||||
BrickSample.OutputIndex = OutputIndex;
|
||||
}
|
||||
for (int32 BrickIndex = 0; BrickIndex < Bricks.Num(); ++BrickIndex)
|
||||
{
|
||||
if (!Bricks[BrickIndex].RequestedSamples.IsEmpty())
|
||||
{
|
||||
WorkBricks.Add(BrickIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (OutDensities != nullptr)
|
||||
{
|
||||
if (SampleCount > MAX_int32)
|
||||
@@ -157,24 +268,78 @@ namespace VoxelForgeRegionEval
|
||||
const int32 OriginX = OriginVoxels.X;
|
||||
const int32 OriginY = OriginVoxels.Y;
|
||||
const int32 OriginZ = OriginVoxels.Z;
|
||||
double WorkerSeconds[WorkerCount] = {};
|
||||
double WorkerCpuSeconds[WorkerCount] = {};
|
||||
bool WorkerCpuFromOsThreadTimes[WorkerCount] = {};
|
||||
TArray<double> WorkerSeconds;
|
||||
TArray<double> WorkerCpuSeconds;
|
||||
TArray<uint8> WorkerCpuFromOsThreadTimes;
|
||||
TArray<uint32> WorkerThreadIds;
|
||||
TArray<uint64> WorkerNonFinite;
|
||||
TArray<uint64> WorkerEvaluatedSamples;
|
||||
TArray<uint64> WorkerFallbackSamples;
|
||||
TArray<uint64> WorkerEvaluationFailures;
|
||||
WorkerSeconds.SetNumZeroed(NumWorkers);
|
||||
WorkerCpuSeconds.SetNumZeroed(NumWorkers);
|
||||
WorkerCpuFromOsThreadTimes.SetNumZeroed(NumWorkers);
|
||||
WorkerThreadIds.SetNumZeroed(NumWorkers);
|
||||
WorkerNonFinite.SetNumZeroed(NumWorkers);
|
||||
WorkerEvaluatedSamples.SetNumZeroed(NumWorkers);
|
||||
WorkerFallbackSamples.SetNumZeroed(NumWorkers);
|
||||
WorkerEvaluationFailures.SetNumZeroed(NumWorkers);
|
||||
bool bAllWorkerCpuFromOsThreadTimes = true;
|
||||
uint32 WorkerThreadIds[WorkerCount] = {};
|
||||
uint64 WorkerNonFinite[WorkerCount] = {};
|
||||
ParallelFor(WorkerCount, [&](int32 WorkerIndex)
|
||||
ParallelFor(NumWorkers, [&](int32 WorkerIndex)
|
||||
{
|
||||
// Region-eval workers are not launched through AVoxelWorld::LoadTile, so they do not
|
||||
// inherit its task-level sampler registration. Keep this registration scoped to the
|
||||
// research worker partition; with sampling disabled it is one relaxed flag load.
|
||||
FScopedVoxelStackRegistration StackRegistration;
|
||||
VoxelCaveMorphology::SetResearchBrickBinningEnabled(bPrepared);
|
||||
VoxelCaveMorphology::SetResearchBrickRoomReachScale(ReachScale);
|
||||
WorkerThreadIds[WorkerIndex] = FPlatformTLS::GetCurrentThreadId();
|
||||
const int64 Begin = (SampleCount * WorkerIndex) / WorkerCount;
|
||||
const int64 End = (SampleCount * (WorkerIndex + 1)) / WorkerCount;
|
||||
const int64 WorkCount = bBrickProgram ? WorkBricks.Num() : SampleCount;
|
||||
const int64 Begin = (WorkCount * WorkerIndex) / NumWorkers;
|
||||
const int64 End = (WorkCount * (WorkerIndex + 1)) / NumWorkers;
|
||||
const double WorkerStart = FPlatformTime::Seconds();
|
||||
double WorkerCpuStart = 0.0;
|
||||
const bool bHaveWorkerCpuStart = GetCurrentThreadCpuSeconds(WorkerCpuStart);
|
||||
TArray<float> BrickDensities;
|
||||
for (int64 OutputIndex = Begin; OutputIndex < End; ++OutputIndex)
|
||||
{
|
||||
if (bBrickProgram)
|
||||
{
|
||||
const FRegionBrick& Brick = Bricks[WorkBricks[static_cast<int32>(OutputIndex)]];
|
||||
int32 BrickFallbackSamples = 0;
|
||||
if (!Generator.EvaluateDensityBrick(
|
||||
Brick.Origin, Brick.SizeX, Brick.SizeY, Brick.SizeZ,
|
||||
BrickDensities, BrickFallbackSamples)
|
||||
|| BrickDensities.Num() != Brick.SizeX * Brick.SizeY * Brick.SizeZ)
|
||||
{
|
||||
++WorkerEvaluationFailures[WorkerIndex];
|
||||
continue;
|
||||
}
|
||||
const int32 BrickSampleCount = BrickDensities.Num();
|
||||
WorkerEvaluatedSamples[WorkerIndex] += static_cast<uint64>(BrickSampleCount);
|
||||
WorkerFallbackSamples[WorkerIndex] += static_cast<uint64>(BrickFallbackSamples);
|
||||
if (bFullRing && OutDensities != nullptr)
|
||||
{
|
||||
for (int32 Bz = 0; Bz < Brick.SizeZ; ++Bz)
|
||||
for (int32 By = 0; By < Brick.SizeY; ++By)
|
||||
for (int32 Bx = 0; Bx < Brick.SizeX; ++Bx)
|
||||
{
|
||||
const int32 LocalIndex = (Bz * Brick.SizeY + By) * Brick.SizeX + Bx;
|
||||
const int64 LatticeIndex = LinearIndex(
|
||||
Brick.LatticeX + Bx, Brick.LatticeY + By,
|
||||
Brick.LatticeZ + Bz, Dimension);
|
||||
(*OutDensities)[static_cast<int32>(LatticeIndex)] = BrickDensities[LocalIndex];
|
||||
}
|
||||
}
|
||||
else if (OutDensities != nullptr)
|
||||
{
|
||||
for (const FRegionBrickSample& Sample : Brick.RequestedSamples)
|
||||
{
|
||||
(*OutDensities)[Sample.OutputIndex] = BrickDensities[Sample.LocalIndex];
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const int64 LatticeIndex = bFullRing
|
||||
? OutputIndex : (*SampleIndices)[static_cast<int32>(OutputIndex)];
|
||||
if (LatticeIndex < 0 || LatticeIndex >= TotalLatticeSamples)
|
||||
@@ -212,11 +377,20 @@ namespace VoxelForgeRegionEval
|
||||
|
||||
OutTiming.WallSeconds = FPlatformTime::Seconds() - StartSeconds;
|
||||
OutTiming.SampleCount = SampleCount;
|
||||
for (int32 WorkerIndex = 0; WorkerIndex < WorkerCount; ++WorkerIndex)
|
||||
OutTiming.bBrickProgram = bBrickProgram;
|
||||
OutTiming.BrickCount = bBrickProgram ? WorkBricks.Num() : 0;
|
||||
for (int32 WorkerIndex = 0; WorkerIndex < NumWorkers; ++WorkerIndex)
|
||||
{
|
||||
OutTiming.WorkerCpuSeconds += WorkerCpuSeconds[WorkerIndex];
|
||||
bAllWorkerCpuFromOsThreadTimes &= WorkerCpuFromOsThreadTimes[WorkerIndex];
|
||||
bAllWorkerCpuFromOsThreadTimes &= WorkerCpuFromOsThreadTimes[WorkerIndex] != 0;
|
||||
OutTiming.NonFiniteSamples += WorkerNonFinite[WorkerIndex];
|
||||
OutTiming.EvaluatedSamples += bBrickProgram
|
||||
? static_cast<int64>(WorkerEvaluatedSamples[WorkerIndex]) : 0;
|
||||
OutTiming.FallbackSamples += static_cast<int64>(WorkerFallbackSamples[WorkerIndex]);
|
||||
if (WorkerEvaluationFailures[WorkerIndex] != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool bAlreadyCounted = false;
|
||||
for (int32 PriorWorker = 0; PriorWorker < WorkerIndex; ++PriorWorker)
|
||||
{
|
||||
@@ -225,6 +399,11 @@ namespace VoxelForgeRegionEval
|
||||
OutTiming.ObservedWorkerThreads += bAlreadyCounted ? 0 : 1;
|
||||
}
|
||||
OutTiming.bWorkerCpuFromOsThreadTimes = bAllWorkerCpuFromOsThreadTimes;
|
||||
if (OutTiming.EvaluatedSamples > 0)
|
||||
{
|
||||
OutTiming.FallbackShare = static_cast<double>(OutTiming.FallbackSamples)
|
||||
/ static_cast<double>(OutTiming.EvaluatedSamples);
|
||||
}
|
||||
OutTiming.NanosecondsPerSample = OutTiming.SampleCount > 0
|
||||
? OutTiming.WorkerCpuSeconds * 1.0e9 / static_cast<double>(OutTiming.SampleCount)
|
||||
: 0.0;
|
||||
|
||||
@@ -17,10 +17,15 @@ namespace VoxelForgeRegionEval
|
||||
int32 Workers = WorkerCount;
|
||||
int32 ObservedWorkerThreads = 0;
|
||||
int64 SampleCount = 0;
|
||||
int64 EvaluatedSamples = 0;
|
||||
int64 BrickCount = 0;
|
||||
int64 FallbackSamples = 0;
|
||||
double FallbackShare = 0.0;
|
||||
double WallSeconds = 0.0;
|
||||
double WorkerCpuSeconds = 0.0;
|
||||
double NanosecondsPerSample = 0.0;
|
||||
bool bWorkerCpuFromOsThreadTimes = false;
|
||||
bool bBrickProgram = false;
|
||||
uint64 QuantizedHash = 0;
|
||||
uint64 RawFloatHash = 0;
|
||||
uint64 SaturatedSamples = 0;
|
||||
@@ -67,7 +72,9 @@ namespace VoxelForgeRegionEval
|
||||
bool bPrepared,
|
||||
float ReachScale,
|
||||
TArray<float>* OutDensities,
|
||||
FTiming& OutTiming);
|
||||
FTiming& OutTiming,
|
||||
bool bBrickProgram = false,
|
||||
int32 RequestedWorkerCount = WorkerCount);
|
||||
|
||||
/** Count exact room/tunnel/core/passage candidate visits with instrumentation enabled. */
|
||||
FCandidateVisits CountCandidates(
|
||||
|
||||
Reference in new issue
Block a user