From f2044135ea11455ca76b2941a2b306cd7d5d460d Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sat, 12 Sep 2026 13:19:42 +0200 Subject: [PATCH] fix(mesher): stop forcing the nested block classifier on -- but I cannot reproduce the 3.5x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb --- .../Private/VoxelMarchingCubesMesher.cpp | 25 ++- Source/VoxelForge/Private/VoxelWorld.cpp | 171 ++++++++++++++++-- .../Public/VoxelMarchingCubesMesher.h | 25 ++- Source/VoxelForge/Public/VoxelWorld.h | 18 ++ 4 files changed, 224 insertions(+), 15 deletions(-) diff --git a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index 1cc1724..a51c868 100644 --- a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -37,12 +37,15 @@ namespace FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, int32 Step, int32 InCellsPerAxis, TArray* OutCaptureGrid, - int32 BandZMinVox, int32 BandZMaxVox) + int32 BandZMinVox, int32 BandZMaxVox, + const FVoxelMesherCoreDensityGrid* ReusableCoreGrid, + int64* OutDensitySampleCount) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh); VF_ParseOperatorBlockSwitch(); FVoxelMeshData MeshData; if (OutCaptureGrid) { OutCaptureGrid->Reset(); } + if (OutDensitySampleCount) { *OutDensitySampleCount = 0; } if (!Generator) return MeshData; VoxelDensityProfile::FScopedTimer MesherProfileTimer( 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 GridDim = CellsPerAxis + 1; 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 // 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. VoxelDensityProfile::FScopedTimer MesherDensityGridTimer( VoxelDensityProfile::EBucket::MesherDensityGrid); - const bool bUseOperatorBlock = !bUseSharedDensityGrid + const bool bUseOperatorBlock = !bUseSharedDensityGrid && !bUseReusableCoreGrid && GVoxelForgeUseOperatorBlock != 0; if (bUseOperatorBlock) { @@ -567,6 +576,14 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, DensityGrid[LocalIndex] = ReuseGrid->Samples[ (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 { // 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.Y + gy * Step, OriginVoxels.Z + gz * Step); + if (OutDensitySampleCount) + { + ++(*OutDensitySampleCount); + } } } } diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 6369f78..731ca1c 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -74,6 +74,22 @@ namespace GVoxelForgePerfAttribution, 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 // 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 @@ -1036,6 +1052,21 @@ void AVoxelWorld::BeginPlay() LOD0ReadySamples.Reset(); ObsoleteTileAbortCount = 0; 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(); // 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 CommandLineProfileFull = GVoxelForgeProfileDensityFull; int32 CommandLinePerfAttribution = GVoxelForgePerfAttribution; + int32 CommandLineUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut; + int32 CommandLineOuterClassifierMode = GVoxelForgeOuterClassifierMode; if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ProfileDensity="), CommandLineProfile)) { GVoxelForgeProfileDensity = CommandLineProfile; @@ -1056,6 +1089,16 @@ void AVoxelWorld::BeginPlay() { 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) { @@ -1159,7 +1202,11 @@ void AVoxelWorld::BeginPlay() Mesher->bGenerateSkirts = Settings->bGenerateSkirts; Mesher->SkirtCells = Settings->SkirtCells; 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( 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 TravelP95Metres = RequestP95 * static_cast(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(OuterClassifierCallsByLOD[LOD].load(std::memory_order_relaxed)), + static_cast(OuterClassifierVerdictsByLOD[LOD][0].load(std::memory_order_relaxed)), + static_cast(OuterClassifierVerdictsByLOD[LOD][1].load(std::memory_order_relaxed)), + static_cast(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 || VoxelForgeStartupTrace::IsActive() || VoxelDensityProfile::GetMode() != VoxelDensityProfile::EMode::Disabled; @@ -2111,7 +2177,13 @@ void AVoxelWorld::LogStreamingLatencySummary() const "queue_wait_s[p50=%.6f p95=%.6f max=%.6f] " "generation_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("on") : TEXT("off"), LOD0ReadySamples.Num(), @@ -2119,7 +2191,16 @@ void AVoxelWorld::LogStreamingLatencySummary() const Percentile(QueueWait, 0.50), Percentile(QueueWait, 0.95), Maximum(QueueWait), Percentile(Generation, 0.50), Percentile(Generation, 0.95), Maximum(Generation), Percentile(CollisionCook, 0.50), Percentile(CollisionCook, 0.95), Maximum(CollisionCook), - PeakObservedPawnSpeedCmPerSecond, TravelP95Metres, ObsoleteTileAbortCount); + PeakObservedPawnSpeedCmPerSecond, TravelP95Metres, ObsoleteTileAbortCount, + GVoxelForgeOuterClassifierMode, GVoxelForgeUseBlockEarlyOut, + static_cast(TotalWorkerGenerationTasks.load(std::memory_order_relaxed)), + FPlatformTime::ToSeconds64(WorkerGenerationCycles), + static_cast(TotalValidationDensityCalls.load(std::memory_order_relaxed)), + static_cast(TotalMesherDensityCalls.load(std::memory_order_relaxed)), + static_cast(TotalValidationCoreReuseTiles.load(std::memory_order_relaxed)), + static_cast(TotalObsoleteWorkerTasks.load(std::memory_order_relaxed)), + FPlatformTime::ToSeconds64(ObsoleteWorkerCycles), + *ClassifierVerdictSummary); } void AVoxelWorld::HandleTileCollisionCookComplete( @@ -3644,7 +3725,19 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority) ? FPlatformTime::Cycles64() : 0; 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()) { @@ -3790,6 +3883,9 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector Result.MeshSeconds = 0.0; Result.StreamSeconds = 0.0; Result.NumTriangles = 0; + Result.ValidationDensityCalls = 0; + Result.MesherDensityCalls = 0; + Result.bValidationCoreReused = false; Result.Streams.Reset(); Result.CaptureGrid.Reset(); 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). // (Gate IsoLevel == 0 : les verdicts du classifieur supposent l'iso MC à zéro exactement.) 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); INC_DWORD_STAT(STAT_VoxelForgeTilesClassified); @@ -3849,37 +3950,73 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector TGuardValue ValidationSampleStepGuard( 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; - for (int32 Z = 0; Z <= ValidationCells && bExactUniform && !bValidationAborted; ++Z) + for (int32 Z = 0; Z <= ValidationCells + && (bUseValidationCoreReuse || bExactUniform) && !bValidationAborted; ++Z) { if (ShouldAbortWork(ObsoleteFlag)) { bValidationAborted = true; 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) { + if (ShouldAbortWork(ObsoleteFlag)) + { + bValidationAborted = true; + break; + } const float Density = Generator->GetDensityAt( OriginVoxels.X + X * ValidationStep, OriginVoxels.Y + Y * ValidationStep, OriginVoxels.Z + Z * ValidationStep); - if (!FMath::IsFinite(Density) || Density == 0.0f - || (Verdict == EVoxelTileClass::AllSolid && Density >= 0.0f) - || (Verdict == EVoxelTileClass::AllAir && Density <= 0.0f)) + ++Result.ValidationDensityCalls; + if (bUseValidationCoreReuse) + { + 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; - break; + if (!bUseValidationCoreReuse) + { + break; + } } } } } + TotalValidationDensityCalls.fetch_add( + static_cast(Result.ValidationDensityCalls), std::memory_order_relaxed); if (!bExactUniform) { // 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. ValidatedVerdict = EVoxelTileClass::Mixed; + if (bUseValidationCoreReuse && !bValidationAborted) + { + ReusableCoreGrid = &ValidationCoreGrid; + Result.bValidationCoreReused = true; + TotalValidationCoreReuseTiles.fetch_add(1, std::memory_order_relaxed); + } } } if (bValidationAborted) @@ -3891,6 +4028,13 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector ClassifySeconds = bMeasureTile ? FPlatformTime::Seconds() - ClassifyStartSeconds : 0.0; ClassifyVerdict = static_cast(ValidatedVerdict); 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)) { AbortResult(); @@ -3923,7 +4067,10 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector HoleMinX, HoleMinY, HoleMaxX, HoleMaxY) : Mesher->GenerateMesh(OriginVoxels, Step, Cells, bWantCapture ? &Result.CaptureGrid : nullptr, - BandVoxLo, BandVoxHi); + BandVoxLo, BandVoxHi, ReusableCoreGrid, + &Result.MesherDensityCalls); + TotalMesherDensityCalls.fetch_add( + static_cast(Result.MesherDensityCalls), std::memory_order_relaxed); MeshSeconds = bMeasureTile ? FPlatformTime::Seconds() - MeshStartSeconds : 0.0; INC_DWORD_STAT(STAT_VoxelForgeTilesMeshed); if (ShouldAbortWork(ObsoleteFlag)) diff --git a/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h b/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h index dc83592..c06cc65 100644 --- a/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h +++ b/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h @@ -38,6 +38,8 @@ struct FVoxelSharedDensityGrid } }; +struct FVoxelMesherCoreDensityGrid; + UCLASS(BlueprintType) class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject { @@ -69,7 +71,9 @@ public: */ FVoxelMeshData GenerateMesh(FIntVector OriginVoxels, int32 Step = 1, int32 CellsPerAxis = CHUNK_SIZE, TArray* 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). @@ -162,3 +166,22 @@ public: // Réduction d'octaves sur les tuiles grossières ; 0 = désactivé. 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 Samples; + + void Reset() + { + OriginVoxels = FIntVector::ZeroValue; + Step = 1; + CellsPerAxis = 0; + Samples.Reset(); + } +}; diff --git a/Source/VoxelForge/Public/VoxelWorld.h b/Source/VoxelForge/Public/VoxelWorld.h index db74fc8..d38fdb0 100644 --- a/Source/VoxelForge/Public/VoxelWorld.h +++ b/Source/VoxelForge/Public/VoxelWorld.h @@ -107,6 +107,11 @@ struct FChunkResult uint64 GenerationEndCycles = 0; uint64 ApplyStartCycles = 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 bObsolete = false; // Worker observed per-tile desired-set cancellation 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 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 TotalWorkerGenerationCycles{0}; + std::atomic TotalWorkerGenerationTasks{0}; + std::atomic TotalObsoleteWorkerCycles{0}; + std::atomic TotalObsoleteWorkerTasks{0}; + std::atomic TotalValidationDensityCalls{0}; + std::atomic TotalMesherDensityCalls{0}; + std::atomic TotalValidationCoreReuseTiles{0}; + std::atomic OuterClassifierCallsByLOD[TrackedClassifierLODCount]{}; + std::atomic OuterClassifierVerdictsByLOD[TrackedClassifierLODCount][3]{}; + struct FStreamingLatencySample { double RequestToReadySeconds = 0.0;