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:
2026-08-17 00:41:51 +02:00
parent 1e02c6314f
commit f90c4e56c3
5 changed files with 48 additions and 19 deletions
@@ -129,22 +129,20 @@ namespace VoxelForgeTest
// elle n'était jusqu'ici masquée que par un accident.
//
// `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
// `(ChunkCoord, LayoutVersion)` : deux mondes différents, même version, même chunk ⇒ le
// second se voit servir les params — ET le drapeau `CP_UseOpStack` — du premier.
// Personne ne l'a vu parce que `bUseOperatorStack` valait false partout : les deux
// mondes étaient d'accord par défaut. Le premier monde qui coche la case fait tomber
// cette coïncidence, dans les DEUX sens (il contamine, et il est contaminé).
// rendaient tous les deux **1**. Historiquement, les caches `CP_*` de `GetDensityAt`
// n'avaient que `(ChunkCoord, LayoutVersion)` et le second monde pouvait hériter les
// params — ET `CP_UseOpStack` — du premier. `DensityCacheOwnerId` ferme maintenant CE
// chemin prouvé. Les bumps restent ici comme isolation conservatrice des autres caches
// TLS que cette correction n'a volontairement pas audités ni modifiés.
//
// 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
// (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.
//
// Each test world gets a process-unique LayoutVersion. Two worlds both reporting 1 made
// GetDensityAt's per-chunk caches serve the previous world's params — and its
// CP_UseOpStack flag — for the same chunk coord. Invisible while every world agreed that
// the flag was false.
// Each test world still gets a process-unique LayoutVersion. DensityCacheOwnerId now
// prevents the proved CP_* cross-world reuse directly; the version bumps remain as
// conservative isolation for other TLS caches not audited or changed by that fix.
static int32 GWorldSerial = 0;
const int32 Bumps = ++GWorldSerial;
for (int32 b = 0; b < Bumps; ++b)
+26 -5
View File
@@ -18,6 +18,8 @@
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
#include "VoxelStats.h"
#include <atomic>
//=============================================================================
// 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.
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
{
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)
{
// 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
// 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).
// 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 ECaveGeneratorType CP_GenType = ECaveGeneratorType::TunnelNetwork;
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."
thread_local uint32 CP_Version = 0xFFFFFFFFu;
// 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é.
thread_local FVoxelOpStack CP_OpStack;
thread_local bool CP_UseOpStack = false;
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
// 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.
if (LayoutVersion != CP_Version) { CP_BiomeCache.Invalidate(); }
// même si la boîte couvre encore la requête. Même invalidation quand le propriétaire
// 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_Chunk = ChunkCoord;
CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
@@ -68,6 +68,8 @@ class VOXELFORGE_API UVoxelGenerator : public UObject
GENERATED_BODY()
public:
UVoxelGenerator();
//=========================================================================
// SEED (source unique: Settings->Seed)
//=========================================================================
@@ -321,6 +323,10 @@ public:
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const;
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. */
int32 ClassifyBiomeAtSite(float SiteX, float SiteY, const FBiomeContext& Ctx, uint32 SiteHash) const;