From 410788f7dd5288b6329817017b6265cdbe570413 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Tue, 15 Sep 2026 19:33:48 +0200 Subject: [PATCH] perf(generation): per-chunk routing cache -- ~4% less worker CPU, exact GetDensityAt asked the strate manager on every sample whether the chunk is a gap chunk, which generator type it uses, and whether it uses the operator stack. All three are constant per chunk. voxel.ChunkRoutingCache (default 1) memoises them per worker thread (Codex; reviewed by me): - thread_local storage only, so no cross-thread sharing; - key = generator owner id + manager pointer + manager lifetime id + layout version + full chunk coord, the same identity pattern as the existing CP_* refetch; - the editor composer override is still queried in the CP_* refetch, not cached here. Attribution for the re-rank's ~28% shared cost (offline-symbolised sampler, exclusive): GetDensityWithParams 16.4%, GetDensityAt 6.2%, unknown 5.8%, FChunkSDFSpatialIndex::GetRange 3.8%, FScopedTimer::End 1.3%, VF_ForEachSpatialCandidate 1.2%, UsesOperatorStackForChunk 1.0%. The tunnel-cache lookup was already near-perfect (48 misses in 2,248,091 samples). A thread-local timer cache was tried and rejected as slower (+0.4% static, +1.2% moving). Spatial-iteration and mesher-loop changes found no isolated exact win. Routing A/B: static worker 28.50 -> 27.78 s (-2.5%), moving 63.01 -> 59.81 s (-5.1%). Final DEFAULT build vs the committed-tip DLL (two interleaved runs each, same command line): static worker 28.79 -> 27.58 s (-4.2%) LOD0 p50/p95 52.3/94.8 -> 50.1/92.6 ms moving worker 62.25 -> 59.97 s (-3.7%) LOD0 p50/p95 51.4/95.8 -> 48.8/92.3 ms Field exact: OBJ SHA-256 b3e5f4c3..9b377 on both builds, 0 per-tile differences, capability 20,830 / 10,909; TilePostReachProof, sealed-proof and strate-crossing tests pass. Ledger: every launch was serialized with 0 editors before it. Two attribution attempts were rejected on harness bookkeeping/output-path bugs before a clean run; one crossing run was invalid (profiling left on, exit -1) and its retry passed. No crash folders. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y --- Source/VoxelForge/Private/VoxelGenerator.cpp | 87 ++++++++++++++++++-- 1 file changed, 79 insertions(+), 8 deletions(-) diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index c1cb0a3..1bf0060 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -194,6 +194,29 @@ namespace GVoxelForgeSpatialIndex = FMath::Clamp(GVoxelForgeSpatialIndex, -1, 1); } + int32 GVoxelForgeChunkRoutingCache = 1; + FAutoConsoleVariableRef CVarVoxelForgeChunkRoutingCache( + TEXT("voxel.ChunkRoutingCache"), + GVoxelForgeChunkRoutingCache, + TEXT("Memoize chunk-Z gap/generator/operator-stack routing for density samples.")); + bool GVoxelForgeChunkRoutingCacheSwitchParsed = false; + + void VF_ParseChunkRoutingCacheSwitch() + { + if (GVoxelForgeChunkRoutingCacheSwitchParsed) + { + return; + } + GVoxelForgeChunkRoutingCacheSwitchParsed = true; + int32 CommandLineValue = GVoxelForgeChunkRoutingCache; + if (FParse::Value( + FCommandLine::Get(), TEXT("voxel.ChunkRoutingCache="), CommandLineValue)) + { + GVoxelForgeChunkRoutingCache = CommandLineValue; + } + GVoxelForgeChunkRoutingCache = GVoxelForgeChunkRoutingCache != 0 ? 1 : 0; + } + } //============================================================================= @@ -2797,6 +2820,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co VF_ParseFastIsFiniteSwitch(); VF_ParseFusedEvaluatorSwitch(); VF_ParseWormBlockSkipSwitch(); + VF_ParseChunkRoutingCacheSwitch(); VoxelDensityProfile::FScopedTimer DensityProfileTimer( VoxelDensityProfile::EBucket::GetDensityAt); VoxelDensityProfile::FScopedTimer DensityPrologueTimer( @@ -2815,7 +2839,45 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co FMath::FloorToInt(WorldZ / CHUNK_SIZE) ); - const bool bIsGapChunk = StrateManager && StrateManager->IsGapChunk(ChunkCoord); + const bool bUseChunkRoutingCache = GVoxelForgeChunkRoutingCache != 0; + thread_local uint64 CP_RouteGeneratorOwnerId = 0; + thread_local const UVoxelStrateManager* CP_RouteManager = nullptr; + thread_local uint64 CP_RouteManagerLifetimeId = 0; + thread_local uint32 CP_RouteLayoutVersion = 0xFFFFFFFFu; + thread_local FIntVector CP_RouteChunk(INT32_MAX, INT32_MAX, INT32_MAX); + thread_local bool CP_RouteIsGapChunk = false; + thread_local ECaveGeneratorType CP_RouteGeneratorType = ECaveGeneratorType::TunnelNetwork; + thread_local bool CP_RouteUsesOperatorStack = false; + uint32 RouteLayoutVersion = 0; + uint64 RouteManagerLifetimeId = 0; + bool bIsGapChunk = false; + if (StrateManager && bUseChunkRoutingCache) + { + RouteLayoutVersion = StrateManager->GetLayoutVersion(); + RouteManagerLifetimeId = StrateManager->GetCacheLifetimeId(); + const bool bRouteKeyChanged = CP_RouteGeneratorOwnerId != DensityCacheOwnerId + || CP_RouteManager != StrateManager + || CP_RouteManagerLifetimeId != RouteManagerLifetimeId + || CP_RouteLayoutVersion != RouteLayoutVersion + || CP_RouteChunk != ChunkCoord; + if (bRouteKeyChanged) + { + CP_RouteGeneratorOwnerId = DensityCacheOwnerId; + CP_RouteManager = StrateManager; + CP_RouteManagerLifetimeId = RouteManagerLifetimeId; + CP_RouteLayoutVersion = RouteLayoutVersion; + CP_RouteChunk = ChunkCoord; + CP_RouteIsGapChunk = StrateManager->IsGapChunk(ChunkCoord); + CP_RouteGeneratorType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord); + CP_RouteUsesOperatorStack = + StrateManager->UsesOperatorStackForChunk(ChunkCoord); + } + bIsGapChunk = CP_RouteIsGapChunk; + } + else if (StrateManager) + { + bIsGapChunk = StrateManager->IsGapChunk(ChunkCoord); + } if (StrateManager && bIsGapChunk) { @@ -2908,20 +2970,25 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co thread_local FVoxelStrateRegionManifest CP_ComposerRegions; #endif - const uint32 LayoutVersion = StrateManager->GetLayoutVersion(); - const uint64 ManagerLifetimeId = StrateManager->GetCacheLifetimeId(); + const uint32 LayoutVersion = bUseChunkRoutingCache + ? RouteLayoutVersion : StrateManager->GetLayoutVersion(); + const uint64 ManagerLifetimeId = bUseChunkRoutingCache + ? RouteManagerLifetimeId : StrateManager->GetCacheLifetimeId(); // TunnelNetwork/Underwater operator-stack parameters are selected from chunk Z only. // A coarse tile samples thousands of exact XY chunk keys, but those keys describe the // same immutable stack whenever the selected archetype is operator-stack backed. Share // that prepared state by Z; the stack still queries the actual world position, and its // room-graph cache remains keyed by the real XY search window. Legacy tunnel states keep // the full XYZ key because their native cache is built for one XY window. - const ECaveGeneratorType QueryGeneratorType = - StrateManager->GetGeneratorTypeForChunk(ChunkCoord); + const ECaveGeneratorType QueryGeneratorType = bUseChunkRoutingCache + ? CP_RouteGeneratorType + : StrateManager->GetGeneratorTypeForChunk(ChunkCoord); const bool bShareTunnelStackByZ = !bIsGapChunk && (QueryGeneratorType == ECaveGeneratorType::TunnelNetwork || QueryGeneratorType == ECaveGeneratorType::Underwater) - && StrateManager->UsesOperatorStackForChunk(ChunkCoord); + && (bUseChunkRoutingCache + ? CP_RouteUsesOperatorStack + : StrateManager->UsesOperatorStackForChunk(ChunkCoord)); const FIntVector TunnelCacheKey = bShareTunnelStackByZ ? FIntVector(0, 0, ChunkCoord.Z) : ChunkCoord; FIntVector RequestedTileOrigin = FIntVector::ZeroValue; @@ -3052,7 +3119,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co CP_Version = LayoutVersion; CP_Chunk = ChunkCoord; CP_UsesTileCacheWindow = bTileCacheContext; - CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord); + CP_GenType = bUseChunkRoutingCache + ? CP_RouteGeneratorType + : StrateManager->GetGeneratorTypeForChunk(ChunkCoord); CP_UseCustomRecipe = false; bool bAllowCustomRecipe = true; #if WITH_EDITOR @@ -3106,7 +3175,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co // ── OPSTACK Phase 1 : (re)construire la pile si cette strate l'a demandée. ── // Une seule branche ajoutée au chemin densité, et elle est FROIDE : la construction est // par chunk (comme le refetch de params juste au-dessus), jamais par voxel. - CP_UseOpStack = StrateManager->UsesOperatorStackForChunk(ChunkCoord); + CP_UseOpStack = bUseChunkRoutingCache + ? CP_RouteUsesOperatorStack + : StrateManager->UsesOperatorStackForChunk(ChunkCoord); CP_UseOpStack = CP_UseOpStack || CP_UseCustomRecipe; #if WITH_EDITOR // A composer override is already an explicit, validated stack description. It must