perf(opstack): direct-indexed column box (measured 14.7% miss); + T1.d bail attribution
BUILD RESULTS FIRST: 14/14 tests green, 0 violations. VerticalShafts box verdicts went 0 -> 30 of 60 (zero was the number for the project's whole life). TunnelNetwork production held at 11 proved / 14641 voxels, byte-identical -- that line was the isolated diagnostic forcab8e8f, so its stillness proves Jahni's room/tunnel Min <= Max are correctly ordered. All eight equivalence tests bit-identical, which TESTS the "bounds only, cannot reach density" claim made on7dbdf51/eaa44bf/cab8e8frather than asserting it. PART A -- the column memo thrash is confirmed quantitatively. Measured 14.74% miss rate against a prediction of 14.0% if thrashing and 1.4% if not; and 8,300 recomputes per tile against a healthy 1,225 (6.8x) from an independent statistic. Replaced the 4096-slot direct-mapped hashed table with the direct-indexed box scheme GSurfColCache has always used: no hash, no collisions, every column computed exactly once. ParamsFingerprint is retained in the box key -- its absence was the shipped bug that silently deleted the overhang, and GSurfColCache's own omission of it was deliberately not copied. PART B -- T1.d never fires in game: the two op-stack counters never appeared, and since every displayed row has Min 1.00 rather than 0.00, rows only render when a counter fires. The cave branch has 13 return-Mixed paths; guessing which is the mistake this project keeps paying for. Six attribution counters now name the bail category outright. Only edits there are brace-expansions so a counter fits before each existing return -- every condition and returned value is byte-identical. Open falsifiable hypothesis: Tiles Meshed averages 2.13/frame, plausibly the reason LOD rings update slowly. If misses drop ~10x and LODs speed up, the memo was the ceiling; if not, they are separate problems, cleanly separated. Not built -- Jahni builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
49a9959aed
commit
3ad2720d0a
7 files changed
+350
-45
No files matched your search
@@ -661,43 +661,79 @@ namespace
|
||||
}
|
||||
|
||||
/** La colonne complète, exactement les cinq sorties de `ComputeSurfaceColumn`.
|
||||
* Mémoïsée par (instance, X, Y) : la pile évalue tous les Z d'une colonne au même XY, donc
|
||||
* le taux de succès est ~1 et l'overhang lit la MÊME colonne que la source, par
|
||||
* Le mémo est une boîte à index direct, comme `FSurfaceColumnBox` : la pile évalue tous
|
||||
* les Z d'une colonne au même XY, donc l'overhang lit la MÊME colonne que la source, par
|
||||
* construction plutôt que par convention. */
|
||||
struct FColumn { float TerrainZ, CeilSurf, OverhangAmp, DirX, DirY; };
|
||||
|
||||
const FColumn& GetColumn(float WorldX, float WorldY) const
|
||||
{
|
||||
// ⚠️ POURQUOI UNE TABLE ET PAS UNE SEULE ENTRÉE. Un mémo à une entrée n'est correct que
|
||||
// si l'appelant descend une colonne Z avant de changer de XY. Le mesher n'en promet
|
||||
// RIEN — s'il itère X en premier dans une tranche Z, chaque voxel raterait et on
|
||||
// relancerait toute la pile de hauteur par voxel, cliff compris (4 resamples
|
||||
// structurels). Ce n'est pas « un peu plus lent », c'est un ordre de grandeur sur
|
||||
// l'archétype le plus cher du plugin.
|
||||
//
|
||||
// Table à correspondance directe, clé COMPLÈTE comparée sur touche : une collision ne
|
||||
// peut que coûter un recalcul, jamais rendre une mauvaise colonne.
|
||||
//
|
||||
// TAILLE : un chunk fait CHUNK_SIZE² colonnes (1024 à 32³). Les 256 entrées du premier
|
||||
// jet ne tenaient donc même pas UN chunk — la table se piétinait elle-même à
|
||||
// l'intérieur d'une seule tuile. 4096 entrées couvrent quatre chunks de front, pour
|
||||
// ~150 Ko par worker : du même ordre qu'une boîte de `GSurfColCache` (~59 Ko × 6).
|
||||
//
|
||||
// A chunk is CHUNK_SIZE² columns (1024), so the first draft's 256 entries could not
|
||||
// even hold one chunk and thrashed inside a single tile. 4096 covers four chunks.
|
||||
struct FSlot { uint64 Key; float X, Y; FColumn C; };
|
||||
thread_local FSlot Slots[4096] = {};
|
||||
// Même schéma éprouvé que `GSurfColCache` : index direct dans une boîte XY, puis un
|
||||
// drapeau `Computed` par cellule. Une tuile MC pleine résolution demande
|
||||
// (CHUNK_SIZE + 3)² = 35×35 = 1225 colonnes (anneau de marge inclus) ; cette boîte
|
||||
// de Dim×Dim, recentrée sur le premier échantillon, les garde toutes sans collision.
|
||||
// Same proven scheme as `GSurfColCache`: direct XY indexing plus one `Computed` flag per
|
||||
// cell. A full-resolution MC tile needs 35×35 = 1225 columns including its margin ring;
|
||||
// the box is sized so one tile fits without eviction.
|
||||
struct FColumnBox
|
||||
{
|
||||
enum : int32 { Halo = CHUNK_SIZE + 8, Dim = 2 * Halo + 1 };
|
||||
int32 BaseX = 0, BaseY = 0;
|
||||
uint64 Key = 0; // strate + layout + seed + ParamsFingerprint
|
||||
bool bValid = false;
|
||||
FColumn Cols[Dim * Dim];
|
||||
bool Computed[Dim * Dim];
|
||||
};
|
||||
thread_local FColumnBox Box = {};
|
||||
thread_local FColumn DirectColumn = {};
|
||||
|
||||
const uint32 HX = *reinterpret_cast<const uint32*>(&WorldX);
|
||||
const uint32 HY = *reinterpret_cast<const uint32*>(&WorldY);
|
||||
const uint32 Idx = ((HX * 0x9E3779B9u) ^ (HY * 0x85EBCA6Bu)) >> 20; // [0,4095]
|
||||
// The production mesher and the exact-lattice classifier use integer XY. Fractional
|
||||
// XY is still valid for the public density/equivalence probes: compute it directly so
|
||||
// no integer cell can ever be returned for a different full (WorldX, WorldY) pair.
|
||||
const bool bIntegerXY = WorldX == FMath::FloorToFloat(WorldX)
|
||||
&& WorldY == FMath::FloorToFloat(WorldY);
|
||||
FColumn* MemoColumn = &DirectColumn;
|
||||
int32 CI = 0;
|
||||
bool bNeedsCompute = true;
|
||||
|
||||
FSlot& S = Slots[Idx];
|
||||
if (S.Key != ColumnKey || S.X != WorldX || S.Y != WorldY)
|
||||
if (bIntegerXY)
|
||||
{
|
||||
const int32 IX = (int32)WorldX;
|
||||
const int32 IY = (int32)WorldY;
|
||||
|
||||
// Bounds are checked exactly before deriving CI; the index itself is the XY key.
|
||||
// Les bornes sont vérifiées exactement avant CI : l'index EST la clé XY.
|
||||
if (!Box.bValid || Box.Key != ColumnKey
|
||||
|| IX < Box.BaseX || IX >= Box.BaseX + FColumnBox::Dim
|
||||
|| IY < Box.BaseY || IY >= Box.BaseY + FColumnBox::Dim)
|
||||
{
|
||||
Box.BaseX = IX - FColumnBox::Halo;
|
||||
Box.BaseY = IY - FColumnBox::Halo;
|
||||
Box.Key = ColumnKey;
|
||||
Box.bValid = true;
|
||||
FMemory::Memzero(Box.Computed, sizeof(Box.Computed));
|
||||
}
|
||||
|
||||
CI = (IY - Box.BaseY) * FColumnBox::Dim + (IX - Box.BaseX);
|
||||
MemoColumn = &Box.Cols[CI];
|
||||
if (Box.Computed[CI])
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeColumnMemoHit);
|
||||
bNeedsCompute = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeColumnMemoMiss);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeColumnMemoMiss);
|
||||
S.Key = ColumnKey; S.X = WorldX; S.Y = WorldY;
|
||||
FColumn& C = S.C;
|
||||
}
|
||||
|
||||
if (bNeedsCompute)
|
||||
{
|
||||
FColumn& C = *MemoColumn;
|
||||
|
||||
C.TerrainZ = TerrainStack.EvalHeight(WorldX, WorldY);
|
||||
C.CeilSurf = CeilingStack.EvalHeight(WorldX, WorldY);
|
||||
@@ -754,12 +790,10 @@ namespace
|
||||
// plat — mais l'amplitude y vaut 0 de toute façon.
|
||||
if (Slope > KINDA_SMALL_NUMBER) { C.DirX = GX / Slope; C.DirY = GY / Slope; }
|
||||
}
|
||||
|
||||
if (bIntegerXY) { Box.Computed[CI] = true; }
|
||||
}
|
||||
else
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeColumnMemoHit);
|
||||
}
|
||||
return S.C;
|
||||
return *MemoColumn;
|
||||
}
|
||||
|
||||
/** Le champ structurel nu — l'overhang s'en sert pour emprunter la roche amont.
|
||||
|
||||
Reference in new issue
Block a user