fix(generator): key CP cache by world owner
Function-static thread_local CP state previously trusted only chunk coordinates and each manager's locally-reused layout version, allowing a worker to carry params, biome context, CP_UseOpStack, and the built stack into another world. Allocate each generator a monotonic owner ID and add one uint64 comparison to the hot key. This intentionally fixes only the proved CP_* path; other TLS caches remain outside this change.
This commit is contained in:
+3
-1
@@ -306,7 +306,9 @@ driven by `EditorBrush*` props.
|
|||||||
- **SDF cache** (`GetDensityWithParams`): search-BOX validity, not chunk-key — gradient ±1
|
- **SDF cache** (`GetDensityWithParams`): search-BOX validity, not chunk-key — gradient ±1
|
||||||
sampling must not thrash the (expensive) rebuild.
|
sampling must not thrash the (expensive) rebuild.
|
||||||
- **Per-chunk param cache** in `GetDensityAt`: GenType + param struct + disturbance cached
|
- **Per-chunk param cache** in `GetDensityAt`: GenType + param struct + disturbance cached
|
||||||
thread-locally per chunk; don't move the fetch/blend back to per-voxel.
|
thread-locally by `(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`; the process-unique owner ID
|
||||||
|
prevents cross-world reuse while adding only one `uint64` compare per voxel. Don't remove the owner
|
||||||
|
or layout key, and don't move the fetch/blend back to per-voxel.
|
||||||
- **Biome cache** (`ResolveBiomeSampleAt`/`FChunkBiomeCache`, §8.14): validity is a world-XY BOX +
|
- **Biome cache** (`ResolveBiomeSampleAt`/`FChunkBiomeCache`, §8.14): validity is a world-XY BOX +
|
||||||
ChunkZ + Seed, NOT a chunk key — same reason as the SDF cache. The cell classification is
|
ChunkZ + Seed, NOT a chunk key — same reason as the SDF cache. The cell classification is
|
||||||
noise-heavy; a chunk-key would thrash it on gradient-normal / +X/+Y boundary samples. Keep
|
noise-heavy; a chunk-key would thrash it on gradient-normal / +X/+Y boundary samples. Keep
|
||||||
|
|||||||
+5
-3
@@ -251,18 +251,20 @@ redesign; tile identity lives in `FVoxelTileKey` (VoxelWorld.h).
|
|||||||
> **Game-thread profiling (Perf):** `AVoxelWorld::Tick` and its sub-steps are wrapped in `TRACE_CPUPROFILER_EVENT_SCOPE` — `VoxelForge_Tick / UpdateChunks / BuildDesiredTiles / CullTiles / SubmitTiles / ProcessPending / ProcessUnload / UpdateDecorations / UpdateWater`. Capture a `Count/Incl/Excl` Insights timer export and read the `Excl` column to see which step owns the per-frame cost (the actor tick shows as `BP_VoxelWorld_C` if subclassed in BP). `VoxelForge_ClassifyTile` (T1.d) / `VoxelForge_GenerateMesh` + `VoxelForge_BuildStreams` are worker-side (off the frame): the RMC `FRealtimeMeshStreamSet` is now built on the gen worker (`BuildTileStreamSet`) and carried on `FChunkResult::Streams` (TSharedPtr), so `ApplyMeshToTile` is game-thread-cheap — just material/ceiling resolve + `CreateSectionGroup(MoveTemp)`. See ARCHITECTURE §8.10 "Worker-built StreamSet (T1.f)".
|
> **Game-thread profiling (Perf):** `AVoxelWorld::Tick` and its sub-steps are wrapped in `TRACE_CPUPROFILER_EVENT_SCOPE` — `VoxelForge_Tick / UpdateChunks / BuildDesiredTiles / CullTiles / SubmitTiles / ProcessPending / ProcessUnload / UpdateDecorations / UpdateWater`. Capture a `Count/Incl/Excl` Insights timer export and read the `Excl` column to see which step owns the per-frame cost (the actor tick shows as `BP_VoxelWorld_C` if subclassed in BP). `VoxelForge_ClassifyTile` (T1.d) / `VoxelForge_GenerateMesh` + `VoxelForge_BuildStreams` are worker-side (off the frame): the RMC `FRealtimeMeshStreamSet` is now built on the gen worker (`BuildTileStreamSet`) and carried on `FChunkResult::Streams` (TSharedPtr), so `ApplyMeshToTile` is game-thread-cheap — just material/ceiling resolve + `CreateSectionGroup(MoveTemp)`. See ARCHITECTURE §8.10 "Worker-built StreamSet (T1.f)".
|
||||||
|
|
||||||
### 3.6 Density generator — `Public/VoxelGenerator.h` + `Private/VoxelGenerator.cpp`
|
### 3.6 Density generator — `Public/VoxelGenerator.h` + `Private/VoxelGenerator.cpp`
|
||||||
`UVoxelGenerator : UObject` — lightweight; holds `Seed`, and injected services
|
`UVoxelGenerator : UObject` — lightweight; holds `Seed`, a process-unique
|
||||||
`StrateManager` + `DiffLayer` (both nullable). This is **where terrain shape lives.**
|
`DensityCacheOwnerId`, and injected services `StrateManager` + `DiffLayer` (both nullable).
|
||||||
|
This is **where terrain shape lives.**
|
||||||
|
|
||||||
| Symbol | .cpp line | Role |
|
| Symbol | .cpp line | Role |
|
||||||
|--------|-----------|------|
|
|--------|-----------|------|
|
||||||
|
| `UVoxelGenerator` / `DensityCacheOwnerId` | — | Constructor allocates a process-unique integer identity (relaxed atomic, once per object). `GetDensityAt` includes it in the `CP_*` thread-local key, preventing a worker from serving another generator/world's params, biome context, `CP_UseOpStack`, or stack when `(ChunkCoord, LayoutVersion)` happens to match. Hot-path cost: one `uint64` compare per voxel. Scope is deliberately only the proved `CP_*` path. |
|
||||||
| `FractalNoise3D` (static) | 25 | fBM (layered Perlin). |
|
| `FractalNoise3D` (static) | 25 | fBM (layered Perlin). |
|
||||||
| `RidgedNoise3D` (static) | 55 | Ridged multifractal — craggy. |
|
| `RidgedNoise3D` (static) | 55 | Ridged multifractal — craggy. |
|
||||||
| `CellularNoise3D` (static) | 101 | Worley/cellular — grotto/scallop. |
|
| `CellularNoise3D` (static) | 101 | Worley/cellular — grotto/scallop. |
|
||||||
| `ApplyBoundarySeal` (static) | 170 | Solidifies strate top/bottom shells. |
|
| `ApplyBoundarySeal` (static) | 170 | Solidifies strate top/bottom shells. |
|
||||||
| `ApplyPassageCarving` (static) | 197 | Punches passages/elevator through the seal. |
|
| `ApplyPassageCarving` (static) | 197 | Punches passages/elevator through the seal. |
|
||||||
| `InitializeSettings` | 211 | Copies seed from settings. |
|
| `InitializeSettings` | 211 | Copies seed from settings. |
|
||||||
| **`GetDensityAt`** | 218 | **Entry point.** Picks strate + generator type, dispatches, adds diff offset. |
|
| **`GetDensityAt`** | 218 | **Entry point.** Picks strate + generator type, dispatches, adds diff offset. Its `CP_*` per-chunk state is keyed by `(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`; every key component is an integer compare and a different generator/world cannot inherit the previous owner's cached params or op stack. |
|
||||||
| **`GetDensityWithParams`** | 277 | TunnelNetwork pipeline (~1000 lines). See §4. ⚠️ Takes **required** `ParamsFingerprint` + `LayoutVersion` since the AUDIT §C2 fix (2026-07-28) — they go into the SDF cache key so a chunk can no longer be evaluated against a neighbour's rooms. Callers compute the CRC **once per chunk** (`CP_TunnelFP`), never per voxel. |
|
| **`GetDensityWithParams`** | 277 | TunnelNetwork pipeline (~1000 lines). See §4. ⚠️ Takes **required** `ParamsFingerprint` + `LayoutVersion` since the AUDIT §C2 fix (2026-07-28) — they go into the SDF cache key so a chunk can no longer be evaluated against a neighbour's rooms. Callers compute the CRC **once per chunk** (`CP_TunnelFP`), never per voxel. |
|
||||||
| **`GetSlabDensity`** | 1306 | FlatPlain/CrystalChamber pipeline. See §4.2. |
|
| **`GetSlabDensity`** | 1306 | FlatPlain/CrystalChamber pipeline. See §4.2. |
|
||||||
| `SampleSurfaceStructuralZ` | — | **F20:** the RAW SurfaceWorld heightfield (continents+mountains+detail), BEFORE any terrain op; returns terrain Z + relief M. Cliff re-samples it at an XY offset for a cheap analytic slope. |
|
| `SampleSurfaceStructuralZ` | — | **F20:** the RAW SurfaceWorld heightfield (continents+mountains+detail), BEFORE any terrain op; returns terrain Z + relief M. Cliff re-samples it at an XY offset for a cheap analytic slope. |
|
||||||
|
|||||||
@@ -129,22 +129,20 @@ namespace VoxelForgeTest
|
|||||||
// elle n'était jusqu'ici masquée que par un accident.
|
// elle n'était jusqu'ici masquée que par un accident.
|
||||||
//
|
//
|
||||||
// `PassagesVersion` est PAR INSTANCE et part de 0, donc deux `FTestWorld` successifs
|
// `PassagesVersion` est PAR INSTANCE et part de 0, donc deux `FTestWorld` successifs
|
||||||
// rendaient tous les deux **1**. Or les caches par chunk de `GetDensityAt` sont clés sur
|
// rendaient tous les deux **1**. Historiquement, les caches `CP_*` de `GetDensityAt`
|
||||||
// `(ChunkCoord, LayoutVersion)` : deux mondes différents, même version, même chunk ⇒ le
|
// n'avaient que `(ChunkCoord, LayoutVersion)` et le second monde pouvait hériter les
|
||||||
// second se voit servir les params — ET le drapeau `CP_UseOpStack` — du premier.
|
// params — ET `CP_UseOpStack` — du premier. `DensityCacheOwnerId` ferme maintenant CE
|
||||||
// Personne ne l'a vu parce que `bUseOperatorStack` valait false partout : les deux
|
// chemin prouvé. Les bumps restent ici comme isolation conservatrice des autres caches
|
||||||
// mondes étaient d'accord par défaut. Le premier monde qui coche la case fait tomber
|
// TLS que cette correction n'a volontairement pas audités ni modifiés.
|
||||||
// cette coïncidence, dans les DEUX sens (il contamine, et il est contaminé).
|
|
||||||
//
|
//
|
||||||
// Un compteur de processus donne à chaque monde une version distincte, donc tout cache
|
// Un compteur de processus donne à chaque monde une version distincte, donc tout cache
|
||||||
// survivant d'un test à l'autre est forcément invalidé. `Initialize` est déterministe
|
// survivant d'un test à l'autre est forcément invalidé. `Initialize` est déterministe
|
||||||
// (le pool est mélangé par le seed, les fixed strates sont épinglées), donc le rappeler
|
// (le pool est mélangé par le seed, les fixed strates sont épinglées), donc le rappeler
|
||||||
// ne change pas le layout — seulement le compteur.
|
// ne change pas le layout — seulement le compteur.
|
||||||
//
|
//
|
||||||
// Each test world gets a process-unique LayoutVersion. Two worlds both reporting 1 made
|
// Each test world still gets a process-unique LayoutVersion. DensityCacheOwnerId now
|
||||||
// GetDensityAt's per-chunk caches serve the previous world's params — and its
|
// prevents the proved CP_* cross-world reuse directly; the version bumps remain as
|
||||||
// CP_UseOpStack flag — for the same chunk coord. Invisible while every world agreed that
|
// conservative isolation for other TLS caches not audited or changed by that fix.
|
||||||
// the flag was false.
|
|
||||||
static int32 GWorldSerial = 0;
|
static int32 GWorldSerial = 0;
|
||||||
const int32 Bumps = ++GWorldSerial;
|
const int32 Bumps = ++GWorldSerial;
|
||||||
for (int32 b = 0; b < Bumps; ++b)
|
for (int32 b = 0; b < Bumps; ++b)
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
|
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
|
||||||
#include "VoxelStats.h"
|
#include "VoxelStats.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// L'ADAPTATEUR DE CHAMP DE BIOMES / THE BIOME FIELD ADAPTER
|
// L'ADAPTATEUR DE CHAMP DE BIOMES / THE BIOME FIELD ADAPTER
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -445,6 +447,13 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
|
|||||||
// never fetched here — both callers already have them.
|
// never fetched here — both callers already have them.
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
// Une identité monotone évite qu'un worker réutilise les CP_* d'un monde détruit même si
|
||||||
|
// l'allocateur UObject recycle plus tard la même adresse. Relaxed suffit : on ne publie aucune
|
||||||
|
// donnée, on alloue seulement une valeur distincte par instance.
|
||||||
|
// A monotonic identity prevents stale CP_* reuse even if UObject allocation later recycles an
|
||||||
|
// address. Relaxed ordering is sufficient: this allocates uniqueness, it publishes no data.
|
||||||
|
std::atomic<uint64> GNextDensityCacheOwnerId { 0 };
|
||||||
|
|
||||||
struct FVoxelStackParamRefs
|
struct FVoxelStackParamRefs
|
||||||
{
|
{
|
||||||
const FSlabGenerationParams* Slab = nullptr;
|
const FSlabGenerationParams* Slab = nullptr;
|
||||||
@@ -542,6 +551,11 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UVoxelGenerator::UVoxelGenerator()
|
||||||
|
: DensityCacheOwnerId(GNextDensityCacheOwnerId.fetch_add(1, std::memory_order_relaxed) + 1)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
|
void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
|
||||||
{
|
{
|
||||||
// Seul le seed est copié ici. Tout le reste (params de cave, transitions,
|
// Seul le seed est copié ici. Tout le reste (params de cave, transitions,
|
||||||
@@ -579,7 +593,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
// The generator type, the (boundary-blended) param struct, and the disturbance
|
// The generator type, the (boundary-blended) param struct, and the disturbance
|
||||||
// params are identical for the whole chunk, yet resolving them re-runs a strate
|
// params are identical for the whole chunk, yet resolving them re-runs a strate
|
||||||
// lookup + copies large structs (and a ~60-field Lerp for blended cave chunks).
|
// lookup + copies large structs (and a ~60-field Lerp for blended cave chunks).
|
||||||
// Cache them thread-locally, keyed by chunk coord — refetch only on chunk change.
|
// Cache them thread-locally, keyed by owner + chunk coord + layout version — refetch only
|
||||||
|
// when one of those integer identities changes.
|
||||||
|
thread_local uint64 CP_OwnerId = 0;
|
||||||
thread_local FIntVector CP_Chunk(INT32_MAX, INT32_MAX, INT32_MAX);
|
thread_local FIntVector CP_Chunk(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||||
thread_local ECaveGeneratorType CP_GenType = ECaveGeneratorType::TunnelNetwork;
|
thread_local ECaveGeneratorType CP_GenType = ECaveGeneratorType::TunnelNetwork;
|
||||||
thread_local FStrateGenerationParams CP_Tunnel;
|
thread_local FStrateGenerationParams CP_Tunnel;
|
||||||
@@ -613,18 +629,23 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
// "I tweaked the strate asset, regenerated, and one patch kept the old shape."
|
// "I tweaked the strate asset, regenerated, and one patch kept the old shape."
|
||||||
thread_local uint32 CP_Version = 0xFFFFFFFFu;
|
thread_local uint32 CP_Version = 0xFFFFFFFFu;
|
||||||
// OPSTACK Phase 1 — la pile d'opérateurs, construite dans le MÊME bloc de refetch que les
|
// OPSTACK Phase 1 — la pile d'opérateurs, construite dans le MÊME bloc de refetch que les
|
||||||
// params (donc même clé chunk+version, aucune logique d'invalidation en plus). Vide tant que
|
// params (donc même clé owner+chunk+version, aucune logique d'invalidation en plus). Vide tant que
|
||||||
// la strate n'a pas coché `bUseOperatorStack` ET que son archétype n'est pas porté.
|
// la strate n'a pas coché `bUseOperatorStack` ET que son archétype n'est pas porté.
|
||||||
thread_local FVoxelOpStack CP_OpStack;
|
thread_local FVoxelOpStack CP_OpStack;
|
||||||
thread_local bool CP_UseOpStack = false;
|
thread_local bool CP_UseOpStack = false;
|
||||||
|
|
||||||
const uint32 LayoutVersion = StrateManager->GetLayoutVersion();
|
const uint32 LayoutVersion = StrateManager->GetLayoutVersion();
|
||||||
if (ChunkCoord != CP_Chunk || LayoutVersion != CP_Version)
|
const bool bOwnerChanged = DensityCacheOwnerId != CP_OwnerId;
|
||||||
|
if (bOwnerChanged || ChunkCoord != CP_Chunk || LayoutVersion != CP_Version)
|
||||||
{
|
{
|
||||||
// La grille de biome est validée par une BOÎTE XY, qui ne dit rien du FBiomeContext
|
// La grille de biome est validée par une BOÎTE XY, qui ne dit rien du FBiomeContext
|
||||||
// ayant servi à classer ses cellules : sur un changement de version elle est périmée
|
// ayant servi à classer ses cellules : sur un changement de version elle est périmée
|
||||||
// même si la boîte couvre encore la requête.
|
// même si la boîte couvre encore la requête. Même invalidation quand le propriétaire
|
||||||
if (LayoutVersion != CP_Version) { CP_BiomeCache.Invalidate(); }
|
// change : deux mondes peuvent partager version et coordonnées, jamais leur contexte.
|
||||||
|
// The biome grid's XY box says nothing about its context. Owner changes invalidate it
|
||||||
|
// too: two worlds may share version and coordinates, never cached params/context.
|
||||||
|
if (bOwnerChanged || LayoutVersion != CP_Version) { CP_BiomeCache.Invalidate(); }
|
||||||
|
CP_OwnerId = DensityCacheOwnerId;
|
||||||
CP_Version = LayoutVersion;
|
CP_Version = LayoutVersion;
|
||||||
CP_Chunk = ChunkCoord;
|
CP_Chunk = ChunkCoord;
|
||||||
CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
|
CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ class VOXELFORGE_API UVoxelGenerator : public UObject
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
UVoxelGenerator();
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// SEED (source unique: Settings->Seed)
|
// SEED (source unique: Settings->Seed)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -321,6 +323,10 @@ public:
|
|||||||
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const;
|
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/** Identité process-unique du propriétaire des caches `CP_*` thread_local.
|
||||||
|
* Process-unique owner identity for the `CP_*` thread-local cache key. */
|
||||||
|
uint64 DensityCacheOwnerId = 0;
|
||||||
|
|
||||||
/** Pick the biome (index into Ctx.Biomes) for a Voronoi site, by its climate. */
|
/** Pick the biome (index into Ctx.Biomes) for a Voronoi site, by its climate. */
|
||||||
int32 ClassifyBiomeAtSite(float SiteX, float SiteY, const FBiomeContext& Ctx, uint32 SiteHash) const;
|
int32 ClassifyBiomeAtSite(float SiteX, float SiteY, const FBiomeContext& Ctx, uint32 SiteHash) const;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user