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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-15 19:33:48 +02:00
1 parent c7b832db4c
commit 410788f7dd
1 file changed
+79 -8
+79 -8
View File
@@ -194,6 +194,29 @@ namespace
GVoxelForgeSpatialIndex = FMath::Clamp(GVoxelForgeSpatialIndex, -1, 1); 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_ParseFastIsFiniteSwitch();
VF_ParseFusedEvaluatorSwitch(); VF_ParseFusedEvaluatorSwitch();
VF_ParseWormBlockSkipSwitch(); VF_ParseWormBlockSkipSwitch();
VF_ParseChunkRoutingCacheSwitch();
VoxelDensityProfile::FScopedTimer DensityProfileTimer( VoxelDensityProfile::FScopedTimer DensityProfileTimer(
VoxelDensityProfile::EBucket::GetDensityAt); VoxelDensityProfile::EBucket::GetDensityAt);
VoxelDensityProfile::FScopedTimer DensityPrologueTimer( VoxelDensityProfile::FScopedTimer DensityPrologueTimer(
@@ -2815,7 +2839,45 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
FMath::FloorToInt(WorldZ / CHUNK_SIZE) 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) if (StrateManager && bIsGapChunk)
{ {
@@ -2908,20 +2970,25 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
thread_local FVoxelStrateRegionManifest CP_ComposerRegions; thread_local FVoxelStrateRegionManifest CP_ComposerRegions;
#endif #endif
const uint32 LayoutVersion = StrateManager->GetLayoutVersion(); const uint32 LayoutVersion = bUseChunkRoutingCache
const uint64 ManagerLifetimeId = StrateManager->GetCacheLifetimeId(); ? RouteLayoutVersion : StrateManager->GetLayoutVersion();
const uint64 ManagerLifetimeId = bUseChunkRoutingCache
? RouteManagerLifetimeId : StrateManager->GetCacheLifetimeId();
// TunnelNetwork/Underwater operator-stack parameters are selected from chunk Z only. // 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 // 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 // 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 // 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 // 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. // the full XYZ key because their native cache is built for one XY window.
const ECaveGeneratorType QueryGeneratorType = const ECaveGeneratorType QueryGeneratorType = bUseChunkRoutingCache
StrateManager->GetGeneratorTypeForChunk(ChunkCoord); ? CP_RouteGeneratorType
: StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
const bool bShareTunnelStackByZ = !bIsGapChunk const bool bShareTunnelStackByZ = !bIsGapChunk
&& (QueryGeneratorType == ECaveGeneratorType::TunnelNetwork && (QueryGeneratorType == ECaveGeneratorType::TunnelNetwork
|| QueryGeneratorType == ECaveGeneratorType::Underwater) || QueryGeneratorType == ECaveGeneratorType::Underwater)
&& StrateManager->UsesOperatorStackForChunk(ChunkCoord); && (bUseChunkRoutingCache
? CP_RouteUsesOperatorStack
: StrateManager->UsesOperatorStackForChunk(ChunkCoord));
const FIntVector TunnelCacheKey = bShareTunnelStackByZ const FIntVector TunnelCacheKey = bShareTunnelStackByZ
? FIntVector(0, 0, ChunkCoord.Z) : ChunkCoord; ? FIntVector(0, 0, ChunkCoord.Z) : ChunkCoord;
FIntVector RequestedTileOrigin = FIntVector::ZeroValue; FIntVector RequestedTileOrigin = FIntVector::ZeroValue;
@@ -3052,7 +3119,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_Version = LayoutVersion; CP_Version = LayoutVersion;
CP_Chunk = ChunkCoord; CP_Chunk = ChunkCoord;
CP_UsesTileCacheWindow = bTileCacheContext; CP_UsesTileCacheWindow = bTileCacheContext;
CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord); CP_GenType = bUseChunkRoutingCache
? CP_RouteGeneratorType
: StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
CP_UseCustomRecipe = false; CP_UseCustomRecipe = false;
bool bAllowCustomRecipe = true; bool bAllowCustomRecipe = true;
#if WITH_EDITOR #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. ── // ── 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 // 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. // 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; CP_UseOpStack = CP_UseOpStack || CP_UseCustomRecipe;
#if WITH_EDITOR #if WITH_EDITOR
// A composer override is already an explicit, validated stack description. It must // A composer override is already an explicit, validated stack description. It must