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:
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),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user