perf(generation): memoise room player-fit points -- 3.8x less worker CPU in game

The sampler (8e40bdd) showed ~3/4 of in-game worker CPU inside BuildChunkCache ->
VF_FindPlayerFitPointForRoom, recomputed for the same rooms on every per-worker cache rebuild.
The search reads only the strate params, the room site, the strate Z bounds and the warp seed,
so its answer is pure. It is now memoised process-wide.

- The key is exact, 97 words: every FStrateGenerationParams field via VF_STRATE_PARAM_FIELDS
  (83 scalar fields, audited: no pointers or TArrays), the room centre/radii/hash/bOrigin,
  both Z bounds, the warp seed and a key version. The hash only picks a shard; lookup compares
  every word. A miss is the worst case, and eviction just recomputes the same value.
- 4096 sets x 4 ways, a per-slot reader/writer state, a per-set lock only on miss (single
  flight), 7.1 MB fixed. The search algorithm is unchanged.
- voxel.PlayerFitMemo (CVar + command line), default on. The logs confirm =0 reaches the path.
- Diagnostics: the SdfCacheBuild count moves to BuildChunkCache entry, per-tile cache_builds in
  the tile profile, tile coordinates in the startup trace, and memo stats logged at EndPlay.

Game path, two runs each, sampler off (Codex; checked against its report):
                      worker CPU     LOD0 ready p50 / p95      tiles applied
  static  off -> on   183.7 -> 48.2 s  (3.81x)   109 / 178 -> 103 / 172 ms   841 = 841
  moving  off -> on   256.9 -> 124.7 s (2.06x)   104 / 192 ->  78 / 157 ms   2226 -> 2348
Hit rate 99.95%, 463 real searches in the moving run, 0 evictions.

Field and capability: per-tile triangle counts are identical on/off for 841/841 tiles
(669,834 triangles), the export OBJ SHA-256 is unchanged (b3e5f4c3..9b377), and player-fit
20,830 / walk-reachable 10,909 are unchanged.

Next, measured by this round: coarse tiles thrash the 4-chunk room-cache window. BuildChunkCache
calls per tile are 1.7 at LOD0, 18 at LOD2, 633 at LOD3 and 1,062 at LOD4.

Known gap, closed in the next round: nothing yet asserts that VF_STRATE_PARAM_FIELDS covers every
FStrateGenerationParams field. A field added to the struct but not to the list would be missing
from the memo key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZS94UH8bSsBeg5sZaxe2Y
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-13 04:48:44 +02:00
1 parent 8e40bdd92c
commit 2990009f3b
6 files changed
+515 -9

No files matched your search

