fix(mesher): stop forcing the nested block classifier on -- but I cannot reproduce the 3.5x
The code contradicted its own documentation. VoxelMarchingCubesMesher.h:150 says to leave
the optional 8^3-block refinement DISABLED in streaming, "repeating a proof on ..." --
and VoxelWorld.cpp forced `Mesher->bUseBlockEarlyOut = true` regardless.
It is now off by default, behind `voxel.UseBlockEarlyOut` (CVar + command line), so the
capability stays available for the focused diagnostics where it was measured to help.
⚠️ THE HEADLINE NUMBER DOES NOT REPRODUCE, AND I AM NOT CLAIMING IT
Sol's second pass read a 3.5x from the attribution table:
classifier disabled 587.991 ms
classifier enabled 2,056.596 ms
My own clean A/B on this flag, quiet machine, 3 runs each:
nested OFF 0.597 / 0.613 / 0.607 s mean 0.606
nested ON 0.637 / 0.599 / 0.605 s mean 0.614
Identical within noise.
So either the 2,056 ms control exercised more than this flag does, or the 3.5x lives on
a path the 128^3 export does not take. PERF-ATTRIBUTION.md describes that figure as "the
separate classifier control", which is not obviously the same lever. Until the game-path
A/B is run, treat the 3.5x as UNCONFIRMED.
What this commit is worth on its own: the code now does what its documentation says, the
behaviour is switchable rather than hardcoded, and geometry is unchanged --
SHA-256 b3e5f4c3..9b377 identical with the flag on, off, and against bb8ec51.
⚠️ An earlier measurement pass in this round was contaminated: a local llama-server was
using 5 GB and competing for cores. Those runs came out 0.67-0.78 s with the on/off
ranges overlapping. Discarded and re-measured on a quiet machine; only the clean numbers
above are reported. Worth recording as a reminder that the machine is part of the
instrument.
NOT DONE -- the round was OOM-killed partway (same llama-server):
- the three-way outer-classifier A/B (no outer / current+discarded validation / ⭐ outer
with validation samples REUSED as the mesher core grid). That third mode is the
interesting one: exact validation currently costs 33^3 = 35,937 density calls whose
results are then DISCARDED, while the mesher then fills 35^3 = 42,875. A successful
uniform verdict saves at most 16.18%, and a Mixed verdict saves nothing.
- game-path measurement of any of it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
bb8ec51939
commit
f2044135ea
4 files changed
+223
-14
No files matched your search
@@ -37,12 +37,15 @@ namespace
|
|||||||
|
|
||||||
FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, int32 Step, int32 InCellsPerAxis,
|
FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, int32 Step, int32 InCellsPerAxis,
|
||||||
TArray<uint8>* OutCaptureGrid,
|
TArray<uint8>* OutCaptureGrid,
|
||||||
int32 BandZMinVox, int32 BandZMaxVox)
|
int32 BandZMinVox, int32 BandZMaxVox,
|
||||||
|
const FVoxelMesherCoreDensityGrid* ReusableCoreGrid,
|
||||||
|
int64* OutDensitySampleCount)
|
||||||
{
|
{
|
||||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh);
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh);
|
||||||
VF_ParseOperatorBlockSwitch();
|
VF_ParseOperatorBlockSwitch();
|
||||||
FVoxelMeshData MeshData;
|
FVoxelMeshData MeshData;
|
||||||
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
|
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
|
||||||
|
if (OutDensitySampleCount) { *OutDensitySampleCount = 0; }
|
||||||
if (!Generator) return MeshData;
|
if (!Generator) return MeshData;
|
||||||
VoxelDensityProfile::FScopedTimer MesherProfileTimer(
|
VoxelDensityProfile::FScopedTimer MesherProfileTimer(
|
||||||
VoxelDensityProfile::EBucket::MesherGenerateMesh);
|
VoxelDensityProfile::EBucket::MesherGenerateMesh);
|
||||||
@@ -227,6 +230,12 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
const int32 CellsPerAxis = FMath::Clamp(InCellsPerAxis, 2, CHUNK_SIZE); // coarse tiles use fewer
|
const int32 CellsPerAxis = FMath::Clamp(InCellsPerAxis, 2, CHUNK_SIZE); // coarse tiles use fewer
|
||||||
const int32 GridDim = CellsPerAxis + 1;
|
const int32 GridDim = CellsPerAxis + 1;
|
||||||
const int32 MDim = GridDim + 2; // +1 marge de chaque côté
|
const int32 MDim = GridDim + 2; // +1 marge de chaque côté
|
||||||
|
const bool bUseReusableCoreGrid = !OutCaptureGrid
|
||||||
|
&& ReusableCoreGrid
|
||||||
|
&& ReusableCoreGrid->OriginVoxels == OriginVoxels
|
||||||
|
&& ReusableCoreGrid->Step == Step
|
||||||
|
&& ReusableCoreGrid->CellsPerAxis == CellsPerAxis
|
||||||
|
&& ReusableCoreGrid->Samples.Num() == GridDim * GridDim * GridDim;
|
||||||
|
|
||||||
// COUPE DE CONTENU PAR STRATE — restreint le maillage (et l'échantillonnage) aux cellules
|
// COUPE DE CONTENU PAR STRATE — restreint le maillage (et l'échantillonnage) aux cellules
|
||||||
// dont l'intervalle Z chevauche la bande [BandZMinVox, BandZMaxVox] (voxels inclusifs).
|
// dont l'intervalle Z chevauche la bande [BandZMinVox, BandZMaxVox] (voxels inclusifs).
|
||||||
@@ -532,7 +541,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
// gradient) sont échantillonnées — le reste du tampon reste non initialisé et non lu.
|
// gradient) sont échantillonnées — le reste du tampon reste non initialisé et non lu.
|
||||||
VoxelDensityProfile::FScopedTimer MesherDensityGridTimer(
|
VoxelDensityProfile::FScopedTimer MesherDensityGridTimer(
|
||||||
VoxelDensityProfile::EBucket::MesherDensityGrid);
|
VoxelDensityProfile::EBucket::MesherDensityGrid);
|
||||||
const bool bUseOperatorBlock = !bUseSharedDensityGrid
|
const bool bUseOperatorBlock = !bUseSharedDensityGrid && !bUseReusableCoreGrid
|
||||||
&& GVoxelForgeUseOperatorBlock != 0;
|
&& GVoxelForgeUseOperatorBlock != 0;
|
||||||
if (bUseOperatorBlock)
|
if (bUseOperatorBlock)
|
||||||
{
|
{
|
||||||
@@ -567,6 +576,14 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
DensityGrid[LocalIndex] = ReuseGrid->Samples[
|
DensityGrid[LocalIndex] = ReuseGrid->Samples[
|
||||||
(SharedZ * ReuseGrid->Dim + SharedY) * ReuseGrid->Dim + SharedX];
|
(SharedZ * ReuseGrid->Dim + SharedY) * ReuseGrid->Dim + SharedX];
|
||||||
}
|
}
|
||||||
|
else if (bUseReusableCoreGrid
|
||||||
|
&& gx >= 0 && gx < GridDim
|
||||||
|
&& gy >= 0 && gy < GridDim
|
||||||
|
&& gz >= 0 && gz < GridDim)
|
||||||
|
{
|
||||||
|
DensityGrid[LocalIndex] = ReusableCoreGrid->Samples[
|
||||||
|
(gz * GridDim + gy) * GridDim + gx];
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// World voxel = tile origin + grid offset scaled by the cell size (Step).
|
// World voxel = tile origin + grid offset scaled by the cell size (Step).
|
||||||
@@ -574,6 +591,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
OriginVoxels.X + gx * Step,
|
OriginVoxels.X + gx * Step,
|
||||||
OriginVoxels.Y + gy * Step,
|
OriginVoxels.Y + gy * Step,
|
||||||
OriginVoxels.Z + gz * Step);
|
OriginVoxels.Z + gz * Step);
|
||||||
|
if (OutDensitySampleCount)
|
||||||
|
{
|
||||||
|
++(*OutDensitySampleCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,22 @@ namespace
|
|||||||
GVoxelForgePerfAttribution,
|
GVoxelForgePerfAttribution,
|
||||||
TEXT("Collect low-overhead component attribution for the game path."));
|
TEXT("Collect low-overhead component attribution for the game path."));
|
||||||
|
|
||||||
|
// The nested 8^3 proof is useful for focused/offline measurements, but it is opt-in for
|
||||||
|
// streaming because every qualifying mixed tile would pay the recursive proof again.
|
||||||
|
int32 GVoxelForgeUseBlockEarlyOut = 0;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeUseBlockEarlyOut(
|
||||||
|
TEXT("voxel.UseBlockEarlyOut"),
|
||||||
|
GVoxelForgeUseBlockEarlyOut,
|
||||||
|
TEXT("Use the optional nested 8x8x8 mesher classifier in streaming."));
|
||||||
|
|
||||||
|
// Outer classifier benchmark modes: 0 = disabled, 1 = current discarded exact validation,
|
||||||
|
// 2 = exact validation core reused by the mesher when the candidate fails validation.
|
||||||
|
int32 GVoxelForgeOuterClassifierMode = 1;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeOuterClassifierMode(
|
||||||
|
TEXT("voxel.OuterClassifierMode"),
|
||||||
|
GVoxelForgeOuterClassifierMode,
|
||||||
|
TEXT("Outer tile classifier mode: 0 off, 1 discarded validation, 2 validation-core reuse."));
|
||||||
|
|
||||||
// Startup command-line cvars can arrive before the game world has spawned AVoxelWorld (or
|
// Startup command-line cvars can arrive before the game world has spawned AVoxelWorld (or
|
||||||
// before its first tiles have become visible). Keep the diagnostic edit pending until Tick has
|
// before its first tiles have become visible). Keep the diagnostic edit pending until Tick has
|
||||||
// a loaded tile; otherwise a startup measurement silently becomes a no-op or edits the wrong
|
// a loaded tile; otherwise a startup measurement silently becomes a no-op or edits the wrong
|
||||||
@@ -1036,6 +1052,21 @@ void AVoxelWorld::BeginPlay()
|
|||||||
LOD0ReadySamples.Reset();
|
LOD0ReadySamples.Reset();
|
||||||
ObsoleteTileAbortCount = 0;
|
ObsoleteTileAbortCount = 0;
|
||||||
PeakObservedPawnSpeedCmPerSecond = 0.0f;
|
PeakObservedPawnSpeedCmPerSecond = 0.0f;
|
||||||
|
TotalWorkerGenerationCycles.store(0, std::memory_order_relaxed);
|
||||||
|
TotalWorkerGenerationTasks.store(0, std::memory_order_relaxed);
|
||||||
|
TotalObsoleteWorkerCycles.store(0, std::memory_order_relaxed);
|
||||||
|
TotalObsoleteWorkerTasks.store(0, std::memory_order_relaxed);
|
||||||
|
TotalValidationDensityCalls.store(0, std::memory_order_relaxed);
|
||||||
|
TotalMesherDensityCalls.store(0, std::memory_order_relaxed);
|
||||||
|
TotalValidationCoreReuseTiles.store(0, std::memory_order_relaxed);
|
||||||
|
for (int32 LOD = 0; LOD < TrackedClassifierLODCount; ++LOD)
|
||||||
|
{
|
||||||
|
OuterClassifierCallsByLOD[LOD].store(0, std::memory_order_relaxed);
|
||||||
|
for (int32 Verdict = 0; Verdict < 3; ++Verdict)
|
||||||
|
{
|
||||||
|
OuterClassifierVerdictsByLOD[LOD][Verdict].store(0, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
ConfigureHeadlessStreamingTest();
|
ConfigureHeadlessStreamingTest();
|
||||||
|
|
||||||
// Some headless harnesses load module CVars after the engine's startup-CVar pass. Read the
|
// Some headless harnesses load module CVars after the engine's startup-CVar pass. Read the
|
||||||
@@ -1044,6 +1075,8 @@ void AVoxelWorld::BeginPlay()
|
|||||||
int32 CommandLineProfile = GVoxelForgeProfileDensity;
|
int32 CommandLineProfile = GVoxelForgeProfileDensity;
|
||||||
int32 CommandLineProfileFull = GVoxelForgeProfileDensityFull;
|
int32 CommandLineProfileFull = GVoxelForgeProfileDensityFull;
|
||||||
int32 CommandLinePerfAttribution = GVoxelForgePerfAttribution;
|
int32 CommandLinePerfAttribution = GVoxelForgePerfAttribution;
|
||||||
|
int32 CommandLineUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut;
|
||||||
|
int32 CommandLineOuterClassifierMode = GVoxelForgeOuterClassifierMode;
|
||||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ProfileDensity="), CommandLineProfile))
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ProfileDensity="), CommandLineProfile))
|
||||||
{
|
{
|
||||||
GVoxelForgeProfileDensity = CommandLineProfile;
|
GVoxelForgeProfileDensity = CommandLineProfile;
|
||||||
@@ -1056,6 +1089,16 @@ void AVoxelWorld::BeginPlay()
|
|||||||
{
|
{
|
||||||
GVoxelForgePerfAttribution = CommandLinePerfAttribution;
|
GVoxelForgePerfAttribution = CommandLinePerfAttribution;
|
||||||
}
|
}
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.UseBlockEarlyOut="), CommandLineUseBlockEarlyOut))
|
||||||
|
{
|
||||||
|
GVoxelForgeUseBlockEarlyOut = CommandLineUseBlockEarlyOut;
|
||||||
|
}
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.OuterClassifierMode="), CommandLineOuterClassifierMode))
|
||||||
|
{
|
||||||
|
GVoxelForgeOuterClassifierMode = CommandLineOuterClassifierMode;
|
||||||
|
}
|
||||||
|
GVoxelForgeUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut != 0 ? 1 : 0;
|
||||||
|
GVoxelForgeOuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 2);
|
||||||
|
|
||||||
if (GVoxelForgePerfAttribution != 0)
|
if (GVoxelForgePerfAttribution != 0)
|
||||||
{
|
{
|
||||||
@@ -1159,7 +1202,11 @@ void AVoxelWorld::BeginPlay()
|
|||||||
Mesher->bGenerateSkirts = Settings->bGenerateSkirts;
|
Mesher->bGenerateSkirts = Settings->bGenerateSkirts;
|
||||||
Mesher->SkirtCells = Settings->SkirtCells;
|
Mesher->SkirtCells = Settings->SkirtCells;
|
||||||
Mesher->LODOctaveDrop = Settings->LODOctaveDrop; // T2.b — 0 = off
|
Mesher->LODOctaveDrop = Settings->LODOctaveDrop; // T2.b — 0 = off
|
||||||
Mesher->bUseBlockEarlyOut = true;
|
Mesher->bUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut != 0;
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeClassifierConfig] outer_mode=%d nested_block_early_out=%d"),
|
||||||
|
GVoxelForgeOuterClassifierMode, Mesher->bUseBlockEarlyOut ? 1 : 0);
|
||||||
|
|
||||||
VoxelForgeStartupTrace::RecordEvent(TEXT("generator_mesher_constructed"), FString::Printf(
|
VoxelForgeStartupTrace::RecordEvent(TEXT("generator_mesher_constructed"), FString::Printf(
|
||||||
TEXT("\"generator\":1,\"mesher\":1,\"generate_skirts\":%d,\"full_res_clip_levels\":%d,\"coarse_tile_cells\":%d"),
|
TEXT("\"generator\":1,\"mesher\":1,\"generate_skirts\":%d,\"full_res_clip_levels\":%d,\"coarse_tile_cells\":%d"),
|
||||||
@@ -2102,6 +2149,25 @@ void AVoxelWorld::LogStreamingLatencySummary() const
|
|||||||
const double RequestP95 = Percentile(RequestToReady, 0.95);
|
const double RequestP95 = Percentile(RequestToReady, 0.95);
|
||||||
const double TravelP95Metres = RequestP95
|
const double TravelP95Metres = RequestP95
|
||||||
* static_cast<double>(PeakObservedPawnSpeedCmPerSecond) / 100.0;
|
* static_cast<double>(PeakObservedPawnSpeedCmPerSecond) / 100.0;
|
||||||
|
FString ClassifierVerdictSummary;
|
||||||
|
for (int32 LOD = 0; LOD < TrackedClassifierLODCount; ++LOD)
|
||||||
|
{
|
||||||
|
if (LOD > 0)
|
||||||
|
{
|
||||||
|
ClassifierVerdictSummary += TEXT(";");
|
||||||
|
}
|
||||||
|
ClassifierVerdictSummary += FString::Printf(
|
||||||
|
TEXT("l%d[c=%llu,m=%llu,s=%llu,a=%llu]"),
|
||||||
|
LOD,
|
||||||
|
static_cast<unsigned long long>(OuterClassifierCallsByLOD[LOD].load(std::memory_order_relaxed)),
|
||||||
|
static_cast<unsigned long long>(OuterClassifierVerdictsByLOD[LOD][0].load(std::memory_order_relaxed)),
|
||||||
|
static_cast<unsigned long long>(OuterClassifierVerdictsByLOD[LOD][1].load(std::memory_order_relaxed)),
|
||||||
|
static_cast<unsigned long long>(OuterClassifierVerdictsByLOD[LOD][2].load(std::memory_order_relaxed)));
|
||||||
|
}
|
||||||
|
const uint64 WorkerGenerationCycles =
|
||||||
|
TotalWorkerGenerationCycles.load(std::memory_order_relaxed);
|
||||||
|
const uint64 ObsoleteWorkerCycles =
|
||||||
|
TotalObsoleteWorkerCycles.load(std::memory_order_relaxed);
|
||||||
const bool bDiagnosticsEnabled = GVoxelForgeProfileTileGeneration != 0
|
const bool bDiagnosticsEnabled = GVoxelForgeProfileTileGeneration != 0
|
||||||
|| VoxelForgeStartupTrace::IsActive()
|
|| VoxelForgeStartupTrace::IsActive()
|
||||||
|| VoxelDensityProfile::GetMode() != VoxelDensityProfile::EMode::Disabled;
|
|| VoxelDensityProfile::GetMode() != VoxelDensityProfile::EMode::Disabled;
|
||||||
@@ -2111,7 +2177,13 @@ void AVoxelWorld::LogStreamingLatencySummary() const
|
|||||||
"queue_wait_s[p50=%.6f p95=%.6f max=%.6f] "
|
"queue_wait_s[p50=%.6f p95=%.6f max=%.6f] "
|
||||||
"generation_s[p50=%.6f p95=%.6f max=%.6f] "
|
"generation_s[p50=%.6f p95=%.6f max=%.6f] "
|
||||||
"collision_cook_s[p50=%.6f p95=%.6f max=%.6f] "
|
"collision_cook_s[p50=%.6f p95=%.6f max=%.6f] "
|
||||||
"peak_speed_cm_s=%.3f p95_travel_m=%.6f obsolete_tile_aborts=%d"),
|
"peak_speed_cm_s=%.3f p95_travel_m=%.6f obsolete_tile_aborts=%d "
|
||||||
|
"outer_classifier_mode=%d nested_block_early_out=%d "
|
||||||
|
"worker_generation_tasks=%llu worker_generation_sum_s=%.6f "
|
||||||
|
"validation_density_calls=%llu mesher_density_calls=%llu "
|
||||||
|
"validation_core_reuse_tiles=%llu "
|
||||||
|
"obsolete_worker_tasks=%llu obsolete_worker_sum_s=%.6f "
|
||||||
|
"outer_classifier_verdicts=%s"),
|
||||||
bDiagnosticsEnabled ? TEXT("profiled") : TEXT("clean"),
|
bDiagnosticsEnabled ? TEXT("profiled") : TEXT("clean"),
|
||||||
bDiagnosticsEnabled ? TEXT("on") : TEXT("off"),
|
bDiagnosticsEnabled ? TEXT("on") : TEXT("off"),
|
||||||
LOD0ReadySamples.Num(),
|
LOD0ReadySamples.Num(),
|
||||||
@@ -2119,7 +2191,16 @@ void AVoxelWorld::LogStreamingLatencySummary() const
|
|||||||
Percentile(QueueWait, 0.50), Percentile(QueueWait, 0.95), Maximum(QueueWait),
|
Percentile(QueueWait, 0.50), Percentile(QueueWait, 0.95), Maximum(QueueWait),
|
||||||
Percentile(Generation, 0.50), Percentile(Generation, 0.95), Maximum(Generation),
|
Percentile(Generation, 0.50), Percentile(Generation, 0.95), Maximum(Generation),
|
||||||
Percentile(CollisionCook, 0.50), Percentile(CollisionCook, 0.95), Maximum(CollisionCook),
|
Percentile(CollisionCook, 0.50), Percentile(CollisionCook, 0.95), Maximum(CollisionCook),
|
||||||
PeakObservedPawnSpeedCmPerSecond, TravelP95Metres, ObsoleteTileAbortCount);
|
PeakObservedPawnSpeedCmPerSecond, TravelP95Metres, ObsoleteTileAbortCount,
|
||||||
|
GVoxelForgeOuterClassifierMode, GVoxelForgeUseBlockEarlyOut,
|
||||||
|
static_cast<unsigned long long>(TotalWorkerGenerationTasks.load(std::memory_order_relaxed)),
|
||||||
|
FPlatformTime::ToSeconds64(WorkerGenerationCycles),
|
||||||
|
static_cast<unsigned long long>(TotalValidationDensityCalls.load(std::memory_order_relaxed)),
|
||||||
|
static_cast<unsigned long long>(TotalMesherDensityCalls.load(std::memory_order_relaxed)),
|
||||||
|
static_cast<unsigned long long>(TotalValidationCoreReuseTiles.load(std::memory_order_relaxed)),
|
||||||
|
static_cast<unsigned long long>(TotalObsoleteWorkerTasks.load(std::memory_order_relaxed)),
|
||||||
|
FPlatformTime::ToSeconds64(ObsoleteWorkerCycles),
|
||||||
|
*ClassifierVerdictSummary);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AVoxelWorld::HandleTileCollisionCookComplete(
|
void AVoxelWorld::HandleTileCollisionCookComplete(
|
||||||
@@ -3644,7 +3725,19 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
? FPlatformTime::Cycles64() : 0;
|
? FPlatformTime::Cycles64() : 0;
|
||||||
Result.RequestStartCycles = RequestStartCycles;
|
Result.RequestStartCycles = RequestStartCycles;
|
||||||
|
|
||||||
if (Cancellation->bObsolete.load(std::memory_order_relaxed) || Result.bAborted)
|
const uint64 WorkerGenerationCycles = Result.GenerationEndCycles >= GenerationStartCycles
|
||||||
|
? Result.GenerationEndCycles - GenerationStartCycles : 0;
|
||||||
|
TotalWorkerGenerationTasks.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
TotalWorkerGenerationCycles.fetch_add(WorkerGenerationCycles, std::memory_order_relaxed);
|
||||||
|
const bool bObsoleteWork = Cancellation->bObsolete.load(std::memory_order_relaxed)
|
||||||
|
|| Result.bObsolete;
|
||||||
|
if (bObsoleteWork)
|
||||||
|
{
|
||||||
|
TotalObsoleteWorkerTasks.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
TotalObsoleteWorkerCycles.fetch_add(WorkerGenerationCycles, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bObsoleteWork || Result.bAborted)
|
||||||
{
|
{
|
||||||
if (!ShouldAbortWork())
|
if (!ShouldAbortWork())
|
||||||
{
|
{
|
||||||
@@ -3790,6 +3883,9 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
Result.MeshSeconds = 0.0;
|
Result.MeshSeconds = 0.0;
|
||||||
Result.StreamSeconds = 0.0;
|
Result.StreamSeconds = 0.0;
|
||||||
Result.NumTriangles = 0;
|
Result.NumTriangles = 0;
|
||||||
|
Result.ValidationDensityCalls = 0;
|
||||||
|
Result.MesherDensityCalls = 0;
|
||||||
|
Result.bValidationCoreReused = false;
|
||||||
Result.Streams.Reset();
|
Result.Streams.Reset();
|
||||||
Result.CaptureGrid.Reset();
|
Result.CaptureGrid.Reset();
|
||||||
Result.BandChunkLo = BandChunkLo; // strate content cut (MIN/MAX = uncut)
|
Result.BandChunkLo = BandChunkLo; // strate content cut (MIN/MAX = uncut)
|
||||||
@@ -3821,7 +3917,12 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
// même pour les cellules uniformes, et ces tuiles sont rares (fenêtre d'ombre).
|
// même pour les cellules uniformes, et ces tuiles sont rares (fenêtre d'ombre).
|
||||||
// (Gate IsoLevel == 0 : les verdicts du classifieur supposent l'iso MC à zéro exactement.)
|
// (Gate IsoLevel == 0 : les verdicts du classifieur supposent l'iso MC à zéro exactement.)
|
||||||
bool bTrivialEmpty = false;
|
bool bTrivialEmpty = false;
|
||||||
if (!bSheetTile && !bWantCapture && Generator && Mesher && Mesher->IsoLevel == 0.0f)
|
FVoxelMesherCoreDensityGrid ValidationCoreGrid;
|
||||||
|
const FVoxelMesherCoreDensityGrid* ReusableCoreGrid = nullptr;
|
||||||
|
const int32 OuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 2);
|
||||||
|
const bool bUseValidationCoreReuse = OuterClassifierMode == 2;
|
||||||
|
if (OuterClassifierMode != 0
|
||||||
|
&& !bSheetTile && !bWantCapture && Generator && Mesher && Mesher->IsoLevel == 0.0f)
|
||||||
{
|
{
|
||||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile);
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile);
|
||||||
INC_DWORD_STAT(STAT_VoxelForgeTilesClassified);
|
INC_DWORD_STAT(STAT_VoxelForgeTilesClassified);
|
||||||
@@ -3849,37 +3950,73 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
TGuardValue<int32> ValidationSampleStepGuard(
|
TGuardValue<int32> ValidationSampleStepGuard(
|
||||||
VoxelGenLOD::SampleStep, ValidationStep);
|
VoxelGenLOD::SampleStep, ValidationStep);
|
||||||
|
|
||||||
|
if (bUseValidationCoreReuse)
|
||||||
|
{
|
||||||
|
ValidationCoreGrid.OriginVoxels = OriginVoxels;
|
||||||
|
ValidationCoreGrid.Step = ValidationStep;
|
||||||
|
ValidationCoreGrid.CellsPerAxis = ValidationCells;
|
||||||
|
const int32 ValidationDim = ValidationCells + 1;
|
||||||
|
ValidationCoreGrid.Samples.SetNumUninitialized(
|
||||||
|
ValidationDim * ValidationDim * ValidationDim);
|
||||||
|
}
|
||||||
|
|
||||||
bool bExactUniform = true;
|
bool bExactUniform = true;
|
||||||
for (int32 Z = 0; Z <= ValidationCells && bExactUniform && !bValidationAborted; ++Z)
|
for (int32 Z = 0; Z <= ValidationCells
|
||||||
|
&& (bUseValidationCoreReuse || bExactUniform) && !bValidationAborted; ++Z)
|
||||||
{
|
{
|
||||||
if (ShouldAbortWork(ObsoleteFlag))
|
if (ShouldAbortWork(ObsoleteFlag))
|
||||||
{
|
{
|
||||||
bValidationAborted = true;
|
bValidationAborted = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
for (int32 Y = 0; Y <= ValidationCells && bExactUniform && !bValidationAborted; ++Y)
|
for (int32 Y = 0; Y <= ValidationCells
|
||||||
|
&& (bUseValidationCoreReuse || bExactUniform) && !bValidationAborted; ++Y)
|
||||||
{
|
{
|
||||||
for (int32 X = 0; X <= ValidationCells; ++X)
|
for (int32 X = 0; X <= ValidationCells; ++X)
|
||||||
{
|
{
|
||||||
|
if (ShouldAbortWork(ObsoleteFlag))
|
||||||
|
{
|
||||||
|
bValidationAborted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
const float Density = Generator->GetDensityAt(
|
const float Density = Generator->GetDensityAt(
|
||||||
OriginVoxels.X + X * ValidationStep,
|
OriginVoxels.X + X * ValidationStep,
|
||||||
OriginVoxels.Y + Y * ValidationStep,
|
OriginVoxels.Y + Y * ValidationStep,
|
||||||
OriginVoxels.Z + Z * ValidationStep);
|
OriginVoxels.Z + Z * ValidationStep);
|
||||||
if (!FMath::IsFinite(Density) || Density == 0.0f
|
++Result.ValidationDensityCalls;
|
||||||
|| (Verdict == EVoxelTileClass::AllSolid && Density >= 0.0f)
|
if (bUseValidationCoreReuse)
|
||||||
|| (Verdict == EVoxelTileClass::AllAir && Density <= 0.0f))
|
{
|
||||||
|
const int32 ValidationDim = ValidationCells + 1;
|
||||||
|
ValidationCoreGrid.Samples[
|
||||||
|
(Z * ValidationDim + Y) * ValidationDim + X] = Density;
|
||||||
|
}
|
||||||
|
const bool bSampleAgrees = FMath::IsFinite(Density) && Density != 0.0f
|
||||||
|
&& (Verdict != EVoxelTileClass::AllSolid || Density < 0.0f)
|
||||||
|
&& (Verdict != EVoxelTileClass::AllAir || Density > 0.0f);
|
||||||
|
if (!bSampleAgrees)
|
||||||
{
|
{
|
||||||
bExactUniform = false;
|
bExactUniform = false;
|
||||||
|
if (!bUseValidationCoreReuse)
|
||||||
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
TotalValidationDensityCalls.fetch_add(
|
||||||
|
static_cast<uint64>(Result.ValidationDensityCalls), std::memory_order_relaxed);
|
||||||
if (!bExactUniform)
|
if (!bExactUniform)
|
||||||
{
|
{
|
||||||
// A false Mixed costs meshing; a false uniform verdict removes both render and
|
// A false Mixed costs meshing; a false uniform verdict removes both render and
|
||||||
// collision geometry. Fall back to the safe path whenever one exact vertex disagrees.
|
// collision geometry. Fall back to the safe path whenever one exact vertex disagrees.
|
||||||
ValidatedVerdict = EVoxelTileClass::Mixed;
|
ValidatedVerdict = EVoxelTileClass::Mixed;
|
||||||
|
if (bUseValidationCoreReuse && !bValidationAborted)
|
||||||
|
{
|
||||||
|
ReusableCoreGrid = &ValidationCoreGrid;
|
||||||
|
Result.bValidationCoreReused = true;
|
||||||
|
TotalValidationCoreReuseTiles.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bValidationAborted)
|
if (bValidationAborted)
|
||||||
@@ -3891,6 +4028,13 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
ClassifySeconds = bMeasureTile ? FPlatformTime::Seconds() - ClassifyStartSeconds : 0.0;
|
ClassifySeconds = bMeasureTile ? FPlatformTime::Seconds() - ClassifyStartSeconds : 0.0;
|
||||||
ClassifyVerdict = static_cast<int32>(ValidatedVerdict);
|
ClassifyVerdict = static_cast<int32>(ValidatedVerdict);
|
||||||
Result.ClassifyVerdict = ClassifyVerdict;
|
Result.ClassifyVerdict = ClassifyVerdict;
|
||||||
|
const int32 TrackedLOD = FMath::Clamp(Tile.Level, 0, TrackedClassifierLODCount - 1);
|
||||||
|
OuterClassifierCallsByLOD[TrackedLOD].fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (ClassifyVerdict >= 0 && ClassifyVerdict < 3)
|
||||||
|
{
|
||||||
|
OuterClassifierVerdictsByLOD[TrackedLOD][ClassifyVerdict].fetch_add(
|
||||||
|
1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
if (ShouldAbortWork(ObsoleteFlag))
|
if (ShouldAbortWork(ObsoleteFlag))
|
||||||
{
|
{
|
||||||
AbortResult();
|
AbortResult();
|
||||||
@@ -3923,7 +4067,10 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
HoleMinX, HoleMinY, HoleMaxX, HoleMaxY)
|
HoleMinX, HoleMinY, HoleMaxX, HoleMaxY)
|
||||||
: Mesher->GenerateMesh(OriginVoxels, Step, Cells,
|
: Mesher->GenerateMesh(OriginVoxels, Step, Cells,
|
||||||
bWantCapture ? &Result.CaptureGrid : nullptr,
|
bWantCapture ? &Result.CaptureGrid : nullptr,
|
||||||
BandVoxLo, BandVoxHi);
|
BandVoxLo, BandVoxHi, ReusableCoreGrid,
|
||||||
|
&Result.MesherDensityCalls);
|
||||||
|
TotalMesherDensityCalls.fetch_add(
|
||||||
|
static_cast<uint64>(Result.MesherDensityCalls), std::memory_order_relaxed);
|
||||||
MeshSeconds = bMeasureTile ? FPlatformTime::Seconds() - MeshStartSeconds : 0.0;
|
MeshSeconds = bMeasureTile ? FPlatformTime::Seconds() - MeshStartSeconds : 0.0;
|
||||||
INC_DWORD_STAT(STAT_VoxelForgeTilesMeshed);
|
INC_DWORD_STAT(STAT_VoxelForgeTilesMeshed);
|
||||||
if (ShouldAbortWork(ObsoleteFlag))
|
if (ShouldAbortWork(ObsoleteFlag))
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ struct FVoxelSharedDensityGrid
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct FVoxelMesherCoreDensityGrid;
|
||||||
|
|
||||||
UCLASS(BlueprintType)
|
UCLASS(BlueprintType)
|
||||||
class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject
|
class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject
|
||||||
{
|
{
|
||||||
@@ -69,7 +71,9 @@ public:
|
|||||||
*/
|
*/
|
||||||
FVoxelMeshData GenerateMesh(FIntVector OriginVoxels, int32 Step = 1, int32 CellsPerAxis = CHUNK_SIZE,
|
FVoxelMeshData GenerateMesh(FIntVector OriginVoxels, int32 Step = 1, int32 CellsPerAxis = CHUNK_SIZE,
|
||||||
TArray<uint8>* OutCaptureGrid = nullptr,
|
TArray<uint8>* OutCaptureGrid = nullptr,
|
||||||
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX);
|
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX,
|
||||||
|
const FVoxelMesherCoreDensityGrid* ReusableCoreGrid = nullptr,
|
||||||
|
int64* OutDensitySampleCount = nullptr);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* F18 — FEUILLE de champ lointain (anneau render-distance, cf. UVoxelSettings::bFarSheetRing).
|
* F18 — FEUILLE de champ lointain (anneau render-distance, cf. UVoxelSettings::bFarSheetRing).
|
||||||
@@ -162,3 +166,22 @@ public:
|
|||||||
// Réduction d'octaves sur les tuiles grossières ; 0 = désactivé.
|
// Réduction d'octaves sur les tuiles grossières ; 0 = désactivé.
|
||||||
int32 LODOctaveDrop = 0;
|
int32 LODOctaveDrop = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A per-tile core lattice produced by the outer classifier's exact validation. Unlike
|
||||||
|
// FVoxelSharedDensityGrid this is intentionally local to one GenerateMesh call: validation can
|
||||||
|
// run concurrently on several streaming workers without mutating shared mesher state.
|
||||||
|
struct FVoxelMesherCoreDensityGrid
|
||||||
|
{
|
||||||
|
FIntVector OriginVoxels = FIntVector::ZeroValue;
|
||||||
|
int32 Step = 1;
|
||||||
|
int32 CellsPerAxis = 0;
|
||||||
|
TArray<float> Samples;
|
||||||
|
|
||||||
|
void Reset()
|
||||||
|
{
|
||||||
|
OriginVoxels = FIntVector::ZeroValue;
|
||||||
|
Step = 1;
|
||||||
|
CellsPerAxis = 0;
|
||||||
|
Samples.Reset();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -107,6 +107,11 @@ struct FChunkResult
|
|||||||
uint64 GenerationEndCycles = 0;
|
uint64 GenerationEndCycles = 0;
|
||||||
uint64 ApplyStartCycles = 0;
|
uint64 ApplyStartCycles = 0;
|
||||||
int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0;
|
int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0;
|
||||||
|
// Clean, low-overhead work counters. These are carried with the result so the worker can
|
||||||
|
// count exact validation and mesher density calls locally, then publish one aggregate update.
|
||||||
|
int64 ValidationDensityCalls = 0;
|
||||||
|
int64 MesherDensityCalls = 0;
|
||||||
|
bool bValidationCoreReused = false;
|
||||||
bool bAborted = false; // Worker observed shutdown or cancellation; never mark this tile loaded
|
bool bAborted = false; // Worker observed shutdown or cancellation; never mark this tile loaded
|
||||||
bool bObsolete = false; // Worker observed per-tile desired-set cancellation
|
bool bObsolete = false; // Worker observed per-tile desired-set cancellation
|
||||||
uint32 DesiredEpoch = 0; // Desired-set epoch captured at request time
|
uint32 DesiredEpoch = 0; // Desired-set epoch captured at request time
|
||||||
@@ -915,6 +920,19 @@ public:
|
|||||||
int32 ObsoleteTileAbortCount = 0; // cumulative per-world moving-session cancellation count
|
int32 ObsoleteTileAbortCount = 0; // cumulative per-world moving-session cancellation count
|
||||||
float PeakObservedPawnSpeedCmPerSecond = 0.0f;
|
float PeakObservedPawnSpeedCmPerSecond = 0.0f;
|
||||||
|
|
||||||
|
// Clean performance accounting. LOD 0..8 covers every marching-cubes clip level; sheet tiles
|
||||||
|
// above the configured MC range never enter the outer classifier.
|
||||||
|
static constexpr int32 TrackedClassifierLODCount = 9;
|
||||||
|
std::atomic<uint64> TotalWorkerGenerationCycles{0};
|
||||||
|
std::atomic<uint64> TotalWorkerGenerationTasks{0};
|
||||||
|
std::atomic<uint64> TotalObsoleteWorkerCycles{0};
|
||||||
|
std::atomic<uint64> TotalObsoleteWorkerTasks{0};
|
||||||
|
std::atomic<uint64> TotalValidationDensityCalls{0};
|
||||||
|
std::atomic<uint64> TotalMesherDensityCalls{0};
|
||||||
|
std::atomic<uint64> TotalValidationCoreReuseTiles{0};
|
||||||
|
std::atomic<uint64> OuterClassifierCallsByLOD[TrackedClassifierLODCount]{};
|
||||||
|
std::atomic<uint64> OuterClassifierVerdictsByLOD[TrackedClassifierLODCount][3]{};
|
||||||
|
|
||||||
struct FStreamingLatencySample
|
struct FStreamingLatencySample
|
||||||
{
|
{
|
||||||
double RequestToReadySeconds = 0.0;
|
double RequestToReadySeconds = 0.0;
|
||||||
|
|||||||
Reference in new issue
Block a user