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

+19 -2
View File
@@ -17,6 +17,7 @@
#include "VoxelStartupTrace.h"
#include "VoxelStackSampler.h"
#include "VoxelStats.h"
#include "VoxelCaveMorphology.h"
// 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
// 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();
bSampleStacksStarted = false;
}
VoxelCaveMorphology::LogPlayerFitMemoStats();
LogStreamingLatencySummary();
PendingTiles.Empty();
PendingTileCancellation.Empty();
@@ -1058,6 +1060,8 @@ void AVoxelWorld::BeginPlay()
VoxelForgeStartupTrace::BeginFromCommandLine();
VoxelForgeStartupTrace::FStageScope StartupTraceStage(TEXT("BeginPlay"));
Super::BeginPlay();
VoxelCaveMorphology::ConfigurePlayerFitMemoFromCommandLine();
VoxelCaveMorphology::ResetPlayerFitMemoStats();
bShuttingDown.store(false, std::memory_order_relaxed);
LOD0ReadySamples.Reset();
ObsoleteTileAbortCount = 0;
@@ -1084,6 +1088,7 @@ void AVoxelWorld::BeginPlay()
int32 CommandLineProfile = GVoxelForgeProfileDensity;
int32 CommandLineProfileFull = GVoxelForgeProfileDensityFull;
int32 CommandLinePerfAttribution = GVoxelForgePerfAttribution;
int32 CommandLineProfileTileGeneration = GVoxelForgeProfileTileGeneration;
int32 CommandLineUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut;
int32 CommandLineOuterClassifierMode = GVoxelForgeOuterClassifierMode;
int32 CommandLineSampleStacks = GVoxelForgeSampleStacks;
@@ -1099,6 +1104,13 @@ void AVoxelWorld::BeginPlay()
{
GVoxelForgePerfAttribution = CommandLinePerfAttribution;
}
if (FParse::Value(
FCommandLine::Get(),
TEXT("voxel.ProfileTileGeneration="),
CommandLineProfileTileGeneration))
{
GVoxelForgeProfileTileGeneration = CommandLineProfileTileGeneration;
}
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.UseBlockEarlyOut="), CommandLineUseBlockEarlyOut))
{
GVoxelForgeUseBlockEarlyOut = CommandLineUseBlockEarlyOut;
@@ -1111,6 +1123,7 @@ void AVoxelWorld::BeginPlay()
{
GVoxelForgeSampleStacks = CommandLineSampleStacks;
}
GVoxelForgeProfileTileGeneration = GVoxelForgeProfileTileGeneration != 0 ? 1 : 0;
GVoxelForgeUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut != 0 ? 1 : 0;
GVoxelForgeOuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1);
GVoxelForgeSampleStacks = FMath::Clamp(
@@ -2523,6 +2536,9 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
if (bTraceTile)
{
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.Verdict = Result.ClassifyVerdict;
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 "
"whole_mixed=%u whole_solid=%u whole_air=%u final_nodes=%u "
"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,
bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0,
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::OpBlockOperators)),
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)
{