@@ -36,6 +36,12 @@
#include "VoxelNoise.h" // Pure FBM used by the slab landing query #include "VoxelNoise.h" // Pure FBM used by the slab landing query
#include "VoxelPassageGeometry.h" #include "VoxelPassageGeometry.h"
#include "VoxelTerrainOpDefinition.h" #include "VoxelTerrainOpDefinition.h"
#include "HAL/IConsoleManager.h"
#include "HAL/PlatformProcess.h"
#include "Misc/CommandLine.h"
#include "Misc/Parse.h"
#include <atomic>
namespace namespace
{ {
@@ -2700,6 +2706,422 @@ namespace
return false; return false;
} }
// The room-fit result is pure with respect to the complete parameter value, room site, Z
// bounds, and warp seed. Keep the complete key explicit instead of hashing the USTRUCT's
// object representation: the latter would include padding and would become unsafe if a
// pointer/TArray field were ever added.
#define VF_PLAYER_FIT_PARAM_COUNT_LERP(Name) +1
#define VF_PLAYER_FIT_PARAM_COUNT_SNAP(Name) +1
static constexpr int32 VF_PlayerFitParamWordCount =
0 VF_STRATE_PARAM_FIELDS(
VF_PLAYER_FIT_PARAM_COUNT_LERP,
VF_PLAYER_FIT_PARAM_COUNT_SNAP);
#undef VF_PLAYER_FIT_PARAM_COUNT_LERP
#undef VF_PLAYER_FIT_PARAM_COUNT_SNAP
static constexpr int32 VF_PlayerFitVectorWordCount =
static_cast<int32>(sizeof(FVector::FReal) / sizeof(uint32));
static_assert(
VF_PlayerFitVectorWordCount == 1 || VF_PlayerFitVectorWordCount == 2,
"player-fit memo expects float or double FVector components");
static constexpr int32 VF_PlayerFitExtraKeyWordCount =
3 * VF_PlayerFitVectorWordCount + 8;
static constexpr int32 VF_PlayerFitKeyWordCount =
VF_PlayerFitParamWordCount + VF_PlayerFitExtraKeyWordCount;
static constexpr uint32 VF_PlayerFitMemoKeyVersion = 0x00010001u;
struct FVFPlayerFitMemoKey
{
uint32 Words[VF_PlayerFitKeyWordCount] = {};
};
template <typename T>
FORCEINLINE void VF_AppendPlayerFitMemoBits(
FVFPlayerFitMemoKey& Key,
int32& InOutWordIndex,
const T& Value)
{
static_assert(
sizeof(T) <= sizeof(uint64),
"player-fit memo key fields must be scalar values no wider than uint64");
constexpr int32 WordCount =
static_cast<int32>((sizeof(T) + sizeof(uint32) - 1) / sizeof(uint32));
for (int32 WordIndex = 0; WordIndex < WordCount; ++WordIndex)
{
Key.Words[InOutWordIndex + WordIndex] = 0u;
}
FMemory::Memcpy(
Key.Words + InOutWordIndex,
&Value,
sizeof(T));
InOutWordIndex += WordCount;
}
static FVFPlayerFitMemoKey VF_MakePlayerFitMemoKey(
const FStrateGenerationParams& Params,
const FVFRoomLandingSite& Site,
float StrateTopZ,
float StrateBottomZ,
uint32 WarpSeed)
{
FVFPlayerFitMemoKey Key;
int32 WordIndex = 0;
#define VF_PLAYER_FIT_KEY_APPEND_LERP(Name) \
VF_AppendPlayerFitMemoBits(Key, WordIndex, Params.Name);
#define VF_PLAYER_FIT_KEY_APPEND_SNAP(Name) \
VF_AppendPlayerFitMemoBits(Key, WordIndex, Params.Name);
VF_STRATE_PARAM_FIELDS(
VF_PLAYER_FIT_KEY_APPEND_LERP,
VF_PLAYER_FIT_KEY_APPEND_SNAP)
#undef VF_PLAYER_FIT_KEY_APPEND_LERP
#undef VF_PLAYER_FIT_KEY_APPEND_SNAP
// FVector is double-valued in the UE5 build used by this plugin. Append each component
// as raw words so the site key preserves its exact input bits on either float/double build.
VF_AppendPlayerFitMemoBits(Key, WordIndex, Site.Center.X);
VF_AppendPlayerFitMemoBits(Key, WordIndex, Site.Center.Y);
VF_AppendPlayerFitMemoBits(Key, WordIndex, Site.Center.Z);
VF_AppendPlayerFitMemoBits(Key, WordIndex, Site.RadiusXY);
VF_AppendPlayerFitMemoBits(Key, WordIndex, Site.RadiusZ);
VF_AppendPlayerFitMemoBits(Key, WordIndex, Site.Hash);
VF_AppendPlayerFitMemoBits(Key, WordIndex, Site.bOrigin);
VF_AppendPlayerFitMemoBits(Key, WordIndex, StrateTopZ);
VF_AppendPlayerFitMemoBits(Key, WordIndex, StrateBottomZ);
VF_AppendPlayerFitMemoBits(Key, WordIndex, WarpSeed);
VF_AppendPlayerFitMemoBits(Key, WordIndex, VF_PlayerFitMemoKeyVersion);
check(WordIndex == VF_PlayerFitKeyWordCount);
return Key;
}
FORCEINLINE uint32 VF_HashPlayerFitMemoKey(const FVFPlayerFitMemoKey& Key)
{
uint32 Hash = 0xD14C4B7Du;
for (int32 WordIndex = 0; WordIndex < VF_PlayerFitKeyWordCount; ++WordIndex)
{
Hash ^= Key.Words[WordIndex]
+ 0x9e3779b9u
+ (Hash << 6)
+ (Hash >> 2);
}
return VoxelHash::Mix(Hash);
}
// A slot has an atomic reader/writer state, but its key/result payload stays ordinary data.
// Readers increment the low counter before touching the payload; writers set the high bit,
// wait for existing readers, replace the payload, then publish the valid bit. This avoids
// a seqlock data race and avoids the ABA window of a finite sequence number.
struct FVFPlayerFitMemoSlot
{
static constexpr uint32 ReaderMask = (1u << 30) - 1u;
static constexpr uint32 ValidBit = 1u << 30;
static constexpr uint32 WriterBit = 1u << 31;
std::atomic<uint32> State;
std::atomic<uint32> KeyHash;
FVFPlayerFitMemoKey Key;
FVector Result = FVector::ZeroVector;
bool bFound = false;
FVFPlayerFitMemoSlot()
: State(0u)
, KeyHash(0u)
{
}
bool TryAcquireRead()
{
uint32 Current = State.load(std::memory_order_acquire);
for (;;)
{
if ((Current & ValidBit) == 0u
|| (Current & WriterBit) != 0u
|| (Current & ReaderMask) == ReaderMask)
{
return false;
}
if (State.compare_exchange_weak(
Current,
Current + 1u,
std::memory_order_acquire,
std::memory_order_relaxed))
{
return true;
}
}
}
void ReleaseRead()
{
State.fetch_sub(1u, std::memory_order_release);
}
bool AcquireWrite()
{
uint32 Current = State.load(std::memory_order_acquire);
for (;;)
{
if ((Current & WriterBit) != 0u)
{
FPlatformProcess::YieldThread();
Current = State.load(std::memory_order_acquire);
continue;
}
const bool bWasValid = (Current & ValidBit) != 0u;
if (State.compare_exchange_weak(
Current,
Current | WriterBit,
std::memory_order_acq_rel,
std::memory_order_acquire))
{
while ((State.load(std::memory_order_acquire) & ReaderMask) != 0u)
{
FPlatformProcess::YieldThread();
}
return bWasValid;
}
}
}
};
struct FVFPlayerFitMemoSetLock
{
std::atomic<bool> bLocked;
FVFPlayerFitMemoSetLock()
: bLocked(false)
{
}
void Lock()
{
bool bExpected = false;
while (!bLocked.compare_exchange_weak(
bExpected,
true,
std::memory_order_acquire,
std::memory_order_relaxed))
{
bExpected = false;
FPlatformProcess::YieldThread();
}
}
void Unlock()
{
bLocked.store(false, std::memory_order_release);
}
};
struct FVFPlayerFitMemo
{
static constexpr int32 NumSets = 4096;
static constexpr int32 NumWays = 4;
static constexpr int32 Capacity = NumSets * NumWays;
static_assert((NumSets & (NumSets - 1)) == 0, "player-fit memo sets must be a power of two");
static_assert((NumWays & (NumWays - 1)) == 0, "player-fit memo ways must be a power of two");
FVFPlayerFitMemoSlot Slots[Capacity];
FVFPlayerFitMemoSetLock SetLocks[NumSets];
std::atomic<uint32> ReplacementCursor[NumSets];
std::atomic<uint64> Requests{0};
std::atomic<uint64> Hits{0};
std::atomic<uint64> Misses{0};
std::atomic<uint64> Searches{0};
std::atomic<uint64> Evictions{0};
std::atomic<uint64> Bypasses{0};
std::atomic<uint64> OccupiedEntries{0};
FVFPlayerFitMemo()
{
for (int32 SetIndex = 0; SetIndex < NumSets; ++SetIndex)
{
ReplacementCursor[SetIndex].store(0u, std::memory_order_relaxed);
}
}
static FORCEINLINE int32 SlotIndex(uint32 KeyHash, int32 Way)
{
const int32 SetIndex = static_cast<int32>(KeyHash & (NumSets - 1));
return SetIndex * NumWays + Way;
}
bool TryFind(
const FVFPlayerFitMemoKey& InKey,
uint32 InKeyHash,
FVector& OutPoint,
bool& OutFound)
{
for (int32 Way = 0; Way < NumWays; ++Way)
{
FVFPlayerFitMemoSlot& Slot = Slots[SlotIndex(InKeyHash, Way)];
// A stale hash can only cause a miss. Exact key comparison below is still
// mandatory before returning the cached result.
if (Slot.KeyHash.load(std::memory_order_relaxed) != InKeyHash
|| !Slot.TryAcquireRead())
{
continue;
}
bool bMatches = true;
for (int32 WordIndex = 0;
WordIndex < VF_PlayerFitKeyWordCount;
++WordIndex)
{
if (Slot.Key.Words[WordIndex] != InKey.Words[WordIndex])
{
bMatches = false;
break;
}
}
if (bMatches)
{
OutPoint = Slot.Result;
OutFound = Slot.bFound;
Slot.ReleaseRead();
return true;
}
Slot.ReleaseRead();
}
return false;
}
void Publish(
int32 SetIndex,
const FVFPlayerFitMemoKey& InKey,
uint32 InKeyHash,
bool bFound,
const FVector& Point)
{
int32 SelectedWay = INDEX_NONE;
for (int32 Way = 0; Way < NumWays; ++Way)
{
const FVFPlayerFitMemoSlot& Slot = Slots[SlotIndex(InKeyHash, Way)];
if ((Slot.State.load(std::memory_order_acquire)
& FVFPlayerFitMemoSlot::ValidBit) == 0u)
{
SelectedWay = Way;
break;
}
}
if (SelectedWay == INDEX_NONE)
{
SelectedWay = static_cast<int32>(
ReplacementCursor[SetIndex].fetch_add(
1u, std::memory_order_relaxed)
& (NumWays - 1));
}
FVFPlayerFitMemoSlot& Slot = Slots[SlotIndex(InKeyHash, SelectedWay)];
const bool bWasValid = Slot.AcquireWrite();
Slot.Key = InKey;
Slot.Result = Point;
Slot.bFound = bFound;
Slot.KeyHash.store(InKeyHash, std::memory_order_relaxed);
Slot.State.store(FVFPlayerFitMemoSlot::ValidBit, std::memory_order_release);
if (bWasValid)
{
Evictions.fetch_add(1u, std::memory_order_relaxed);
}
else
{
OccupiedEntries.fetch_add(1u, std::memory_order_relaxed);
}
}
template <typename ComputeFn>
bool FindOrCompute(
const FVFPlayerFitMemoKey& InKey,
FVector& OutPoint,
ComputeFn&& Compute)
{
const uint32 KeyHash = VF_HashPlayerFitMemoKey(InKey);
Requests.fetch_add(1u, std::memory_order_relaxed);
bool bFound = false;
FVector CachedPoint = FVector::ZeroVector;
if (TryFind(InKey, KeyHash, CachedPoint, bFound))
{
OutPoint = CachedPoint;
Hits.fetch_add(1u, std::memory_order_relaxed);
return bFound;
}
const int32 SetIndex = static_cast<int32>(KeyHash & (NumSets - 1));
SetLocks[SetIndex].Lock();
if (TryFind(InKey, KeyHash, CachedPoint, bFound))
{
SetLocks[SetIndex].Unlock();
OutPoint = CachedPoint;
Hits.fetch_add(1u, std::memory_order_relaxed);
return bFound;
}
FVector ComputedPoint = FVector::ZeroVector;
bFound = Compute(ComputedPoint);
Searches.fetch_add(1u, std::memory_order_relaxed);
Misses.fetch_add(1u, std::memory_order_relaxed);
Publish(SetIndex, InKey, KeyHash, bFound, ComputedPoint);
SetLocks[SetIndex].Unlock();
OutPoint = ComputedPoint;
return bFound;
}
void ResetStats()
{
Requests.store(0u, std::memory_order_relaxed);
Hits.store(0u, std::memory_order_relaxed);
Misses.store(0u, std::memory_order_relaxed);
Searches.store(0u, std::memory_order_relaxed);
Evictions.store(0u, std::memory_order_relaxed);
Bypasses.store(0u, std::memory_order_relaxed);
}
};
static FVFPlayerFitMemo GPlayerFitMemo;
static TAutoConsoleVariable<int32> CVarVoxelForgePlayerFitMemo(
TEXT("voxel.PlayerFitMemo"),
1,
TEXT("Memoize deterministic room player-fit points; 0 disables the memo."),
ECVF_Default);
bool VF_FindPlayerFitPointForRoomMemoized(
const FStrateGenerationParams& Params,
const FVFRoomLandingSite& Site,
float StrateTopZ,
float StrateBottomZ,
uint32 WarpSeed,
FVector& OutPoint)
{
if (CVarVoxelForgePlayerFitMemo.GetValueOnAnyThread() == 0)
{
GPlayerFitMemo.Bypasses.fetch_add(1u, std::memory_order_relaxed);
return VF_FindPlayerFitPointForRoom(
Params, Site, StrateTopZ, StrateBottomZ, WarpSeed, OutPoint);
}
const FVFPlayerFitMemoKey Key = VF_MakePlayerFitMemoKey(
Params, Site, StrateTopZ, StrateBottomZ, WarpSeed);
return GPlayerFitMemo.FindOrCompute(
Key,
OutPoint,
[&](FVector& ComputedPoint)
{
return VF_FindPlayerFitPointForRoom(
Params,
Site,
StrateTopZ,
StrateBottomZ,
WarpSeed,
ComputedPoint);
});
}
float VF_EvaluateSlabLandingDensity( float VF_EvaluateSlabLandingDensity(
const FSlabGenerationParams& Params, const FSlabGenerationParams& Params,
uint32 Seed, uint32 Seed,
@@ -4305,6 +4727,63 @@ FVector VoxelCaveMorphology::ApplyCaveWarp(
return VF_ApplyCaveWarp(WorldPoint, Params, Seed); return VF_ApplyCaveWarp(WorldPoint, Params, Seed);
} }
void VoxelCaveMorphology::ConfigurePlayerFitMemoFromCommandLine()
{
int32 CommandLineValue = CVarVoxelForgePlayerFitMemo.GetValueOnAnyThread();
const bool bCommandLineValue = FParse::Value(
FCommandLine::Get(),
TEXT("voxel.PlayerFitMemo="),
CommandLineValue);
if (bCommandLineValue)
{
CVarVoxelForgePlayerFitMemo->Set(
CommandLineValue != 0 ? 1 : 0,
ECVF_SetByCommandline);
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelPlayerFitMemo] configured enabled=%d source=%s sets=%d ways=%d ")
TEXT("capacity=%d key_words=%d entry_bytes=%llu table_bytes=%llu"),
CVarVoxelForgePlayerFitMemo.GetValueOnAnyThread() != 0 ? 1 : 0,
bCommandLineValue ? TEXT("commandline") : TEXT("cvar/default"),
FVFPlayerFitMemo::NumSets,
FVFPlayerFitMemo::NumWays,
FVFPlayerFitMemo::Capacity,
VF_PlayerFitKeyWordCount,
static_cast<unsigned long long>(sizeof(FVFPlayerFitMemoSlot)),
static_cast<unsigned long long>(sizeof(FVFPlayerFitMemo)));
}
void VoxelCaveMorphology::ResetPlayerFitMemoStats()
{
GPlayerFitMemo.ResetStats();
}
void VoxelCaveMorphology::LogPlayerFitMemoStats()
{
const uint64 Requests = GPlayerFitMemo.Requests.load(std::memory_order_relaxed);
const uint64 Hits = GPlayerFitMemo.Hits.load(std::memory_order_relaxed);
const uint64 Misses = GPlayerFitMemo.Misses.load(std::memory_order_relaxed);
const double HitRate = Requests > 0
? static_cast<double>(Hits) / static_cast<double>(Requests)
: 0.0;
UE_LOG(LogTemp, Display,
TEXT("[VoxelPlayerFitMemo] enabled=%d requests=%llu hits=%llu misses=%llu ")
TEXT("hit_rate=%.6f searches=%llu bypasses=%llu entries=%llu/%d ")
TEXT("bytes=%llu evictions=%llu"),
CVarVoxelForgePlayerFitMemo.GetValueOnAnyThread() != 0 ? 1 : 0,
static_cast<unsigned long long>(Requests),
static_cast<unsigned long long>(Hits),
static_cast<unsigned long long>(Misses),
HitRate,
static_cast<unsigned long long>(GPlayerFitMemo.Searches.load(std::memory_order_relaxed)),
static_cast<unsigned long long>(GPlayerFitMemo.Bypasses.load(std::memory_order_relaxed)),
static_cast<unsigned long long>(GPlayerFitMemo.OccupiedEntries.load(std::memory_order_relaxed)),
FVFPlayerFitMemo::Capacity,
static_cast<unsigned long long>(sizeof(FVFPlayerFitMemo)),
static_cast<unsigned long long>(GPlayerFitMemo.Evictions.load(std::memory_order_relaxed)));
}
FVoxelPassageLanding VF_BuildPassageLanding( FVoxelPassageLanding VF_BuildPassageLanding(
const FVector& InStandingPoint, const FVector& InStandingPoint,
float MouthRadius, float MouthRadius,
@@ -4729,6 +5208,10 @@ void VoxelCaveMorphology::BuildChunkCache(
uint32 Seed, int32 StrateIndex, uint32 Seed, int32 StrateIndex,
const TArray<FStrateTerrainOpEntry>* TerrainOps) const TArray<FStrateTerrainOpEntry>* TerrainOps)
{ {
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(VoxelDensityProfile::ECounter::SdfCacheBuild);
}
VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::RoomGraphBuild); VoxelDensityProfile::EBucket::RoomGraphBuild);
// Clear previous data (arrays keep their allocation for reuse) // Clear previous data (arrays keep their allocation for reuse)
@@ -5150,7 +5633,7 @@ void VoxelCaveMorphology::BuildChunkCache(
Room.RadiusZ, Room.RadiusZ,
Room.Hash, Room.Hash,
false}; false};
Room.bHasPlayerFitPoint = VF_FindPlayerFitPointForRoom( Room.bHasPlayerFitPoint = VF_FindPlayerFitPointForRoomMemoized(
InParams, InParams,
Site, Site,
InParams.StrateTopWorldZ, InParams.StrateTopWorldZ,
@@ -2745,11 +2745,6 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
if (bNeedRebuild) if (bNeedRebuild)
{ {
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::SdfCacheBuild);
}
// The lowered evaluator uses the same window-invariance contract as the prepared // The lowered evaluator uses the same window-invariance contract as the prepared
// room-graph source, but keeps a four-chunk XY window per worker. A tile worker can // room-graph source, but keeps a four-chunk XY window per worker. A tile worker can
// visit neighbouring chunk keys in an arbitrary order; the larger deterministic // visit neighbouring chunk keys in an arbitrary order; the larger deterministic
@@ -260,9 +260,10 @@ void RecordTile(const FTileSample& Sample)
GState->Samples.Add(Sample); GState->Samples.Add(Sample);
++GState->TileRecordsWritten; ++GState->TileRecordsWritten;
WriteEventLocked(*GState, TEXT("tile"), FString::Printf( WriteEventLocked(*GState, TEXT("tile"), FString::Printf(
TEXT("\"level\":%d,\"verdict\":%d,\"empty\":%d,\"triangles\":%d," TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"verdict\":%d,\"empty\":%d,\"triangles\":%d,"
"\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s," "\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s,"
"\"generation_s\":%s,\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"), "\"generation_s\":%s,\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"),
Sample.TileX, Sample.TileY, Sample.TileZ,
Sample.Level, Sample.Verdict, Sample.bEmpty ? 1 : 0, Sample.Triangles, Sample.Level, Sample.Verdict, Sample.bEmpty ? 1 : 0, Sample.Triangles,
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds), *Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds), *Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
+19 -2
View File
@@ -17,6 +17,7 @@
#include "VoxelStartupTrace.h" #include "VoxelStartupTrace.h"
#include "VoxelStackSampler.h" #include "VoxelStackSampler.h"
#include "VoxelStats.h" #include "VoxelStats.h"
#include "VoxelCaveMorphology.h"
// IWYU (FPSemantics = Precise ⇒ plus de PCH partagé) : GetPlayerPosition déréférence le pawn, donc // IWYU (FPSemantics = Precise ⇒ plus de PCH partagé) : GetPlayerPosition déréférence le pawn, donc
// APawn doit être COMPLET — `Casts.h` n'en donne qu'une déclaration avant. APlayerController était // APawn doit être COMPLET — `Casts.h` n'en donne qu'une déclaration avant. APlayerController était
// complet par transitivité seulement : on l'inclut explicitement, c'est exactement la fragilité // complet par transitivité seulement : on l'inclut explicitement, c'est exactement la fragilité
@@ -999,6 +1000,7 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
FVoxelStackSampler::Get().StopAndWrite(); FVoxelStackSampler::Get().StopAndWrite();
bSampleStacksStarted = false; bSampleStacksStarted = false;
} }
VoxelCaveMorphology::LogPlayerFitMemoStats();
LogStreamingLatencySummary(); LogStreamingLatencySummary();
PendingTiles.Empty(); PendingTiles.Empty();
PendingTileCancellation.Empty(); PendingTileCancellation.Empty();
@@ -1058,6 +1060,8 @@ void AVoxelWorld::BeginPlay()
VoxelForgeStartupTrace::BeginFromCommandLine(); VoxelForgeStartupTrace::BeginFromCommandLine();
VoxelForgeStartupTrace::FStageScope StartupTraceStage(TEXT("BeginPlay")); VoxelForgeStartupTrace::FStageScope StartupTraceStage(TEXT("BeginPlay"));
Super::BeginPlay(); Super::BeginPlay();
VoxelCaveMorphology::ConfigurePlayerFitMemoFromCommandLine();
VoxelCaveMorphology::ResetPlayerFitMemoStats();
bShuttingDown.store(false, std::memory_order_relaxed); bShuttingDown.store(false, std::memory_order_relaxed);
LOD0ReadySamples.Reset(); LOD0ReadySamples.Reset();
ObsoleteTileAbortCount = 0; ObsoleteTileAbortCount = 0;
@@ -1084,6 +1088,7 @@ void AVoxelWorld::BeginPlay()
int32 CommandLineProfile = GVoxelForgeProfileDensity; int32 CommandLineProfile = GVoxelForgeProfileDensity;
int32 CommandLineProfileFull = GVoxelForgeProfileDensityFull; int32 CommandLineProfileFull = GVoxelForgeProfileDensityFull;
int32 CommandLinePerfAttribution = GVoxelForgePerfAttribution; int32 CommandLinePerfAttribution = GVoxelForgePerfAttribution;
int32 CommandLineProfileTileGeneration = GVoxelForgeProfileTileGeneration;
int32 CommandLineUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut; int32 CommandLineUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut;
int32 CommandLineOuterClassifierMode = GVoxelForgeOuterClassifierMode; int32 CommandLineOuterClassifierMode = GVoxelForgeOuterClassifierMode;
int32 CommandLineSampleStacks = GVoxelForgeSampleStacks; int32 CommandLineSampleStacks = GVoxelForgeSampleStacks;
@@ -1099,6 +1104,13 @@ void AVoxelWorld::BeginPlay()
{ {
GVoxelForgePerfAttribution = CommandLinePerfAttribution; GVoxelForgePerfAttribution = CommandLinePerfAttribution;
} }
if (FParse::Value(
FCommandLine::Get(),
TEXT("voxel.ProfileTileGeneration="),
CommandLineProfileTileGeneration))
{
GVoxelForgeProfileTileGeneration = CommandLineProfileTileGeneration;
}
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.UseBlockEarlyOut="), CommandLineUseBlockEarlyOut)) if (FParse::Value(FCommandLine::Get(), TEXT("voxel.UseBlockEarlyOut="), CommandLineUseBlockEarlyOut))
{ {
GVoxelForgeUseBlockEarlyOut = CommandLineUseBlockEarlyOut; GVoxelForgeUseBlockEarlyOut = CommandLineUseBlockEarlyOut;
@@ -1111,6 +1123,7 @@ void AVoxelWorld::BeginPlay()
{ {
GVoxelForgeSampleStacks = CommandLineSampleStacks; GVoxelForgeSampleStacks = CommandLineSampleStacks;
} }
GVoxelForgeProfileTileGeneration = GVoxelForgeProfileTileGeneration != 0 ? 1 : 0;
GVoxelForgeUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut != 0 ? 1 : 0; GVoxelForgeUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut != 0 ? 1 : 0;
GVoxelForgeOuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1); GVoxelForgeOuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1);
GVoxelForgeSampleStacks = FMath::Clamp( GVoxelForgeSampleStacks = FMath::Clamp(
@@ -2523,6 +2536,9 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
if (bTraceTile) if (bTraceTile)
{ {
VoxelForgeStartupTrace::FTileSample Sample; VoxelForgeStartupTrace::FTileSample Sample;
Sample.TileX = Result.Tile.Coord.X;
Sample.TileY = Result.Tile.Coord.Y;
Sample.TileZ = Result.Tile.Coord.Z;
Sample.Level = Result.Tile.Level; Sample.Level = Result.Tile.Level;
Sample.Verdict = Result.ClassifyVerdict; Sample.Verdict = Result.ClassifyVerdict;
Sample.bEmpty = bResultEmpty; Sample.bEmpty = bResultEmpty;
@@ -3843,7 +3859,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
"rooms=%d tunnels=%d joins=%d pits=%d chimneys=%d " "rooms=%d tunnels=%d joins=%d pits=%d chimneys=%d "
"whole_mixed=%u whole_solid=%u whole_air=%u final_nodes=%u " "whole_mixed=%u whole_solid=%u whole_air=%u final_nodes=%u "
"split_nodes=%u max_depth=%u block_builds=%llu block_samples=%llu " "split_nodes=%u max_depth=%u block_builds=%llu block_samples=%llu "
"block_ops=%llu block_active=%llu block_pruned=%llu"), "block_ops=%llu block_active=%llu block_pruned=%llu cache_builds=%llu"),
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells, Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells,
bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0, bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0,
Result.bEmpty ? 1 : 0, ClassifyVerdict, ClassifySeconds, MeshSeconds, StreamSeconds, Result.bEmpty ? 1 : 0, ClassifyVerdict, ClassifySeconds, MeshSeconds, StreamSeconds,
@@ -3870,7 +3886,8 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::OpBlockSamples)), static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::OpBlockSamples)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::OpBlockOperators)), static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::OpBlockOperators)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::OpBlockActiveOperators)), static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::OpBlockActiveOperators)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::OpBlockPrunedOperators))); static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::OpBlockPrunedOperators)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfCacheBuild)));
if (bProfileOps) if (bProfileOps)
{ {
@@ -872,6 +872,13 @@ struct FTunnelCoreWorldEvaluation
namespace VoxelCaveMorphology namespace VoxelCaveMorphology
{ {
// The player-fit memo is process-wide and uses a fixed-size table. These controls are kept
// beside the cave source so the command-line value is applied before generation workers start,
// while the CVar remains available for an in-process A/B switch.
VOXELFORGE_API void ConfigurePlayerFitMemoFromCommandLine();
VOXELFORGE_API void ResetPlayerFitMemoStats();
VOXELFORGE_API void LogPlayerFitMemoStats();
// The room graph's seed is the world seed salted by the strate index. Keep this formula // The room graph's seed is the world seed salted by the strate index. Keep this formula
// shared by BuildChunkCache and landing-site queries so a query cannot inspect another // shared by BuildChunkCache and landing-site queries so a query cannot inspect another
// strate's room layout. / La seed du graphe des salles est la seed monde salée par l'index // strate's room layout. / La seed du graphe des salles est la seed monde salée par l'index
@@ -9,6 +9,9 @@ namespace VoxelForgeStartupTrace
{ {
struct VOXELFORGE_API FTileSample struct VOXELFORGE_API FTileSample
{ {
int32 TileX = 0;
int32 TileY = 0;
int32 TileZ = 0;
int32 Level = -1; int32 Level = -1;
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (sheet/early path) int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (sheet/early path)
bool bEmpty = true; bool bEmpty = true;