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:
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(
|
||||
|
||||
Reference in new issue
Block a user