wip(assembler): round L4 -- lazy per-thread caches: 17.3 MiB -> 43 KiB per thread, view 7% faster

VoxelForgeCoreThreadLocal::FLazy<T> (new public header ThreadLocalStorage.h) makes every large
thread_local cache a pointer that heap-allocates on that thread's first use (still worker-private,
lock-free). Static TLS 17.33 MiB -> 43 KiB (initialised image 5.8 MiB -> 21 KiB); worker start
5.2 -> 0.12 ms; idle worker RSS 17.7 MiB -> 52 KiB. Linux workers back to 8 MiB stacks.
World unchanged (L1 hashes Clang off/GCC default, W1/W12; FloatingIslands seed 0).
Reviewer interleaved W12: 2.256 -> 2.091 s. MSVC/UBT not compiled here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ED33VnqepD4YY6gDAAA832
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-19 19:16:14 +02:00
1 parent 046f2af495
commit 7770074503
15 files changed
+361 -48

No files matched your search

@@ -27,6 +27,7 @@
// - STORE region (tight): only rooms/tunnels that can actually reach a voxel in
// this chunk are kept for the per-voxel loop, so hot-path cost stays low.
#include "VoxelForgeCore/ThreadLocalStorage.h"
#include "VoxelCaveMorphology.h"
#include "VoxelDensityProfile.h"
#include "VoxelDensityPrimitives.h"
@@ -265,7 +266,8 @@ namespace
}
};
static thread_local FVFFloorReliefCache GFloorReliefCache;
static thread_local VoxelForgeCoreThreadLocal::FLazy<FVFFloorReliefCache>
GFloorReliefCache;
FORCEINLINE float VF_ComputeFloorReliefNoiseDouble(
double X, double Y, uint32 FloorSeed, float Frequency)
@@ -300,7 +302,7 @@ namespace
int32 FeatureIndex,
const FVFFloorReliefColumnKey& Column)
{
return GFloorReliefCache.GetOrCompute(
return GFloorReliefCache.Get().GetOrCompute(
X, Y, FloorSeed, Frequency, CacheIdentity, Feature, FeatureIndex,
Column,
&VF_ComputeFloorReliefNoiseDouble);
@@ -313,7 +315,7 @@ namespace
int32 FeatureIndex,
const FVFFloorReliefColumnKey& Column)
{
return GFloorReliefCache.GetOrCompute(
return GFloorReliefCache.Get().GetOrCompute(
static_cast<double>(X), static_cast<double>(Y),
FloorSeed, Frequency, CacheIdentity, Feature, FeatureIndex,
Column,
@@ -8,6 +8,7 @@
// Convention de signe INTERNE : positif = solide ; la pile négative une fois pour le MC.
// Internal sign convention: positive = solid; the stack negates once for MC.
#include "VoxelForgeCore/ThreadLocalStorage.h"
#include "VoxelDensityOpStack.h"
#include "VoxelStrateComposer.h"
@@ -1148,7 +1149,8 @@ namespace
}
};
thread_local FColumnCache Cache = {};
thread_local VoxelForgeCoreThreadLocal::FLazy<FColumnCache> Cache;
FColumnCache& columnCache = Cache.Get();
thread_local FColumn DirectColumn = {};
// The production mesher and the exact-lattice classifier use integer XY. Fractional
@@ -1172,7 +1174,7 @@ namespace
// Acquire vérifie la clé uint64 complète et les bornes exactes avant de dériver CI.
// Acquire checks the exact uint64 key and exact bounds before deriving CI.
FColumnBox& Box = Cache.Acquire(IX, IY, ColumnKey);
FColumnBox& Box = columnCache.Acquire(IX, IY, ColumnKey);
AcquiredBox = &Box;
CI = (IY - Box.BaseY) * FColumnBox::Dim + (IX - Box.BaseX);
@@ -3158,8 +3160,10 @@ namespace
FChunkSDFCache Cache;
};
static thread_local FRoomGraphCacheEntry GRoomGraphCache[
RoomGraphCacheBankCount][RoomGraphCacheSlotCount];
using FRoomGraphCacheStorage = std::array<
std::array<FRoomGraphCacheEntry, RoomGraphCacheSlotCount>, RoomGraphCacheBankCount>;
static thread_local VoxelForgeCoreThreadLocal::FLazy<FRoomGraphCacheStorage>
GRoomGraphCache;
static thread_local uint32 GRoomGraphCacheClock = 0;
FORCEINLINE int32 RoomGraphCacheBank(int32 RegionSize)
@@ -3190,6 +3194,7 @@ namespace
uint64 ManagerLifetimeId, const TArray<FStrateTerrainOpEntry>* TerrainOps,
int32 RegionMinX, int32 RegionMinY, int32 RegionSize)
{
auto& RoomGraphCache = GRoomGraphCache.Get();
auto SelectForReuse = [](FRoomGraphCacheEntry* Selected, uint32 Use)
-> FRoomGraphCacheEntry*
{
@@ -3219,7 +3224,7 @@ namespace
FRoomGraphCacheEntry* LeastRecentlyUsed = nullptr;
for (int32 Probe = 0; Probe < RoomGraphCacheSlotCount; ++Probe)
{
FRoomGraphCacheEntry& Candidate = GRoomGraphCache[Bank][
FRoomGraphCacheEntry& Candidate = RoomGraphCache[Bank][
(StartSlot + Probe) & (RoomGraphCacheSlotCount - 1)];
if (!Candidate.bValid)
{
@@ -3251,7 +3256,7 @@ namespace
FRoomGraphCacheEntry* Selected = ReuseEntry != nullptr
? ReuseEntry : LeastRecentlyUsed;
return SelectForReuse(
Selected != nullptr ? Selected : &GRoomGraphCache[Bank][StartSlot], Use);
Selected != nullptr ? Selected : &RoomGraphCache[Bank][StartSlot], Use);
}
// Classification is dispatched across many workers before most of those workers have
@@ -8740,11 +8745,12 @@ void VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint()
uint64 LargestEntryBytes = 0;
uint64 ValidEntries = 0;
VoxelDensityProfile::FCacheMemoryBreakdown Breakdown;
auto& RoomGraphCache = GRoomGraphCache.Get();
for (int32 Bank = 0; Bank < RoomGraphCacheBankCount; ++Bank)
{
for (int32 Index = 0; Index < RoomGraphCacheSlotCount; ++Index)
{
const FRoomGraphCacheEntry& Entry = GRoomGraphCache[Bank][Index];
const FRoomGraphCacheEntry& Entry = RoomGraphCache[Bank][Index];
if (!Entry.bValid)
{
continue;
@@ -8760,12 +8766,12 @@ void VoxelDensityOps::ReportWorkerRoomGraphCacheFootprint()
LargestEntryBytes = FMath::Max(LargestEntryBytes, FullEntryBytes);
}
}
Breakdown.SlotStorageBytes = static_cast<uint64>(sizeof(GRoomGraphCache));
Breakdown.SlotStorageBytes = static_cast<uint64>(sizeof(RoomGraphCache));
VoxelDensityProfile::SetWorkerRoomGraphCacheFootprint(
RoomGraphCacheBankCount * RoomGraphCacheSlotCount,
ValidEntries,
static_cast<uint64>(sizeof(GRoomGraphCache)),
static_cast<uint64>(sizeof(RoomGraphCache)),
DynamicBytes,
EntryBytes,
LargestEntryBytes,
@@ -4,6 +4,7 @@
// Convention INTERNE: positif = solide, négatif = air (plus lisible).
// Convention de SORTIE (marching cubes): on négate → négatif = solide.
#include "VoxelForgeCore/ThreadLocalStorage.h"
#include "VoxelGenerator.h"
#include "StandaloneResearchTunnels.h"
#include "VoxelSettings.h"
@@ -388,7 +389,7 @@ struct FSurfaceColumnCache
// Cache de colonnes par thread de ClassifyTile (T1.d) : les pré-passes et passes complètes
// successives d'une même tuile, et les tuiles voisines, réutilisent leurs colonnes.
// Per-thread column cache for ClassifyTile: successive passes and neighbouring tiles reuse columns.
static thread_local FSurfaceColumnCache GSurfColCache;
static thread_local VoxelForgeCoreThreadLocal::FLazy<FSurfaceColumnCache> GSurfColCache;
//=============================================================================
// WORM N1 BLOCK PROOF (worker-local, scalar GetDensityAt hand-off)
@@ -3023,7 +3024,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
thread_local FVoxelOpStack CP_OpStack;
thread_local bool CP_UseOpStack = false;
thread_local uint64 CP_ManagerLifetimeId = 0;
thread_local FTunnelNetworkDensityCacheEntry CP_TunnelDensityCache[TunnelDensityCacheSlotCount];
thread_local VoxelForgeCoreThreadLocal::FLazy<
std::array<FTunnelNetworkDensityCacheEntry, TunnelDensityCacheSlotCount>>
CP_TunnelDensityCache;
thread_local FTunnelNetworkDensityCacheEntry* CP_ActiveTunnelDensityCache = nullptr;
// Cooked-season recipes and editor candidate recipes share this immutable worker-local
// hand-off. Recipe materialisation is runtime; rolling/selection remains editor-only.
@@ -3080,12 +3083,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
auto FindTunnelDensityCacheEntry = [&](const FIntVector& QueryChunk,
FTunnelNetworkDensityCacheEntry*& OutEntry) -> bool
{
auto& TunnelDensityCache = CP_TunnelDensityCache.Get();
const int32 StartSlot = TunnelDensityCacheSlot(TunnelCacheKey);
FTunnelNetworkDensityCacheEntry* ReuseEntry = nullptr;
for (int32 Probe = 0; Probe < TunnelDensityCacheSlotCount; ++Probe)
{
const int32 Slot = (StartSlot + Probe) & (TunnelDensityCacheSlotCount - 1);
FTunnelNetworkDensityCacheEntry& Candidate = CP_TunnelDensityCache[Slot];
FTunnelNetworkDensityCacheEntry& Candidate = TunnelDensityCache[Slot];
if (!Candidate.bValid || Candidate.OwnerId != DensityCacheOwnerId)
{
if (ReuseEntry == nullptr) { ReuseEntry = &Candidate; }
@@ -3107,11 +3111,14 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
}
OutEntry = ReuseEntry != nullptr
? ReuseEntry
: &CP_TunnelDensityCache[StartSlot];
: &TunnelDensityCache[StartSlot];
return false;
};
if (!bIsGapChunk)
const bool bTunnelDensityCacheRoute =
QueryGeneratorType == ECaveGeneratorType::TunnelNetwork
|| QueryGeneratorType == ECaveGeneratorType::Underwater;
if (!bIsGapChunk && bTunnelDensityCacheRoute)
{
if (VoxelDensityProfile::AreCountersEnabled())
{
@@ -3387,7 +3394,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
{
FTunnelNetworkDensityCacheEntry& CachedEntry = TunnelDensityCacheEntryForWrite
? *TunnelDensityCacheEntryForWrite
: CP_TunnelDensityCache[TunnelDensityCacheSlot(ChunkCoord)];
: CP_TunnelDensityCache.Get()[TunnelDensityCacheSlot(ChunkCoord)];
CachedEntry.bValid = false;
CachedEntry.OwnerId = DensityCacheOwnerId;
CachedEntry.ManagerLifetimeId = ManagerLifetimeId;
@@ -3406,7 +3413,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
CP_ActiveTunnelDensityCache = &CachedEntry;
ActiveOpStack = &CachedEntry.OpStack;
ActiveTunnelCoreCache = &CachedEntry.TunnelCore;
ReportTunnelDensityCacheFootprint(CP_TunnelDensityCache);
ReportTunnelDensityCacheFootprint(CP_TunnelDensityCache.Get().data());
}
else if (!bLoadedTunnelDensityCache)
{
@@ -4023,11 +4030,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
uint32 Version = 0;
TArray<FVoxelModification> Mods;
};
static thread_local FDiffSlot DiffSlots[64];
static thread_local VoxelForgeCoreThreadLocal::FLazy<std::array<FDiffSlot, 64>>
DiffSlots;
auto& diffSlots = DiffSlots.Get();
const uint64 DiffLayerOwnerId = DiffLayer->GetCacheLifetimeId();
const uint32 V = DiffLayer->GetModsVersion();
FDiffSlot& Slot = DiffSlots[(ChunkCoord.X & 3) | ((ChunkCoord.Y & 3) << 2) | ((ChunkCoord.Z & 3) << 4)];
FDiffSlot& Slot = diffSlots[(ChunkCoord.X & 3) | ((ChunkCoord.Y & 3) << 2) | ((ChunkCoord.Z & 3) << 4)];
if (Slot.OwnerId != DiffLayerOwnerId || Slot.Chunk != ChunkCoord || Slot.Version != V)
{
Slot.OwnerId = DiffLayerOwnerId;
@@ -4233,7 +4242,8 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
uint64 LastUse = 0;
bool bValid = false;
};
static thread_local std::array<FRoomGraphCacheSlot, 4> RoomGraphCacheSlots{};
static thread_local VoxelForgeCoreThreadLocal::FLazy<
std::array<FRoomGraphCacheSlot, 4>> RoomGraphCacheStorage;
static thread_local uint64 RoomGraphCacheUseSerial = 0;
VF_ParseRoomGraphCacheSlots();
// The envelope proof is constant for a cache-policy key. Do not run the complete room/edge
@@ -4417,6 +4427,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
const int32 ActiveCacheSlots = FMath::Max(
FMath::Clamp(GVoxelForgeRoomGraphCacheSlots, 1, 4),
bNeedsTwoStrataWindows ? 2 : 1);
auto& RoomGraphCacheSlots = RoomGraphCacheStorage.Get();
FRoomGraphCacheSlot* CacheSlot = nullptr;
FRoomGraphCacheSlot* EvictionSlot = &RoomGraphCacheSlots[0];
for (int32 SlotIndex = 0; SlotIndex < ActiveCacheSlots; ++SlotIndex)
@@ -7349,7 +7360,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
{
FSurfSlot& S = Slots[s];
if (S.InteriorZ.Num() == 0) continue;
FSurfaceColumnBox& Box = GSurfColCache.Acquire(
FSurfaceColumnBox& Box = GSurfColCache.Get().Acquire(
Xi, Yi, S.StrateKey, Seed, TC_LayoutVersion,
TC_OwnerId, TC_ManagerLifetimeId);
const int32 CI = (Yi - Box.BaseY) * FSurfaceColumnBox::Dim + (Xi - Box.BaseX);
@@ -1,6 +1,7 @@
// VoxelStrateManager.cpp
// Runtime strate layout generation and queries.
#include "VoxelForgeCore/ThreadLocalStorage.h"
#include "VoxelStrateManager.h"
#include "VoxelSettings.h"
#include "VoxelSeasonAsset.h"
@@ -435,8 +436,8 @@ namespace
FVoxelPassageReliefCache& VF_GetPassageReliefCache()
{
thread_local FVoxelPassageReliefCache Cache;
return Cache;
thread_local VoxelForgeCoreThreadLocal::FLazy<FVoxelPassageReliefCache> Cache;
return Cache.Get();
}
FORCEINLINE float VF_ComputePassageReliefNoise(