diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index 078870b..f3c8856 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -1814,9 +1814,9 @@ namespace void Eval(float X, float Y, float Z, FVoxelOpSample& InOut) const override { - if (Manager) + if (const UVoxelStrateManager* LiveManager = Manager.Get()) { - Manager->ApplyPassageModifier(InOut.Density, X, Y, Z, Base, Seal); + LiveManager->ApplyPassageModifier(InOut.Density, X, Y, Z, Base, Seal); } // Keep the source and op-stack operation order identical: the origin room is carved // before the boundary seal, then its support floor is reasserted after the passage @@ -1829,23 +1829,24 @@ namespace // simplement branchée au bon endroit au lieu d'être un cas particulier du classifieur. EVoxelOpEffect EffectOverBox(const FBox& VoxelBox, const FVoxelOpContext&) const override { - if (!Manager) { return EVoxelOpEffect::Identity; } + const UVoxelStrateManager* LiveManager = Manager.Get(); + if (!LiveManager) { return EVoxelOpEffect::Identity; } if (VoxelPassageGeometry::OriginLandingFloorTouchesBox( VoxelBox, TopZ, BotZ, Seal, SpineRadius) - || Manager->AnyPassageLandingFloorNearBox(VoxelBox.Min, VoxelBox.Max)) + || LiveManager->AnyPassageLandingFloorNearBox(VoxelBox.Min, VoxelBox.Max)) { // The room carves air, while its support slab force-writes solid. Both // hypotheses must therefore be killed for a box touching that slab. return EVoxelOpEffect::Both; } - return Manager->AnyPassageNearBox(VoxelBox.Min, VoxelBox.Max) + return LiveManager->AnyPassageNearBox(VoxelBox.Min, VoxelBox.Max) ? EVoxelOpEffect::CarveOnly : EVoxelOpEffect::Identity; } const TCHAR* DebugName() const override { return TEXT("PassageCarveOp"); } private: - const UVoxelStrateManager* Manager; + TWeakObjectPtr Manager; float Base, Seal, TopZ, BotZ, SpineRadius; }; @@ -2805,6 +2806,7 @@ namespace FRoomGraphSource(const FStrateGenerationParams& InP, int32 InSeed, const UVoxelStrateManager* InManager) : P(InP), Seed(InSeed), SeedU((uint32)InSeed), Manager(InManager) + , ManagerLifetimeId(InManager ? InManager->GetCacheLifetimeId() : 0) , ParamsFingerprint(FCrc::MemCrc32(&InP, sizeof(InP))) {} @@ -2853,6 +2855,7 @@ namespace uint32 CachedSeed = 0; uint32 CachedFingerprint = 0xFFFFFFFFu; uint32 CachedLayout = 0xFFFFFFFFu; + uint64 CachedManagerLifetimeId = 0; /** La salle de SDF minimal pour le dernier voxel évalué. -1 = aucune. */ int32 NearestRoom = -1; @@ -2991,6 +2994,18 @@ namespace // sur chaque voxel de roc profond, ce que l'original ne paie pas.) S.bLocalParamsValid = false; + const UVoxelStrateManager* LiveManager = Manager.Get(); + // A cached stack may outlive its UObject owner when an editor world is stopped. The + // generator's manager-lifetime key prevents selecting it normally; this guard is the + // final boundary for a task that is already inside an old stack. Keep commandlet stacks + // (which deliberately have no manager) functional, but never evaluate stale room data. + if (ManagerLifetimeId != 0 && LiveManager == nullptr) + { + S.Cache = FChunkSDFCache(); + InOut.Sdf = FLT_MAX; + return; + } + if (!(P.RoomDensity > 0.0f && P.RoomSpacing > 0.0f)) { return; } // Sdf reste FLT_MAX const float EffectiveZ = EffZ(WorldZ); @@ -3046,18 +3061,19 @@ namespace // Index de strate — mémo (chunk-Z, version de layout), transcrit tel quel. La requête // vise le CENTRE de la bande, donc le résultat est une fonction pure de la clé. int32 StrateIdx = 0; - if (Manager) + if (LiveManager) { thread_local int32 SI_ChunkZ = INT32_MAX; thread_local uint32 SI_Version = 0xFFFFFFFFu; thread_local int32 SI_Index = 0; const int32 QZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE); - const uint32 LV = Manager->GetLayoutVersion(); + const uint32 LV = LiveManager->GetLayoutVersion(); if (QZ != SI_ChunkZ || LV != SI_Version) { SI_ChunkZ = QZ; SI_Version = LV; - SI_Index = Manager->GetStrateIndex(((float)QZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE); + SI_Index = LiveManager->GetStrateIndex( + ((float)QZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE); } StrateIdx = SI_Index; } @@ -3065,6 +3081,7 @@ namespace const bool bNeedRebuild = StrateIdx != S.CachedStrate || SeedU != S.CachedSeed || ParamsFingerprint != S.CachedFingerprint || LayoutVersion != S.CachedLayout || + ManagerLifetimeId != S.CachedManagerLifetimeId || WarpedX < S.CachedSMinX || WarpedX > S.CachedSMaxX || WarpedY < S.CachedSMinY || WarpedY > S.CachedSMaxY; @@ -3084,10 +3101,10 @@ namespace const float SMaxY = ChunkMaxY + Expansion; const TArray* TerrainOps = nullptr; - if (Manager) + if (LiveManager) { const int32 ChunkZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE); - UVoxelStrateDefinition* Def = Manager->GetStrateForChunk( + UVoxelStrateDefinition* Def = LiveManager->GetStrateForChunk( FIntVector(CacheChunkX, CacheChunkY, ChunkZ)); if (Def) { TerrainOps = &Def->TerrainOperations; } } @@ -3101,6 +3118,7 @@ namespace S.CachedSeed = SeedU; S.CachedFingerprint = ParamsFingerprint; S.CachedLayout = LayoutVersion; + S.CachedManagerLifetimeId = ManagerLifetimeId; } float CaveSDF = VoxelCaveMorphology::EvaluateSDFCached( @@ -3198,6 +3216,7 @@ namespace uint32 KeySeed = 0; uint32 KeyFingerprint = 0xFFFFFFFFu; uint32 KeyLayout = 0xFFFFFFFFu; + uint64 KeyManagerLifetimeId = 0; bool bValid = false; EVoxelOpEffect Verdict = EVoxelOpEffect::Both; FVoxelBoxSdfInterval SdfInterval; @@ -3281,6 +3300,7 @@ namespace const FVoxelOpContext& Ctx) const override { FBoxState& B = BoxState(); + const UVoxelStrateManager* LiveManager = Manager.Get(); auto Unknown = [&]() { @@ -3290,6 +3310,12 @@ namespace }; auto Finite = [](float V) { return FMath::IsFinite(V); }; + if (ManagerLifetimeId != 0 && LiveManager == nullptr) + { + Unknown(); + return; + } + const float BoxMinX = (float)VoxelBox.Min.X; const float BoxMinY = (float)VoxelBox.Min.Y; const float BoxMinZ = (float)VoxelBox.Min.Z; @@ -3362,13 +3388,13 @@ namespace int32 StrateIdx = 0; const TArray* TerrainOps = nullptr; - if (Manager) + if (LiveManager) { - StrateIdx = Manager->GetStrateIndex( + StrateIdx = LiveManager->GetStrateIndex( ((float)CZ0 + 0.5f) * CHUNK_SIZE * VOXEL_SIZE); for (int32 CZ = CZ0 + 1; CZ <= CZ1; ++CZ) { - if (Manager->GetStrateIndex( + if (LiveManager->GetStrateIndex( ((float)CZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE) != StrateIdx) { Unknown(); @@ -3380,12 +3406,12 @@ namespace // columns from it. Every chunk touched by this query must therefore resolve to the // same definition before one cache can represent the box. UVoxelStrateDefinition* Def0 = - Manager->GetStrateForChunk(FIntVector(CX0, CY0, CZ0)); + LiveManager->GetStrateForChunk(FIntVector(CX0, CY0, CZ0)); for (int32 CZ = CZ0; CZ <= CZ1; ++CZ) for (int32 CY = CY0; CY <= CY1; ++CY) for (int32 CX = CX0; CX <= CX1; ++CX) { - if (Manager->GetStrateForChunk(FIntVector(CX, CY, CZ)) != Def0) + if (LiveManager->GetStrateForChunk(FIntVector(CX, CY, CZ)) != Def0) { Unknown(); return; @@ -3396,7 +3422,8 @@ namespace const uint32 LV = Ctx.LayoutVersion; if (B.bValid && B.KeyBox == VoxelBox && B.KeyStrate == StrateIdx - && B.KeySeed == SeedU && B.KeyFingerprint == ParamsFingerprint && B.KeyLayout == LV) + && B.KeySeed == SeedU && B.KeyFingerprint == ParamsFingerprint && B.KeyLayout == LV + && B.KeyManagerLifetimeId == ManagerLifetimeId) { InOut = B.SdfInterval; return; @@ -3603,6 +3630,7 @@ namespace B.KeySeed = SeedU; B.KeyFingerprint = ParamsFingerprint; B.KeyLayout = LV; + B.KeyManagerLifetimeId = ManagerLifetimeId; B.bValid = true; InOut = Own; } @@ -3620,7 +3648,8 @@ namespace FStrateGenerationParams P; int32 Seed; uint32 SeedU; - const UVoxelStrateManager* Manager; // NON possédant + TWeakObjectPtr Manager; + uint64 ManagerLifetimeId = 0; uint32 ParamsFingerprint; uint32 LayoutVersion = 0; }; diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index a32d546..a850439 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -470,6 +470,7 @@ namespace { FChunkSDFCache Cache; uint64 OwnerId = 0; + uint64 ManagerLifetimeId = 0; FIntVector Chunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX); uint32 ParamsFingerprint = 0xFFFFFFFFu; uint32 LayoutVersion = 0xFFFFFFFFu; @@ -492,6 +493,7 @@ namespace { bool bValid = false; uint64 OwnerId = 0; + uint64 ManagerLifetimeId = 0; FIntVector Chunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX); uint32 LayoutVersion = 0xFFFFFFFFu; ECaveGeneratorType GenType = ECaveGeneratorType::TunnelNetwork; @@ -518,11 +520,13 @@ namespace const FStrateGenerationParams& Params, uint32 ParamsFingerprint, uint32 LayoutVersion, + uint64 ManagerLifetimeId, uint64 OwnerId, FTunnelCoreCacheState& OutState) { if (OutState.bValid && OutState.OwnerId == OwnerId + && OutState.ManagerLifetimeId == ManagerLifetimeId && OutState.Chunk == ChunkCoord && OutState.ParamsFingerprint == ParamsFingerprint && OutState.LayoutVersion == LayoutVersion) @@ -546,6 +550,7 @@ namespace Params, static_cast(Manager.GetWorldSeed()), StrateIndex, nullptr); OutState.OwnerId = OwnerId; + OutState.ManagerLifetimeId = ManagerLifetimeId; OutState.Chunk = ChunkCoord; OutState.ParamsFingerprint = ParamsFingerprint; OutState.LayoutVersion = LayoutVersion; @@ -799,6 +804,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co // 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; + thread_local uint64 CP_ManagerLifetimeId = 0; thread_local FTunnelNetworkDensityCacheEntry CP_TunnelDensityCache[TunnelDensityCacheSlotCount]; thread_local FTunnelNetworkDensityCacheEntry* CP_ActiveTunnelDensityCache = nullptr; // Cooked-season recipes and editor candidate recipes share this immutable worker-local @@ -817,6 +823,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co #endif const uint32 LayoutVersion = StrateManager->GetLayoutVersion(); + const uint64 ManagerLifetimeId = StrateManager->GetCacheLifetimeId(); FVoxelOpStack* ActiveOpStack = &CP_OpStack; FTunnelCoreCacheState* ActiveTunnelCoreCache = >unnelCoreCache; bool bLoadedTunnelDensityCache = false; @@ -827,6 +834,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co CP_TunnelDensityCache[TunnelDensityCacheSlot(ChunkCoord)]; const bool bCacheKeyMatches = CachedEntry.bValid && CachedEntry.OwnerId == DensityCacheOwnerId + && CachedEntry.ManagerLifetimeId == ManagerLifetimeId && CachedEntry.Chunk == ChunkCoord && CachedEntry.LayoutVersion == LayoutVersion && (CachedEntry.GenType == ECaveGeneratorType::TunnelNetwork @@ -836,6 +844,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co if (CP_ActiveTunnelDensityCache != &CachedEntry) { CP_OwnerId = CachedEntry.OwnerId; + CP_ManagerLifetimeId = CachedEntry.ManagerLifetimeId; CP_Version = CachedEntry.LayoutVersion; CP_Chunk = CachedEntry.Chunk; CP_GenType = CachedEntry.GenType; @@ -856,8 +865,10 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co } const bool bOwnerChanged = DensityCacheOwnerId != CP_OwnerId; + const bool bManagerChanged = ManagerLifetimeId != CP_ManagerLifetimeId; if (!bLoadedTunnelDensityCache - && (bOwnerChanged || ChunkCoord != CP_Chunk || LayoutVersion != CP_Version)) + && (bOwnerChanged || bManagerChanged || ChunkCoord != CP_Chunk + || LayoutVersion != CP_Version)) { CP_ActiveTunnelDensityCache = nullptr; @@ -867,8 +878,12 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co // 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(); } + if (bOwnerChanged || bManagerChanged || LayoutVersion != CP_Version) + { + CP_BiomeCache.Invalidate(); + } CP_OwnerId = DensityCacheOwnerId; + CP_ManagerLifetimeId = ManagerLifetimeId; CP_Version = LayoutVersion; CP_Chunk = ChunkCoord; CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord); @@ -944,7 +959,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co { PrepareTunnelCoreCache( *StrateManager, ChunkCoord, CP_Tunnel, CP_TunnelFP, - LayoutVersion, DensityCacheOwnerId, GTunnelCoreCache); + LayoutVersion, ManagerLifetimeId, DensityCacheOwnerId, GTunnelCoreCache); } else { @@ -1072,6 +1087,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co CP_TunnelDensityCache[TunnelDensityCacheSlot(ChunkCoord)]; CachedEntry.bValid = false; CachedEntry.OwnerId = DensityCacheOwnerId; + CachedEntry.ManagerLifetimeId = ManagerLifetimeId; CachedEntry.Chunk = ChunkCoord; CachedEntry.LayoutVersion = LayoutVersion; CachedEntry.GenType = CP_GenType; diff --git a/Source/VoxelForge/Private/VoxelStrateManager.cpp b/Source/VoxelForge/Private/VoxelStrateManager.cpp index a8611ea..2fe76e7 100644 --- a/Source/VoxelForge/Private/VoxelStrateManager.cpp +++ b/Source/VoxelForge/Private/VoxelStrateManager.cpp @@ -12,6 +12,18 @@ #include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten) #include "UObject/UObjectGlobals.h" +#include + +namespace +{ + std::atomic GNextStrateManagerLifetimeId { 0 }; +} + +UVoxelStrateManager::UVoxelStrateManager() + : CacheLifetimeId(GNextStrateManagerLifetimeId.fetch_add(1, std::memory_order_relaxed) + 1) +{ +} + #if WITH_EDITOR #include "VoxelStrateComposer.h" @@ -73,6 +85,7 @@ namespace struct FPassageEvaluationCache { const UVoxelStrateManager* Owner = nullptr; + uint64 OwnerLifetimeId = 0; FIntVector Chunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX); uint32 Version = 0xFFFFFFFFu; TArray Nearby; @@ -90,12 +103,15 @@ namespace { FPassageEvaluationCache& Cache = VF_GetPassageEvaluationCache(); const uint32 Version = Manager ? Manager->GetLayoutVersion() : 0u; - if (Cache.Owner == Manager && Cache.Chunk == ChunkCoord && Cache.Version == Version) + const uint64 LifetimeId = Manager ? Manager->GetCacheLifetimeId() : 0; + if (Cache.Owner == Manager && Cache.OwnerLifetimeId == LifetimeId + && Cache.Chunk == ChunkCoord && Cache.Version == Version) { return Cache.Nearby; } Cache.Owner = Manager; + Cache.OwnerLifetimeId = LifetimeId; Cache.Chunk = ChunkCoord; Cache.Version = Version; Cache.Nearby.Reset(); diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 2b13f6b..4b8b6be 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -857,17 +857,18 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason) // Signal all async tasks to bail out ASAP bShuttingDown.store(true, std::memory_order_release); - // Wait for all running tasks to finish before destroying UObjects. - // Tasks check bShuttingDown and exit early, so this should be fast. - // Timeout after 3 seconds to avoid hanging the editor. + // Wait for every running task before destroying UObjects. A timeout here is unsafe: a task + // already inside GenerateTileResult may still be evaluating a cached op stack that points at + // this world's manager. Log a slow drain, but never tear the world down underneath a reader. const double Deadline = FPlatformTime::Seconds() + 3.0; + bool bReportedSlowDrain = false; while (ActiveTaskCount.load(std::memory_order_relaxed) > 0) { - if (FPlatformTime::Seconds() > Deadline) + if (!bReportedSlowDrain && FPlatformTime::Seconds() > Deadline) { - UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] EndPlay: %d tasks still running after 3s timeout"), + UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] EndPlay: waiting for %d tasks after 3s; UObject teardown is deferred"), ActiveTaskCount.load(std::memory_order_relaxed)); - break; + bReportedSlowDrain = true; } FPlatformProcess::Yield(); // Give CPU to other threads } diff --git a/Source/VoxelForge/Public/VoxelStrateManager.h b/Source/VoxelForge/Public/VoxelStrateManager.h index 948a90b..9d8ea7d 100644 --- a/Source/VoxelForge/Public/VoxelStrateManager.h +++ b/Source/VoxelForge/Public/VoxelStrateManager.h @@ -145,6 +145,8 @@ class VOXELFORGE_API UVoxelStrateManager : public UObject GENERATED_BODY() public: + UVoxelStrateManager(); + //========================================================================= // INITIALIZATION //========================================================================= @@ -178,6 +180,15 @@ public: const FString& GetSeasonContentHash() const { return ActiveSeasonContentHash; } int32 GetWorldSeed() const { return CachedSeed; } + /** + * Monotonic identity of this manager UObject instance. + * + * PassagesVersion intentionally starts over for every layout, so it cannot distinguish a + * cached stack built by an older PIE manager that happened to have the same layout version. + * Worker-local generator caches use this identity as their manager-lifetime boundary. + */ + uint64 GetCacheLifetimeId() const { return CacheLifetimeId; } + #if WITH_EDITOR /** * Install an editor-only candidate over one already-built slot. The slot's definition, @@ -430,6 +441,10 @@ protected: // cached passage/strate data after either kind of live change. uint32 PassagesVersion = 0; + // Never reused for the lifetime of the process. This is deliberately not a UObject pointer: + // allocator address reuse must not make a stale worker-local stack look current. + uint64 CacheLifetimeId = 0; + // How many chunks at strate boundaries are blended (transition zone) int32 BlendChunks = 2;