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

+115 -3
View File
@@ -1,6 +1,7 @@
#pragma once
#include "StandaloneWorkerThread.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include <algorithm>
#include <atomic>
@@ -10,12 +11,15 @@
#include <cstdint>
#include <fstream>
#include <mutex>
#include <numeric>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#if defined(__linux__)
#include <unistd.h>
#include <sys/resource.h>
#endif
namespace StandaloneTlsProbe
@@ -38,14 +42,58 @@ namespace StandaloneTlsProbe
residentPages*static_cast<std::uint64_t>(pageSize)/1024};
}
inline int Run(int workerCount)
inline int Run(int workerCount,std::string_view touchKind="none")
{
workerCount=std::clamp(workerCount,0,12);
VoxelForgeCore::WorldAssemblyRecipeV1 surfaceRecipe;
VoxelForgeCore::WorldAssemblyStratumPlanV1 surfacePlan;
VoxelForgeCore::Heightfield2D surfaceField(surfaceRecipe,surfacePlan);
if(touchKind=="surface-world")
{
VoxelForgeCore::WorldAssemblyBiomeProfileV1 biome;
biome.stableId=1;
surfaceRecipe.biomes.push_back(biome);
surfacePlan.strateIndex=0;
surfacePlan.topChunkZ=1;
surfacePlan.bottomChunkZ=-1;
surfaceField=VoxelForgeCore::Heightfield2D(surfaceRecipe,surfacePlan);
if(!surfaceField.isValid())
{
std::fprintf(stderr,"[TlsProbe] invalid synthetic SurfaceWorld cache probe input\n");
return 2;
}
}
VoxelForgeCore::FloatingIslandRecipeV1 islandRecipe;
VoxelForgeCore::FloatingIslandPlanV1 islandPlan;
for(std::uint32_t index=0;index<islandRecipe.primaryIslandCount;++index)
{
VoxelForgeCore::FloatingIslandAnchorV1 island;
island.stableId=index+1;
island.ordinal=index;
island.centerSubvoxels={static_cast<std::int64_t>(index)*64*256,0,0};
island.radiusSubvoxels=20*256;
island.topHalfSubvoxels=20*256;
island.topZSubvoxels=0;
island.bottomZSubvoxels=-80*256;
island.taperEndQ16=islandRecipe.taperEndMinimumQ16;
islandPlan.islands.push_back(island);
}
VoxelForgeCore::FloatingIslandFieldV1 islandField(islandRecipe,islandPlan);
if(touchKind=="floating-islands"&&!islandField.isValid())
{
std::fprintf(stderr,"[TlsProbe] invalid synthetic FloatingIslands cache probe input\n");
return 2;
}
std::mutex mutex;
std::condition_variable changed;
bool release=false;
bool touchRequested=false;
std::atomic<int> ready{0};
std::atomic<int> touchReady{0};
std::vector<FStandaloneWorkerThread> workers;
std::vector<long> startupMinorFaults(static_cast<std::size_t>(workerCount),0);
std::vector<long> firstTouchMinorFaults(static_cast<std::size_t>(workerCount),0);
std::vector<std::int64_t> checksums(static_cast<std::size_t>(workerCount),0);
workers.reserve(static_cast<std::size_t>(workerCount));
FMemorySample previous=ReadMemory();
std::printf("[TlsProbe] workers=0 create_ms=0.000 rss_kib=%llu rss_delta_kib=0 vms_kib=%llu stack_bytes=%zu\n",
@@ -55,10 +103,47 @@ namespace StandaloneTlsProbe
for(int index=1;index<=workerCount;++index)
{
const auto start=std::chrono::steady_clock::now();
workers.emplace_back([&]
workers.emplace_back([&,index]
{
#if defined(__linux__)
rusage startupUsage{};
getrusage(RUSAGE_THREAD,&startupUsage);
startupMinorFaults[static_cast<std::size_t>(index-1)]=startupUsage.ru_minflt;
#endif
ready.fetch_add(1,std::memory_order_release);
changed.notify_all();
{
std::unique_lock lock(mutex);
changed.wait(lock,[&]{return touchRequested||release;});
}
if(touchRequested)
{
#if defined(__linux__)
rusage beforeTouch{};
rusage afterTouch{};
getrusage(RUSAGE_THREAD,&beforeTouch);
#endif
std::int64_t checksum=0;
if(touchKind=="surface-world")
{
for(int sample=0;sample<4;++sample)
checksum+=surfaceField.point((index*4+sample)*256,0,0).densitySubvoxels;
}
else if(touchKind=="floating-islands")
{
for(int sample=0;sample<4;++sample)
checksum+=islandField.densitySubvoxels(
(index*4+sample)*256,0,-20*256);
}
checksums[static_cast<std::size_t>(index-1)]=checksum;
#if defined(__linux__)
getrusage(RUSAGE_THREAD,&afterTouch);
firstTouchMinorFaults[static_cast<std::size_t>(index-1)]=
afterTouch.ru_minflt-beforeTouch.ru_minflt;
#endif
touchReady.fetch_add(1,std::memory_order_release);
changed.notify_all();
}
std::unique_lock lock(mutex);
changed.wait(lock,[&]{return release;});
});
@@ -76,6 +161,33 @@ namespace StandaloneTlsProbe
static_cast<long long>(rssDelta),static_cast<unsigned long long>(current.virtualKiB));
previous=current;
}
const FMemorySample beforeTouch=ReadMemory();
std::printf("[TlsProbeTouch] kind=%.*s rss_before_kib=%llu\n",
static_cast<int>(touchKind.size()),touchKind.data(),
static_cast<unsigned long long>(beforeTouch.residentKiB));
if(touchKind!="none")
{
{
std::lock_guard lock(mutex);
touchRequested=true;
}
changed.notify_all();
{
std::unique_lock lock(mutex);
changed.wait(lock,[&]{return touchReady.load(std::memory_order_acquire)==workerCount;});
}
const FMemorySample afterTouch=ReadMemory();
std::printf("[TlsProbeTouch] kind=%.*s rss_after_kib=%llu rss_delta_kib=%lld checksum_sum=%lld\n",
static_cast<int>(touchKind.size()),touchKind.data(),
static_cast<unsigned long long>(afterTouch.residentKiB),
static_cast<long long>(static_cast<std::int64_t>(afterTouch.residentKiB)
-static_cast<std::int64_t>(beforeTouch.residentKiB)),
static_cast<long long>(std::accumulate(checksums.begin(),checksums.end(),std::int64_t{0})));
for(int index=0;index<workerCount;++index)
std::printf("[TlsProbeTouch] worker=%d startup_minor_faults=%ld first_touch_minor_faults=%ld\n",
index+1,startupMinorFaults[static_cast<std::size_t>(index)],
firstTouchMinorFaults[static_cast<std::size_t>(index)]);
}
{
std::lock_guard lock(mutex);
release=true;
@@ -86,7 +198,7 @@ namespace StandaloneTlsProbe
return 0;
}
#else
inline int Run(int)
inline int Run(int,std::string_view="none")
{
std::fprintf(stderr,"TLS probe is Linux-only.\n");
return 2;
+3 -3
View File
@@ -12,13 +12,13 @@
#include <pthread.h>
#endif
// Linux's static TLS image is larger than the default pthread stack in this standalone build.
// Give only host-owned generation workers enough stack for that TLS plus normal call frames.
// Keep host-owned Linux workers on the ordinary 8 MiB stack size. Core's large caches now live
// behind lazy thread-local handles, so they no longer need to be hidden by oversized stacks.
#if defined(__linux__)
class FStandaloneWorkerThread
{
public:
static constexpr std::size_t StackSizeBytes=32u*1024u*1024u;
static constexpr std::size_t StackSizeBytes=8u*1024u*1024u;
FStandaloneWorkerThread()=default;
+13 -1
View File
@@ -44,6 +44,7 @@ namespace
bool FloatingIslandsRecipe=false;
bool TlsProbeRequested=false;
int TlsProbeWorkers=0;
std::string TlsProbeTouch="none";
bool HasCamera=false;
bool AutoSurfaceCamera=false;
FVector CameraMeters=FVector::ZeroVector;
@@ -90,6 +91,7 @@ namespace
" --mesh-coverage (report non-empty tiles and indexed triangles)\n"
" --render-distance-chunks=N (0 disables the extra far ring)\n"
" --tls-probe=N (Linux worker TLS/stack measurement; N=0..12)\n"
" --tls-probe-touch=none|surface-world|floating-islands (first-use probe)\n"
" --process-affinity=MASK --process-priority=Normal --frame-limit=FPS (viewer compatibility only)\n",
argv[0]);
return false;
@@ -124,6 +126,13 @@ namespace
{std::fprintf(stderr,"--tls-probe must be in 0..12\n");return false;}
options.TlsProbeRequested=true;
}
else if(arg.starts_with("--tls-probe-touch="))
{
const std::string_view touch=arg.substr(std::string_view("--tls-probe-touch=").size());
if(touch!="none"&&touch!="surface-world"&&touch!="floating-islands")
{std::fprintf(stderr,"Invalid --tls-probe-touch value: %.*s\n",static_cast<int>(touch.size()),touch.data());return false;}
options.TlsProbeTouch=touch;
}
else if(arg.starts_with("--camera="))
{
if(!ParseCamera(arg.substr(9),options.CameraMeters))
@@ -154,6 +163,8 @@ namespace
options.ClipRadius=std::clamp(options.ClipRadius,1,64);
options.MaxClipLevel=std::clamp(options.MaxClipLevel,0,8);
options.FarHorizonLOD8InteriorStepVoxels=std::max(0,options.FarHorizonLOD8InteriorStepVoxels);
if(!options.TlsProbeRequested&&options.TlsProbeTouch!="none")
{std::fprintf(stderr,"--tls-probe-touch requires --tls-probe=N\n");return false;}
if(options.TlsProbeRequested)return true;
if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline)
{
@@ -170,7 +181,8 @@ int main(int argc,char** argv)
const auto coldStart=Clock::now();
FOptions options;
if(!ParseOptions(argc,argv,options))return 2;
if(options.TlsProbeRequested)return StandaloneTlsProbe::Run(options.TlsProbeWorkers);
if(options.TlsProbeRequested)
return StandaloneTlsProbe::Run(options.TlsProbeWorkers,options.TlsProbeTouch);
std::printf("[HeadlessConfig] data=%s workers=%d clip_radius=%d max_clip_level=%d max_level=%d hash=%s column_overlap=%s force_generate=%s recipe=%s seed_override=%s render_distance_override=%s\n",
options.DataPath.string().c_str(),options.WorkerCount,options.ClipRadius,options.MaxClipLevel,
options.MaxLevel,options.StreamHash?"on":"off",options.ProfileColumnOverlap?"on":"off",
@@ -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(