// VoxelWorld.cpp // Implementation of the voxel world manager #include "VoxelWorld.h" #include "VoxelDiffLayer.h" #include "RealtimeMeshComponent.h" #include "RealtimeMeshSimple.h" #include "Core/RealtimeMeshCollision.h" #include "VoxelMarchingCubesMesher.h" #include "VoxelStrateDefinition.h" #include "VoxelBiomeDefinition.h" #include "VoxelTerrainOpDefinition.h" #include "VoxelContentManager.h" #include "VoxelDensityVolume.h" #include "VoxelDensityOpStack.h" #include "VoxelDensityProfile.h" #include "VoxelGeometryHash.h" #include "VoxelStartupTrace.h" #include "VoxelStackSampler.h" #include "VoxelStats.h" #include "VoxelCaveMorphology.h" #include "VoxelPassageGeometry.h" #include "VoxelTilePostReach.h" #include "VoxelClipmapDesiredTiles.h" #include "VoxelCollisionDesiredTiles.h" #include "VoxelPreEditDensityCache.h" #include "RealtimeMeshLibrary.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 n'est // complet que par transitivité : on l'inclut explicitement. // GetPlayerPosition dereferences the pawn, so APawn must be COMPLETE — Casts.h only forward-declares // it. APlayerController is complete only transitively; include it explicitly. #include "GameFramework/Pawn.h" #include "GameFramework/PlayerController.h" #include "GameFramework/PawnMovementComponent.h" #include "GameFramework/Character.h" #include "GameFramework/CharacterMovementComponent.h" #include "Components/CapsuleComponent.h" #include "Components/PrimitiveComponent.h" #include "CollisionQueryParams.h" #include "Engine/World.h" #include "Misc/IQueuedWork.h" #include "Misc/QueuedThreadPool.h" #include "Misc/SecureHash.h" #include "RealtimeMeshCollisionLibrary.h" #include "Camera/PlayerCameraManager.h" #include "Async/Async.h" #include "Materials/MaterialInstanceDynamic.h" #include "Materials/MaterialParameterCollection.h" #include "Kismet/KismetMaterialLibrary.h" #include "Engine/VolumeTexture.h" #include "Engine/Engine.h" #include "Engine/GameViewportClient.h" #include "Slate/SceneViewport.h" #include "Camera/CameraActor.h" #include "Async/Fundamental/Scheduler.h" #include "Async/TaskGraphInterfaces.h" #include "Components/PointLightComponent.h" #include "VoxelAtmosphereManager.h" #include "DrawDebugHelpers.h" #include "IImageWrapper.h" #include "IImageWrapperModule.h" #include "Modules/ModuleManager.h" #include "Misc/FileHelper.h" #include "Misc/CommandLine.h" #include "Misc/Paths.h" #include "Misc/ScopeLock.h" #include "Misc/ScopeRWLock.h" #include "Async/ParallelFor.h" #include "HAL/IConsoleManager.h" #include "HAL/PlatformMisc.h" #include "HAL/PlatformProcess.h" #include "HAL/PlatformTime.h" #include "Policies/CondensedJsonPrintPolicy.h" #include "Serialization/JsonWriter.h" #include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0) #include "ProfilingDebugging/CsvProfiler.h" // Delayed CSV capture for Step 1 upload runs #include "VoxelForgeCore/VoxelForgeCore.h" #include "VoxelForgeCoreAssetAdapter.h" #include #include #include static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile, const FVector& BoundsMin, const FVector& BoundsMax); static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B); class FVoxelForgeCoreTaskScheduler final : public VoxelForgeCore::JobScheduler { public: double parallel_for(std::size_t Count, std::size_t BatchSize, std::size_t WorkerLimit, void* Context, Task Callback, double* WorkerActiveSeconds, std::size_t WorkerActiveCapacity) const override { if (WorkerActiveSeconds != nullptr) { std::fill(WorkerActiveSeconds, WorkerActiveSeconds + WorkerActiveCapacity, 0.0); } if (Count == 0 || Callback == nullptr) return 0.0; const std::size_t WorkerCount = std::clamp( WorkerLimit, 1, std::min(16, Count)); const std::size_t Grain = std::max(1, BatchSize); std::atomic Next{0}; std::vector ActiveSeconds(WorkerCount, 0.0); // Each ParallelFor lane is a stable Core worker index. Unreal owns the actual threads; // callbacks keep claiming fixed-size batches until every input index has run once. ParallelFor(static_cast(WorkerCount), [&](int32 Lane) { const double Started = FPlatformTime::Seconds(); for (;;) { const std::size_t First = Next.fetch_add(Grain, std::memory_order_relaxed); if (First >= Count) break; const std::size_t End = First + std::min(Grain, Count - First); for (std::size_t Index = First; Index < End; ++Index) { Callback(Context, Index, static_cast(Lane)); } } ActiveSeconds[static_cast(Lane)] = FPlatformTime::Seconds() - Started; }); double TotalSeconds = 0.0; for (std::size_t Lane = 0; Lane < WorkerCount; ++Lane) { TotalSeconds += ActiveSeconds[Lane]; if (WorkerActiveSeconds != nullptr && Lane < WorkerActiveCapacity) { WorkerActiveSeconds[Lane] = ActiveSeconds[Lane]; } } return TotalSeconds; } }; class FVoxelForgeCoreRuntimeState { public: std::unique_ptr World; FRWLock WorldLock; FVoxelForgeCoreTaskScheduler Scheduler; std::unordered_set RetainedTiles; FCriticalSection RetainedTilesLock; FCriticalSection ShapeReadinessLock; std::unordered_set ShapeFreeTiles; std::unordered_set ShapeDependentTiles; std::atomic bSnapshotReady{false}; std::atomic bReady{false}; std::atomic bInitialFailed{false}; std::atomic bRegionCompileInFlight{false}; std::atomic bRegionCompileFinished{false}; std::atomic bRegionCompileSucceeded{false}; std::atomic bRegionCompileFailed{false}; std::atomic bRebuildDesired{false}; std::atomic bShuttingDown{false}; std::atomic NextEditSequence{1}; std::atomic RegionCompileStartCycles{0}; std::atomic RegionCompileEndCycles{0}; std::atomic RegionCompileTileCount{0}; std::atomic RegionCompileSerial{0}; std::atomic RegionCompileWorkerSeconds{0.0}; int32 FullResolutionLodCount = 0; FString InitialFailureReason; }; #if WITH_EDITOR #include "VoxelStrateComposer.h" #include "VoxelStrateMeasure.h" #include "VoxelRedefinedDensityField.h" #endif namespace { bool IsParameterizedResearchFieldActive() { #if WITH_EDITOR return VoxelForgeRedefinedDensityField::IsParameterizedEnabled(); #else return false; #endif } int32 GVoxelForgeCollisionGate = 1; FAutoConsoleVariableRef CVarVoxelForgeCollisionGate( TEXT("voxel.CollisionGate"), GVoxelForgeCollisionGate, TEXT("Protect the pawn from falling through unresolved ground: 0 off, 1 on.")); int32 GVoxelForgeStreamingCoarseFirst = 0; FAutoConsoleVariableRef CVarVoxelForgeStreamingCoarseFirst( TEXT("voxel.StreamingCoarseFirst"), GVoxelForgeStreamingCoarseFirst, TEXT("Prioritize current walkable tiles, then far coarse clipmap levels, then near refinement; 0 restores legacy LOD order.")); int32 GVoxelForgeStreamingDynamicNearReserve = 1; FAutoConsoleVariableRef CVarVoxelForgeStreamingDynamicNearReserve( TEXT("voxel.StreamingDynamicNearReserve"), GVoxelForgeStreamingDynamicNearReserve, TEXT("Keep the authored near-worker reserve while moving or before the walkable ring is ready; default 1 releases idle slots after that gate, 0 restores the legacy fixed reserve.")); int32 GVoxelForgeStreamingCollisionLookahead = 1; FAutoConsoleVariableRef CVarVoxelForgeStreamingCollisionLookahead( TEXT("voxel.StreamingCollisionLookahead"), GVoxelForgeStreamingCollisionLookahead, TEXT("Request up to two collision rings ahead along player velocity; 0 disables predictive ring admission.")); int32 GVoxelForgeStreamingLegacyPendingResultCap = 0; FAutoConsoleVariableRef CVarVoxelForgeStreamingLegacyPendingResultCap( TEXT("voxel.StreamingLegacyPendingResultCap"), GVoxelForgeStreamingLegacyPendingResultCap, TEXT("Compatibility control: 1 counts completed results awaiting apply against the generation task cap; default 0 caps only queued or running generation tasks.")); int32 GVoxelForgeCoreRegionConcurrentAdmission = 1; FAutoConsoleVariableRef CVarVoxelForgeCoreRegionConcurrentAdmission( TEXT("voxel.CoreRegionConcurrentAdmission"), GVoxelForgeCoreRegionConcurrentAdmission, TEXT("Allow tiles independent of a pending Core region compile to proceed; 0 restores the global streaming stop.")); int32 GVoxelForgeStreamingResultBacklogCap = 256; FAutoConsoleVariableRef CVarVoxelForgeStreamingResultBacklogCap( TEXT("voxel.StreamingResultBacklogCap"), GVoxelForgeStreamingResultBacklogCap, TEXT("Maximum queued results plus admitted tile tasks; 0 disables the safety cap. This is separate from the worker-slot cap.")); int32 GVoxelForgeStreamingReadyQueueDepth = 8; FAutoConsoleVariableRef CVarVoxelForgeStreamingReadyQueueDepth( TEXT("voxel.StreamingReadyQueueDepth"), GVoxelForgeStreamingReadyQueueDepth, TEXT("Extra admitted tile tasks kept ready beyond the worker slots, up to one worker-set; 0 disables the refill queue.")); int32 GVoxelForgeStreamingBackgroundWorkerCap = 1; FAutoConsoleVariableRef CVarVoxelForgeStreamingBackgroundWorkerCap( TEXT("voxel.StreamingBackgroundWorkerCap"), GVoxelForgeStreamingBackgroundWorkerCap, TEXT("Size tile admission slots to the TaskGraph background workers used by tile tasks; 0 restores the logical-core cap.")); int32 GVoxelForgeCoreColumnCacheDirectMapped = 0; FAutoConsoleVariableRef CVarVoxelForgeCoreColumnCacheDirectMapped( TEXT("voxel.CoreColumnCacheDirectMapped"), GVoxelForgeCoreColumnCacheDirectMapped, TEXT("Negative control: 0 uses the four-way deterministic XY column cache; 1 restores S13 direct-map indexing.")); int32 GVoxelForgeCoreMCGradientMemoization = 1; FAutoConsoleVariableRef CVarVoxelForgeCoreMCGradientMemoization( TEXT("voxel.CoreMCGradientMemoization"), GVoxelForgeCoreMCGradientMemoization, TEXT("Negative control: 1 reuses exact LOD0 lattice gradients across adjacent MC cells; 0 restores per-cell gradient probes.")); int32 GVoxelForgeProfileTileGeneration = 0; FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration( TEXT("voxel.ProfileTileGeneration"), GVoxelForgeProfileTileGeneration, TEXT("Log worker tile generation time with level/step/cell count.")); int32 GVoxelForgeProfileCollisionCookStages = 0; FAutoConsoleVariableRef CVarVoxelForgeProfileCollisionCookStages( TEXT("voxel.ProfileCollisionCookStages"), GVoxelForgeProfileCollisionCookStages, TEXT("Log per-tile collision queue/apply/RMC/future timing and cooked geometry counts.")); int32 GVoxelForgeStep1GenerationCap = -1; FAutoConsoleVariableRef CVarVoxelForgeStep1GenerationCap( TEXT("voxel.Step1GenerationCap"), GVoxelForgeStep1GenerationCap, TEXT("Step 1 measurement override: -1 uses the normal cap, 0 pauses generation, positive values cap worker tasks.")); int32 GVoxelForgeStep1ExitAfterFrames = 0; FAutoConsoleVariableRef CVarVoxelForgeStep1ExitAfterFrames( TEXT("voxel.Step1ExitAfterFrames"), GVoxelForgeStep1ExitAfterFrames, TEXT("Step 1 measurement: request clean exit after this many engine frames; 0 disables.")); uint64 GVoxelForgeStep1ExitStartFrame = 0; bool bVoxelForgeStep1ExitRequested = false; int32 GVoxelForgeStep1CsvCaptureOnBeginPlay = 0; FAutoConsoleVariableRef CVarVoxelForgeStep1CsvCaptureOnBeginPlay( TEXT("voxel.Step1CsvCaptureOnBeginPlay"), GVoxelForgeStep1CsvCaptureOnBeginPlay, TEXT("Step 1 measurement: emit a delayed CSV start event from BeginPlay; 0 disables.")); float GVoxelForgeStep1PauseGenerationAfterSeconds = 0.0f; FAutoConsoleVariableRef CVarVoxelForgeStep1PauseGenerationAfterSeconds( TEXT("voxel.Step1PauseGenerationAfterSeconds"), GVoxelForgeStep1PauseGenerationAfterSeconds, TEXT("Step 1 measurement: switch the generation cap to paused after this many run seconds; 0 disables the timed pause.")); double GVoxelForgeStep1StartSeconds = 0.0; bool bVoxelForgeStep1GenerationPauseApplied = false; int32 GVoxelForgeStep1MaxMeshApplies = 0; FAutoConsoleVariableRef CVarVoxelForgeStep1MaxMeshApplies( TEXT("voxel.Step1MaxMeshApplies"), GVoxelForgeStep1MaxMeshApplies, TEXT("Step 1 measurement override: 0 uses the asset MaxMeshAppliesPerFrame, otherwise caps visible applies per frame.")); int32 GVoxelForgeStreamingStartupApplyBoost = 1; FAutoConsoleVariableRef CVarVoxelForgeStreamingStartupApplyBoost( TEXT("voxel.StreamingStartupApplyBoost"), GVoxelForgeStreamingStartupApplyBoost, TEXT("Bounded cold-view upload budget (default on): 1 allows a 14 ms / 12-mesh budget until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget.")); constexpr int32 StreamingStartupMaxMeshApplies = 12; constexpr float StreamingStartupApplyBudgetMilliseconds = 14.0f; constexpr double StreamingStartupApplyMaxSeconds = 30.0; int32 GVoxelForgeStep1PrequeueVisibleLOD0 = 0; FAutoConsoleVariableRef CVarVoxelForgeStep1PrequeueVisibleLOD0( TEXT("voxel.Step1PrequeueVisibleLOD0"), GVoxelForgeStep1PrequeueVisibleLOD0, TEXT("Step 1 measurement: hold generated results until this many non-empty level-0 meshes are ready, then release them for upload.")); int32 GVoxelForgeStep1UploadDisableCollision = 0; FAutoConsoleVariableRef CVarVoxelForgeStep1UploadDisableCollision( TEXT("voxel.Step1UploadDisableCollision"), GVoxelForgeStep1UploadDisableCollision, TEXT("Step 1 upload isolation: suppress terrain collision requests while measuring prequeued render uploads.")); int32 GVoxelForgeStep1CollisionProbe = 0; FAutoConsoleVariableRef CVarVoxelForgeStep1CollisionProbe( TEXT("voxel.Step1CollisionProbe"), GVoxelForgeStep1CollisionProbe, TEXT("Step 1 measurement: observe physics-state readiness and the first support capsule sweep after RMC body install.")); int32 GVoxelForgeStep1CoreCookBenchmark = 0; FAutoConsoleVariableRef CVarVoxelForgeStep1CoreCookBenchmark( TEXT("voxel.Step1CoreCookBenchmark"), GVoxelForgeStep1CoreCookBenchmark, TEXT("Step 1 measurement: directly extract and CookComplexMesh for the pawn's exact L0 support cohort plus critical support tiles on a dedicated one-thread pool; does not install this sidecar body.")); struct FStep1CoreCookPoolOwner { FCriticalSection Guard; FQueuedThreadPool* Pool = nullptr; FQueuedThreadPool* GetOrCreate() { FScopeLock Lock(&Guard); if (Pool == nullptr) { Pool = FQueuedThreadPool::Allocate(); if (Pool == nullptr || !Pool->Create(1, 32 * 1024, TPri_AboveNormal, TEXT("VoxelForgeStep1Cook"))) { if (Pool != nullptr) { delete Pool; Pool = nullptr; } } } return Pool; } ~FStep1CoreCookPoolOwner() { if (Pool != nullptr) { Pool->Destroy(); delete Pool; Pool = nullptr; } } }; FStep1CoreCookPoolOwner GVoxelForgeStep1CoreCookPool; class FVoxelStep1CoreCookWork final : public IQueuedWork { public: FVoxelStep1CoreCookWork( TSharedPtr InStreams, FVoxelTileKey InTile, int32 InTriangleCount, bool bInSupportCohort, bool bInPausedReplay, uint64 InQueueCycles) : Streams(MoveTemp(InStreams)), Tile(InTile), TriangleCount(InTriangleCount), bSupportCohort(bInSupportCohort), bPausedReplay(bInPausedReplay), QueueCycles(InQueueCycles) { } virtual void DoThreadedWork() override { const uint64 WorkerStartCycles = FPlatformTime::Cycles64(); FRealtimeMeshCollisionMesh CollisionMesh; const uint64 ExtractionStartCycles = FPlatformTime::Cycles64(); const bool bExtracted = Streams.IsValid() && URealtimeMeshCollisionTools::AppendStreamsToCollisionMesh( CollisionMesh, *Streams, 0); const uint64 ExtractionEndCycles = FPlatformTime::Cycles64(); const uint64 CookStartCycles = FPlatformTime::Cycles64(); if (bExtracted) { URealtimeMeshCollisionTools::CookComplexMesh(CollisionMesh); } const uint64 CookEndCycles = FPlatformTime::Cycles64(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStep1CoreCook] tile=(%d,%d,%d) level=%d cohort=%d paused_replay=%d extracted=%d " "triangles=%d collision_vertices=%d queue_to_worker_ms=%.3f " "stream_extract_ms=%.3f cook_ms=%.3f total_ms=%.3f thread_id=%u"), Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, bSupportCohort ? 1 : 0, bPausedReplay ? 1 : 0, bExtracted ? 1 : 0, TriangleCount, CollisionMesh.GetVertices().Num(), FPlatformTime::ToSeconds64(WorkerStartCycles - QueueCycles) * 1000.0, FPlatformTime::ToSeconds64(ExtractionEndCycles - ExtractionStartCycles) * 1000.0, FPlatformTime::ToSeconds64(CookEndCycles - CookStartCycles) * 1000.0, FPlatformTime::ToSeconds64(CookEndCycles - WorkerStartCycles) * 1000.0, FPlatformTLS::GetCurrentThreadId()); delete this; } virtual void Abandon() override { delete this; } private: TSharedPtr Streams; FVoxelTileKey Tile; int32 TriangleCount = 0; bool bSupportCohort = false; bool bPausedReplay = false; uint64 QueueCycles = 0; }; int32 GVoxelForgeTestFullResClipLevels = -1; FAutoConsoleVariableRef CVarVoxelForgeTestFullResClipLevels( TEXT("voxel.TestFullResClipLevels"), GVoxelForgeTestFullResClipLevels, TEXT("Measurement-only override for the streaming full-resolution clip level count; -1 uses the asset.")); int32 GVoxelForgeProfileDensity = 0; FAutoConsoleVariableRef CVarVoxelForgeProfileDensity( TEXT("voxel.ProfileDensity"), GVoxelForgeProfileDensity, TEXT("Collect sampled per-density profiling for the game path.")); int32 GVoxelForgeProfileDensityFull = 0; FAutoConsoleVariableRef CVarVoxelForgeProfileDensityFull( TEXT("voxel.ProfileDensityFull"), GVoxelForgeProfileDensityFull, TEXT("Collect full per-operator profiling for the game path.")); int32 GVoxelForgePerfAttribution = 0; FAutoConsoleVariableRef CVarVoxelForgePerfAttribution( TEXT("voxel.PerfAttribution"), GVoxelForgePerfAttribution, TEXT("Collect low-overhead component attribution for the game path.")); int32 GVoxelForgeSampleStacks = 0; FAutoConsoleVariableRef CVarVoxelForgeSampleStacks( TEXT("voxel.SampleStacks"), GVoxelForgeSampleStacks, TEXT("Sample active generation worker stacks at the requested interval in microseconds; 0 is off.")); // The nested 8^3 proof is useful for focused/offline measurements, but it is opt-in for // streaming because every qualifying mixed tile would pay the recursive proof again. int32 GVoxelForgeUseBlockEarlyOut = 0; FAutoConsoleVariableRef CVarVoxelForgeUseBlockEarlyOut( TEXT("voxel.UseBlockEarlyOut"), GVoxelForgeUseBlockEarlyOut, TEXT("Use the optional nested 8x8x8 mesher classifier in streaming.")); // Outer classifier benchmark modes: 0 = disabled, 1 = current discarded exact validation. int32 GVoxelForgeOuterClassifierMode = 0; FAutoConsoleVariableRef CVarVoxelForgeOuterClassifierMode( TEXT("voxel.OuterClassifierMode"), GVoxelForgeOuterClassifierMode, TEXT("Outer tile classifier mode: 0 off, 1 discarded exact validation.")); // Cheap structural proofs for LOD tiles whose exact core lattice lies entirely in a gap/seal // (AllSolid) or constant out-of-layout air (AllAir). They are independent of the discarded // full classifier and default on; 0 is retained for per-tile identity/A-B traces. int32 GVoxelForgeSealedSolidProof = 1; FAutoConsoleVariableRef CVarVoxelForgeSealedSolidProof( TEXT("voxel.SealedSolidProof"), GVoxelForgeSealedSolidProof, TEXT("Skip meshing only when an exact gap/seal or constant-air proof holds: 0 off, 1 on.")); // Startup command-line cvars can arrive before the game world has spawned AVoxelWorld (or // before its first tiles have become visible). Keep the diagnostic edit pending until Tick has // a loaded tile; otherwise a startup measurement silently becomes a no-op or edits the wrong // position. int32 GVoxelForgePendingTestModification = 0; float GVoxelForgePendingModificationRadius = 3.0f; float GVoxelForgePendingModificationStrength = 10.0f; bool GVoxelForgeTestModificationLoadedProofTarget = false; bool GVoxelForgeTestModificationLoadedGeometryTarget = false; bool GVoxelForgeTestModificationCommandLineConsumed = false; bool GVoxelForgeStreamingBudgetReported = false; bool GVoxelForgeGenerationCapHitReported = false; int32 GVoxelForgeMaxPendingTilesObserved = 0; struct FVisualGapWitnessKey { FVoxelTileKey Tile; int32 Index = 0; bool operator==(const FVisualGapWitnessKey& Other) const { return Tile == Other.Tile && Index == Other.Index; } }; FORCEINLINE uint32 GetTypeHash(const FVisualGapWitnessKey& Key) { return HashCombine(::GetTypeHash(Key.Tile), ::GetTypeHash(Key.Index)); } struct FVisualGapWitness { FVisualGapWitnessKey Key; FVector WorldPosition = FVector::ZeroVector; }; struct FVisualGapMissingSample { double BeginSeconds = 0.0; uint64 BeginFrame = 0; FVector WorldPosition = FVector::ZeroVector; }; struct FVisualGapAuditState { bool bEnabled = false; bool bInjectEarlyHide = false; bool bLegacyVisualHandoff = false; bool bInjectionDone = false; bool bInjectionCaught = false; bool bCollectionStarted = false; bool bCollecting = false; uint64 Frame = 0; uint64 CollectionFrames = 0; uint64 LongestGapFrames = 0; uint64 InjectionRestoreFrame = 0; double BeginSeconds = 0.0; double LastSampleSeconds = 0.0; int32 Samples = 0; int32 GapEvents = 0; int32 GapPointFrames = 0; int32 GapFrames = 0; int32 MaxGapPoints = 0; int32 CameraViewFrames = 0; int32 MissingSamples = 0; int32 LoggedGapEvents = 0; double LongestGapSeconds = 0.0; bool bReportLogged = false; TMap> WitnessesByTile; TArray PreviousVisibleWitnesses; TMap MissingSinceSeconds; TArray> EditedBounds; FVoxelTileKey InjectedTile; FVisualGapWitnessKey InjectedWitnessKey; TWeakObjectPtr InjectedComponent; }; struct FDigMotionTestState { bool bEnabled = false; bool bStarted = false; bool bUnderDigIssued = false; bool bUnderfootFieldOverride = false; bool bUnderfootProbePending = false; bool bSawFalling = false; bool bResultLogged = false; int32 NearDigCount = 8; int32 NearDigsIssued = 0; int32 UnderfootDigCount = 1; int32 UnderfootDigsIssued = 0; int32 UnderfootFirstFrameChecks = 0; int32 UnderfootSupportObservedFrames = 0; int32 UnderfootFalseSupportFrames = 0; int32 UnderfootFalseAirFrames = 0; int32 UnderfootInventedVelocityEvents = 0; int32 Frame = 0; int32 NearFrames = 0; int32 NearGateFrames = 0; int32 NearFallingFrames = 0; int32 NearVerticalMotionFrames = 0; int32 UnderFrames = 0; int32 UnderGateFrames = 0; float NearMaxDownwardVelocity = 0.0f; float UnderMaxDownwardVelocity = 0.0f; double BeginSeconds = 0.0; double NextDigSeconds = 0.0; double UnderDigSeconds = 0.0; double LastFrameSeconds = 0.0; double DigIntervalSeconds = 0.35; double UnderfootDigIntervalSeconds = 0.1; double NextUnderfootDigSeconds = 0.0; uint64 UnderfootEditFrame = 0; uint64 UnderfootLastObservedFrame = 0; float UnderfootVelocityBefore = 0.0f; float UnderfootFieldAfter = 0.0f; FVector StartPosition = FVector::ZeroVector; FVector UnderDigStartPosition = FVector::ZeroVector; }; TMap GVoxelForgeVisualGapAudits; TMap GVoxelForgeDigMotionTests; void VF_StartVisualGapAuditWindow(FVisualGapAuditState& Audit, double Now) { Audit.bCollecting = true; Audit.bCollectionStarted = true; Audit.CollectionFrames = 0; Audit.BeginSeconds = Now; Audit.LastSampleSeconds = Now; Audit.Samples = 0; Audit.GapEvents = 0; Audit.GapPointFrames = 0; Audit.GapFrames = 0; Audit.MaxGapPoints = 0; Audit.CameraViewFrames = 0; Audit.MissingSamples = 0; Audit.LoggedGapEvents = 0; Audit.LongestGapSeconds = 0.0; Audit.LongestGapFrames = 0; Audit.MissingSinceSeconds.Reset(); } } static bool VF_ProjectHeadlessPassageFloor( const FVoxelPassage& Passage, const FVector& PositionVoxel, float& OutFloorZ) { const auto NearLanding = [&PositionVoxel](const FVoxelPassageLanding& Landing) -> bool { if (!(Landing.HalfWidth > 0.0f)) { return false; } const FVector2D Delta( PositionVoxel.X - Landing.StandingPoint.X, PositionVoxel.Y - Landing.StandingPoint.Y); return Delta.SizeSquared() <= FMath::Square(Landing.HalfWidth + 1.0f); }; if (NearLanding(Passage.UpperLanding)) { OutFloorZ = Passage.UpperLanding.FloorZ; return VoxelMath::IsFinite(OutFloorZ); } if (NearLanding(Passage.LowerLanding)) { OutFloorZ = Passage.LowerLanding.FloorZ; return VoxelMath::IsFinite(OutFloorZ); } float SupportRadius = 0.0f; return VoxelPassageGeometry::ProjectWalkableTunnelFloor( Passage.ControlPoints, Passage.ControlRadii, PositionVoxel, OutFloorZ, SupportRadius); } AVoxelWorld::AVoxelWorld() { PrimaryActorTick.bCanEverTick = true; // Run the collision-readiness check before physics. The pawn receives this actor as a tick // prerequisite once it is possessed, so a movement step cannot race the gate on the same frame. PrimaryActorTick.TickGroup = TG_PrePhysics; } //============================================================================= // T1.f — build the RMC geometry buffers OFF the game thread. //============================================================================= // FRealtimeMeshStreamSet is plain CPU data; on the game thread (in ApplyMeshToTile) the // per-vertex/per-triangle builder loop is the dominant streaming cost (game thread >6 ms while // moving, GPU/draw idle). It touches ONLY the POD MeshData arrays — no UObject, no generator — so // it's safe on the gen worker. The game thread then just uploads the finished streams // (CreateSectionGroup). Byte-identical geometry; only WHERE it runs differs. static void BuildTileStreamSet(RealtimeMesh::FRealtimeMeshStreamSet& Streams, const FVoxelMeshData& MeshData) { RealtimeMesh::TRealtimeMeshBuilderLocal Builder(Streams); Builder.EnableTangents(); Builder.EnableTexCoords(); Builder.EnableColors(); // masques matériau F6 (palette biome / pente / fondu) — voir le mesher Builder.EnablePolyGroups(); const int32 NumVertices = MeshData.Vertices.Num(); Builder.ReserveAdditionalVertices(NumVertices); for (int32 i = 0; i < NumVertices; i++) { auto Vertex = Builder.AddVertex((FVector3f)MeshData.Vertices[i]); if (MeshData.Normals.IsValidIndex(i)) { Vertex.SetNormalAndTangent((FVector3f)MeshData.Normals[i], FVector3f(1, 0, 0)); } if (MeshData.UVs.IsValidIndex(i)) { Vertex.SetTexCoord(0, (FVector2f)MeshData.UVs[i]); } if (MeshData.Colors.IsValidIndex(i)) { Vertex.SetColor(MeshData.Colors[i]); } } // F17 — the mesher packs the index buffer as [ground run | sky-cap run] (see // FVoxelMeshData::NumCeilingTriangles): polygroup 0 = ground, 1 = sky-cap ceiling. // RMC derives one section per contiguous group run (material slot = group index). const int32 NumIndices = MeshData.Triangles.Num(); const int32 FirstCapIndex = NumIndices - MeshData.NumCeilingTriangles * 3; Builder.ReserveAdditionalTriangles(NumIndices / 3); for (int32 i = 0; i < NumIndices; i += 3) { Builder.AddTriangle((uint32)MeshData.Triangles[i], (uint32)MeshData.Triangles[i + 1], (uint32)MeshData.Triangles[i + 2], (i >= FirstCapIndex) ? 1 : 0 /*poly group*/); } } class FScopedGenerationPause { public: explicit FScopedGenerationPause(AVoxelWorld* InWorld) : World(InWorld) { if (!World) return; World->bGenerationPaused.store(true, std::memory_order_release); // The game thread owns this gate; workers only read Generator/Mesher and enqueue results. // La barrière est prise sur le thread de jeu ; les workers ne font qu'énumérer et Enqueue. const double Deadline = FPlatformTime::Seconds() + 5.0; while (World->ActiveTaskCount.load(std::memory_order_relaxed) > 0) { if (FPlatformTime::Seconds() > Deadline) return; FPlatformProcess::Yield(); } if (World->ContentManager && !World->ContentManager->WaitForDecorationTasks(Deadline)) return; // DensityVolume owns a separate FRunnableThread; the world task counter and decoration // counter cannot observe it. Stop/join it before mutating the generator or manager. if (World->DensityVolume) { World->DensityVolume->PauseForGenerationChange(); bDensityVolumePaused = true; } bAcquired = true; } ~FScopedGenerationPause() { if (World) { if (bDensityVolumePaused && World->DensityVolume) { World->DensityVolume->ResumeAfterGenerationChange(); } World->bGenerationPaused.store(false, std::memory_order_release); } } bool Acquired() const { return bAcquired; } private: AVoxelWorld* World = nullptr; bool bAcquired = false; bool bDensityVolumePaused = false; }; #if WITH_EDITOR namespace { void VF_SetComposerRuntimeBounds(FVoxelStrateArchetypeParams& Params, float TopWorldZ, float BottomWorldZ) { Params.TunnelNetworkParams.StrateTopWorldZ = TopWorldZ; Params.TunnelNetworkParams.StrateBottomWorldZ = BottomWorldZ; Params.SlabParams.StrateTopWorldZ = TopWorldZ; Params.SlabParams.StrateBottomWorldZ = BottomWorldZ; Params.MazeParams.StrateTopWorldZ = TopWorldZ; Params.MazeParams.StrateBottomWorldZ = BottomWorldZ; Params.SurfaceParams.StrateTopWorldZ = TopWorldZ; Params.SurfaceParams.StrateBottomWorldZ = BottomWorldZ; Params.VerticalShaftParams.StrateTopWorldZ = TopWorldZ; Params.VerticalShaftParams.StrateBottomWorldZ = BottomWorldZ; Params.FloatingIslandParams.StrateTopWorldZ = TopWorldZ; Params.FloatingIslandParams.StrateBottomWorldZ = BottomWorldZ; } float VF_ComposerBoundarySeal(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype) { switch (Archetype) { case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: return Params.SlabParams.BoundarySealThickness; case ECaveGeneratorType::Maze: return Params.MazeParams.BoundarySealThickness; case ECaveGeneratorType::SurfaceWorld: return Params.SurfaceParams.BoundarySealThickness; case ECaveGeneratorType::VerticalShafts: return Params.VerticalShaftParams.BoundarySealThickness; case ECaveGeneratorType::FloatingIslands: return Params.FloatingIslandParams.BoundarySealThickness; case ECaveGeneratorType::Underwater: case ECaveGeneratorType::TunnelNetwork: default: return Params.TunnelNetworkParams.BoundarySealThickness; } } class FComposerStackDensitySampler final : public IVoxelStrateDensitySampler { public: explicit FComposerStackDensitySampler(const FVoxelOpStack& InStack) : Stack(InStack) {} float SampleDensity(float WorldX, float WorldY, float WorldZ) const override { return Stack.EvalMC(WorldX, WorldY, WorldZ); } private: const FVoxelOpStack& Stack; }; bool VF_BuildComposerReferenceStack( const FVoxelStrateComposerCandidate& Candidate, const UVoxelGenerator& Generator, const UVoxelStrateManager& StrateManager, int32 TargetStrateIndex, int32 TargetTopChunkZ, int32 TargetBottomChunkZ, FVoxelOpStack& OutStack, FVoxelOpContext& OutContext, FString& OutError) { const float TopWorldZ = (float)(TargetTopChunkZ + 1) * CHUNK_SIZE; const float BottomWorldZ = (float)TargetBottomChunkZ * CHUNK_SIZE; FVoxelStrateArchetypeParams Params = Candidate.ArchetypeParams; VF_SetComposerRuntimeBounds(Params, TopWorldZ, BottomWorldZ); bool bBuilt = false; if (Candidate.Regions.RegionCount > 1) { FVoxelStrateRegionManifest RegionManifest = Candidate.Regions; VF_RekeyStrateRegionManifest(RegionManifest, Candidate.Seed, TargetStrateIndex); RegionManifest.StrateTopWorldZ = TopWorldZ; RegionManifest.StrateBottomWorldZ = BottomWorldZ; RegionManifest.bHasGlobalStructuralParams = true; for (FVoxelStrateRegion& Region : RegionManifest.Regions) { VF_SetStrateArchetypeRuntimeBounds(Region.ArchetypeParams, TopWorldZ, BottomWorldZ); } bBuilt = VF_BuildStrateRegionStack( RegionManifest, Generator.OriginSpineRadius, &StrateManager, OutStack, OutContext, &OutError); } else if (Candidate.bStructureRoll) { bBuilt = VF_BuildStackFromRecipe( Candidate.Recipe, Params, Generator.Seed, Generator.OriginSpineRadius, &StrateManager, OutStack, OutContext, &OutError); } else { bBuilt = VF_BuildNativeStrateStackForCandidate( Candidate.Archetype, Params, Generator.Seed, Generator.OriginSpineRadius, Generator.WorldRadiusVoxels, Generator.EdgeSealThickness, &StrateManager, OutStack, OutContext); if (!bBuilt && OutError.IsEmpty()) { OutError = TEXT("The production native candidate stack could not be materialised."); } } if (!bBuilt) { return false; } // The recipe builder deliberately defaults the world-edge context to 0 for offline // measurements. A live candidate must use the actual world's edge law, while its // vertical bounds and structural seal remain candidate-owned. const int32 MidChunkZ = TargetBottomChunkZ + (TargetTopChunkZ - TargetBottomChunkZ) / 2; OutContext.ChunkCoord = FIntVector(0, 0, MidChunkZ); OutContext.Step = 1; OutContext.LayoutVersion = StrateManager.GetLayoutVersion(); OutContext.WorldRadiusVoxels = Generator.WorldRadiusVoxels; OutContext.EdgeSealThickness = Generator.EdgeSealThickness; OutStack.PrepareChunk(OutContext); return true; } bool VF_ComposerReferenceIsComparable( const AVoxelWorld& World, const FStrateSlot& TargetSlot, ECaveGeneratorType Archetype, const FIntVector& RepresentativeChunk, FString& OutReason) { OutReason.Reset(); if (World.DiffLayer && World.DiffLayer->HasAnyMods()) { OutReason = TEXT("diff-layer-modifications-present"); return false; } const FStrateDisturbanceParams Disturbances = World.StrateManager->GetDisturbanceParamsForChunk(RepresentativeChunk); if (Disturbances.ChasmDensity > 0.0f || Disturbances.BridgeDensity > 0.0f || Disturbances.RidgeDensity > 0.0f) { OutReason = TEXT("live-disturbance-post-present"); return false; } // The production SurfaceWorld stack may include a generator-owned biome field and // per-biome params. The standalone candidate stack intentionally does not invent that // context; fall back to the live metric and mark the density check as unavailable. if (Archetype == ECaveGeneratorType::SurfaceWorld && TargetSlot.Definition != nullptr && TargetSlot.Definition->Biomes.Num() > 0) { OutReason = TEXT("surface-biome-context-present"); return false; } return true; } FVoxelStrateMeasureSettings VF_ComposerMeasureSettings() { FVoxelStrateMeasureSettings Settings; Settings.SampleStep = 8; Settings.RadiusInVoxels = 512; Settings.CenterXY = FVector2D::ZeroVector; Settings.MaxCells = 8000000; Settings.MaxRouteRetries = 16; Settings.HeadroomCells = 2; Settings.InteriorMarginVoxels = -1; return Settings; } bool VF_ComposerDensitySanityCheck( const UVoxelGenerator& Generator, const FVoxelOpStack& ReferenceStack, int32 TargetTopChunkZ, int32 TargetBottomChunkZ, float& OutMaxDelta, int32& OutMismatches, int32& OutSamples) { // Keep all probes inside one representative chunk column where possible. This exercises // both the live thread-local refetch and the prepared reference stack without making the // check depend on a particular room being present at a hand-picked coordinate. static const FIntVector Offsets[] = { FIntVector(1, 1, -15), FIntVector(7, 13, -7), FIntVector(17, 3, 1), FIntVector(29, 27, 9), FIntVector(3, 23, 17), FIntVector(19, 19, 25), FIntVector(11, 29, 31), FIntVector(27, 5, -23), }; const int32 MidChunkZ = TargetBottomChunkZ + (TargetTopChunkZ - TargetBottomChunkZ) / 2; const int32 BottomWorldZ = TargetBottomChunkZ * CHUNK_SIZE; const int32 TopWorldZExclusive = (TargetTopChunkZ + 1) * CHUNK_SIZE; const int32 MidWorldZ = MidChunkZ * CHUNK_SIZE + CHUNK_SIZE / 2; OutMaxDelta = 0.0f; OutMismatches = 0; OutSamples = 0; for (const FIntVector& Offset : Offsets) { const float X = (float)Offset.X; const float Y = (float)Offset.Y; const float Z = (float)FMath::Clamp( MidWorldZ + Offset.Z, BottomWorldZ + 1, TopWorldZExclusive - 1); const float Live = Generator.GetDensityAt(X, Y, Z); const float Reference = ReferenceStack.EvalMC(X, Y, Z); uint32 LiveBits = 0; uint32 ReferenceBits = 0; FMemory::Memcpy(&LiveBits, &Live, sizeof(LiveBits)); FMemory::Memcpy(&ReferenceBits, &Reference, sizeof(ReferenceBits)); const bool bBitEqual = LiveBits == ReferenceBits; if (!bBitEqual) { ++OutMismatches; } const float Delta = (VoxelMath::IsFinite(Live) && VoxelMath::IsFinite(Reference)) ? FMath::Abs(Live - Reference) : FLT_MAX; OutMaxDelta = FMath::Max(OutMaxDelta, Delta); ++OutSamples; } return OutMismatches == 0; } } #endif //============================================================================= // LIVE EDIT — regenerate all chunks when params change in the Details panel //============================================================================= void AVoxelWorld::RegenerateAllChunks() { // Bump the generation epoch so in-flight async tasks become stale. // ProcessPendingChunks will discard any result with an old epoch. GenerationEpoch++; PreEditDensityCache.Reset(); // Tear down every tile component, then clear all tile state. Components are GC-safe via // actor ownership. T2.c: PARK them instead of destroying — the reload right after this // is exactly the burst the pool exists for (overflow past the cap is destroyed). const int32 Count = LoadedTiles.Num(); for (auto& Pair : TileComponents) { if (Pair.Value) ReleaseTileComponent(Pair.Value); } TileComponents.Empty(); LoadedTiles.Empty(); LoadedTileGeometry.Reset(); ModificationEpoch = 0; LastModificationMinVoxel = FVector::ZeroVector; LastModificationMaxVoxel = FVector::ZeroVector; ModificationRevisions.Reset(); AppliedModificationEpoch.Reset(); CollisionReadyTiles.Empty(); CollisionSuppressedUntilCook.Empty(); CollisionSuppressedAtCycles.Empty(); CollisionNotRequiredTiles.Empty(); CollisionSolidTiles.Empty(); PendingCollisionCooks.Empty(); // Decorations/water are keyed per level-0 chunk — clear them all. if (ContentManager) { ContentManager->ClearAll(); } // Density volume: bump epoch (drop in-flight fills) + drop data → full refill next Tick. if (DensityVolume) { DensityVolume->Reset(); } // Mark any still-running request obsolete. Keep its PendingTiles slot and token map until the // worker/result drains; otherwise a late old result could remove the slot belonging to a new // request for the same key. Live-edit callers normally arrive with generation paused (and no // active tasks), in which case the stale queue can be discarded immediately. for (TPair>& Pair : PendingTileCancellation) { if (Pair.Value) { Pair.Value->bObsolete.store(true, std::memory_order_release); } } if (ActiveTaskCount.load(std::memory_order_relaxed) == 0) { FChunkResult StaleResult; while (CriticalProcessQueue.Dequeue(StaleResult)) { RecordStreamingResultDequeued(StaleResult); } while (EditedProcessQueue.Dequeue(StaleResult)) { RecordStreamingResultDequeued(StaleResult); } while (NearProcessQueue.Dequeue(StaleResult)) { RecordStreamingResultDequeued(StaleResult); } while (ProcessQueue.Dequeue(StaleResult)) { RecordStreamingResultDequeued(StaleResult); } PendingTiles.Empty(); PendingTileCancellation.Empty(); } // Tiles are already destroyed above — drop any deferred-teardown keys so the drain doesn't // try to UnloadTile coords that no longer exist. PendingUnload.Empty(); // Re-mesh queues reference now-unloaded tiles — drop them (they'd be skipped anyway). DirtyRemeshQueue.Empty(); BandRemeshQueue.Empty(); // §9.4 collision-only tracking references destroyed tiles — clear (rebuilt on the next crossing). CollisionOnlyTiles.Empty(); PrevCollisionOnlyTiles.Empty(); CollisionDesiredTiles.Empty(); CollisionGateDesiredTiles.Empty(); // Reset streaming state so the next Tick rebuilds the desired set and reloads. LastUpdateCenter = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX); bAllChunksLoaded = false; DesiredSorted.Reset(); CriticalDesiredTiles.Reset(); DesiredTransitionDescendants.Reset(); DesiredTransitionMaxLevel = 0; DesiredStamped.Reset(); ++DesiredEpoch; TransitionHold.Reset(); TransitionHoldQueue.Reset(); TransitionHoldCursor = 0; // Tick will reload all tiles on the next frame with fresh params. UE_LOG(LogTemp, Log, TEXT("[VoxelWorld] RegenerateAllChunks (epoch %u): cleared %d tiles"), GenerationEpoch, Count); } void AVoxelWorld::RebuildStrates() { bool bLayoutRebuilt = true; bool bCreatedStrateManager = false; { FScopedGenerationPause Guard(this); if (!Guard.Acquired()) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] RebuildStrates: generation pause timed out; no mutation applied.")); return; } // A live Settings reference can change from no strata to a populated pool/season. Create // the manager lazily in that case; Initialize itself is transactional, so an existing // valid manager remains untouched when the new asset is invalid. if (!StrateManager && Settings && (!Settings->Season.IsNull() || Settings->StratePool.Num() > 0 || Settings->FixedStrates.Num() > 0)) { StrateManager = NewObject(this); bCreatedStrateManager = true; } if (StrateManager && Settings) { // Re-applies layout + inter-strate gap + passage/spine settings from VoxelSettings. bLayoutRebuilt = StrateManager->Initialize( Settings, Settings->GetEffectiveWorldSeed()); } if (!bLayoutRebuilt) { // A newly-created manager has no valid previous state to preserve. Do not leave the // generator wired to a failed temporary object when this was the first live setup. if (bCreatedStrateManager) { StrateManager = nullptr; } } else { // Rebind every consumer of the settings snapshot while generation is paused. The // manager layout alone is not sufficient: world radius, edge sealing, seed, mesher // policy, edit budget, decoration state, and the optional density volume all cache // settings-derived values independently. if (Generator) { Generator->InitializeSettings(Settings); Generator->SetStrateManager(StrateManager); Generator->SetDiffLayer(DiffLayer); } if (Mesher && Settings) { Mesher->bGenerateSkirts = Settings->bGenerateSkirts; Mesher->SkirtCells = Settings->SkirtCells; Mesher->LODOctaveDrop = Settings->LODOctaveDrop; Mesher->bUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut != 0; } if (DiffLayer && Settings) { DiffLayer->SetBudget(Settings->MaxModifications, Settings->MaxBrushRadius, Settings->MaxTotalVolume); } if (ContentManager) { ContentManager->Initialize( this, StrateManager, Generator, Settings, Settings ? Settings->GetEffectiveWorldSeed() : 0); ContentManager->ClearAll(); } if (Settings && Settings->bEnableDensityVolume) { if (!DensityVolume) { DensityVolume = NewObject(this); } if (DensityVolume) { DensityVolume->Initialize(this, Generator, Settings); DensityVolume->Reset(); } } else { if (DensityVolume) { DensityVolume->NotifyShutdown(); DensityVolume = nullptr; } SetDensityVolumeMaterialParamsDisabled(); } if (AtmosphereManager) AtmosphereManager->Reset(); } } if (!bLayoutRebuilt) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] RebuildStrates refused an invalid season; loaded tiles were not regenerated.")); return; } // Reload all chunks against the rebuilt strate data. RegenerateAllChunks(); UE_LOG(LogTemp, Log, TEXT("[VoxelWorld] RebuildStrates: strate layout + passages rebuilt from settings.")); } void AVoxelWorld::ValidateDeterminism() { // F2 — window-invariance regression test (§8.4). The density function must return the SAME // value for a coordinate no matter which chunk's thread_local caches (SDF rooms, strate // memo, biome grid, surface columns, lattice bakes) happen to be warm. Historically THE // source of chunk seams — and the invariant every "bit-identical" hot-path refactor claims // to preserve. This runs on the game thread, whose caches are isolated from the workers. if (bUseVoxelForgeCore) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForge] ValidateDeterminism tests the legacy generator and is unavailable in Core mode.")); return; } if (!Generator) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForge] ValidateDeterminism: no Generator — run during PIE.")); return; } FVector PlayerPosition = FVector::ZeroVector; if (!TryGetPlayerPosition(PlayerPosition)) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForge] ValidateDeterminism: no player pawn — run during PIE after possession.")); return; } const FIntVector CenterChunk = WorldToChunkCoord(WorldToLocalCm(PlayerPosition)); float MaxRepeatDelta = 0.0f; // same alignment sampled twice — must be 0 (statelessness) float MaxWindowDelta = 0.0f; // left-warmed vs right-warmed — must be 0 (window invariance) FVector WorstP = FVector::ZeroVector; int32 Mismatches = 0, Points = 0; // Points hugging the X boundary between chunk (CX,CY) and (CX+1,CY): they sit inside BOTH // chunks' cache search boxes (box = chunk extent + margin), so either alignment may legally // serve them — exactly the cross-window case that seams when an invariant breaks. const float BoundaryX = (float)((CenterChunk.X + 1) * CHUNK_SIZE); for (int32 iy = 0; iy < 16; ++iy) { for (int32 iz = 0; iz < 8; ++iz) { const float Y = (float)(CenterChunk.Y * CHUNK_SIZE) + (float)iy * 2.0f + 0.5f; const float Z = (float)(CenterChunk.Z * CHUNK_SIZE) + (float)iz * 4.0f + 0.5f; for (const float Side : { -0.5f, 0.5f }) // just left / just right of the boundary { const float X = BoundaryX + Side; ++Points; // Warm every cache from the LEFT chunk's middle, sample the point twice. Generator->GetDensityAt(BoundaryX - (float)CHUNK_SIZE * 0.5f, Y, Z); const float DLeft = Generator->GetDensityAt(X, Y, Z); const float DLeft2 = Generator->GetDensityAt(X, Y, Z); // Re-warm from the RIGHT chunk (rebuilds the boxes centred there), resample. Generator->GetDensityAt(BoundaryX + (float)CHUNK_SIZE * 0.5f, Y, Z); const float DRight = Generator->GetDensityAt(X, Y, Z); MaxRepeatDelta = FMath::Max(MaxRepeatDelta, FMath::Abs(DLeft - DLeft2)); const float WDelta = FMath::Abs(DLeft - DRight); if (WDelta > MaxWindowDelta) { MaxWindowDelta = WDelta; WorstP = FVector(X, Y, Z); } if (WDelta > 0.0f) { ++Mismatches; } } } } if (MaxWindowDelta == 0.0f && MaxRepeatDelta == 0.0f) { UE_LOG(LogTemp, Log, TEXT("[VoxelForge] ValidateDeterminism: OK — %d boundary points at chunk (%d,%d,%d), window delta 0, repeat delta 0."), Points, CenterChunk.X, CenterChunk.Y, CenterChunk.Z); } else { UE_LOG(LogTemp, Error, TEXT("[VoxelForge] ValidateDeterminism: FAIL — %d/%d points mismatch, max window delta %.6f (repeat %.6f) at voxel (%.1f, %.1f, %.1f). Window-invariance regression — see ARCHITECTURE §8.4."), Mismatches, Points, MaxWindowDelta, MaxRepeatDelta, WorstP.X, WorstP.Y, WorstP.Z); } } #if WITH_EDITOR void AVoxelWorld::ApplyComposerCandidate() { UWorld* World = GetWorld(); if (World == nullptr || !World->IsPlayInEditor()) { UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] ApplyComposerCandidate: run this button from a PIE world.")); return; } if (bShuttingDown.load(std::memory_order_acquire) || bGenerationPaused.load(std::memory_order_acquire)) { UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] ApplyComposerCandidate: generation is already stopping or paused.")); return; } if (!Settings || !Generator || !StrateManager) { UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] ApplyComposerCandidate: BeginPlay has not initialized Settings, Generator, and StrateManager.")); return; } const TArray& Layout = StrateManager->GetLayout(); const FStrateSlot* TargetSlot = nullptr; for (const FStrateSlot& Slot : Layout) { if (Slot.StrateIndex == ComposerTargetStrateIndex) { TargetSlot = &Slot; break; } } if (TargetSlot == nullptr || TargetSlot->Definition == nullptr) { UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] ApplyComposerCandidate: ComposerTargetStrateIndex=%d is not a live layout slot (layout has %d slots)."), ComposerTargetStrateIndex, Layout.Num()); return; } FVoxelStrateCorpus Corpus; FString CorpusReport; if (!Corpus.LoadFromAssetRegistry(CorpusReport)) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] ApplyComposerCandidate: composer corpus is unavailable: %s"), *CorpusReport); return; } const FVoxelStrateComposerCandidate Candidate = VF_RollStrateCandidate( Corpus, ComposerSeed, ComposerCandidateIndex, bComposerRollStructure); if (!Candidate.bValid) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] ApplyComposerCandidate: seed=%d index=%d roll failed: %s"), ComposerSeed, ComposerCandidateIndex, *Candidate.FailureReason); return; } const int32 TargetTopChunkZ = TargetSlot->TopChunkZ; const int32 TargetBottomChunkZ = TargetSlot->BottomChunkZ; const float TargetTopWorldZ = (float)(TargetTopChunkZ + 1) * CHUNK_SIZE; const float TargetBottomWorldZ = (float)TargetBottomChunkZ * CHUNK_SIZE; const FIntVector RepresentativeChunk( 0, 0, TargetBottomChunkZ + (TargetTopChunkZ - TargetBottomChunkZ) / 2); // Materialise before mutating the live manager. A bad recipe or unsupported native family // therefore leaves the running world untouched. FVoxelStrateArchetypeParams PreflightParams = Candidate.ArchetypeParams; VF_SetComposerRuntimeBounds(PreflightParams, TargetTopWorldZ, TargetBottomWorldZ); FVoxelOpStack PreflightStack; FVoxelOpContext PreflightContext; FString PreflightError; bool bPreflightBuilt = false; FVoxelStrateRegionManifest PreflightRegions; if (Candidate.Regions.RegionCount > 1) { PreflightRegions = Candidate.Regions; VF_RekeyStrateRegionManifest(PreflightRegions, Candidate.Seed, ComposerTargetStrateIndex); PreflightRegions.StrateTopWorldZ = TargetTopWorldZ; PreflightRegions.StrateBottomWorldZ = TargetBottomWorldZ; PreflightRegions.bHasGlobalStructuralParams = true; for (FVoxelStrateRegion& Region : PreflightRegions.Regions) { VF_SetStrateArchetypeRuntimeBounds(Region.ArchetypeParams, TargetTopWorldZ, TargetBottomWorldZ); } bPreflightBuilt = VF_BuildStrateRegionStack( PreflightRegions, Generator->OriginSpineRadius, StrateManager, PreflightStack, PreflightContext, &PreflightError); } else if (Candidate.bStructureRoll) { bPreflightBuilt = VF_BuildStackFromRecipe( Candidate.Recipe, PreflightParams, Generator->Seed, Generator->OriginSpineRadius, StrateManager, PreflightStack, PreflightContext, &PreflightError); } else { bPreflightBuilt = VF_BuildNativeStrateStackForCandidate( Candidate.Archetype, PreflightParams, Generator->Seed, Generator->OriginSpineRadius, Generator->WorldRadiusVoxels, Generator->EdgeSealThickness, StrateManager, PreflightStack, PreflightContext); if (!bPreflightBuilt) { PreflightError = TEXT("The production native candidate stack could not be materialised."); } } if (!bPreflightBuilt) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] ApplyComposerCandidate: seed=%d index=%d was not applied: %s"), ComposerSeed, ComposerCandidateIndex, *PreflightError); return; } FString ApplyError; { // The manager override is a density-only replacement. It does not call Initialize or // GeneratePassages while the layout is half-built: the existing slot Z span, content // definition, and passage geometry remain stable. The pause drains current workers before // the map is changed, and the subsequent RegenerateAllChunks supplies the epoch bump. FScopedGenerationPause Guard(this); if (!Guard.Acquired()) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] ApplyComposerCandidate: generation pause timed out; no mutation applied.")); return; } if (!StrateManager->SetComposerOverrideForStrate( ComposerTargetStrateIndex, Candidate.Seed, Candidate.Archetype, Candidate.ArchetypeParams, Candidate.bStructureRoll, Candidate.bStructureRoll ? &Candidate.Recipe : nullptr, ApplyError, Candidate.Regions.RegionCount > 1 ? &Candidate.Regions : nullptr)) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] ApplyComposerCandidate: seed=%d index=%d was not applied: %s"), ComposerSeed, ComposerCandidateIndex, *ApplyError); return; } // These are the same game-thread-owned side systems reset by RebuildStrates. The full // chunk reset below also resets the density volume and preserves DiffLayer edits. if (AtmosphereManager) { AtmosphereManager->Reset(); } if (ContentManager) { ContentManager->ClearAll(); } } // Reuse the established seed/live-edit invalidation path: GenerationEpoch increments here, // stale ProcessQueue results are dropped, and the normal Tick submits a fresh stream around // the player. No async worker can observe the half-applied manager state. RegenerateAllChunks(); FVoxelOpStack ReferenceStack; FVoxelOpContext ReferenceContext; FString ReferenceError; const bool bReferenceBuilt = VF_BuildComposerReferenceStack( Candidate, *Generator, *StrateManager, ComposerTargetStrateIndex, TargetTopChunkZ, TargetBottomChunkZ, ReferenceStack, ReferenceContext, ReferenceError); FString ComparableReason; const bool bReferenceComparable = bReferenceBuilt && VF_ComposerReferenceIsComparable( *this, *TargetSlot, Candidate.Archetype, RepresentativeChunk, ComparableReason); const FVoxelStrateMeasureSettings MeasureSettings = VF_ComposerMeasureSettings(); FVoxelStrateMetrics Metrics; FString MetricSource; if (bReferenceComparable) { FComposerStackDensitySampler Sampler(ReferenceStack); Metrics = VF_MeasureStrateWithSampler( Sampler, (int32)TargetBottomWorldZ, (int32)TargetTopWorldZ, VF_ComposerBoundarySeal(PreflightParams, Candidate.Archetype), MeasureSettings); MetricSource = TEXT("candidate-stack"); } else { // A live diff, disturbance post, or surface biome context makes a standalone stack an // unlike oracle. Keep the owner's metric useful by measuring the actual applied generator; // the log records why the bit-level oracle was skipped. Metrics = VF_MeasureStrate( *Generator, *StrateManager, ComposerTargetStrateIndex, MeasureSettings); MetricSource = TEXT("live-generator"); } float MaxDensityDelta = 0.0f; int32 DensityMismatches = 0; int32 DensitySamples = 0; bool bDensityMatch = false; if (bReferenceComparable) { bDensityMatch = VF_ComposerDensitySanityCheck( *Generator, ReferenceStack, TargetTopChunkZ, TargetBottomChunkZ, MaxDensityDelta, DensityMismatches, DensitySamples); } const FString RecipeText = Candidate.bStructureRoll ? VF_FormatStrateStructureRecipe(Candidate.Recipe) : TEXT("parameter-roll"); const FString DeterminismText = !bReferenceBuilt ? FString::Printf(TEXT("UNAVAILABLE(%s)"), *ReferenceError) : !bReferenceComparable ? FString::Printf(TEXT("SKIPPED(%s)"), *ComparableReason) : FString::Printf(TEXT("%s/%d"), bDensityMatch ? TEXT("PASS") : TEXT("FAIL"), DensitySamples); const float AirFraction = Metrics.bValid ? Metrics.AirFraction : -1.0f; const float LargestShare = Metrics.bValid ? Metrics.LargestComponentShare : -1.0f; const float WalkableFraction = Metrics.bValid ? Metrics.WalkableFraction : -1.0f; const float FeatureScale = Metrics.bValid ? Metrics.MedianFeatureScale : -1.0f; UE_LOG(LogTemp, Log, TEXT("[VoxelWorld] ComposerCandidate applied seed=%d index=%d target=%d recipe=%s archetype=%s metrics=%s air=%.6f largest=%.6f walkable=%.6f feature=%.3f components=%d determinism=%s max_delta=%.9g mismatches=%d epoch=%u"), Candidate.Seed, Candidate.Index, ComposerTargetStrateIndex, *RecipeText, VF_GetStrateArchetypeName(Candidate.Archetype), *MetricSource, AirFraction, LargestShare, WalkableFraction, FeatureScale, Metrics.bValid ? Metrics.NumAirComponents : -1, *DeterminismText, MaxDensityDelta, DensityMismatches, GenerationEpoch); } void AVoxelWorld::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent); // During PIE with live edit on, a Settings reference change needs the full rebind path: the // manager, generator, mesher, diff budget, decorations and density volume each own a snapshot. // Data asset edits (and the settings asset itself) are handled separately by // OnObjectModifiedInEditor. if (bLiveEditStrates && GetWorld() && GetWorld()->IsPlayInEditor()) { const FName ChangedProperty = PropertyChangedEvent.GetPropertyName(); if (ChangedProperty == GET_MEMBER_NAME_CHECKED(AVoxelWorld, Settings)) { RebuildStrates(); } else { RegenerateAllChunks(); } } } void AVoxelWorld::OnObjectModifiedInEditor(UObject* ModifiedObject) { // Only react during PIE with live edit enabled if (!bLiveEditStrates || !GetWorld() || !GetWorld()->IsPlayInEditor()) return; // Only care about generation-owned assets. This delegate fires for every UObject modification // in the editor. bool bIsRelevant = false; FString AssetName; // Case 1: the settings asset itself was modified if (UVoxelSettings* ModifiedSettings = Cast(ModifiedObject)) { if (ModifiedSettings != Settings) return; bIsRelevant = true; AssetName = ModifiedSettings->GetName(); } // Case 2: A strate definition was modified else if (UVoxelStrateDefinition* ModifiedStrate = Cast(ModifiedObject)) { if (!Settings) return; AssetName = ModifiedStrate->GetName(); // Check the strate pool for (const TSoftObjectPtr& PoolEntry : Settings->StratePool) { if (PoolEntry.Get() == ModifiedStrate) { bIsRelevant = true; break; } } // Check fixed strates if (!bIsRelevant) { for (const auto& FixedEntry : Settings->FixedStrates) { if (FixedEntry.Value.Get() == ModifiedStrate) { bIsRelevant = true; break; } } } } // Case 3: A terrain op definition was modified — check if any strate references it else if (UVoxelTerrainOpDefinition* ModifiedOp = Cast(ModifiedObject)) { if (!Settings || !StrateManager) return; AssetName = ModifiedOp->GetName(); // Check every strate definition's terrain op list for (const TSoftObjectPtr& PoolEntry : Settings->StratePool) { UVoxelStrateDefinition* Def = PoolEntry.Get(); if (!Def) continue; for (const FStrateTerrainOpEntry& Entry : Def->TerrainOperations) { if (Entry.Operation.Get() == ModifiedOp) { bIsRelevant = true; break; } } if (bIsRelevant) break; } if (!bIsRelevant) { for (const auto& FixedEntry : Settings->FixedStrates) { UVoxelStrateDefinition* Def = FixedEntry.Value.Get(); if (!Def) continue; for (const FStrateTerrainOpEntry& Entry : Def->TerrainOperations) { if (Entry.Operation.Get() == ModifiedOp) { bIsRelevant = true; break; } } if (bIsRelevant) break; } } } // Case 4: a biome definition can change both terrain parameters and decoration/material // resolution. Treat it like the owning strate definition, but only when this world references it. else if (UVoxelBiomeDefinition* ModifiedBiome = Cast(ModifiedObject)) { if (!Settings || !StrateManager) return; AssetName = ModifiedBiome->GetName(); for (const FStrateSlot& Slot : StrateManager->GetLayout()) { if (Slot.Definition && Slot.Definition->Biomes.Contains(ModifiedBiome)) { bIsRelevant = true; break; } } } if (!bIsRelevant) return; UE_LOG(LogTemp, Log, TEXT("[VoxelWorld] Live edit: '%s' modified, regenerating..."), *AssetName); // Use the same transactional full rebind as a Settings-reference change. In particular, do not // ignore Initialize's return value: an invalid live asset must leave the previous field intact. RebuildStrates(); } #endif void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason) { // Signal all async tasks to bail out ASAP const double EndPlayStartSeconds = FPlatformTime::Seconds(); bShuttingDown.store(true, std::memory_order_release); if (VoxelForgeCoreState.IsValid()) { VoxelForgeCoreState->bShuttingDown.store(true, std::memory_order_release); } ReleasePawnCollisionGate(); if (APawn* Pawn = PawnTickPrerequisite.Get()) { Pawn->RemoveTickPrerequisiteActor(this); } if (UPawnMovementComponent* PawnMovement = PawnMovementTickPrerequisite.Get()) { PawnMovement->RemoveTickPrerequisiteActor(this); } PawnTickPrerequisite.Reset(); PawnMovementTickPrerequisite.Reset(); UE_LOG(LogTemp, Display, TEXT("[VoxelWorld] EndPlay: shutdown signalled; waiting for %d tasks"), ActiveTaskCount.load(std::memory_order_relaxed)); // 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 (!bReportedSlowDrain && FPlatformTime::Seconds() > Deadline) { UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] EndPlay: waiting for %d tasks after 3s; UObject teardown is deferred"), ActiveTaskCount.load(std::memory_order_relaxed)); bReportedSlowDrain = true; } FPlatformProcess::Yield(); // Give CPU to other threads } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeResultBacklog] queued_peak=%d queued_peak_bytes=%llu queued_final=%d " "queued_final_bytes=%llu cap=%d cap_stops=%llu"), PeakQueuedStreamingResultCount.load(std::memory_order_relaxed), static_cast(PeakQueuedStreamingResultBytes.load(std::memory_order_relaxed)), QueuedStreamingResultCount.load(std::memory_order_relaxed), static_cast(QueuedStreamingResultBytes.load(std::memory_order_relaxed)), GVoxelForgeStreamingResultBacklogCap, static_cast(StreamingResultBacklogCapStops.load(std::memory_order_relaxed))); if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("result_queue_summary"), FString::Printf( TEXT("\"queued_peak\":%d,\"queued_peak_bytes\":%llu,\"queued_final\":%d," "\"queued_final_bytes\":%llu,\"backlog_cap\":%d,\"cap_stops\":%llu"), PeakQueuedStreamingResultCount.load(std::memory_order_relaxed), static_cast(PeakQueuedStreamingResultBytes.load(std::memory_order_relaxed)), QueuedStreamingResultCount.load(std::memory_order_relaxed), static_cast(QueuedStreamingResultBytes.load(std::memory_order_relaxed)), GVoxelForgeStreamingResultBacklogCap, static_cast(StreamingResultBacklogCapStops.load(std::memory_order_relaxed)))); } // Drain any queued results FChunkResult Discard; while (CriticalProcessQueue.Dequeue(Discard)) { if (Discard.bObsolete) { ++ObsoleteTileAbortCount; } } while (EditedProcessQueue.Dequeue(Discard)) { if (Discard.bObsolete) { ++ObsoleteTileAbortCount; } } while (NearProcessQueue.Dequeue(Discard)) { if (Discard.bObsolete) { ++ObsoleteTileAbortCount; } } while (ProcessQueue.Dequeue(Discard)) { if (Discard.bObsolete) { ++ObsoleteTileAbortCount; } } UE_LOG(LogTemp, Display, TEXT("[VoxelWorld] EndPlay: workers drained in %.6fs; obsolete_tile_aborts=%d"), FPlatformTime::Seconds() - EndPlayStartSeconds, ObsoleteTileAbortCount); VoxelForgeCoreState.Reset(); if (bSampleStacksStarted) { FVoxelStackSampler::Get().StopAndWrite(); bSampleStacksStarted = false; } VoxelCaveMorphology::LogPlayerFitMemoStats(); LogStreamingLatencySummary(); WriteTileHashDump(); const uint64 TilePostReachDifferences = VoxelGenLOD::GSkippedPostDifferences.load(std::memory_order_relaxed); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTilePostReach] tiles=%llu origin_reachable=%llu origin_skipped=%llu " "passage_landing_reachable=%llu passage_landing_skipped=%llu " "passage_structural_reachable=%llu passage_structural_skipped=%llu " "debug=%d comparisons=%llu differences=%llu"), static_cast( VoxelGenLOD::GTilePostReachTileCount.load(std::memory_order_relaxed)), static_cast( VoxelGenLOD::GOriginLandingReachableTiles.load(std::memory_order_relaxed)), static_cast( VoxelGenLOD::GOriginLandingSkippedTiles.load(std::memory_order_relaxed)), static_cast( VoxelGenLOD::GPassageLandingReachableTiles.load(std::memory_order_relaxed)), static_cast( VoxelGenLOD::GPassageLandingSkippedTiles.load(std::memory_order_relaxed)), static_cast( VoxelGenLOD::GPassageStructuralReachableTiles.load(std::memory_order_relaxed)), static_cast( VoxelGenLOD::GPassageStructuralSkippedTiles.load(std::memory_order_relaxed)), VoxelGenLOD::GTilePostReachDebugEnabled.load(std::memory_order_relaxed) ? 1 : 0, static_cast( VoxelGenLOD::GSkippedPostComparisons.load(std::memory_order_relaxed)), static_cast(TilePostReachDifferences)); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTilePostReachKinds] comparisons=" "origin_air=%llu/origin_floor=%llu/passage_sdf=%llu/landing_air=%llu/" "tunnel_air=%llu/landing_floor=%llu/structural=%llu/native_floor=%llu/room_floor=%llu " "final_field=%llu differences=%llu/%llu/%llu/%llu/%llu/%llu/%llu/%llu/%llu/%llu"), static_cast(VoxelGenLOD::GSkippedPostComparisonsByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::OriginAir)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostComparisonsByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::OriginFloor)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostComparisonsByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageLandingSDF)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostComparisonsByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageLandingAir)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostComparisonsByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageTunnelAir)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostComparisonsByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageLandingFloor)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostComparisonsByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageStructuralPosts)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostComparisonsByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageNativeFloor)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostComparisonsByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageLandingRoomFloor)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostComparisonsByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::FinalField)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostDifferencesByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::OriginAir)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostDifferencesByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::OriginFloor)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostDifferencesByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageLandingSDF)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostDifferencesByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageLandingAir)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostDifferencesByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageTunnelAir)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostDifferencesByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageLandingFloor)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostDifferencesByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageStructuralPosts)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostDifferencesByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageNativeFloor)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostDifferencesByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::PassageLandingRoomFloor)].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GSkippedPostDifferencesByKind[ static_cast(VoxelGenLOD::ETilePostComparisonKind::FinalField)].load( std::memory_order_relaxed))); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTileReachDecisions] core_reachable=%llu core_skipped=%llu " "passage_carving_reachable=%llu passage_carving_skipped=%llu"), static_cast(VoxelGenLOD::GTunnelCoreReachableTiles.load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTunnelCoreSkippedTiles.load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GPassageCarvingReachableTiles.load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GPassageCarvingSkippedTiles.load( std::memory_order_relaxed))); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTileReachCost] core_world_far_calls=%llu far_cycles=%llu " "near_calls=%llu near_cycles=%llu"), static_cast(VoxelGenLOD::GTileReachCostCalls[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][0].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachCostCycles[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][0].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachCostCalls[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][1].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachCostCycles[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][1].load( std::memory_order_relaxed))); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTileReachCost] core_tail_far_calls=%llu far_cycles=%llu " "near_calls=%llu near_cycles=%llu"), static_cast(VoxelGenLOD::GTileReachCostCalls[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][0].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachCostCycles[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][0].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachCostCalls[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][1].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachCostCycles[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][1].load( std::memory_order_relaxed))); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTileReachCost] passage_carving_far_calls=%llu far_cycles=%llu " "near_calls=%llu near_cycles=%llu"), static_cast(VoxelGenLOD::GTileReachCostCalls[ static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][0].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachCostCycles[ static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][0].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachCostCalls[ static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][1].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachCostCycles[ static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][1].load( std::memory_order_relaxed))); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTileBlockReachCost] core_world_far_calls=%llu far_cycles=%llu " "near_calls=%llu near_cycles=%llu; core_tail_far_calls=%llu far_cycles=%llu " "near_calls=%llu near_cycles=%llu; passage_far_calls=%llu far_cycles=%llu " "near_calls=%llu near_cycles=%llu"), static_cast(VoxelGenLOD::GTileReachBlockCostCalls[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][0].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachBlockCostCycles[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][0].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachBlockCostCalls[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][1].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachBlockCostCycles[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreWorld)][1].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachBlockCostCalls[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][0].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachBlockCostCycles[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][0].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachBlockCostCalls[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][1].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachBlockCostCycles[ static_cast(VoxelGenLOD::ETileReachCostKind::TunnelCoreTail)][1].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachBlockCostCalls[ static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][0].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachBlockCostCycles[ static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][0].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachBlockCostCalls[ static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][1].load( std::memory_order_relaxed)), static_cast(VoxelGenLOD::GTileReachBlockCostCycles[ static_cast(VoxelGenLOD::ETileReachCostKind::PassageCarving)][1].load( std::memory_order_relaxed))); if (VoxelGenLOD::GTilePostReachDebugEnabled.load(std::memory_order_relaxed) && TilePostReachDifferences != 0) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeTilePostReach] soundness failure: skipped post changed the field.")); FPlatformMisc::RequestExitWithStatus( false, 1, TEXT("VoxelForge tile post reach soundness failure")); } PendingTiles.Empty(); PendingTileCancellation.Empty(); PendingUnload.Empty(); DirtyRemeshQueue.Empty(); BandRemeshQueue.Empty(); PendingCollisionCooks.Empty(); CollisionReadyTiles.Empty(); CollisionSuppressedUntilCook.Empty(); CollisionSuppressedAtCycles.Empty(); CollisionDesiredTiles.Empty(); CollisionNotRequiredTiles.Empty(); CollisionSolidTiles.Empty(); for (const TPair& Pair : CollisionBodyUpdatedHandles) { if (Pair.Key != nullptr) { Pair.Key->OnCollisionBodyUpdated().Remove(Pair.Value); } } CollisionBodyUpdatedHandles.Empty(); // Stop + drain the decoration march tasks (they read the Generator) before UObject teardown. if (ContentManager) { ContentManager->NotifyShutdown(); } // Stop + drain the density-volume fill tasks (they read the Generator) before UObject teardown. if (DensityVolume) { DensityVolume->NotifyShutdown(); } // Destroy any spawned atmosphere layer actors. if (AtmosphereManager) { AtmosphereManager->Reset(); } // Unbind the data asset monitoring delegate #if WITH_EDITOR if (OnObjectModifiedHandle.IsValid()) { FCoreUObjectDelegates::OnObjectModified.Remove(OnObjectModifiedHandle); OnObjectModifiedHandle.Reset(); } #endif if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::Finish(TEXT("end_play_before_steady_state")); } if (bResearchGamePerfEnabled) { ResearchGamePerfMovingFrameTimesMs.Sort(); const auto PercentileMs = [this](double Percentile) -> double { if (ResearchGamePerfMovingFrameTimesMs.IsEmpty()) return 0.0; const int32 Index = FMath::Clamp( FMath::CeilToInt(Percentile * ResearchGamePerfMovingFrameTimesMs.Num()) - 1, 0, ResearchGamePerfMovingFrameTimesMs.Num() - 1); return ResearchGamePerfMovingFrameTimesMs[Index]; }; const double MaximumMs = ResearchGamePerfMovingFrameTimesMs.IsEmpty() ? 0.0 : ResearchGamePerfMovingFrameTimesMs.Last(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeResearchGamePerf] moving_frames=%d moving_window_s=%.3f " "p50_ms=%.3f p95_ms=%.3f p99_ms=%.3f max_ms=%.3f"), ResearchGamePerfMovingFrameTimesMs.Num(), ResearchGamePerfMovingSeconds, PercentileMs(0.50), PercentileMs(0.95), PercentileMs(0.99), MaximumMs); } LogVisualStalenessAudit(); GVoxelForgeDigMotionTests.Remove(this); GVoxelForgeVisualGapAudits.Remove(this); LogReadyTransitionVisibilityMeasure(); Super::EndPlay(EndPlayReason); } void AVoxelWorld::BeginPlay() { #if WITH_EDITOR int32 ResearchParameterizedCave = 0; if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ResearchParameterizedCave="), ResearchParameterizedCave)) { VoxelForgeRedefinedDensityField::SetParameterizedEnabled( ResearchParameterizedCave != 0); } int32 ResearchParameterizedCaveSet = -1; if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ResearchParameterizedCaveSet="), ResearchParameterizedCaveSet)) { VoxelForgeRedefinedDensityField::SetParameterSet(ResearchParameterizedCaveSet); } #endif #if WITH_EDITOR AppliedResearchFieldEnabled = VoxelForgeRedefinedDensityField::IsParameterizedEnabled() ? 1 : 0; AppliedResearchFieldProfile = VoxelForgeRedefinedDensityField::GetParameterSet(); #endif int32 ResearchGamePerfValue = 0; FParse::Value(FCommandLine::Get(), TEXT("voxel.ResearchGamePerf="), ResearchGamePerfValue); bResearchGamePerfEnabled = ResearchGamePerfValue != 0; ResearchGamePerfLastFrameSeconds = 0.0; ResearchGamePerfMovingSeconds = 0.0; ResearchGamePerfMovingFrameTimesMs.Reset(); VoxelForgeStartupTrace::BeginFromCommandLine(); VoxelForgeStartupTrace::FStageScope StartupTraceStage(TEXT("BeginPlay")); Super::BeginPlay(); int32 MeasureReadyTransitionVisibility = 0; FParse::Value(FCommandLine::Get(), TEXT("voxel.MeasureReadyTransitionVisibility="), MeasureReadyTransitionVisibility); bMeasureReadyTransitionVisibility = MeasureReadyTransitionVisibility != 0; ReadyTransitionVisibilitySamples.Reset(); FarFieldSubmitSamples.Reset(); if (bMeasureReadyTransitionVisibility) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeReadyVisibilityMeasure] enabled=1 instrumentation=command_line")); } VoxelCaveMorphology::ConfigurePlayerFitMemoFromCommandLine(); VoxelCaveMorphology::ResetPlayerFitMemoStats(); bShuttingDown.store(false, std::memory_order_relaxed); LOD0ReadySamples.Reset(); ObsoleteTileAbortCount = 0; PeakObservedPawnSpeedCmPerSecond = 0.0f; TotalWorkerGenerationCycles.store(0, std::memory_order_relaxed); TotalWorkerGenerationTasks.store(0, std::memory_order_relaxed); TotalGenerationWorkerTaskCycles.store(0, std::memory_order_relaxed); QueuedStreamingResultCount.store(0, std::memory_order_relaxed); QueuedStreamingResultBytes.store(0, std::memory_order_relaxed); PeakQueuedStreamingResultCount.store(0, std::memory_order_relaxed); PeakQueuedStreamingResultBytes.store(0, std::memory_order_relaxed); ActiveTileTaskCount.store(0, std::memory_order_relaxed); StreamingResultBacklogCapStops.store(0, std::memory_order_relaxed); bCoreRegionCompilePending = false; PendingCoreRegionTiles.Reset(); PendingCoreRegionTileSet.Reset(); PendingCoreRetainedTiles.Reset(); CoreRegionCompileTilesInFlight.Reset(); LastCoreRegionRequestCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32); PendingCoreRegionRequestCenter = FIntVector::ZeroValue; ActiveCoreRegionCompileCenter = FIntVector::ZeroValue; bHasLastCoreRegionRequestCenter = false; CoreRegionPendingSinceSeconds = 0.0; ActiveCoreRegionCompileStartSeconds = 0.0; ActiveCoreRegionCompileStartCycles = 0; CoreRegionIndependentSubmissions = 0; CoreRegionDependentDeclines = 0; CoreRegionCompileRequestSerial = 0; ActiveCoreRegionCompileRequestSerial = 0; LastStreamingSchedulerSampleSeconds = 0.0; LastStreamingSchedulerWorkerCycles = 0; TotalObsoleteWorkerCycles.store(0, std::memory_order_relaxed); TotalObsoleteWorkerTasks.store(0, std::memory_order_relaxed); TotalValidationDensityCalls.store(0, std::memory_order_relaxed); TotalMesherDensityCalls.store(0, std::memory_order_relaxed); TotalSealedSolidProofCandidates.store(0, std::memory_order_relaxed); TotalSealedSolidProofSkips.store(0, std::memory_order_relaxed); TotalOutOfLayoutAirProofSkips.store(0, std::memory_order_relaxed); AppliedTileCount = 0; AppliedVisibleTileCount = 0; AppliedTriangleCount = 0; LoadedTileGeometry.Reset(); for (int32 LOD = 0; LOD < TrackedClassifierLODCount; ++LOD) { AppliedVisibleTileCountByLevel[LOD] = 0; AppliedTriangleCountByLevel[LOD] = 0; AppliedBandTileCountByLevel[LOD] = 0; } for (int32 LOD = 0; LOD < TrackedClassifierLODCount; ++LOD) { OuterClassifierCallsByLOD[LOD].store(0, std::memory_order_relaxed); for (int32 Verdict = 0; Verdict < 3; ++Verdict) { OuterClassifierVerdictsByLOD[LOD][Verdict].store(0, std::memory_order_relaxed); } } ConfigureHeadlessStreamingTest(); // Some headless harnesses load module CVars after the engine's startup-CVar pass. Read the // profiling switches explicitly as well, so a reported game profile cannot silently run with // the default mode because of startup ordering. int32 CommandLineProfile = GVoxelForgeProfileDensity; int32 CommandLineProfileFull = GVoxelForgeProfileDensityFull; int32 CommandLinePerfAttribution = GVoxelForgePerfAttribution; int32 CommandLineProfileTileGeneration = GVoxelForgeProfileTileGeneration; int32 CommandLineProfileCollisionCookStages = GVoxelForgeProfileCollisionCookStages; int32 CommandLineTestFullResClipLevels = GVoxelForgeTestFullResClipLevels; int32 CommandLineUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut; int32 CommandLineOuterClassifierMode = GVoxelForgeOuterClassifierMode; int32 CommandLineSealedSolidProof = GVoxelForgeSealedSolidProof; int32 CommandLineSampleStacks = GVoxelForgeSampleStacks; int32 CommandLineCollisionGate = GVoxelForgeCollisionGate; int32 CommandLineCoreRegionConcurrentAdmission = GVoxelForgeCoreRegionConcurrentAdmission; int32 CommandLineStreamingCollisionLookahead = GVoxelForgeStreamingCollisionLookahead; int32 CommandLineStreamingResultBacklogCap = GVoxelForgeStreamingResultBacklogCap; int32 CommandLineStreamingReadyQueueDepth = GVoxelForgeStreamingReadyQueueDepth; int32 CommandLineStreamingBackgroundWorkerCap = GVoxelForgeStreamingBackgroundWorkerCap; int32 CommandLineCoreColumnCacheDirectMapped = GVoxelForgeCoreColumnCacheDirectMapped; int32 CommandLineCoreMCGradientMemoization = GVoxelForgeCoreMCGradientMemoization; int32 CommandLineStep1GenerationCap = GVoxelForgeStep1GenerationCap; int32 CommandLineStep1ExitAfterFrames = GVoxelForgeStep1ExitAfterFrames; int32 CommandLineStep1CsvCaptureOnBeginPlay = GVoxelForgeStep1CsvCaptureOnBeginPlay; float CommandLineStep1PauseGenerationAfterSeconds = GVoxelForgeStep1PauseGenerationAfterSeconds; int32 CommandLineStep1MaxMeshApplies = GVoxelForgeStep1MaxMeshApplies; int32 CommandLineStep1PrequeueVisibleLOD0 = GVoxelForgeStep1PrequeueVisibleLOD0; int32 CommandLineStep1UploadDisableCollision = GVoxelForgeStep1UploadDisableCollision; int32 CommandLineStep1CollisionProbe = GVoxelForgeStep1CollisionProbe; int32 CommandLineStep1CoreCookBenchmark = GVoxelForgeStep1CoreCookBenchmark; if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ProfileDensity="), CommandLineProfile)) { GVoxelForgeProfileDensity = CommandLineProfile; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ProfileDensityFull="), CommandLineProfileFull)) { GVoxelForgeProfileDensityFull = CommandLineProfileFull; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.PerfAttribution="), CommandLinePerfAttribution)) { GVoxelForgePerfAttribution = CommandLinePerfAttribution; } if (FParse::Value( FCommandLine::Get(), TEXT("voxel.ProfileTileGeneration="), CommandLineProfileTileGeneration)) { GVoxelForgeProfileTileGeneration = CommandLineProfileTileGeneration; } if (FParse::Value( FCommandLine::Get(), TEXT("voxel.ProfileCollisionCookStages="), CommandLineProfileCollisionCookStages)) { GVoxelForgeProfileCollisionCookStages = CommandLineProfileCollisionCookStages; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1GenerationCap="), CommandLineStep1GenerationCap)) { GVoxelForgeStep1GenerationCap = CommandLineStep1GenerationCap; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1ExitAfterFrames="), CommandLineStep1ExitAfterFrames)) { GVoxelForgeStep1ExitAfterFrames = FMath::Max(0, CommandLineStep1ExitAfterFrames); } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1CsvCaptureOnBeginPlay="), CommandLineStep1CsvCaptureOnBeginPlay)) { GVoxelForgeStep1CsvCaptureOnBeginPlay = CommandLineStep1CsvCaptureOnBeginPlay != 0 ? 1 : 0; } GVoxelForgeStep1ExitStartFrame = GFrameCounter; bVoxelForgeStep1ExitRequested = false; if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1PauseGenerationAfterSeconds="), CommandLineStep1PauseGenerationAfterSeconds)) { GVoxelForgeStep1PauseGenerationAfterSeconds = FMath::Max( 0.0f, CommandLineStep1PauseGenerationAfterSeconds); } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1MaxMeshApplies="), CommandLineStep1MaxMeshApplies)) { GVoxelForgeStep1MaxMeshApplies = CommandLineStep1MaxMeshApplies; } FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingStartupApplyBoost="), GVoxelForgeStreamingStartupApplyBoost); // Headless BuildHost does not reliably apply arbitrary console-variable command-line tokens // before BeginPlay; parse the test switches explicitly so A/B runs exercise the requested path. // BuildHost sans interface n'applique pas toujours les CVar arbitraires avant BeginPlay ; // lire explicitement ces options afin que les essais A/B utilisent bien le chemin demande. FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingCoarseFirst="), GVoxelForgeStreamingCoarseFirst); FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingDynamicNearReserve="), GVoxelForgeStreamingDynamicNearReserve); FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingLegacyPendingResultCap="), GVoxelForgeStreamingLegacyPendingResultCap); if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingCollisionLookahead="), CommandLineStreamingCollisionLookahead)) { GVoxelForgeStreamingCollisionLookahead = CommandLineStreamingCollisionLookahead != 0 ? 1 : 0; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.CoreRegionConcurrentAdmission="), CommandLineCoreRegionConcurrentAdmission)) { GVoxelForgeCoreRegionConcurrentAdmission = CommandLineCoreRegionConcurrentAdmission != 0 ? 1 : 0; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingResultBacklogCap="), CommandLineStreamingResultBacklogCap)) { GVoxelForgeStreamingResultBacklogCap = FMath::Max(0, CommandLineStreamingResultBacklogCap); } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingReadyQueueDepth="), CommandLineStreamingReadyQueueDepth)) { GVoxelForgeStreamingReadyQueueDepth = FMath::Clamp( CommandLineStreamingReadyQueueDepth, 0, 32); } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingBackgroundWorkerCap="), CommandLineStreamingBackgroundWorkerCap)) { GVoxelForgeStreamingBackgroundWorkerCap = CommandLineStreamingBackgroundWorkerCap != 0 ? 1 : 0; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.CoreColumnCacheDirectMapped="), CommandLineCoreColumnCacheDirectMapped)) { GVoxelForgeCoreColumnCacheDirectMapped= CommandLineCoreColumnCacheDirectMapped!=0?1:0; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.CoreMCGradientMemoization="), CommandLineCoreMCGradientMemoization)) { GVoxelForgeCoreMCGradientMemoization= CommandLineCoreMCGradientMemoization!=0?1:0; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1PrequeueVisibleLOD0="), CommandLineStep1PrequeueVisibleLOD0)) { GVoxelForgeStep1PrequeueVisibleLOD0 = FMath::Max(0, CommandLineStep1PrequeueVisibleLOD0); } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1UploadDisableCollision="), CommandLineStep1UploadDisableCollision)) { GVoxelForgeStep1UploadDisableCollision = CommandLineStep1UploadDisableCollision; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1CollisionProbe="), CommandLineStep1CollisionProbe)) { GVoxelForgeStep1CollisionProbe = CommandLineStep1CollisionProbe; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1CoreCookBenchmark="), CommandLineStep1CoreCookBenchmark)) { GVoxelForgeStep1CoreCookBenchmark = CommandLineStep1CoreCookBenchmark; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStep1] gen_cap=%d exit_after_frames=%d pause_after_s=%.3f " "max_applies=%d prequeue_l0=%d " "disable_collision=%d collision_probe=%d core_cook_benchmark=%d"), GVoxelForgeStep1GenerationCap, GVoxelForgeStep1ExitAfterFrames, GVoxelForgeStep1PauseGenerationAfterSeconds, GVoxelForgeStep1MaxMeshApplies, GVoxelForgeStep1PrequeueVisibleLOD0, GVoxelForgeStep1UploadDisableCollision, GVoxelForgeStep1CollisionProbe, GVoxelForgeStep1CoreCookBenchmark); if (FParse::Value( FCommandLine::Get(), TEXT("voxel.TestFullResClipLevels="), CommandLineTestFullResClipLevels)) { GVoxelForgeTestFullResClipLevels = CommandLineTestFullResClipLevels; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.UseBlockEarlyOut="), CommandLineUseBlockEarlyOut)) { GVoxelForgeUseBlockEarlyOut = CommandLineUseBlockEarlyOut; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.OuterClassifierMode="), CommandLineOuterClassifierMode)) { GVoxelForgeOuterClassifierMode = CommandLineOuterClassifierMode; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.SealedSolidProof="), CommandLineSealedSolidProof)) { GVoxelForgeSealedSolidProof = CommandLineSealedSolidProof; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.SampleStacks="), CommandLineSampleStacks)) { GVoxelForgeSampleStacks = CommandLineSampleStacks; } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.CollisionGate="), CommandLineCollisionGate)) { GVoxelForgeCollisionGate = CommandLineCollisionGate; } GVoxelForgeProfileTileGeneration = GVoxelForgeProfileTileGeneration != 0 ? 1 : 0; GVoxelForgeUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut != 0 ? 1 : 0; GVoxelForgeOuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1); GVoxelForgeSealedSolidProof = GVoxelForgeSealedSolidProof != 0 ? 1 : 0; GVoxelForgeSampleStacks = FMath::Clamp( GVoxelForgeSampleStacks, 0, static_cast(FVoxelStackSampler::MaxIntervalUs)); GVoxelForgeCollisionGate = GVoxelForgeCollisionGate != 0 ? 1 : 0; bCollisionGateEnabled = GVoxelForgeCollisionGate != 0; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGate] startup enabled=%d"), bCollisionGateEnabled ? 1 : 0); if (GVoxelForgePerfAttribution != 0) { VoxelDensityProfile::SetMode( VoxelDensityProfile::EMode::Attribution, VoxelDensityProfile::DefaultSampleInterval); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDensityProfile] game_mode=attribution sample_interval=%u"), VoxelDensityProfile::DefaultSampleInterval); } else if (GVoxelForgeProfileDensityFull != 0) { VoxelDensityProfile::SetMode( VoxelDensityProfile::EMode::Full, VoxelDensityProfile::DefaultSampleInterval); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDensityProfile] game_mode=full sample_interval=%u"), VoxelDensityProfile::DefaultSampleInterval); } else if (GVoxelForgeProfileDensity != 0) { VoxelDensityProfile::SetMode( VoxelDensityProfile::EMode::Sampled, VoxelDensityProfile::DefaultSampleInterval); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDensityProfile] game_mode=sampled sample_interval=%u"), VoxelDensityProfile::DefaultSampleInterval); } if (GVoxelForgeProfileTileGeneration != 0 || GVoxelForgeStep1CsvCaptureOnBeginPlay != 0) { const VoxelDensityProfile::EMode DensityMode = VoxelDensityProfile::GetMode(); const TCHAR* DensityModeName = DensityMode == VoxelDensityProfile::EMode::Disabled ? TEXT("disabled") : DensityMode == VoxelDensityProfile::EMode::Sampled ? TEXT("sampled") : DensityMode == VoxelDensityProfile::EMode::Attribution ? TEXT("attribution") : TEXT("full"); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeProfileState] density_mode=%s profile_density=%d " "profile_density_full=%d perf_attribution=%d profile_tile_generation=%d"), DensityModeName, GVoxelForgeProfileDensity, GVoxelForgeProfileDensityFull, GVoxelForgePerfAttribution, GVoxelForgeProfileTileGeneration); } if (GVoxelForgeStep1CsvCaptureOnBeginPlay != 0) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStep1Csv] stage=beginplay_event frame=%llu"), static_cast(GFrameCounter)); CSV_EVENT_GLOBAL(TEXT("VFStep1GameBeginPlay")); } if (!Settings) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] No Settings assigned — world won't generate.")); return; } int32 TestRenderDistanceChunks = 0; const bool bTestRenderDistance = FParse::Value(FCommandLine::Get(), TEXT("voxel.TestRenderDistanceChunks="), TestRenderDistanceChunks) && TestRenderDistanceChunks > 0; int32 TestClipRadius = 0; const bool bTestClipRadius = FParse::Value(FCommandLine::Get(), TEXT("voxel.TestClipRadius="), TestClipRadius) && TestClipRadius > 0; int32 TestMaxClipLevel = -1; const bool bTestMaxClipLevel = FParse::Value(FCommandLine::Get(), TEXT("voxel.TestMaxClipLevel="), TestMaxClipLevel); if (bTestRenderDistance || bTestClipRadius || bTestMaxClipLevel) { if (UVoxelSettings* TestSettings = DuplicateObject(Settings, this)) { const int32 AuthoredRenderDistance = Settings->RenderDistanceChunks; const int32 AuthoredClipRadius = Settings->ClipRadius; const int32 AuthoredMaxClipLevel = Settings->MaxClipLevel; if (bTestRenderDistance) { TestSettings->RenderDistanceChunks = TestRenderDistanceChunks; } if (bTestClipRadius) { TestSettings->ClipRadius = FMath::Clamp(TestClipRadius, 1, 16); } if (bTestMaxClipLevel) { TestSettings->MaxClipLevel = FMath::Clamp(TestMaxClipLevel, 0, 8); } Settings = TestSettings; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeHorizonMeasure] transient_settings=1 " "render_distance_chunks=%d authored=%d clip_radius=%d authored=%d " "max_clip_level=%d authored=%d"), Settings->RenderDistanceChunks, AuthoredRenderDistance, Settings->ClipRadius, AuthoredClipRadius, Settings->MaxClipLevel, AuthoredMaxClipLevel); } } int32 TestCeilingView = 0; int32 TestCeilingWallCloseup = 0; FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingView="), TestCeilingView); bTestCeilingViewEnabled = TestCeilingView != 0; if (bTestCeilingViewEnabled) { TestCeilingCameraWorldPosition = FVector(10583.003176, 20477.525456, -38442.958976); TestCeilingCameraRotation = FRotator(78.399901, 255.200004, 0.0); FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraX="), TestCeilingCameraWorldPosition.X); FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraY="), TestCeilingCameraWorldPosition.Y); FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraZ="), TestCeilingCameraWorldPosition.Z); FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraPitch="), TestCeilingCameraRotation.Pitch); FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraYaw="), TestCeilingCameraRotation.Yaw); FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCaptureDelaySeconds="), TestCeilingCaptureDelaySeconds); TestCeilingCaptureDelaySeconds = FMath::Max(TestCeilingCaptureDelaySeconds, 1.0f); FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingSecondCaptureDelaySeconds="), TestCeilingSecondCaptureDelaySeconds); FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraMoveSpeedCmPerSecond="), TestCeilingCameraMoveSpeedCmPerSecond); FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraMoveStartSeconds="), TestCeilingCameraMoveStartSeconds); FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraMoveDurationSeconds="), TestCeilingCameraMoveDurationSeconds); FString CameraMoveDirectionText; if (FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraMoveDirection="), CameraMoveDirectionText)) { TArray Components; CameraMoveDirectionText.ParseIntoArray(Components, TEXT(","), true); if (Components.Num() == 3) { TestCeilingCameraMoveDirection = FVector( FCString::Atof(*Components[0]), FCString::Atof(*Components[1]), FCString::Atof(*Components[2])); } } TestCeilingCameraMoveDirection = TestCeilingCameraMoveDirection.GetSafeNormal(); if (TestCeilingCameraMoveDirection.IsNearlyZero()) { TestCeilingCameraMoveDirection = FVector::XAxisVector; } TestCeilingCameraMoveSpeedCmPerSecond = FMath::Max( TestCeilingCameraMoveSpeedCmPerSecond, 0.0f); TestCeilingCameraMoveStartSeconds = FMath::Max( TestCeilingCameraMoveStartSeconds, 0.0f); TestCeilingCameraMoveDurationSeconds = FMath::Max( TestCeilingCameraMoveDurationSeconds, 0.0f); FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingWallCloseup="), TestCeilingWallCloseup); TestCeilingViewBeginSeconds = FPlatformTime::Seconds(); bHeadlessStreamingTestCenterOverride = true; HeadlessStreamingTestCenterVoxel = WorldToLocalVoxel(TestCeilingCameraWorldPosition); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestCeiling] camera_world_cm=(%.3f,%.3f,%.3f) " "camera_rotation=(%.3f,%.3f,%.3f) center_vox=(%.3f,%.3f,%.3f) capture_delay_s=%.1f"), TestCeilingCameraWorldPosition.X, TestCeilingCameraWorldPosition.Y, TestCeilingCameraWorldPosition.Z, TestCeilingCameraRotation.Pitch, TestCeilingCameraRotation.Yaw, TestCeilingCameraRotation.Roll, HeadlessStreamingTestCenterVoxel.X, HeadlessStreamingTestCenterVoxel.Y, HeadlessStreamingTestCenterVoxel.Z, TestCeilingCaptureDelaySeconds); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestCeiling] second_capture_delay_s=%.1f camera_move_cm_s=%.1f " "move_start_s=%.1f move_duration_s=%.1f move_direction=(%.3f,%.3f,%.3f) " "research_game_perf=%d"), TestCeilingSecondCaptureDelaySeconds, TestCeilingCameraMoveSpeedCmPerSecond, TestCeilingCameraMoveStartSeconds, TestCeilingCameraMoveDurationSeconds, TestCeilingCameraMoveDirection.X, TestCeilingCameraMoveDirection.Y, TestCeilingCameraMoveDirection.Z, bResearchGamePerfEnabled ? 1 : 0); } TestResearchProfileSwitchAfterSeconds = -1.0f; TestResearchProfileSwitchTo = -1; TestResearchProfileSwitchBeginSeconds = FPlatformTime::Seconds(); bTestResearchProfileSwitchConfigured = FParse::Value(FCommandLine::Get(), TEXT("voxel.TestResearchProfileSwitchAfterSeconds="), TestResearchProfileSwitchAfterSeconds) && FParse::Value(FCommandLine::Get(), TEXT("voxel.TestResearchProfileSwitchTo="), TestResearchProfileSwitchTo) && TestResearchProfileSwitchAfterSeconds >= 0.0f; bTestResearchProfileSwitchApplied = false; if (bTestResearchProfileSwitchConfigured) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeResearchProfileSwitch] configured=1 after_s=%.3f target_profile=%d"), TestResearchProfileSwitchAfterSeconds, TestResearchProfileSwitchTo); } // The owner's current DA_Settings is intentionally a one-strate surface layout, so it cannot // exercise the separate inter-strate traversal regression. The explicit headless crossing // switch uses a transient copy of the authored four-definition corpus; no package is modified // or saved, and ordinary gameplay keeps the assigned settings untouched. if (bHeadlessStrateCrossingTest) { UVoxelSettings* CrossingSettings = DuplicateObject(Settings, this); TArray CrossingDefinitions; for (int32 DefinitionIndex = 1; DefinitionIndex <= 4; ++DefinitionIndex) { const FString AssetPath = FString::Printf( TEXT("/Game/VoxelForge/DA_Strate%d.DA_Strate%d"), DefinitionIndex, DefinitionIndex); if (UVoxelStrateDefinition* Definition = LoadObject(nullptr, *AssetPath)) { CrossingDefinitions.Add(Definition); } } if (CrossingSettings != nullptr && CrossingDefinitions.Num() == 4) { CrossingSettings->Season.Reset(); CrossingSettings->StratePool.Reset(); CrossingSettings->FixedStrates.Reset(); CrossingSettings->TotalStrates = CrossingDefinitions.Num(); for (int32 DefinitionIndex = 0; DefinitionIndex < CrossingDefinitions.Num(); ++DefinitionIndex) { CrossingSettings->FixedStrates.Add( DefinitionIndex, CrossingDefinitions[DefinitionIndex]); } Settings = CrossingSettings; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStrateCrossingTest] fixture=transient_authored_corpus " "strates=%d assets_not_saved=1"), CrossingDefinitions.Num()); } else { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeStrateCrossingTest] fixture=transient_authored_corpus " "unavailable definitions=%d; using assigned settings"), CrossingDefinitions.Num()); } } if (VoxelForgeStartupTrace::IsActive()) { FString SettingsPath = Settings->GetPathName(); SettingsPath.ReplaceInline(TEXT("\\"), TEXT("\\\\")); SettingsPath.ReplaceInline(TEXT("\""), TEXT("\\\"")); VoxelForgeStartupTrace::RecordEvent(TEXT("settings_resolved"), FString::Printf( TEXT("\"asset\":\"%s\",\"seed\":%d,\"total_strates\":%d,\"inter_strate_gap_chunks\":%d," "\"strate_pool\":%d,\"strate_content_cut_min_level\":%d," "\"effective_strate_content_cut_min_level\":%d,\"max_clip_level\":%d,\"clip_radius\":%d," "\"render_distance_chunks\":%d," "\"enable_density_volume\":%d,\"density_volume_resolution\":%d,\"density_volume_levels\":%d," "\"density_volume_gpu_upload\":%d"), *SettingsPath, Settings->GetEffectiveWorldSeed(), Settings->TotalStrates, Settings->InterStrateGapChunks, Settings->StratePool.Num(), Settings->StrateContentCutMinLevel, Settings->GetEffectiveStrateContentCutMinLevel(), Settings->MaxClipLevel, Settings->ClipRadius, Settings->RenderDistanceChunks, Settings->bEnableDensityVolume ? 1 : 0, Settings->DensityVolumeResolution, Settings->DensityVolumeLevels, Settings->bDensityVolumeGPUUpload ? 1 : 0)); } // Tiles never move once generated, so make the actor root STATIC. RMC already requests the // cached static DRAW path (section group DrawType defaults to Static), but a Movable parent // forces every child back to Movable — which also defeats Virtual Shadow Map caching (Movable // geometry re-renders its shadow every frame; that's the cost that made us turn VSM off). With // a Static root + Static tile components, draws cache AND VSM can cache the terrain's shadows, // so VSM can be turned back on cheaply. Content decoration HISMs are likewise Static (placed once, // never move). Movable children (atmosphere fog/sky, water planes — these follow the player) under a // Static root are allowed. NOTE: a Static actor can't be moved in-editor — // VoxelWorld is expected to sit at the origin. if (USceneComponent* Root = GetRootComponent()) { Root->SetMobility(EComponentMobility::Static); } // Générateur + mesher (UObjects légers) // TRANSLATION SEULEMENT / TRANSLATION ONLY. // Tout le systeme raisonne en boites ALIGNEES SUR LES AXES DU MONDE : bornes de tuile, verdicts // ClassifyBox, fenetre du clipmap, culling, distance de streaming. Une rotation ou une echelle // non unitaire fait qu une tuile cesse d etre alignee sur les axes en espace monde, et TOUTE // cette arithmetique devient fausse -- pas degradee : structurellement fausse. // The whole system reasons in WORLD-AXIS-ALIGNED boxes: tile bounds, ClassifyBox verdicts, the // clipmap window, culling, streaming distance. A rotation or non-unit scale means a tile is no // longer axis-aligned in world space and none of that arithmetic survives - it does not degrade, // it breaks structurally. Translating the actor IS supported (that is the point of the // WorldToLocal* helpers); rotating or scaling it is not. { const FTransform ActorXf = GetActorTransform(); if (!ActorXf.GetRotation().Rotator().IsNearlyZero(0.01f) || !ActorXf.GetScale3D().Equals(FVector::OneVector, 0.001f)) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] Actor has rotation %s / scale %s. VoxelForge supports TRANSLATION ONLY - ") TEXT("tile bounds, ClassifyBox verdicts, the clipmap window and culling all assume ") TEXT("world-axis-aligned boxes. Reset rotation to 0 and scale to 1."), *ActorXf.GetRotation().Rotator().ToString(), *ActorXf.GetScale3D().ToString()); } } Generator = NewObject(this); Mesher = NewObject(this); Generator->InitializeSettings(Settings); Mesher->SetGenerator(Generator); Mesher->SetGeometryVoxelSizeCm(GetActiveVoxelSizeCm()); Mesher->SetShutdownFlag(&bShuttingDown); Mesher->bGenerateSkirts = Settings->bGenerateSkirts; Mesher->SkirtCells = Settings->SkirtCells; Mesher->LODOctaveDrop = Settings->LODOctaveDrop; // T2.b — 0 = off Mesher->bUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut != 0 && !IsParameterizedResearchFieldActive(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeClassifierConfig] outer_mode=%d sealed_solid_proof=%d nested_block_early_out=%d"), GVoxelForgeOuterClassifierMode, GVoxelForgeSealedSolidProof, Mesher->bUseBlockEarlyOut ? 1 : 0); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeFullResClipConfig] authored=%d effective=%d override=%d"), Settings->FullResClipLevels, GVoxelForgeTestFullResClipLevels >= 0 ? FMath::Clamp(GVoxelForgeTestFullResClipLevels, 1, 9) : FMath::Max(1, Settings->FullResClipLevels), GVoxelForgeTestFullResClipLevels); VoxelForgeStartupTrace::RecordEvent(TEXT("generator_mesher_constructed"), FString::Printf( TEXT("\"generator\":1,\"mesher\":1,\"generate_skirts\":%d,\"full_res_clip_levels\":%d,\"coarse_tile_cells\":%d"), Settings->bGenerateSkirts ? 1 : 0, Settings->FullResClipLevels, Settings->CoarseTileCells)); if (VoxelForgeStartupTrace::IsActive()) { FTaskGraphInterface& TaskGraph = FTaskGraphInterface::Get(); LowLevelTasks::FScheduler& TaskScheduler = LowLevelTasks::FScheduler::Get(); const int32 QueueDepth = FMath::Clamp( GVoxelForgeStreamingReadyQueueDepth, 0, GetMaxConcurrentStreamingTasks()); VoxelForgeStartupTrace::RecordEvent(TEXT("worker_backend_config"), FString::Printf( TEXT("\"logical_cores\":%d,\"effective_tile_slots\":%d," "\"task_api\":\"UE::Tasks::Launch\",\"task_scheduler_workers\":%u," "\"task_scheduler_max_workers\":%u,\"task_scheduler_worker_priority\":%d," "\"task_scheduler_background_priority\":%d," "\"taskgraph_worker_threads_per_priority\":%d,\"taskgraph_foreground_threads\":%d," "\"taskgraph_background_threads\":%d,\"global_queued_thread_pool_threads\":%d," "\"step1_cook_pool_threads\":%d,\"ready_queue_depth_cvar\":%d," "\"ready_queue_depth\":%d," "\"near_priority\":\"BackgroundHigh\"," "\"horizon_priority\":\"BackgroundNormal\"," "\"streaming_coarse_first\":%d,\"streaming_dynamic_near_reserve\":%d," "\"streaming_legacy_pending_result_cap\":%d," "\"streaming_collision_lookahead\":%d,\"core_region_concurrent_admission\":%d," "\"collision_gate\":%d," "\"streaming_result_backlog_cap\":%d,\"streaming_startup_apply_boost\":%d," "\"core_column_cache_direct_mapped\":%d," "\"core_mc_gradient_memoization\":%d"), FPlatformMisc::NumberOfCoresIncludingHyperthreads(), GetMaxConcurrentStreamingTasks(), TaskScheduler.GetNumWorkers(), TaskScheduler.GetMaxNumWorkers(), static_cast(TaskScheduler.GetWorkerPriority()), static_cast(TaskScheduler.GetBackgroundPriority()), TaskGraph.GetNumWorkerThreads(), TaskGraph.GetNumForegroundThreads(), TaskGraph.GetNumBackgroundThreads(), GThreadPool ? GThreadPool->GetNumThreads() : 0, GVoxelForgeStep1CoreCookPool.Pool ? GVoxelForgeStep1CoreCookPool.Pool->GetNumThreads() : 0, GVoxelForgeStreamingReadyQueueDepth, QueueDepth, GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve, GVoxelForgeStreamingLegacyPendingResultCap, GVoxelForgeStreamingCollisionLookahead, GVoxelForgeCoreRegionConcurrentAdmission, GVoxelForgeCollisionGate, GVoxelForgeStreamingResultBacklogCap, GVoxelForgeStreamingStartupApplyBoost, GVoxelForgeCoreColumnCacheDirectMapped, GVoxelForgeCoreMCGradientMemoization)); VoxelForgeStartupTrace::RecordEvent(TEXT("streaming_worker_cap_readback"), FString::Printf( TEXT("\"streaming_background_worker_cap\":%d,\"background_worker_threads\":%d," "\"effective_tile_slots\":%d"), GVoxelForgeStreamingBackgroundWorkerCap, TaskGraph.GetNumBackgroundThreads(), GetMaxConcurrentStreamingTasks())); } // Système de strates — a cooked season takes precedence; otherwise the authored pool path is // exactly the legacy one. if (!Settings->Season.IsNull() || Settings->StratePool.Num() > 0 || Settings->FixedStrates.Num() > 0) { StrateManager = NewObject(this); if (!StrateManager->Initialize(Settings, Settings->GetEffectiveWorldSeed())) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] World generation disabled because its assigned season is invalid or incomplete.")); bShuttingDown.store(true, std::memory_order_release); SetActorTickEnabled(false); return; } Generator->SetStrateManager(StrateManager); UE_LOG(LogTemp, Log, TEXT("[VoxelWorld] Strate system initialized with %d strates"), StrateManager->GetNumStrates()); } InitializeHeadlessStrateCrossingTest(); // Diff layer — stocke les modifications du joueur par dessus la densité // procédurale. Créé systématiquement (coût nul tant qu'il n'y a pas d'édit). DiffLayer = NewObject(this); DiffLayer->SetBudget(Settings->MaxModifications, Settings->MaxBrushRadius, Settings->MaxTotalVolume); Generator->SetDiffLayer(DiffLayer); if (bUseVoxelForgeCore) { FString CoreFailureReason; if (!InitializeVoxelForgeCoreWorld(CoreFailureReason)) { FallBackToLegacyAfterCoreInitializationFailure(CoreFailureReason); } } if (bTestCeilingViewEnabled && IsParameterizedResearchFieldActive()) { const FVector RequestedCenter = HeadlessStreamingTestCenterVoxel; FVector BestAirPoint = RequestedCenter; float BestAirDensity = Generator->GetDensityAt( RequestedCenter.X, RequestedCenter.Y, RequestedCenter.Z); TArray SearchZValues; constexpr int32 SearchZMargin = 8; constexpr int32 SearchZStep = 16; if (StrateManager != nullptr) { for (const FStrateSlot& Slot : StrateManager->GetLayout()) { const int32 MinChunkZ = FMath::Min(Slot.TopChunkZ, Slot.BottomChunkZ); const int32 MaxChunkZ = FMath::Max(Slot.TopChunkZ, Slot.BottomChunkZ); const int32 MinZ = MinChunkZ * CHUNK_SIZE + SearchZMargin; const int32 MaxZ = (MaxChunkZ + 1) * CHUNK_SIZE - SearchZMargin; for (int32 Z = MinZ; Z <= MaxZ; Z += SearchZStep) { SearchZValues.Add(Z); } } } if (SearchZValues.IsEmpty()) { constexpr int32 SearchRadiusZ = 256; for (int32 Z = FMath::FloorToInt(RequestedCenter.Z) - SearchRadiusZ; Z <= FMath::FloorToInt(RequestedCenter.Z) + SearchRadiusZ; Z += SearchZStep) { SearchZValues.Add(Z); } } constexpr int32 SearchRadiusXY = 256; constexpr int32 SearchStepXY = 16; for (const int32 Z : SearchZValues) for (int32 DY = -SearchRadiusXY; DY <= SearchRadiusXY; DY += SearchStepXY) for (int32 DX = -SearchRadiusXY; DX <= SearchRadiusXY; DX += SearchStepXY) { const FVector Candidate(RequestedCenter.X + DX, RequestedCenter.Y + DY, static_cast(Z)); const float Density = Generator->GetDensityAt( Candidate.X, Candidate.Y, Candidate.Z); if (Density > BestAirDensity) { BestAirDensity = Density; BestAirPoint = Candidate; } } if (BestAirDensity > 0.0f) { // Frame the capture from a plausible player-height point in the same chamber, rather // than directly at its maximum-clearance center. This brings the floor into view while // retaining a generous air margin and does not affect normal gameplay or generation. constexpr float CaptureEyeDropVoxels = 32.0f; const FVector LowerCapturePoint = BestAirPoint + FVector(0.0f, 0.0f, -CaptureEyeDropVoxels); const float LowerCaptureDensity = Generator->GetDensityAt( LowerCapturePoint.X, LowerCapturePoint.Y, LowerCapturePoint.Z); if (LowerCaptureDensity > 12.0f) { BestAirPoint = LowerCapturePoint; BestAirDensity = LowerCaptureDensity; } FVector BestViewDirection = FVector(1.0f, 0.0f, 0.0f); float BestViewWallDistance = 0.0f; float BestViewScore = -FLT_MAX; for (int32 DZ = -1; DZ <= 1; ++DZ) for (int32 DY = -1; DY <= 1; ++DY) for (int32 DX = -1; DX <= 1; ++DX) { if (TestCeilingWallCloseup == 0 && DZ != 0) { continue; } const FVector Direction(static_cast(DX), static_cast(DY), static_cast(DZ)); if (Direction.IsNearlyZero()) { continue; } const FVector NormalizedDirection = Direction.GetSafeNormal(); for (float Distance = 4.0f; Distance <= 256.0f; Distance += 4.0f) { const FVector Probe = BestAirPoint + NormalizedDirection * Distance; if (Generator->GetDensityAt(Probe.X, Probe.Y, Probe.Z) <= 0.0f) { const float TargetDistance = TestCeilingWallCloseup != 0 ? 24.0f : 64.0f; const float Score = -FMath::Abs(Distance - TargetDistance); if (Score > BestViewScore) { BestViewScore = Score; BestViewDirection = NormalizedDirection; BestViewWallDistance = Distance; } break; } } } if (BestViewWallDistance > 0.0f) { TestCeilingCameraRotation = BestViewDirection.Rotation(); if (TestCeilingWallCloseup != 0) { constexpr float WallInset = 8.0f; BestAirPoint += BestViewDirection * FMath::Max( 0.0f, BestViewWallDistance - WallInset); } } TestCeilingCameraWorldPosition = LocalVoxelToWorld(BestAirPoint); HeadlessStreamingTestCenterVoxel = BestAirPoint; const FVector WallProbe = BestAirPoint + BestViewDirection * (TestCeilingWallCloseup != 0 ? 8.0f : BestViewWallDistance); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestCeiling] view_ray=(%.3f,%.3f,%.3f) wall_distance_vox=%.1f " "wall_probe_density=%.3f"), BestViewDirection.X, BestViewDirection.Y, BestViewDirection.Z, BestViewWallDistance, Generator->GetDensityAt(WallProbe.X, WallProbe.Y, WallProbe.Z)); } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestCeiling] research_air_search=1 requested_local=(%.1f,%.1f,%.1f) " "selected_local=(%.1f,%.1f,%.1f) selected_density=%.3f wall_closeup=%d"), RequestedCenter.X, RequestedCenter.Y, RequestedCenter.Z, BestAirPoint.X, BestAirPoint.Y, BestAirPoint.Z, BestAirDensity, TestCeilingWallCloseup != 0 ? 1 : 0); } // Content manager — distance-based decoration grid (no LOD pop) + level-0 water planes. ContentManager = NewObject(this); ContentManager->Initialize( this, StrateManager, Generator, Settings, Settings->GetEffectiveWorldSeed()); // Atmosphere manager — per-strate fog + ambient + persistent ceiling/floor layers. if (bManageAtmosphere && StrateManager) { AtmosphereManager = NewObject(this); AtmosphereManager->Initialize(this, StrateManager, Generator); } else if (bTestCeilingViewEnabled) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestCeiling] atmosphere=disabled_by_world_setting")); } // Density volume — player-centred clipmap streamed to the GPU for mini-sun raymarched shadows. if (Settings->bEnableDensityVolume) { DensityVolume = NewObject(this); DensityVolume->Initialize(this, Generator, Settings); } else { SetDensityVolumeMaterialParamsDisabled(); } VoxelForgeStartupTrace::RecordEvent(TEXT("density_volume_resolved"), FString::Printf( TEXT("\"enabled\":%d,\"created\":%d,\"gpu_upload\":%d,\"resolution\":%d,\"levels\":%d"), Settings->bEnableDensityVolume ? 1 : 0, DensityVolume != nullptr ? 1 : 0, Settings->bDensityVolumeGPUUpload ? 1 : 0, Settings->DensityVolumeResolution, Settings->DensityVolumeLevels)); #if WITH_EDITOR // Listen for data asset edits during PIE so live edit can detect // strate definition changes (PostEditChangeProperty only fires for // properties on this actor itself, not on referenced data assets). OnObjectModifiedHandle = FCoreUObjectDelegates::OnObjectModified.AddUObject( this, &AVoxelWorld::OnObjectModifiedInEditor); #endif // A possessed pawn may already exist when BeginPlay runs. Engage the same pre-physics gate // immediately; Tick repeats the check for late possession and for pawn replacement. FVector InitialPlayerPosition = FVector::ZeroVector; APawn* InitialPawn = nullptr; if (TryGetPlayerPosition(InitialPlayerPosition, &InitialPawn)) { UpdatePawnCollisionGate(InitialPawn, InitialPlayerPosition, 0.0f); } if (GVoxelForgeSampleStacks > 0) { const FString SamplerOutputDirectory = FPaths::Combine( FPaths::ProjectDir(), TEXT("Plugins/VoxelForge/Saved")); bSampleStacksStarted = FVoxelStackSampler::Get().Start( static_cast(GVoxelForgeSampleStacks), SamplerOutputDirectory, TEXT("game")); if (!bSampleStacksStarted) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] stack sampler requested but could not be started.")); } } } void AVoxelWorld::ConfigureHeadlessStreamingTest() { GVoxelForgeVisualGapAudits.Remove(this); GVoxelForgeDigMotionTests.Remove(this); bHeadlessStreamingTestMovement = false; bHeadlessStreamingTestExitOnSteady = false; bHeadlessStreamingTestExitRequested = false; bStartupTraceThroughCrossing = false; bHeadlessStreamingTestCenterOverride = false; HeadlessStreamingTestCenterVoxel = FVector::ZeroVector; HeadlessStreamingTestMoveAttempts = 0; HeadlessStreamingTestDistanceCm = 0.0; HeadlessStreamingTestSpeedCmPerSecond = 0.0f; HeadlessStreamingTestStartSeconds = 0.0f; HeadlessStreamingTestExitSeconds = 0.0f; HeadlessStreamingTestTeleportAtSeconds = 0.0f; HeadlessStreamingTestTeleportDistanceCm = 0.0f; bHeadlessStreamingTestTeleportDone = false; HeadlessStreamingTestDirection = FVector::XAxisVector; HeadlessStreamingTestLastActualPosition = FVector::ZeroVector; HeadlessStreamingTestBeginSeconds = 0.0; HeadlessStreamingTestLastElapsedSeconds = 0.0; bHeadlessCollisionGateStressTest = false; bHeadlessCollisionGateStressTestStartPlaced = false; bHeadlessCollisionGateStressTestStarted = false; bHeadlessCollisionGateStressTestPassed = false; bHeadlessCollisionGateStressTestFailed = false; bHeadlessCollisionGateStressTestExitRequested = false; bHeadlessCollisionGateStressTestGateWasEngaged = false; bHeadlessCollisionGateStressTestFullStopProbeApplied = false; bHeadlessCollisionGateStressTestFullStopProbeObserved = false; bHeadlessCollisionGateStressTestFullStop = false; bHeadlessCollisionGateStressTestStartHeightCrossed = false; bHeadlessCollisionGateStressTestTerrainViolation = false; bHeadlessCollisionGateStressTestTerrainMeasurementMissing = false; bHeadlessCollisionGateStressTestLanded = false; bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured = false; bHeadlessCollisionGateStressTestSettingsReported = false; bHeadlessCollisionGateStressDeepDrop = false; bHeadlessCollisionGateStressDeepDropPrepared = false; bHeadlessCollisionGateStressDeepDropPrewarmReported = false; bHeadlessCollisionGateStressDeepDropSurfaceCached = false; HeadlessCollisionGateStressDeepDropSurfaceXYVox = FVector2D::ZeroVector; HeadlessCollisionGateStressDeepDropSurfaceZVox = 0.0f; HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f; HeadlessCollisionGateStressTestDurationSeconds = 2.0f; HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f; HeadlessCollisionGateStressTestBeginSeconds = 0.0; HeadlessCollisionGateStressTestStartSeconds = 0.0; HeadlessCollisionGateStressTestLandedSeconds = 0.0; HeadlessCollisionGateStressTestGateStartSeconds = 0.0; HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0; HeadlessCollisionGateStressTestFullStopProbeStartSeconds = 0.0; HeadlessCollisionGateStressTestDistanceCm = 0.0; HeadlessCollisionGateStressTestGateDistanceCm = 0.0; HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0; HeadlessCollisionGateStressTestMaxGateDurationSeconds = 0.0; HeadlessCollisionGateStressTestFullStopDurationSeconds = 0.0; HeadlessCollisionGateStressTestLaunchSupportCheckSeconds = 0.0; HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = 0.0; HeadlessCollisionGateStressTestLaunchSupportReadySeconds = 0.0; HeadlessCollisionGateStressTestMinClearanceCm = 1.0e30; HeadlessCollisionGateStressTestMinFeetDensity = 0.0f; HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ = 0.0f; HeadlessCollisionGateStressTestTerrainSampleCount = 0; HeadlessCollisionGateStressTestTraceFrame = 0; HeadlessCollisionGateStressTestGateHoldCount = 0; HeadlessCollisionGateStressTestFullStopCount = 0; HeadlessCollisionGateStressTestDirection = FVector::ZeroVector; HeadlessCollisionGateStressTestLastActualPosition = FVector::ZeroVector; HeadlessCollisionGateStressTestStartPosition = FVector::ZeroVector; HeadlessCollisionGateStressTestMinClearancePosition = FVector::ZeroVector; HeadlessCollisionGateStressTestLaunchSupportTile = FVoxelTileKey(); HeadlessCollisionGateStressTestFailureReason.Reset(); bHeadlessSurfaceFallTest = false; bHeadlessSurfaceFallTestStartPlaced = false; bHeadlessSurfaceFallTestPassed = false; bHeadlessSurfaceFallTestFailed = false; bHeadlessSurfaceFallTestExitRequested = false; bHeadlessSurfaceFallTestPredictionProbeInjected = false; bHeadlessSurfaceFallTestPredictionProbeCleared = false; HeadlessSurfaceFallTestSpawnHeightVoxels = 5.0f * 100.0f / VOXEL_SIZE; HeadlessSurfaceFallTestGroundToleranceVoxels = 1.0f * 100.0f / VOXEL_SIZE; HeadlessSurfaceFallTestTimeoutSeconds = 8.0f; HeadlessSurfaceFallTestPlayerHalfHeightVoxels = VoxelPassageGeometry::PlayerHalfHeightVoxels; HeadlessSurfaceFallTestTerrainZ = 0.0f; HeadlessSurfaceFallTestBeginSeconds = 0.0; HeadlessSurfaceFallTestStartSeconds = 0.0; HeadlessSurfaceFallTestDistanceCm = 0.0; HeadlessSurfaceFallTestStartPosition = FVector::ZeroVector; HeadlessSurfaceFallTestLastActualPosition = FVector::ZeroVector; HeadlessSurfaceFallTestPredictionProbePosition = FVector::ZeroVector; HeadlessSurfaceFallTestFailureReason.Reset(); bTileHashDumpEnabled = false; TileHashDumpPath.Reset(); TileHashDumpRecords.Reset(); bHeadlessStrateCrossingTest = false; bHeadlessStrateCrossingTestStartPlaced = false; bHeadlessStrateCrossingTestStarted = false; bHeadlessStrateCrossingTestPassed = false; bHeadlessStrateCrossingTestFailed = false; bHeadlessStrateCrossingTestExitRequested = false; bHeadlessStrateCrossingTestGateWasEngaged = false; bHeadlessStrateCrossingTestInitialSupportReported = false; bHeadlessStrateCrossingTestPendingFloorOverlap = false; HeadlessStrateCrossingTestRequestedPassageIndex = INDEX_NONE; HeadlessStrateCrossingTestPassageIndex = INDEX_NONE; HeadlessStrateCrossingTestUpperStrateIndex = INDEX_NONE; HeadlessStrateCrossingTestLowerStrateIndex = INDEX_NONE; HeadlessStrateCrossingTestRouteTargetIndex = 1; HeadlessStrateCrossingTestLastReportedStrate = INDEX_NONE; HeadlessStrateCrossingTestPlayerHalfHeightVoxels = VoxelPassageGeometry::PlayerHalfHeightVoxels; HeadlessStrateCrossingTestMinFloorClearanceVoxels = 1000000.0f; HeadlessStrateCrossingTestBeginSeconds = 0.0; HeadlessStrateCrossingTestStartSeconds = 0.0; HeadlessStrateCrossingTestGateStartSeconds = 0.0; HeadlessStrateCrossingTestMaxGateDurationSeconds = 0.0; HeadlessStrateCrossingTestTotalGateDurationSeconds = 0.0; HeadlessStrateCrossingTestLastActualPosition = FVector::ZeroVector; HeadlessStrateCrossingTestFailureReason.Reset(); HeadlessStrateCrossingTestRoute.Reset(); bVisualStalenessAudit = false; VisualStalenessAuditMode = 0; bVisualStalenessAuditExitLogged = false; VisualAuditEditEpoch = 0; VisualAuditEditMinVoxel = FVector::ZeroVector; VisualAuditEditMaxVoxel = FVector::ZeroVector; VisualAuditEditBeginSeconds = 0.0; VisualAuditEditStaleDurationSeconds = -1.0; VisualAuditEditMaxStaleTiles = 0; bVisualAuditEditSawStale = false; bVisualAuditEditResolved = false; VisualAuditLODOverlapStartSeconds = 0.0; VisualAuditLODOverlapMaxSeconds = 0.0; VisualAuditLODOverlapEvents = 0; VisualAuditLODOverlapMaxPairs = 0; const TCHAR* CommandLine = FCommandLine::Get(); int32 VisualAuditValue = 0; if (FParse::Value(CommandLine, TEXT("voxel.TestVisualStaleness="), VisualAuditValue)) { VisualStalenessAuditMode = FMath::Clamp(VisualAuditValue, 0, 2); bVisualStalenessAudit = VisualStalenessAuditMode != 0; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeVisualStaleness] configured=1 mode=%d"), VisualStalenessAuditMode); } int32 VisualGapAuditValue = 0; int32 VisualGapInjectionValue = 0; int32 LegacyVisualHandoffValue = 0; FParse::Value(CommandLine, TEXT("voxel.TestVisualGapAudit="), VisualGapAuditValue); FParse::Value(CommandLine, TEXT("voxel.TestVisualGapInjectEarlyHide="), VisualGapInjectionValue); FParse::Value(CommandLine, TEXT("voxel.TestGapAuditLegacyHandoff="), LegacyVisualHandoffValue); if (VisualGapAuditValue != 0 || VisualGapInjectionValue != 0) { FVisualGapAuditState& Audit = GVoxelForgeVisualGapAudits.Add(this); Audit.bEnabled = true; Audit.bInjectEarlyHide = VisualGapInjectionValue != 0; Audit.bLegacyVisualHandoff = LegacyVisualHandoffValue != 0; Audit.bCollecting = Audit.bInjectEarlyHide; Audit.bCollectionStarted = Audit.bInjectEarlyHide; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeVisualGapAudit] configured=1 witness_samples_per_tile=24 inject_early_hide=%d legacy_handoff=%d"), Audit.bInjectEarlyHide ? 1 : 0, Audit.bLegacyVisualHandoff ? 1 : 0); } int32 DigMotionValue = 0; FParse::Value(CommandLine, TEXT("voxel.TestDigMotion="), DigMotionValue); if (DigMotionValue != 0) { FDigMotionTestState& Test = GVoxelForgeDigMotionTests.Add(this); Test.bEnabled = true; int32 FieldOverrideValue = 0; FParse::Value(CommandLine, TEXT("voxel.TestDigMotionNearDigCount="), Test.NearDigCount); FParse::Value(CommandLine, TEXT("voxel.TestDigMotionIntervalSeconds="), Test.DigIntervalSeconds); FParse::Value(CommandLine, TEXT("voxel.TestDigMotionUnderfootDigCount="), Test.UnderfootDigCount); FParse::Value(CommandLine, TEXT("voxel.TestDigMotionUnderfootIntervalSeconds="), Test.UnderfootDigIntervalSeconds); FParse::Value(CommandLine, TEXT("voxel.TestDigMotionFieldOverride="), FieldOverrideValue); Test.NearDigCount = FMath::Clamp(Test.NearDigCount, 0, 32); Test.UnderfootDigCount = FMath::Clamp(Test.UnderfootDigCount, 1, 100); Test.DigIntervalSeconds = FMath::Clamp(Test.DigIntervalSeconds, 0.0, 3.0); Test.UnderfootDigIntervalSeconds = FMath::Clamp( Test.UnderfootDigIntervalSeconds, 0.0, 1.0); Test.bUnderfootFieldOverride = FieldOverrideValue != 0; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDigMotion] configured=1 near_dig_count=%d interval_s=%.3f " "underfoot_dig_count=%d underfoot_interval_s=%.3f field_override=%d"), Test.NearDigCount, Test.DigIntervalSeconds, Test.UnderfootDigCount, Test.UnderfootDigIntervalSeconds, Test.bUnderfootFieldOverride ? 1 : 0); } FString RequestedTileHashDumpPath; if (FParse::Value(CommandLine, TEXT("voxel.TileHashDump="), RequestedTileHashDumpPath)) { RequestedTileHashDumpPath.TrimQuotesInline(); if (FPaths::IsRelative(RequestedTileHashDumpPath)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeParity] voxel.TileHashDump must be an absolute path: '%s'."), *RequestedTileHashDumpPath); } else { TileHashDumpPath = FPaths::ConvertRelativePathToFull(RequestedTileHashDumpPath); FPaths::NormalizeFilename(TileHashDumpPath); bTileHashDumpEnabled = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeParity] tile_hash_dump_enabled=1 path=%s"), *TileHashDumpPath); } } int32 MoveValue = 0; const bool bMoveRequested = (FParse::Value(CommandLine, TEXT("voxel.TestMove="), MoveValue) && MoveValue != 0) || FParse::Param(CommandLine, TEXT("voxel.TestMove")); float Speed = 800.0f; float StartSeconds = 20.0f; float ExitSeconds = 0.0f; int32 ExitOnSteadyValue = 0; FParse::Value(CommandLine, TEXT("voxel.TestMoveSpeedCmPerSecond="), Speed); FParse::Value(CommandLine, TEXT("voxel.TestMoveStartSeconds="), StartSeconds); FParse::Value(CommandLine, TEXT("voxel.TestExitSeconds="), ExitSeconds); FParse::Value(CommandLine, TEXT("voxel.TestTeleportAfterSeconds="), HeadlessStreamingTestTeleportAtSeconds); FParse::Value(CommandLine, TEXT("voxel.TestTeleportDistanceCm="), HeadlessStreamingTestTeleportDistanceCm); HeadlessStreamingTestTeleportAtSeconds = FMath::Max( HeadlessStreamingTestTeleportAtSeconds, 0.0f); HeadlessStreamingTestTeleportDistanceCm = FMath::Max( HeadlessStreamingTestTeleportDistanceCm, 0.0f); if (FParse::Value(CommandLine, TEXT("voxel.TestExitOnSteady="), ExitOnSteadyValue)) { bHeadlessStreamingTestExitOnSteady = ExitOnSteadyValue != 0; } int32 CrossingValue = 0; bHeadlessStrateCrossingTest = (FParse::Value(CommandLine, TEXT("voxel.TestStrateCrossing="), CrossingValue) && CrossingValue != 0) || FParse::Param(CommandLine, TEXT("voxel.TestStrateCrossing")); if (bHeadlessStrateCrossingTest) { bHeadlessStreamingTestMovement = false; HeadlessStreamingTestExitSeconds = 0.0f; HeadlessStrateCrossingTestSpeedCmPerSecond = 800.0f; HeadlessStrateCrossingTestStartDelaySeconds = 1.0f; HeadlessStrateCrossingTestTimeoutSeconds = 120.0f; HeadlessStrateCrossingTestMaxGateSeconds = 10.0f; HeadlessStrateCrossingTestRouteTargetRadiusVoxels = 0.5f * 100.0f / VOXEL_SIZE; FParse::Value(CommandLine, TEXT("voxel.TestStrateCrossingSpeedCmPerSecond="), HeadlessStrateCrossingTestSpeedCmPerSecond); FParse::Value(CommandLine, TEXT("voxel.TestStrateCrossingStartDelaySeconds="), HeadlessStrateCrossingTestStartDelaySeconds); FParse::Value(CommandLine, TEXT("voxel.TestStrateCrossingTimeoutSeconds="), HeadlessStrateCrossingTestTimeoutSeconds); FParse::Value(CommandLine, TEXT("voxel.TestStrateCrossingMaxGateSeconds="), HeadlessStrateCrossingTestMaxGateSeconds); FParse::Value(CommandLine, TEXT("voxel.TestStrateCrossingTargetRadiusVoxels="), HeadlessStrateCrossingTestRouteTargetRadiusVoxels); FParse::Value(CommandLine, TEXT("voxel.TestStrateCrossingPassageIndex="), HeadlessStrateCrossingTestRequestedPassageIndex); HeadlessStrateCrossingTestSpeedCmPerSecond = FMath::Max( HeadlessStrateCrossingTestSpeedCmPerSecond, 1.0f); HeadlessStrateCrossingTestStartDelaySeconds = FMath::Max( HeadlessStrateCrossingTestStartDelaySeconds, 0.0f); HeadlessStrateCrossingTestTimeoutSeconds = FMath::Max( HeadlessStrateCrossingTestTimeoutSeconds, 1.0f); HeadlessStrateCrossingTestMaxGateSeconds = FMath::Max( HeadlessStrateCrossingTestMaxGateSeconds, 0.1f); HeadlessStrateCrossingTestRouteTargetRadiusVoxels = FMath::Max( HeadlessStrateCrossingTestRouteTargetRadiusVoxels, 0.25f); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStrateCrossingTest] configured=1 speed_cm_s=%.1f start_delay_s=%.3f " "timeout_s=%.3f max_gate_s=%.3f target_radius_vox=%.3f requested_passage=%d"), HeadlessStrateCrossingTestSpeedCmPerSecond, HeadlessStrateCrossingTestStartDelaySeconds, HeadlessStrateCrossingTestTimeoutSeconds, HeadlessStrateCrossingTestMaxGateSeconds, HeadlessStrateCrossingTestRouteTargetRadiusVoxels, HeadlessStrateCrossingTestRequestedPassageIndex); } int32 SurfaceFallValue = 0; bHeadlessSurfaceFallTest = (FParse::Value(CommandLine, TEXT("voxel.TestSurfaceFall="), SurfaceFallValue) && SurfaceFallValue != 0) || FParse::Param(CommandLine, TEXT("voxel.TestSurfaceFall")); if (bHeadlessSurfaceFallTest) { if (bHeadlessStrateCrossingTest) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeSurfaceFallTest] result=FAIL reason=conflicting_headless_tests")); bHeadlessSurfaceFallTestFailed = true; HeadlessSurfaceFallTestFailureReason = TEXT("conflicting_headless_tests"); } FParse::Value(CommandLine, TEXT("voxel.TestSurfaceFallSpawnHeightVoxels="), HeadlessSurfaceFallTestSpawnHeightVoxels); FParse::Value(CommandLine, TEXT("voxel.TestSurfaceFallGroundToleranceVoxels="), HeadlessSurfaceFallTestGroundToleranceVoxels); FParse::Value(CommandLine, TEXT("voxel.TestSurfaceFallTimeoutSeconds="), HeadlessSurfaceFallTestTimeoutSeconds); HeadlessSurfaceFallTestSpawnHeightVoxels = FMath::Max( HeadlessSurfaceFallTestSpawnHeightVoxels, 1.0f); HeadlessSurfaceFallTestGroundToleranceVoxels = FMath::Max( HeadlessSurfaceFallTestGroundToleranceVoxels, 0.5f); HeadlessSurfaceFallTestTimeoutSeconds = FMath::Max( HeadlessSurfaceFallTestTimeoutSeconds, 1.0f); HeadlessSurfaceFallTestBeginSeconds = FPlatformTime::Seconds(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeSurfaceFallTest] configured=1 spawn_height_vox=%.2f " "ground_tolerance_vox=%.2f timeout_s=%.3f"), HeadlessSurfaceFallTestSpawnHeightVoxels, HeadlessSurfaceFallTestGroundToleranceVoxels, HeadlessSurfaceFallTestTimeoutSeconds); } int32 TraceThroughCrossingValue = 0; if (FParse::Value( CommandLine, TEXT("voxel.StartupTraceThroughCrossing="), TraceThroughCrossingValue)) { bStartupTraceThroughCrossing = TraceThroughCrossingValue != 0; } int32 TraceUntilEndPlayValue = 0; if (FParse::Value(CommandLine, TEXT("voxel.StartupTraceUntilEndPlay="), TraceUntilEndPlayValue)) { bStartupTraceUntilEndPlay = TraceUntilEndPlayValue != 0; } float CenterX = 0.0f; float CenterY = 0.0f; float CenterZ = 0.0f; const bool bCenterX = FParse::Value(CommandLine, TEXT("voxel.StreamingTestCenterX="), CenterX); const bool bCenterY = FParse::Value(CommandLine, TEXT("voxel.StreamingTestCenterY="), CenterY); const bool bCenterZ = FParse::Value(CommandLine, TEXT("voxel.StreamingTestCenterZ="), CenterZ); if (bCenterX && bCenterY && bCenterZ && VoxelMath::IsFinite(CenterX) && VoxelMath::IsFinite(CenterY) && VoxelMath::IsFinite(CenterZ)) { bHeadlessStreamingTestCenterOverride = true; HeadlessStreamingTestCenterVoxel = FVector(CenterX, CenterY, CenterZ); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStreamingTest] center_override=1 local_vox=(%.2f,%.2f,%.2f)"), CenterX, CenterY, CenterZ); } if (bMoveRequested && !bHeadlessStrateCrossingTest) { // FParse::Value treats commas as separators in some commandlet argument builds; read the // whole vector token so the requested X,Y,Z direction survives intact. // FParse::Value separe parfois les virgules dans les commandes ; lire le jeton X,Y,Z complet. FString DirectionText; const FString CommandLineText(CommandLine); const FString DirectionKey(TEXT("voxel.TestMoveDirection=")); const int32 DirectionKeyIndex = CommandLineText.Find( DirectionKey, ESearchCase::IgnoreCase, ESearchDir::FromStart); if (DirectionKeyIndex != INDEX_NONE) { DirectionText = CommandLineText.Mid(DirectionKeyIndex + DirectionKey.Len()); const int32 DirectionEnd = DirectionText.Find( TEXT(" "), ESearchCase::CaseSensitive, ESearchDir::FromStart); if (DirectionEnd != INDEX_NONE) { DirectionText.LeftInline(DirectionEnd, EAllowShrinking::No); } DirectionText.TrimStartAndEndInline(); DirectionText.RemoveFromStart(TEXT("\"")); DirectionText.RemoveFromEnd(TEXT("\"")); TArray Components; DirectionText.ParseIntoArray(Components, TEXT(","), true); if (Components.Num() == 3) { HeadlessStreamingTestDirection = FVector( FCString::Atof(*Components[0]), FCString::Atof(*Components[1]), FCString::Atof(*Components[2])); } } HeadlessStreamingTestDirection = HeadlessStreamingTestDirection.GetSafeNormal(); if (HeadlessStreamingTestDirection.IsNearlyZero()) { HeadlessStreamingTestDirection = FVector::XAxisVector; } HeadlessStreamingTestSpeedCmPerSecond = FMath::Max(Speed, 0.0f); HeadlessStreamingTestStartSeconds = FMath::Max(StartSeconds, 0.0f); bHeadlessStreamingTestMovement = HeadlessStreamingTestSpeedCmPerSecond > 0.0f; } HeadlessStreamingTestExitSeconds = FMath::Max(ExitSeconds, 0.0f); if (!bHeadlessStrateCrossingTest && (bHeadlessStreamingTestMovement || HeadlessStreamingTestExitSeconds > 0.0f)) { HeadlessStreamingTestBeginSeconds = FPlatformTime::Seconds(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStreamingTest] clean=1 movement=%d speed_cm_s=%.3f start_s=%.3f " "exit_s=%.3f direction=(%.3f,%.3f,%.3f)"), bHeadlessStreamingTestMovement ? 1 : 0, HeadlessStreamingTestSpeedCmPerSecond, HeadlessStreamingTestStartSeconds, HeadlessStreamingTestExitSeconds, HeadlessStreamingTestDirection.X, HeadlessStreamingTestDirection.Y, HeadlessStreamingTestDirection.Z); } int32 CollisionGateStressValue = 0; bHeadlessCollisionGateStressTest = (FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStress="), CollisionGateStressValue) && CollisionGateStressValue != 0) || FParse::Param(CommandLine, TEXT("voxel.TestCollisionGateStress")); if (bHeadlessCollisionGateStressTest) { if (bHeadlessStrateCrossingTest || bHeadlessSurfaceFallTest) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("conflicting_headless_tests"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] result=FAIL reason=%s"), *HeadlessCollisionGateStressTestFailureReason); } float StressSpeed = HeadlessCollisionGateStressTestSpeedCmPerSecond; float StressSpawnHeight = HeadlessCollisionGateStressTestSpawnHeightVoxels; float StressDuration = HeadlessCollisionGateStressTestDurationSeconds; float StressTimeout = HeadlessCollisionGateStressTestTimeoutSeconds; int32 StressDeepDrop = 0; FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressSpeedCmPerSecond="), StressSpeed); FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressSpawnHeightVoxels="), StressSpawnHeight); FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressDeepDrop="), StressDeepDrop); FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressDurationSeconds="), StressDuration); FParse::Value(CommandLine, TEXT("voxel.TestCollisionGateStressTimeoutSeconds="), StressTimeout); HeadlessCollisionGateStressTestSpeedCmPerSecond = FMath::Max(StressSpeed, 1.0f); HeadlessCollisionGateStressTestSpawnHeightVoxels = FMath::Max(StressSpawnHeight, 1.0f); bHeadlessCollisionGateStressDeepDrop = StressDeepDrop != 0; HeadlessCollisionGateStressTestDurationSeconds = FMath::Max(StressDuration, 0.25f); HeadlessCollisionGateStressTestTimeoutSeconds = FMath::Max(StressTimeout, 1.0f); // The stress path owns movement and keeps the streamer centred at the launch tile. Do // not let the older synthetic TestMove path run in parallel with it. bHeadlessStreamingTestMovement = false; HeadlessStreamingTestExitSeconds = 0.0f; HeadlessCollisionGateStressTestBeginSeconds = 0.0; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] configured=1 speed_cm_s=%.1f " "spawn_height_vox=%.2f deep_drop=%d duration_after_landing_s=%.3f timeout_s=%.3f"), HeadlessCollisionGateStressTestSpeedCmPerSecond, HeadlessCollisionGateStressTestSpawnHeightVoxels, bHeadlessCollisionGateStressDeepDrop ? 1 : 0, HeadlessCollisionGateStressTestDurationSeconds, HeadlessCollisionGateStressTestTimeoutSeconds); } } void AVoxelWorld::RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels, int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi, const FVoxelMeshData& MeshData) { if (!bTileHashDumpEnabled) { return; } FVoxelTileHashDumpRecord Record; Record.Tile = Tile; Record.OriginVoxels = OriginVoxels; Record.Step = Step; Record.Cells = Cells; Record.BandChunkLo = BandChunkLo; Record.BandChunkHi = BandChunkHi; Record.bEmpty = MeshData.IsEmpty(); Record.NumTriangles = MeshData.Triangles.Num() / 3; Record.GeometryHash = VoxelForgeGeometryHash::Compute(MeshData); FScopeLock Lock(&TileHashDumpMutex); TileHashDumpRecords.Add(MoveTemp(Record)); } void AVoxelWorld::WriteTileHashDump() { if (!bTileHashDumpEnabled || TileHashDumpPath.IsEmpty()) { return; } TArray Records; { FScopeLock Lock(&TileHashDumpMutex); Records = TileHashDumpRecords; } Records.Sort([](const FVoxelTileHashDumpRecord& A, const FVoxelTileHashDumpRecord& B) { if (A.Tile.Level != B.Tile.Level) return A.Tile.Level < B.Tile.Level; if (A.Tile.Coord.Z != B.Tile.Coord.Z) return A.Tile.Coord.Z < B.Tile.Coord.Z; if (A.Tile.Coord.Y != B.Tile.Coord.Y) return A.Tile.Coord.Y < B.Tile.Coord.Y; if (A.Tile.Coord.X != B.Tile.Coord.X) return A.Tile.Coord.X < B.Tile.Coord.X; return A.GeometryHash < B.GeometryHash; }); IFileManager::Get().MakeDirectory(*FPaths::GetPath(TileHashDumpPath), true); FString Json; TSharedRef>> Writer = TJsonWriterFactory>::Create(&Json); Writer->WriteObjectStart(); Writer->WriteValue(TEXT("schema_version"), 1); Writer->WriteValue(TEXT("source"), TEXT("game_streaming_path")); Writer->WriteValue(TEXT("settings_asset"), Settings ? Settings->GetPathName() : FString()); if (Settings != nullptr) { Writer->WriteObjectStart(TEXT("runtime_config")); Writer->WriteValue(TEXT("generate_skirts"), Settings->bGenerateSkirts); Writer->WriteValue(TEXT("skirt_cells"), static_cast(Settings->SkirtCells)); Writer->WriteValue(TEXT("lod_octave_drop"), Settings->LODOctaveDrop); Writer->WriteValue(TEXT("full_res_clip_levels"), Settings->FullResClipLevels); Writer->WriteValue(TEXT("coarse_tile_cells"), Settings->CoarseTileCells); Writer->WriteValue(TEXT("max_clip_level"), Settings->MaxClipLevel); Writer->WriteValue(TEXT("render_distance_chunks"), Settings->RenderDistanceChunks); Writer->WriteValue(TEXT("strate_content_cut_min_level"), Settings->GetEffectiveStrateContentCutMinLevel()); Writer->WriteObjectEnd(); } Writer->WriteValue(TEXT("record_count"), Records.Num()); Writer->WriteArrayStart(TEXT("records")); for (const FVoxelTileHashDumpRecord& Record : Records) { Writer->WriteObjectStart(); Writer->WriteValue(TEXT("tile_x"), Record.Tile.Coord.X); Writer->WriteValue(TEXT("tile_y"), Record.Tile.Coord.Y); Writer->WriteValue(TEXT("tile_z"), Record.Tile.Coord.Z); Writer->WriteValue(TEXT("level"), Record.Tile.Level); Writer->WriteValue(TEXT("origin_x_voxels"), Record.OriginVoxels.X); Writer->WriteValue(TEXT("origin_y_voxels"), Record.OriginVoxels.Y); Writer->WriteValue(TEXT("origin_z_voxels"), Record.OriginVoxels.Z); Writer->WriteValue(TEXT("step"), Record.Step); Writer->WriteValue(TEXT("cells"), Record.Cells); Writer->WriteValue(TEXT("band_chunk_lo"), Record.BandChunkLo); Writer->WriteValue(TEXT("band_chunk_hi"), Record.BandChunkHi); // The parity harness still compares this field; the runtime has no sheet tiles. Writer->WriteValue(TEXT("sheet_tile"), false); Writer->WriteValue(TEXT("empty"), Record.bEmpty); Writer->WriteValue(TEXT("triangle_count"), Record.NumTriangles); Writer->WriteValue(TEXT("geometry_hash"), Record.GeometryHash); Writer->WriteObjectEnd(); } Writer->WriteArrayEnd(); Writer->WriteObjectEnd(); if (!Writer->Close() || !FFileHelper::SaveStringToFile(Json, *TileHashDumpPath)) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeParity] Could not write tile hash dump '%s'."), *TileHashDumpPath); } else { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeParity] tile_hash_dump=%s records=%d"), *TileHashDumpPath, Records.Num()); } } void AVoxelWorld::InitializeHeadlessStrateCrossingTest() { if (!bHeadlessStrateCrossingTest) { return; } HeadlessStrateCrossingTestBeginSeconds = FPlatformTime::Seconds(); if (StrateManager == nullptr) { bHeadlessStrateCrossingTestFailed = true; HeadlessStrateCrossingTestFailureReason = TEXT("strate_manager_unavailable"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStrateCrossingTest] result=FAIL reason=%s"), *HeadlessStrateCrossingTestFailureReason); return; } const TArray& Passages = StrateManager->GetPassages(); auto IsUsablePassage = [](const FVoxelPassage& Passage) -> bool { return Passage.bWalkableTunnelContract && Passage.UpperStrateIndex >= 0 && Passage.LowerStrateIndex == Passage.UpperStrateIndex + 1 && Passage.UpperLanding.HalfWidth > 0.0f && Passage.LowerLanding.HalfWidth > 0.0f && Passage.ControlPoints.Num() >= 2 && Passage.ControlRadii.Num() == Passage.ControlPoints.Num(); }; int32 SelectedPassageIndex = INDEX_NONE; if (HeadlessStrateCrossingTestRequestedPassageIndex != INDEX_NONE) { if (Passages.IsValidIndex(HeadlessStrateCrossingTestRequestedPassageIndex) && IsUsablePassage(Passages[HeadlessStrateCrossingTestRequestedPassageIndex])) { SelectedPassageIndex = HeadlessStrateCrossingTestRequestedPassageIndex; } else { bHeadlessStrateCrossingTestFailed = true; HeadlessStrateCrossingTestFailureReason = TEXT("requested_passage_unusable"); } } else { // Prefer the first real A→B connection so the default test has a fixed, local route. The // fallback still follows the manager's generated chain when the authored layout has more // than two slots or a future layout changes the first passage's walkability metadata. for (int32 PassageIndex = 0; PassageIndex < Passages.Num(); ++PassageIndex) { if (IsUsablePassage(Passages[PassageIndex]) && Passages[PassageIndex].UpperStrateIndex == 0 && Passages[PassageIndex].LowerStrateIndex == 1) { SelectedPassageIndex = PassageIndex; break; } } if (SelectedPassageIndex == INDEX_NONE) { for (int32 PassageIndex = 0; PassageIndex < Passages.Num(); ++PassageIndex) { if (IsUsablePassage(Passages[PassageIndex])) { SelectedPassageIndex = PassageIndex; break; } } } if (SelectedPassageIndex == INDEX_NONE) { bHeadlessStrateCrossingTestFailed = true; HeadlessStrateCrossingTestFailureReason = TEXT("no_usable_inter_strate_passage"); } } if (bHeadlessStrateCrossingTestFailed) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStrateCrossingTest] result=FAIL reason=%s passages=%d"), *HeadlessStrateCrossingTestFailureReason, Passages.Num()); return; } const FVoxelPassage& Passage = Passages[SelectedPassageIndex]; HeadlessStrateCrossingTestPassageIndex = SelectedPassageIndex; HeadlessStrateCrossingTestUpperStrateIndex = Passage.UpperStrateIndex; HeadlessStrateCrossingTestLowerStrateIndex = Passage.LowerStrateIndex; HeadlessStrateCrossingTestRoute.Reset(); const auto AddRoutePoint = [this](const FVector& Point) { if (!VoxelMath::IsFinite(Point.X) || !VoxelMath::IsFinite(Point.Y) || !VoxelMath::IsFinite(Point.Z)) { return; } if (HeadlessStrateCrossingTestRoute.Num() == 0 || FVector::DistSquared(HeadlessStrateCrossingTestRoute.Last(), Point) > FMath::Square(0.01f)) { HeadlessStrateCrossingTestRoute.Add(Point); } }; // ControlPoints starts/ends at the two DoorPoints. Keep both landing anchors around the chain // so the test covers the actual walk from a standing pose into A, through the gap, and out to // a standing pose in B; duplicate door points are collapsed by AddRoutePoint. AddRoutePoint(Passage.UpperLanding.StandingPoint); AddRoutePoint(Passage.UpperLanding.DoorPoint); for (const FVector& Point : Passage.ControlPoints) { AddRoutePoint(Point); } AddRoutePoint(Passage.LowerLanding.DoorPoint); AddRoutePoint(Passage.LowerLanding.StandingPoint); if (HeadlessStrateCrossingTestRoute.Num() < 4) { bHeadlessStrateCrossingTestFailed = true; HeadlessStrateCrossingTestFailureReason = TEXT("passage_route_degenerate"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStrateCrossingTest] result=FAIL reason=%s passage=%d route_points=%d"), *HeadlessStrateCrossingTestFailureReason, SelectedPassageIndex, HeadlessStrateCrossingTestRoute.Num()); return; } HeadlessStrateCrossingTestRouteTargetIndex = 1; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStrateCrossingTest] route_ready passage=%d upper_strate=%d lower_strate=%d " "route_points=%d upper=(%.2f,%.2f,%.2f) lower=(%.2f,%.2f,%.2f) " "control_points=%d vertical_drop_vox=%.2f horizontal_run_vox=%.2f"), SelectedPassageIndex, Passage.UpperStrateIndex, Passage.LowerStrateIndex, HeadlessStrateCrossingTestRoute.Num(), Passage.UpperLanding.StandingPoint.X, Passage.UpperLanding.StandingPoint.Y, Passage.UpperLanding.FloorZ, Passage.LowerLanding.StandingPoint.X, Passage.LowerLanding.StandingPoint.Y, Passage.LowerLanding.FloorZ, Passage.ControlPoints.Num(), Passage.TunnelVerticalDropVoxels, Passage.TunnelHorizontalPathLengthVoxels); } void AVoxelWorld::AdvanceHeadlessStrateCrossingTest( FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn) { if (!bHeadlessStrateCrossingTest || bHeadlessStrateCrossingTestFailed || bHeadlessStrateCrossingTestPassed || !IsValid(PlayerPawn) || HeadlessStrateCrossingTestBeginSeconds <= 0.0 || !StrateManager || !StrateManager->GetPassages().IsValidIndex(HeadlessStrateCrossingTestPassageIndex)) { return; } const double Now = FPlatformTime::Seconds(); const double ElapsedSeconds = Now - HeadlessStrateCrossingTestBeginSeconds; if (ElapsedSeconds < static_cast(HeadlessStrateCrossingTestStartDelaySeconds)) { return; } const FVoxelPassage& Passage = StrateManager->GetPassages()[HeadlessStrateCrossingTestPassageIndex]; if (!bHeadlessStrateCrossingTestStartPlaced) { if (const ACharacter* Character = Cast(PlayerPawn)) { if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent()) { const float HalfHeightCm = Capsule->GetScaledCapsuleHalfHeight(); if (VoxelMath::IsFinite(HalfHeightCm) && HalfHeightCm > 0.0f) { HeadlessStrateCrossingTestPlayerHalfHeightVoxels = HalfHeightCm / VOXEL_SIZE; } } } FVector StartVoxel = Passage.UpperLanding.StandingPoint; StartVoxel.Z = Passage.UpperLanding.FloorZ + HeadlessStrateCrossingTestPlayerHalfHeightVoxels; const FVector StartWorld = LocalVoxelToWorld(StartVoxel); // The test pawn starts in the map's default spawn area, but the authored passage can be // many tiles away. Do not teleport the capsule into an unresolved landing floor: ask for // that exact support tile first, then let the pending-floor assertion cover only movement // after the crossing has genuinely begun. // Keep the streamer's centre at the prospective landing while that request is serviced; // the pawn itself remains at the map spawn until the collision is ready. bHeadlessStreamingTestCenterOverride = true; HeadlessStreamingTestCenterVoxel = StartVoxel; FVoxelTileKey StartSupportTile; const TCHAR* StartSupportUnavailableReason = nullptr; if (!IsPlayerSupportCollisionReady( PlayerPawn, StartWorld, &StartSupportTile, &StartSupportUnavailableReason)) { RequestCollisionGateSupportTile(StartSupportTile); if (!bHeadlessStrateCrossingTestInitialSupportReported) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStrateCrossingTest] waiting_for_initial_support " "tile=(%d,%d,%d) reason=%s"), StartSupportTile.Coord.X, StartSupportTile.Coord.Y, StartSupportTile.Coord.Z, StartSupportUnavailableReason != nullptr ? StartSupportUnavailableReason : TEXT("collision_pending")); bHeadlessStrateCrossingTestInitialSupportReported = true; } return; } bHeadlessStrateCrossingTestInitialSupportReported = false; const bool bPlaced = PlayerPawn->SetActorLocation( StartWorld, false, nullptr, ETeleportType::TeleportPhysics); const FVector ActualPosition = PlayerPawn->GetActorLocation(); InOutPlayerPosition = ActualPosition; InOutPlayerHeading = FVector::ZeroVector; HeadlessStrateCrossingTestLastActualPosition = ActualPosition; bHeadlessStrateCrossingTestStartPlaced = true; bHeadlessStrateCrossingTestGateWasEngaged = false; HeadlessStrateCrossingTestGateStartSeconds = 0.0; HeadlessStrateCrossingTestStartSeconds = Now; if (ACharacter* Character = Cast(PlayerPawn)) { if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) { // Keep the route test natural (AddMovementInput + the normal swept character // movement), but make its requested speed explicit and identical across binaries. CharacterMovement->MaxWalkSpeed = HeadlessStrateCrossingTestSpeedCmPerSecond; CharacterMovement->MaxAcceleration = FMath::Max( CharacterMovement->MaxAcceleration, HeadlessStrateCrossingTestSpeedCmPerSecond * 4.0f); CharacterMovement->BrakingDecelerationWalking = FMath::Max( CharacterMovement->BrakingDecelerationWalking, HeadlessStrateCrossingTestSpeedCmPerSecond * 4.0f); CharacterMovement->StopMovementImmediately(); } } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStrateCrossingTest] start_placed moved=%d world=(%.1f,%.1f,%.1f) " "local_vox=(%.2f,%.2f,%.2f) floor_vox=%.2f half_height_vox=%.3f"), bPlaced ? 1 : 0, ActualPosition.X, ActualPosition.Y, ActualPosition.Z, StartVoxel.X, StartVoxel.Y, StartVoxel.Z, Passage.UpperLanding.FloorZ, HeadlessStrateCrossingTestPlayerHalfHeightVoxels); return; } // The landing was kept warm for the teleport. From this tick onward the stream follows the // pawn's actual swept movement again. bHeadlessStreamingTestCenterOverride = false; if (!bHeadlessStrateCrossingTestStarted) { if (!IsPlayerSupportCollisionReady(PlayerPawn, InOutPlayerPosition)) { return; } bHeadlessStrateCrossingTestStarted = true; HeadlessStrateCrossingTestStartSeconds = Now; HeadlessStrateCrossingTestRouteTargetIndex = 1; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStrateCrossingTest] natural_walk_started elapsed_s=%.3f " "route_target=%d/%d"), ElapsedSeconds, HeadlessStrateCrossingTestRouteTargetIndex, HeadlessStrateCrossingTestRoute.Num()); return; } const FVector LocalPosition = WorldToLocalVoxel(InOutPlayerPosition); while (HeadlessStrateCrossingTestRouteTargetIndex < HeadlessStrateCrossingTestRoute.Num()) { const FVector& Target = HeadlessStrateCrossingTestRoute[ HeadlessStrateCrossingTestRouteTargetIndex]; const FVector2D Delta( Target.X - LocalPosition.X, Target.Y - LocalPosition.Y); if (Delta.SizeSquared() > FMath::Square( HeadlessStrateCrossingTestRouteTargetRadiusVoxels)) { break; } ++HeadlessStrateCrossingTestRouteTargetIndex; UE_LOG(LogTemp, Verbose, TEXT("[VoxelForgeStrateCrossingTest] route_target_reached index=%d/%d"), HeadlessStrateCrossingTestRouteTargetIndex, HeadlessStrateCrossingTestRoute.Num()); } const int32 CurrentStrate = GetStrateAtPosition(InOutPlayerPosition); if (HeadlessStrateCrossingTestRouteTargetIndex >= HeadlessStrateCrossingTestRoute.Num()) { if (CurrentStrate == HeadlessStrateCrossingTestLowerStrateIndex) { bHeadlessStrateCrossingTestPassed = true; } else { bHeadlessStrateCrossingTestFailed = true; HeadlessStrateCrossingTestFailureReason = TEXT("route_finished_without_lower_strate"); } return; } if (bPawnCollisionGateEngaged) { InOutPlayerHeading = FVector::ZeroVector; return; } const FVector& Target = HeadlessStrateCrossingTestRoute[ HeadlessStrateCrossingTestRouteTargetIndex]; const FVector LocalDirection( Target.X - LocalPosition.X, Target.Y - LocalPosition.Y, 0.0f); const FVector WorldDirection = GetActorTransform().TransformVectorNoScale( LocalDirection.GetSafeNormal()); if (!WorldDirection.IsNearlyZero()) { const FVector NormalizedWorldDirection = WorldDirection.GetSafeNormal(); PlayerPawn->AddMovementInput(NormalizedWorldDirection, 1.0f, false); InOutPlayerHeading = NormalizedWorldDirection * HeadlessStrateCrossingTestSpeedCmPerSecond; } } void AVoxelWorld::ObserveHeadlessStrateCrossingTest( const FVector& PlayerPosition, APawn* PlayerPawn) { if (!bHeadlessStrateCrossingTest || !bHeadlessStrateCrossingTestStartPlaced || !IsValid(PlayerPawn) || bHeadlessStrateCrossingTestExitRequested) { return; } const double Now = FPlatformTime::Seconds(); if (bHeadlessStrateCrossingTestStarted) { HeadlessStreamingTestDistanceCm += FVector::Dist( HeadlessStrateCrossingTestLastActualPosition, PlayerPosition); } HeadlessStrateCrossingTestLastActualPosition = PlayerPosition; const bool bGateEngaged = bPawnCollisionGateEngaged; if (bGateEngaged && !bHeadlessStrateCrossingTestGateWasEngaged) { HeadlessStrateCrossingTestGateStartSeconds = Now; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStrateCrossingTest] gate=engaged elapsed_s=%.3f"), Now - HeadlessStrateCrossingTestBeginSeconds); } else if (!bGateEngaged && bHeadlessStrateCrossingTestGateWasEngaged) { const double GateDuration = FMath::Max( 0.0, Now - HeadlessStrateCrossingTestGateStartSeconds); HeadlessStrateCrossingTestTotalGateDurationSeconds += GateDuration; HeadlessStrateCrossingTestMaxGateDurationSeconds = FMath::Max( HeadlessStrateCrossingTestMaxGateDurationSeconds, GateDuration); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStrateCrossingTest] gate=released duration_s=%.6f total_s=%.6f"), GateDuration, HeadlessStrateCrossingTestTotalGateDurationSeconds); } bHeadlessStrateCrossingTestGateWasEngaged = bGateEngaged; if (bGateEngaged && HeadlessStrateCrossingTestGateStartSeconds > 0.0) { const double GateDuration = FMath::Max( 0.0, Now - HeadlessStrateCrossingTestGateStartSeconds); HeadlessStrateCrossingTestMaxGateDurationSeconds = FMath::Max( HeadlessStrateCrossingTestMaxGateDurationSeconds, GateDuration); if (GateDuration > static_cast(HeadlessStrateCrossingTestMaxGateSeconds)) { bHeadlessStrateCrossingTestFailed = true; HeadlessStrateCrossingTestFailureReason = TEXT("collision_gate_timeout"); } } if (!StrateManager->GetPassages().IsValidIndex(HeadlessStrateCrossingTestPassageIndex)) { bHeadlessStrateCrossingTestFailed = true; HeadlessStrateCrossingTestFailureReason = TEXT("passage_invalidated"); return; } const FVoxelPassage& Passage = StrateManager->GetPassages()[HeadlessStrateCrossingTestPassageIndex]; const FVector LocalPosition = WorldToLocalVoxel(PlayerPosition); FVoxelTileKey PendingFloorTile; if (IsPawnCapsuleOverlappingPendingFloor( PlayerPawn, PlayerPosition, PlayerPosition, &PendingFloorTile)) { bHeadlessStrateCrossingTestPendingFloorOverlap = true; bHeadlessStrateCrossingTestFailed = true; HeadlessStrateCrossingTestFailureReason = TEXT("capsule_overlaps_pending_floor"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStrateCrossingTest] assertion_failed=%s tile=(%d,%d,%d) " "position_vox=(%.2f,%.2f,%.2f) gate=%d"), *HeadlessStrateCrossingTestFailureReason, PendingFloorTile.Coord.X, PendingFloorTile.Coord.Y, PendingFloorTile.Coord.Z, LocalPosition.X, LocalPosition.Y, LocalPosition.Z, bPawnCollisionGateEngaged ? 1 : 0); return; } float PassageFloorZ = 0.0f; if (VF_ProjectHeadlessPassageFloor(Passage, LocalPosition, PassageFloorZ)) { const float FeetZ = LocalPosition.Z - HeadlessStrateCrossingTestPlayerHalfHeightVoxels; const float Clearance = FeetZ - PassageFloorZ; HeadlessStrateCrossingTestMinFloorClearanceVoxels = FMath::Min( HeadlessStrateCrossingTestMinFloorClearanceVoxels, Clearance); constexpr float FloorViolationToleranceVoxels = 0.5f * 100.0f / VOXEL_SIZE; if (Clearance < -FloorViolationToleranceVoxels) { bHeadlessStrateCrossingTestFailed = true; HeadlessStrateCrossingTestFailureReason = TEXT("pawn_below_passage_floor"); } } const int32 CurrentStrate = GetStrateAtPosition(PlayerPosition); if (CurrentStrate != HeadlessStrateCrossingTestLastReportedStrate) { FVoxelTileKey SupportTile; const bool bSupportReady = IsPlayerSupportCollisionReady( PlayerPawn, PlayerPosition, &SupportTile); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStrateCrossingTest] progress current_strate=%d target_strate=%d " "route=%d/%d position_vox=(%.2f,%.2f,%.2f) mesh_band=[%d..%d] " "band_remesh=%d pending_tiles=%d support=(%d,%d,%d) support_ready=%d gate=%d"), CurrentStrate, HeadlessStrateCrossingTestLowerStrateIndex, HeadlessStrateCrossingTestRouteTargetIndex, HeadlessStrateCrossingTestRoute.Num(), LocalPosition.X, LocalPosition.Y, LocalPosition.Z, MeshBandChunkLo, MeshBandChunkHi, BandRemeshQueue.Num(), PendingTiles.Num(), SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z, bSupportReady ? 1 : 0, bGateEngaged ? 1 : 0); HeadlessStrateCrossingTestLastReportedStrate = CurrentStrate; } // Reaching the lower strate is the crossing contract (not the final room anchor). Requiring route index 2 means the pawn // has consumed the upper landing door point and is following the generated passage chain; it // cannot pass while merely standing at the test's initial upper landing. The lower landing // anchor is intentionally not required: after the boundary is crossed, that extra room walk is // outside the inter-strate streaming/collision transition being tested and can be affected by // authored chamber furniture or a non-monotonic final XY turn. if (!bHeadlessStrateCrossingTestFailed && !bGateEngaged && bHeadlessStrateCrossingTestStarted && CurrentStrate == HeadlessStrateCrossingTestLowerStrateIndex && HeadlessStrateCrossingTestRouteTargetIndex >= 2) { bHeadlessStrateCrossingTestPassed = true; } if (!bHeadlessStrateCrossingTestPassed && !bHeadlessStrateCrossingTestFailed && Now - HeadlessStrateCrossingTestBeginSeconds > static_cast(HeadlessStrateCrossingTestTimeoutSeconds)) { bHeadlessStrateCrossingTestFailed = true; HeadlessStrateCrossingTestFailureReason = TEXT("crossing_timeout"); } } void AVoxelWorld::MaybeFinishHeadlessStrateCrossingTest() { if (!bHeadlessStrateCrossingTest || bHeadlessStrateCrossingTestExitRequested || HeadlessStrateCrossingTestBeginSeconds <= 0.0) { return; } const double Now = FPlatformTime::Seconds(); if (!bHeadlessStrateCrossingTestPassed && !bHeadlessStrateCrossingTestFailed && Now - HeadlessStrateCrossingTestBeginSeconds >= static_cast(HeadlessStrateCrossingTestTimeoutSeconds)) { bHeadlessStrateCrossingTestFailed = true; HeadlessStrateCrossingTestFailureReason = TEXT("crossing_timeout"); } if (!bHeadlessStrateCrossingTestPassed && !bHeadlessStrateCrossingTestFailed) { return; } if (bHeadlessStrateCrossingTestGateWasEngaged && HeadlessStrateCrossingTestGateStartSeconds > 0.0) { const double GateDuration = FMath::Max( 0.0, Now - HeadlessStrateCrossingTestGateStartSeconds); HeadlessStrateCrossingTestMaxGateDurationSeconds = FMath::Max( HeadlessStrateCrossingTestMaxGateDurationSeconds, GateDuration); } bHeadlessStrateCrossingTestExitRequested = true; const bool bPassed = bHeadlessStrateCrossingTestPassed && !bHeadlessStrateCrossingTestFailed; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStrateCrossingTest] result=%s reason=%s passage=%d upper_strate=%d " "lower_strate=%d elapsed_s=%.6f distance_m=%.6f route=%d/%d " "max_gate_s=%.6f gate_limit_s=%.3f gate_total_s=%.6f " "min_floor_clearance_vox=%.6f pending_floor_overlap=%d " "final_position=(%.1f,%.1f,%.1f)"), bPassed ? TEXT("PASS") : TEXT("FAIL"), bPassed ? TEXT("reached_lower_strate") : *HeadlessStrateCrossingTestFailureReason, HeadlessStrateCrossingTestPassageIndex, HeadlessStrateCrossingTestUpperStrateIndex, HeadlessStrateCrossingTestLowerStrateIndex, Now - HeadlessStrateCrossingTestBeginSeconds, HeadlessStreamingTestDistanceCm / 100.0, HeadlessStrateCrossingTestRouteTargetIndex, HeadlessStrateCrossingTestRoute.Num(), HeadlessStrateCrossingTestMaxGateDurationSeconds, HeadlessStrateCrossingTestMaxGateSeconds, HeadlessStrateCrossingTestTotalGateDurationSeconds, HeadlessStrateCrossingTestMinFloorClearanceVoxels, bHeadlessStrateCrossingTestPendingFloorOverlap ? 1 : 0, HeadlessStrateCrossingTestLastActualPosition.X, HeadlessStrateCrossingTestLastActualPosition.Y, HeadlessStrateCrossingTestLastActualPosition.Z); if (bStartupTraceThroughCrossing && VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::Finish( bPassed ? TEXT("strate_crossing_passed") : TEXT("strate_crossing_failed")); } FPlatformMisc::RequestExitWithStatus( false, bPassed ? 0 : 1, TEXT("VoxelForge strate crossing test complete")); } void AVoxelWorld::AdvanceHeadlessDigMotionTest(FVector& InOutPlayerPosition, APawn* Pawn) { FDigMotionTestState* Test = GVoxelForgeDigMotionTests.Find(this); if (!Test || !Test->bEnabled || Test->bResultLogged || !IsValid(Pawn)) { return; } ACharacter* Character = Cast(Pawn); UCharacterMovementComponent* Movement = Character ? Character->GetCharacterMovement() : nullptr; if (!Test->bStarted) { if (!Movement || Movement->MovementMode != MOVE_Walking) { return; } FVoxelTileKey SupportTile; if (!IsPlayerSupportCollisionReady(Pawn, InOutPlayerPosition, &SupportTile)) { return; } Test->bStarted = true; Test->BeginSeconds = FPlatformTime::Seconds(); Test->LastFrameSeconds = Test->BeginSeconds; Test->NextDigSeconds = Test->BeginSeconds + 0.25; Test->StartPosition = InOutPlayerPosition; if (FVisualGapAuditState* GapAudit = GVoxelForgeVisualGapAudits.Find(this)) { if (!GapAudit->bCollectionStarted) { VF_StartVisualGapAuditWindow(*GapAudit, Test->BeginSeconds); } } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDigMotion] begin=1 position=(%.2f,%.2f,%.2f) support=(%d,%d,%d) ready=1 " "movement_mode=%d capsule_half_height_cm=%.2f"), InOutPlayerPosition.X, InOutPlayerPosition.Y, InOutPlayerPosition.Z, SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z, static_cast(Movement->MovementMode), Character->GetCapsuleComponent()->GetScaledCapsuleHalfHeight()); return; } const double Now = FPlatformTime::Seconds(); if (Test->bUnderfootProbePending && GFrameCounter > Test->UnderfootEditFrame) { const float HalfHeightVoxels = Character->GetCapsuleComponent() ->GetScaledCapsuleHalfHeight() / VOXEL_SIZE; FVector ProbePosition = WorldToLocalVoxel(InOutPlayerPosition); ProbePosition.Z -= HalfHeightVoxels + 0.5f; const float FieldDensity = Generator ? Generator->GetDensityAt(ProbePosition.X, ProbePosition.Y, ProbePosition.Z) : 0.0f; const bool bFieldSolidAtFeet = VoxelMath::IsFinite(FieldDensity) && FieldDensity < 0.0f; const bool bCmcHasFloor = Movement->IsMovingOnGround() && Movement->CurrentFloor.IsWalkableFloor(); if (!bFieldSolidAtFeet && bCmcHasFloor) { ++Test->UnderfootFalseSupportFrames; } if (bFieldSolidAtFeet && Movement->MovementMode == MOVE_Falling) { ++Test->UnderfootFalseAirFrames; } const float FrameDelta = GetWorld() ? GetWorld()->GetDeltaSeconds() : 0.0f; const float ExpectedGravityDelta = Movement->GetGravityZ() * FrameDelta; const float ActualVelocityDelta = Movement->Velocity.Z - Test->UnderfootVelocityBefore; const float UnexplainedDownwardDelta = FMath::Max( 0.0f, ExpectedGravityDelta - ActualVelocityDelta); if (UnexplainedDownwardDelta > 25.0f) { ++Test->UnderfootInventedVelocityEvents; } ++Test->UnderfootFirstFrameChecks; ++Test->UnderfootSupportObservedFrames; Test->UnderfootLastObservedFrame = GFrameCounter; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDigMotionField] dig=%d first_sim_frame=%llu field_density=%.5f " "field_solid=%d movement_mode=%d floor_walkable=%d support=%d " "velocity_z_cm_s=%.3f expected_gravity_delta_cm_s=%.3f " "actual_velocity_delta_cm_s=%.3f unexplained_downward_cm_s=%.3f"), Test->UnderfootDigsIssued, static_cast(GFrameCounter), FieldDensity, bFieldSolidAtFeet ? 1 : 0, static_cast(Movement->MovementMode), Movement->CurrentFloor.IsWalkableFloor() ? 1 : 0, bCmcHasFloor ? 1 : 0, Movement->Velocity.Z, ExpectedGravityDelta, ActualVelocityDelta, UnexplainedDownwardDelta); Test->bUnderfootProbePending = false; } if (Now - Test->BeginSeconds > 240.0) { Test->bResultLogged = true; UE_LOG(LogTemp, Error, TEXT("[VoxelForgeDigMotion] result=FAIL reason=waiting_for_repro_completion near_digs=%d"), Test->NearDigsIssued); FPlatformMisc::RequestExitWithStatus( false, 1, TEXT("VoxelForge dig-motion repro timed out")); return; } if (Test->NearDigsIssued < Test->NearDigCount && Now >= Test->NextDigSeconds) { FVoxelTileKey SupportTile; if (!IsPlayerSupportCollisionReady(Pawn, InOutPlayerPosition, &SupportTile) || SupportTile.Level != 0) { return; } const FVector LocalPosition = WorldToLocalVoxel(InOutPlayerPosition); const float TileMinX = static_cast(SupportTile.Coord.X * CHUNK_SIZE); const float TileMaxX = TileMinX + CHUNK_SIZE; const float TileMinY = static_cast(SupportTile.Coord.Y * CHUNK_SIZE); const float TileMaxY = TileMinY + CHUNK_SIZE; const float Clearances[4] = { TileMaxX - LocalPosition.X, LocalPosition.X - TileMinX, TileMaxY - LocalPosition.Y, LocalPosition.Y - TileMinY }; const FVector Directions[4] = { FVector(1, 0, 0), FVector(-1, 0, 0), FVector(0, 1, 0), FVector(0, -1, 0) }; int32 BestDirection = 0; for (int32 Index = 1; Index < UE_ARRAY_COUNT(Clearances); ++Index) { if (Clearances[Index] > Clearances[BestDirection]) { BestDirection = Index; } } constexpr float DigOffsetVoxels = 2.0f * 100.0f / VOXEL_SIZE; constexpr float DigClearanceMarginVoxels = 0.75f * 100.0f / VOXEL_SIZE; if (Clearances[BestDirection] < DigOffsetVoxels + DigClearanceMarginVoxels) { return; } const float HalfHeightVoxels = Character->GetCapsuleComponent() ->GetScaledCapsuleHalfHeight() / VOXEL_SIZE; FVector DigCenterVoxel = LocalPosition + Directions[BestDirection] * DigOffsetVoxels; DigCenterVoxel.Z = LocalPosition.Z - HalfHeightVoxels + 1.0f; const FVector DigCenterWorld = LocalVoxelToWorld(DigCenterVoxel); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDigMotion] near_dig=%d center=(%.2f,%.2f,%.2f) pawn=(%.2f,%.2f,%.2f) " "horizontal_offset_cm=%.2f support=(%d,%d,%d)"), Test->NearDigsIssued + 1, DigCenterWorld.X, DigCenterWorld.Y, DigCenterWorld.Z, InOutPlayerPosition.X, InOutPlayerPosition.Y, InOutPlayerPosition.Z, DigOffsetVoxels * VOXEL_SIZE, SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z); CarveAtPosition(DigCenterWorld, 2.0f, 50.0f); ++Test->NearDigsIssued; Test->NextDigSeconds = Now + Test->DigIntervalSeconds; return; } const bool bUnderfootDelayElapsed = Test->bUnderDigIssued ? Now >= Test->NextUnderfootDigSeconds : Now >= Test->NextDigSeconds + 0.5; if (Test->NearDigsIssued >= Test->NearDigCount && Test->UnderfootDigsIssued < Test->UnderfootDigCount && bUnderfootDelayElapsed) { const float HalfHeightVoxels = Character->GetCapsuleComponent() ->GetScaledCapsuleHalfHeight() / VOXEL_SIZE; const FVector LocalPawnPosition = WorldToLocalVoxel(InOutPlayerPosition); FVector DigCenterVoxel = LocalPawnPosition; DigCenterVoxel.Z -= HalfHeightVoxels + 1.0f; if (!Test->bUnderDigIssued) { Test->UnderDigStartPosition = InOutPlayerPosition; Test->UnderDigSeconds = Now; Test->bUnderDigIssued = true; } const FVector DigCenterWorld = LocalVoxelToWorld(DigCenterVoxel); FVector FeetProbe = LocalPawnPosition; FeetProbe.Z -= HalfHeightVoxels + 0.5f; const float FieldDensityBefore = Generator ? Generator->GetDensityAt(FeetProbe.X, FeetProbe.Y, FeetProbe.Z) : 0.0f; Test->UnderfootVelocityBefore = Movement->Velocity.Z; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDigMotion] under_dig=%d center=(%.2f,%.2f,%.2f) " "pawn=(%.2f,%.2f,%.2f) radius_vox=5 field_before=%.5f frame=%llu"), Test->UnderfootDigsIssued + 1, DigCenterWorld.X, DigCenterWorld.Y, DigCenterWorld.Z, InOutPlayerPosition.X, InOutPlayerPosition.Y, InOutPlayerPosition.Z, FieldDensityBefore, static_cast(GFrameCounter)); CarveAtPosition(DigCenterWorld, 5.0f, 100.0f); Test->UnderfootFieldAfter = Generator ? Generator->GetDensityAt(FeetProbe.X, FeetProbe.Y, FeetProbe.Z) : 0.0f; ++Test->UnderfootDigsIssued; Test->UnderfootEditFrame = GFrameCounter; Test->bUnderfootProbePending = true; Test->NextUnderfootDigSeconds = Now + Test->UnderfootDigIntervalSeconds; Test->LastFrameSeconds = Now; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDigMotionField] dig=%d stage=post_edit frame=%llu " "field_before=%.5f field_after=%.5f override=%d"), Test->UnderfootDigsIssued, static_cast(GFrameCounter), FieldDensityBefore, Test->UnderfootFieldAfter, Test->bUnderfootFieldOverride ? 1 : 0); if (Test->bUnderfootFieldOverride && VoxelMath::IsFinite(Test->UnderfootFieldAfter) && Test->UnderfootFieldAfter >= 0.0f) { // Measurement-only prototype: detach immediately from the removed field floor. // Normal play is unchanged because TestDigMotion is an explicit headless switch. Movement->CurrentFloor.Clear(); Movement->SetMovementMode(MOVE_Falling); } return; } if (Test->UnderfootDigsIssued >= Test->UnderfootDigCount && !Test->bUnderfootProbePending && Now - Test->UnderDigSeconds >= 2.5) { Test->bResultLogged = true; const double FallCm = Test->UnderDigStartPosition.Z - InOutPlayerPosition.Z; const bool bPassed = Test->NearDigsIssued == Test->NearDigCount && Test->NearGateFrames == 0 && Test->NearFallingFrames == 0 && Test->NearVerticalMotionFrames == 0 && Test->bSawFalling && FallCm >= 10.0 && Test->UnderMaxDownwardVelocity < 6000.0f && Test->UnderfootDigsIssued == Test->UnderfootDigCount && Test->UnderfootFirstFrameChecks == Test->UnderfootDigCount && Test->UnderfootSupportObservedFrames >= Test->UnderfootFirstFrameChecks && Test->UnderfootFalseSupportFrames == 0 && Test->UnderfootFalseAirFrames == 0 && Test->UnderfootInventedVelocityEvents == 0; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDigMotion] result=%s near_digs=%d near_frames=%d near_gate_frames=%d " "near_falling_frames=%d near_vertical_motion_frames=%d " "under_saw_falling=%d under_gate_frames=%d under_fall_cm=%.2f " "under_max_downward_velocity_cm_s=%.2f under_digs=%d " "first_frame_checks=%d support_observed_frames=%d " "false_support_frames=%d false_air_frames=%d " "invented_downward_velocity_events=%d field_override=%d"), bPassed ? TEXT("PASS") : TEXT("FAIL"), Test->NearDigsIssued, Test->NearFrames, Test->NearGateFrames, Test->NearFallingFrames, Test->NearVerticalMotionFrames, Test->bSawFalling ? 1 : 0, Test->UnderGateFrames, FallCm, Test->UnderMaxDownwardVelocity, Test->UnderfootDigsIssued, Test->UnderfootFirstFrameChecks, Test->UnderfootSupportObservedFrames, Test->UnderfootFalseSupportFrames, Test->UnderfootFalseAirFrames, Test->UnderfootInventedVelocityEvents, Test->bUnderfootFieldOverride ? 1 : 0); FPlatformMisc::RequestExitWithStatus( false, bPassed ? 0 : 1, TEXT("VoxelForge dig-motion repro complete")); } } void AVoxelWorld::RecordHeadlessDigMotionFrame( APawn* Pawn, const FVector& PlayerPosition, float DeltaTime) { FDigMotionTestState* Test = GVoxelForgeDigMotionTests.Find(this); if (!Test || !Test->bEnabled || !Test->bStarted || Test->bResultLogged || !IsValid(Pawn)) { return; } const double Now = FPlatformTime::Seconds(); const double SampleDelta = FMath::Max(DeltaTime, 0.0f); const ACharacter* Character = Cast(Pawn); const UCharacterMovementComponent* Movement = Character ? Character->GetCharacterMovement() : nullptr; const float VerticalVelocity = Movement ? Movement->Velocity.Z : Pawn->GetVelocity().Z; const float GravityZ = Movement ? Movement->GetGravityZ() : 0.0f; const int32 MovementMode = Movement ? static_cast(Movement->MovementMode) : -1; if (Movement && Test->UnderfootDigsIssued > 0 && GFrameCounter > Test->UnderfootEditFrame && Test->UnderfootLastObservedFrame != GFrameCounter) { const float HalfHeightVoxels = Character && Character->GetCapsuleComponent() ? Character->GetCapsuleComponent()->GetScaledCapsuleHalfHeight() / VOXEL_SIZE : 0.0f; FVector ProbePosition = WorldToLocalVoxel(PlayerPosition); ProbePosition.Z -= HalfHeightVoxels + 0.5f; const float FieldDensity = Generator ? Generator->GetDensityAt(ProbePosition.X, ProbePosition.Y, ProbePosition.Z) : 0.0f; const bool bFieldSolidAtFeet = VoxelMath::IsFinite(FieldDensity) && FieldDensity < 0.0f; const bool bCmcHasFloor = Movement->IsMovingOnGround() && Movement->CurrentFloor.IsWalkableFloor(); if (!bFieldSolidAtFeet && bCmcHasFloor) { ++Test->UnderfootFalseSupportFrames; } if (bFieldSolidAtFeet && Movement->MovementMode == MOVE_Falling) { ++Test->UnderfootFalseAirFrames; } ++Test->UnderfootSupportObservedFrames; Test->UnderfootLastObservedFrame = GFrameCounter; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDigMotionField] stage=frame frame=%llu field_density=%.5f " "field_solid=%d movement_mode=%d floor_walkable=%d support=%d"), static_cast(GFrameCounter), FieldDensity, bFieldSolidAtFeet ? 1 : 0, MovementMode, Movement->CurrentFloor.IsWalkableFloor() ? 1 : 0, bCmcHasFloor ? 1 : 0); } FVoxelTileKey SupportTile; const bool bSupportReady = IsPlayerSupportCollisionReady(Pawn, PlayerPosition, &SupportTile); const bool bNearPhase = !Test->bUnderDigIssued; ++Test->Frame; if (bNearPhase) { ++Test->NearFrames; if (bPawnCollisionGateEngaged) { ++Test->NearGateFrames; } if (Movement && Movement->MovementMode == MOVE_Falling) { ++Test->NearFallingFrames; } if (FMath::Abs(VerticalVelocity) > 10.0f) { ++Test->NearVerticalMotionFrames; } Test->NearMaxDownwardVelocity = FMath::Max( Test->NearMaxDownwardVelocity, FMath::Max(0.0f, -VerticalVelocity)); } else { ++Test->UnderFrames; if (bPawnCollisionGateEngaged) { ++Test->UnderGateFrames; } if (Movement && Movement->MovementMode == MOVE_Falling && VerticalVelocity < -10.0f) { Test->bSawFalling = true; } Test->UnderMaxDownwardVelocity = FMath::Max( Test->UnderMaxDownwardVelocity, FMath::Max(0.0f, -VerticalVelocity)); } Test->LastFrameSeconds = Now; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDigMotionFrame] frame=%d elapsed_s=%.4f phase=%s dt_s=%.5f " "velocity_z_cm_s=%.3f gravity_z_cm_s2=%.3f gate=%d support_ready=%d " "support_tile=(%d,%d,%d) movement_mode=%d position_z_cm=%.3f"), Test->Frame, Now - Test->BeginSeconds, bNearPhase ? TEXT("near") : TEXT("under"), SampleDelta, VerticalVelocity, GravityZ, bPawnCollisionGateEngaged ? 1 : 0, bSupportReady ? 1 : 0, SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z, MovementMode, PlayerPosition.Z); } void AVoxelWorld::AdvanceHeadlessStreamingTest( FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn) { if (!bHeadlessStreamingTestMovement || !PlayerPawn || HeadlessStreamingTestBeginSeconds <= 0.0) { return; } const double ElapsedSeconds = FPlatformTime::Seconds() - HeadlessStreamingTestBeginSeconds; if (ElapsedSeconds < static_cast(HeadlessStreamingTestStartSeconds)) { return; } if (!bHeadlessStreamingTestTeleportDone && HeadlessStreamingTestTeleportAtSeconds > 0.0f && ElapsedSeconds - static_cast(HeadlessStreamingTestStartSeconds) >= static_cast(HeadlessStreamingTestTeleportAtSeconds)) { const FVector BeforeTeleport = InOutPlayerPosition; const FVector TeleportedPosition = BeforeTeleport + HeadlessStreamingTestDirection * HeadlessStreamingTestTeleportDistanceCm; const bool bTeleported = PlayerPawn->SetActorLocation( TeleportedPosition, false, nullptr, ETeleportType::TeleportPhysics); bHeadlessStreamingTestTeleportDone = true; HeadlessStreamingTestLastElapsedSeconds = ElapsedSeconds; ++HeadlessStreamingTestMoveAttempts; if (bTeleported) { const FVector ActualPosition = PlayerPawn->GetActorLocation(); HeadlessStreamingTestDistanceCm += FVector::Dist(BeforeTeleport, ActualPosition); HeadlessStreamingTestLastActualPosition = ActualPosition; InOutPlayerPosition = ActualPosition; InOutPlayerHeading = HeadlessStreamingTestDirection * HeadlessStreamingTestSpeedCmPerSecond; if (ACharacter* Character = Cast(PlayerPawn)) { if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) { CharacterMovement->StopMovementImmediately(); CharacterMovement->SetMovementMode(MOVE_None); } } } const FIntVector TeleportCenter = GetTileCoordAtLocalCm(WorldToLocalCm(InOutPlayerPosition)); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStreamingTest] teleport=%d distance_cm=%.3f at_s=%.3f center=(%d,%d,%d)"), bTeleported ? 1 : 0, HeadlessStreamingTestTeleportDistanceCm, ElapsedSeconds, TeleportCenter.X, TeleportCenter.Y, TeleportCenter.Z); if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("streaming_teleport"), FString::Printf( TEXT("\"teleported\":%d,\"distance_cm\":%s,\"center\":[%d,%d,%d]," "\"elapsed_s\":%s"), bTeleported ? 1 : 0, *FString::SanitizeFloat(HeadlessStreamingTestTeleportDistanceCm), TeleportCenter.X, TeleportCenter.Y, TeleportCenter.Z, *FString::SanitizeFloat(ElapsedSeconds))); } return; } // The headless game runs uncapped, so simulation DeltaTime is not a stable movement clock. Use // wall time for this explicit harness path; both A/B runs then cover exactly the same distance // regardless of how much work the scheduler is doing per frame. const double PreviousElapsedSeconds = FMath::Max( HeadlessStreamingTestLastElapsedSeconds, static_cast(HeadlessStreamingTestStartSeconds)); const double MoveSeconds = FMath::Max(0.0, ElapsedSeconds - PreviousElapsedSeconds); HeadlessStreamingTestLastElapsedSeconds = ElapsedSeconds; if (MoveSeconds <= 0.0) { return; } const FVector NewPosition = InOutPlayerPosition + HeadlessStreamingTestDirection * HeadlessStreamingTestSpeedCmPerSecond * static_cast(MoveSeconds); const bool bMoved = PlayerPawn->SetActorLocation( NewPosition, false, nullptr, ETeleportType::TeleportPhysics); ++HeadlessStreamingTestMoveAttempts; if (bMoved) { const FVector ActualPosition = PlayerPawn->GetActorLocation(); HeadlessStreamingTestDistanceCm += FVector::Dist(InOutPlayerPosition, ActualPosition); HeadlessStreamingTestLastActualPosition = ActualPosition; InOutPlayerPosition = ActualPosition; InOutPlayerHeading = HeadlessStreamingTestDirection * HeadlessStreamingTestSpeedCmPerSecond; // The explicit -nullrhi harness moves the pawn from this world's pre-physics tick. Keep // the template character movement component from applying its own zero-input/gravity step // afterward and undoing the synthetic teleport before the next scheduler observation. if (ACharacter* Character = Cast(PlayerPawn)) { if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) { CharacterMovement->StopMovementImmediately(); CharacterMovement->SetMovementMode(MOVE_None); } } } } bool AVoxelWorld::MeasureHeadlessCollisionGateStressTerrain( const FVector& WorldPosition, float CapsuleHalfHeightCm, float& OutFeetDensity, float& OutCenterDensity, float& OutSurfaceLocalVoxelZ, float& OutClearanceCm) { OutFeetDensity = 0.0f; OutCenterDensity = 0.0f; OutSurfaceLocalVoxelZ = 0.0f; OutClearanceCm = 0.0f; if (Generator == nullptr || !VoxelMath::IsFinite(CapsuleHalfHeightCm) || CapsuleHalfHeightCm < 0.0f) { return false; } const FVector LocalPosition = WorldToLocalVoxel(WorldPosition); const float FeetZ = LocalPosition.Z - CapsuleHalfHeightCm / VOXEL_SIZE; OutCenterDensity = Generator->GetDensityAt( LocalPosition.X, LocalPosition.Y, LocalPosition.Z); OutFeetDensity = Generator->GetDensityAt( LocalPosition.X, LocalPosition.Y, FeetZ); if (!VoxelMath::IsFinite(OutCenterDensity) || !VoxelMath::IsFinite(OutFeetDensity)) { return false; } const bool bUseCachedDeepDropSurface = bHeadlessCollisionGateStressDeepDrop && bHeadlessCollisionGateStressDeepDropSurfaceCached && FMath::Abs(LocalPosition.X - HeadlessCollisionGateStressDeepDropSurfaceXYVox.X) < 0.01f && FMath::Abs(LocalPosition.Y - HeadlessCollisionGateStressDeepDropSurfaceXYVox.Y) < 0.01f; if (bUseCachedDeepDropSurface) { OutSurfaceLocalVoxelZ = HeadlessCollisionGateStressDeepDropSurfaceZVox; OutClearanceCm = (FeetZ - OutSurfaceLocalVoxelZ) * VOXEL_SIZE; return true; } // The density field is the authoritative terrain oracle (negative = solid). Find the // nearest air/solid zero crossing adjacent to the capsule's lowest point, then linearly // interpolate it. Searching up when the feet sample is solid reports negative clearance // instead of silently choosing a lower floor after a fall-through. // Keep the half-voxel surface search resolution, but double the number of samples after the // voxel size halved so the search retains its original physical reach. constexpr float SearchStepVoxels = 0.5f; const int32 MaxSearchSteps = bHeadlessCollisionGateStressDeepDrop && !bHeadlessCollisionGateStressDeepDropSurfaceCached ? 1024 : 512; float LastZ = FeetZ; float LastDensity = OutFeetDensity; const bool bSearchDown = OutFeetDensity >= 0.0f; for (int32 Step = 1; Step <= MaxSearchSteps; ++Step) { const float SampleZ = FeetZ + (bSearchDown ? -1.0f : 1.0f) * SearchStepVoxels * static_cast(Step); const float SampleDensity = Generator->GetDensityAt( LocalPosition.X, LocalPosition.Y, SampleZ); if (!VoxelMath::IsFinite(SampleDensity)) { return false; } const bool bCrossedSurface = bSearchDown ? (LastDensity >= 0.0f && SampleDensity < 0.0f) : (LastDensity <= 0.0f && SampleDensity > 0.0f); if (bCrossedSurface) { const float Denominator = LastDensity - SampleDensity; const float Alpha = FMath::IsNearlyZero(Denominator) ? 0.5f : FMath::Clamp(LastDensity / Denominator, 0.0f, 1.0f); OutSurfaceLocalVoxelZ = FMath::Lerp(LastZ, SampleZ, Alpha); OutClearanceCm = (FeetZ - OutSurfaceLocalVoxelZ) * VOXEL_SIZE; if (bHeadlessCollisionGateStressDeepDrop) { HeadlessCollisionGateStressDeepDropSurfaceXYVox = FVector2D( LocalPosition.X, LocalPosition.Y); HeadlessCollisionGateStressDeepDropSurfaceZVox = OutSurfaceLocalVoxelZ; bHeadlessCollisionGateStressDeepDropSurfaceCached = true; } return true; } LastZ = SampleZ; LastDensity = SampleDensity; } return false; } void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest( FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn) { if (!bHeadlessCollisionGateStressTest || bHeadlessCollisionGateStressTestFailed || bHeadlessCollisionGateStressTestPassed || !IsValid(PlayerPawn)) { return; } const double Now = FPlatformTime::Seconds(); if (HeadlessCollisionGateStressTestBeginSeconds <= 0.0) { // Include setup failures in the bounded stress-test lifetime too. Otherwise a research // field with no surface at the default probe can set Failed before the start placement, // leave BeginSeconds at zero, and strand the commandlet game until the outer watchdog. HeadlessCollisionGateStressTestBeginSeconds = Now; } int32 DeepDropTopChunkZ = 0; int32 DeepDropBottomChunkZ = 0; if (bHeadlessCollisionGateStressDeepDrop) { if (HeadlessCollisionGateStressTestBeginSeconds <= 0.0) { HeadlessCollisionGateStressTestBeginSeconds = Now; } if (StrateManager == nullptr || !StrateManager->GetStrateChunkZBounds( /*ChunkZ*/ 0, DeepDropTopChunkZ, DeepDropBottomChunkZ)) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("deep_drop_fixture_strate_unavailable"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed " "reason=deep_drop_fixture_strate_unavailable probe_chunk_z=0")); return; } } if (!bHeadlessCollisionGateStressTestStartPlaced) { if (bHeadlessCollisionGateStressDeepDrop && !bHeadlessCollisionGateStressDeepDropPrepared) { // Carve a tile-wide air column through the top strate. The test later checks the exact // 35^3 density samples for each pre-proven empty tile before allowing the support walk // to skip it. The box ends at the first sample below the floor tile's upper boundary, // avoiding the round end-cap that a capsule would extend into the floor. It extends // along the test's post-landing route so that route does not hit the shaft's side wall. // Its long extent needs a larger brush-radius allowance in this transient commandlet // world only. constexpr float DeepDropCenterX = CHUNK_SIZE * 0.5f; constexpr float DeepDropCenterY = CHUNK_SIZE * 0.5f; constexpr float DeepDropShaftStartX = VoxelPassageGeometry::MetersToVoxels(-8.0f); const float DeepDropShaftEndX = CHUNK_SIZE * 0.5f + (HeadlessCollisionGateStressTestSpeedCmPerSecond * HeadlessCollisionGateStressTestDurationSeconds / VOXEL_SIZE) + VoxelPassageGeometry::MetersToVoxels(16.0f); const float StrateTopExclusiveVoxels = static_cast(DeepDropTopChunkZ + 1) * CHUNK_SIZE; const float ShaftTopVoxels = StrateTopExclusiveVoxels + VoxelPassageGeometry::MetersToVoxels(8.0f); const float ShaftBottomVoxels = static_cast(DeepDropBottomChunkZ + 1) * CHUNK_SIZE - VoxelPassageGeometry::MetersToVoxels(0.25f); if (ShaftBottomVoxels >= ShaftTopVoxels || DiffLayer == nullptr) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("deep_drop_fixture_strate_or_diff_unavailable"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed " "reason=deep_drop_fixture_strate_or_diff_unavailable top_chunk_z=%d " "bottom_chunk_z=%d diff_available=%d"), DeepDropTopChunkZ, DeepDropBottomChunkZ, DiffLayer != nullptr ? 1 : 0); return; } const float ShaftHalfExtentZ = (ShaftTopVoxels - ShaftBottomVoxels) * 0.5f; const float ShaftCenterX = (DeepDropShaftStartX + DeepDropShaftEndX) * 0.5f; const float ShaftHalfExtentX = (DeepDropShaftEndX - DeepDropShaftStartX) * 0.5f; const FVector ShaftCenterWorld = LocalVoxelToWorld(FVector( ShaftCenterX, DeepDropCenterY, (ShaftTopVoxels + ShaftBottomVoxels) * 0.5f)); const FVector ShaftHalfExtent( ShaftHalfExtentX, VoxelPassageGeometry::MetersToVoxels(4.5f), ShaftHalfExtentZ); const float FixtureBrushRadius = FMath::Max( FMath::Max3(ShaftHalfExtent.X, ShaftHalfExtent.Y, ShaftHalfExtent.Z), Settings != nullptr ? Settings->MaxBrushRadius : 0.0f); DiffLayer->SetBudget( Settings != nullptr ? Settings->MaxModifications : 0, FixtureBrushRadius, Settings != nullptr ? Settings->MaxTotalVolume : 0.0f); CarveBox(ShaftCenterWorld, ShaftHalfExtent, 512.0f); int32 SuppressedPlayerAnchorCount = 0; for (const FVoxelStreamingAnchor& Anchor : StreamingAnchors) { if (Anchor.Actor.Get() == PlayerPawn) { ++SuppressedPlayerAnchorCount; } } if (SuppressedPlayerAnchorCount > 0) { // The owner map registers the local pawn as an additional anchor. Suppress only // that redundant test-world anchor so this fixture exercises the gate's fallback // path; all other AI/remote-player anchors remain active. The commandlet exits // with the transient world immediately after this test. UnregisterStreamingAnchor(PlayerPawn); } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] deep_drop_player_anchor_suppressed=%d " "remaining_anchors=%d"), SuppressedPlayerAnchorCount, StreamingAnchors.Num()); FIntVector DeepDropRingMin = FIntVector::ZeroValue; FIntVector DeepDropRingMax = FIntVector::ZeroValue; VoxelClipmapDesiredTiles::AlignedRingBounds( FIntVector(0, 0, DeepDropTopChunkZ), Settings != nullptr ? FMath::Max(1, Settings->ClipRadius) : 3, 2, DeepDropRingMin, DeepDropRingMax); for (int32 TileZ = DeepDropRingMin.Z - 1; TileZ > DeepDropBottomChunkZ; --TileZ) { const FVoxelTileKey EmptyTile(FIntVector(0, 0, TileZ), 0); if (LoadedTiles.Contains(EmptyTile) || PendingTiles.Contains(EmptyTile)) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("deep_drop_empty_tile_already_streamed"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed " "reason=deep_drop_empty_tile_already_streamed tile=(0,0,%d)"), TileZ); return; } const int32 TileMinX = EmptyTile.Coord.X * CHUNK_SIZE - 1; const int32 TileMaxX = (EmptyTile.Coord.X + 1) * CHUNK_SIZE + 1; const int32 TileMinY = EmptyTile.Coord.Y * CHUNK_SIZE - 1; const int32 TileMaxY = (EmptyTile.Coord.Y + 1) * CHUNK_SIZE + 1; const int32 TileMinZ = EmptyTile.Coord.Z * CHUNK_SIZE - 1; const int32 TileMaxZ = (EmptyTile.Coord.Z + 1) * CHUNK_SIZE + 1; int32 CheckedSamples = 0; float MinimumDensity = TNumericLimits::Max(); float FirstNonAirDensity = 0.0f; FIntVector FirstNonAirSample = FIntVector::ZeroValue; bool bEverySampleAir = true; for (int32 Z = TileMinZ; Z <= TileMaxZ && bEverySampleAir; ++Z) for (int32 Y = TileMinY; Y <= TileMaxY && bEverySampleAir; ++Y) for (int32 X = TileMinX; X <= TileMaxX; ++X) { const float Density = Generator != nullptr ? Generator->GetDensityAt( static_cast(X), static_cast(Y), static_cast(Z)) : -1.0f; ++CheckedSamples; if (!VoxelMath::IsFinite(Density) || Density <= 0.0f) { bEverySampleAir = false; FirstNonAirDensity = Density; FirstNonAirSample = FIntVector(X, Y, Z); break; } MinimumDensity = FMath::Min(MinimumDensity, Density); } if (!bEverySampleAir) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("deep_drop_empty_tile_density_proof_failed"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed " "reason=deep_drop_empty_tile_density_proof_failed " "tile=(0,0,%d) samples=%d first_non_air=(%d,%d,%d) " "density=%.6f"), TileZ, CheckedSamples, FirstNonAirSample.X, FirstNonAirSample.Y, FirstNonAirSample.Z, FirstNonAirDensity); return; } CollisionNotRequiredTiles.Add(EmptyTile); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] deep_drop_empty_tile " "tile=(0,0,%d) samples=%d min_density=%.6f seeded_known_empty=1"), TileZ, CheckedSamples, MinimumDensity); } bHeadlessCollisionGateStressDeepDropPrepared = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] deep_drop_fixture=carved " "top_chunk_z=%d bottom_chunk_z=%d " "local_vox_x=(%.2f,%.2f) y=%.2f z=(%.2f,%.2f) " "half_extent_vox=(%.2f,%.2f,%.2f) fixture_brush_radius=%.2f"), DeepDropTopChunkZ, DeepDropBottomChunkZ, DeepDropShaftStartX, DeepDropShaftEndX, DeepDropCenterY, ShaftTopVoxels, ShaftBottomVoxels, ShaftHalfExtent.X, ShaftHalfExtent.Y, ShaftHalfExtent.Z, FixtureBrushRadius); } if (!bHeadlessCollisionGateStressTestSettingsReported) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] owner_settings clip_radius=%d " "max_clip_level=%d render_distance_chunks=%d"), Settings != nullptr ? Settings->ClipRadius : -1, Settings != nullptr ? Settings->MaxClipLevel : -1, Settings != nullptr ? Settings->RenderDistanceChunks : -1); bHeadlessCollisionGateStressTestSettingsReported = true; } float CapsuleHalfHeightCm = 0.0f; if (const ACharacter* Character = Cast(PlayerPawn)) { if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent()) { CapsuleHalfHeightCm = Capsule->GetScaledCapsuleHalfHeight(); } } // The earlier horizontal route ran through an open origin drop and ended more than a // metre above its floor. Start above the measured density surface at this XY so the pawn // must fall onto a freshly requested level-0 tile before the horizontal diagnostic. const float LaunchLocalVoxelX = bHeadlessCollisionGateStressDeepDrop ? CHUNK_SIZE * 0.5f : VoxelPassageGeometry::MetersToVoxels(4.55f); const float LaunchLocalVoxelY = bHeadlessCollisionGateStressDeepDrop ? CHUNK_SIZE * 0.5f : 0.0f; float LaunchHeightAboveGroundVoxels = HeadlessCollisionGateStressTestSpawnHeightVoxels; const FVector SurfaceProbeWorld = LocalVoxelToWorld( FVector(LaunchLocalVoxelX, LaunchLocalVoxelY, 0.0f)); float ProbeFeetDensity = 0.0f; float ProbeCenterDensity = 0.0f; float SurfaceLocalVoxelZ = 0.0f; float ProbeClearanceCm = 0.0f; const bool bSurfaceFound = MeasureHeadlessCollisionGateStressTerrain( SurfaceProbeWorld, CapsuleHalfHeightCm, ProbeFeetDensity, ProbeCenterDensity, SurfaceLocalVoxelZ, ProbeClearanceCm); if (!bSurfaceFound) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("stress_launch_surface_unavailable"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed " "reason=stress_launch_surface_unavailable probe=(%.2f,%.2f,%.2f) " "feet_density=%.6f"), SurfaceProbeWorld.X, SurfaceProbeWorld.Y, SurfaceProbeWorld.Z, ProbeFeetDensity); return; } float LaunchLocalVoxelZ = SurfaceLocalVoxelZ + CapsuleHalfHeightCm / VOXEL_SIZE + LaunchHeightAboveGroundVoxels; int32 FloorChunkZ = FMath::FloorToInt(SurfaceLocalVoxelZ / CHUNK_SIZE); int32 SurfaceStrateTopChunkZ = 0; int32 SurfaceStrateBottomChunkZ = 0; FIntVector LevelZeroRingMin = FIntVector::ZeroValue; FIntVector LevelZeroRingMax = FIntVector::ZeroValue; if (bHeadlessCollisionGateStressDeepDrop) { if (!StrateManager->GetStrateChunkZBounds( FloorChunkZ, SurfaceStrateTopChunkZ, SurfaceStrateBottomChunkZ) || SurfaceStrateTopChunkZ != DeepDropTopChunkZ || FloorChunkZ != DeepDropBottomChunkZ) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("deep_drop_floor_outside_fixture_strate"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed " "reason=deep_drop_floor_outside_fixture_strate surface_vox_z=%.3f " "floor_chunk_z=%d fixture_strate=%d..%d surface_strate=%d..%d"), SurfaceLocalVoxelZ, FloorChunkZ, DeepDropTopChunkZ, DeepDropBottomChunkZ, SurfaceStrateTopChunkZ, SurfaceStrateBottomChunkZ); return; } const float CapsuleHalfHeightVoxels = CapsuleHalfHeightCm / VOXEL_SIZE; const float StrateTopExclusiveVoxels = static_cast(SurfaceStrateTopChunkZ + 1) * CHUNK_SIZE; LaunchLocalVoxelZ = StrateTopExclusiveVoxels - CapsuleHalfHeightVoxels - VoxelPassageGeometry::MetersToVoxels(0.125f); LaunchHeightAboveGroundVoxels = LaunchLocalVoxelZ - SurfaceLocalVoxelZ - CapsuleHalfHeightVoxels; const FIntVector LaunchCenterTile( FMath::FloorToInt(LaunchLocalVoxelX / CHUNK_SIZE), FMath::FloorToInt(LaunchLocalVoxelY / CHUNK_SIZE), FMath::FloorToInt(LaunchLocalVoxelZ / CHUNK_SIZE)); const int32 ClipRadius = Settings != nullptr ? FMath::Max(1, Settings->ClipRadius) : 6; VoxelClipmapDesiredTiles::AlignedRingBounds( LaunchCenterTile, ClipRadius, 2, LevelZeroRingMin, LevelZeroRingMax); if (LaunchHeightAboveGroundVoxels <= 0.0f || FloorChunkZ >= LevelZeroRingMin.Z) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("deep_drop_floor_not_below_level0_ring"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed " "reason=deep_drop_floor_not_below_level0_ring " "start_center_z=%.3f surface_z=%.3f floor_tile_z=%d " "ring_min_z=%d spawn_height_vox=%.3f"), LaunchLocalVoxelZ, SurfaceLocalVoxelZ, FloorChunkZ, LevelZeroRingMin.Z, LaunchHeightAboveGroundVoxels); return; } } HeadlessCollisionGateStressTestSpawnHeightVoxels = LaunchHeightAboveGroundVoxels; const FVector LaunchVoxel( LaunchLocalVoxelX, LaunchLocalVoxelY, LaunchLocalVoxelZ); const FVector RequestedLaunchWorld = LocalVoxelToWorld(LaunchVoxel); FVoxelTileKey ExpectedLaunchSupportTile; if (!GetPlayerSupportTile( PlayerPawn, RequestedLaunchWorld, ExpectedLaunchSupportTile)) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("stress_launch_support_missing"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed " "reason=stress_launch_support_missing")); return; } if (bHeadlessCollisionGateStressDeepDrop) { int32 UnusedTopChunkZ = 0; int32 UnusedBottomChunkZ = 0; if (!StrateManager->GetStrateChunkZBounds( ExpectedLaunchSupportTile.Coord.Z, UnusedTopChunkZ, UnusedBottomChunkZ)) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("deep_drop_start_outside_layout"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed " "reason=deep_drop_start_outside_layout start_tile_z=%d"), ExpectedLaunchSupportTile.Coord.Z); return; } bHeadlessStreamingTestCenterOverride = true; HeadlessStreamingTestCenterVoxel = LaunchVoxel; int32 ResolvedColumnTiles = 0; const int32 RequiredColumnTiles = ExpectedLaunchSupportTile.Coord.Z - LevelZeroRingMin.Z + 1; bool bPrewarmReady = true; for (int32 TileZ = ExpectedLaunchSupportTile.Coord.Z; TileZ >= LevelZeroRingMin.Z; --TileZ) { const FVoxelTileKey ColumnTile( FIntVector(ExpectedLaunchSupportTile.Coord.X, ExpectedLaunchSupportTile.Coord.Y, TileZ), 0); const bool bAirProven = CollisionNotRequiredTiles.Contains(ColumnTile); const bool bResolved = IsDesired(ColumnTile) && bAirProven; if (IsDesired(ColumnTile) && IsTileCollisionReady(ColumnTile) && !bAirProven) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("deep_drop_ring_column_not_empty"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed " "reason=deep_drop_ring_column_not_empty tile=(%d,%d,%d)"), ColumnTile.Coord.X, ColumnTile.Coord.Y, ColumnTile.Coord.Z); return; } if (bResolved) { ++ResolvedColumnTiles; } else { bPrewarmReady = false; } } if (!bPrewarmReady) { if (!bHeadlessCollisionGateStressDeepDropPrewarmReported) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] deep_drop_prewarm " "resolved=%d required=%d start_tile=(%d,%d,%d) " "floor_tile_z=%d ring_min_z=%d"), ResolvedColumnTiles, RequiredColumnTiles, ExpectedLaunchSupportTile.Coord.X, ExpectedLaunchSupportTile.Coord.Y, ExpectedLaunchSupportTile.Coord.Z, FloorChunkZ, LevelZeroRingMin.Z); bHeadlessCollisionGateStressDeepDropPrewarmReported = true; } return; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] deep_drop_prewarm_ready " "empty_tiles=%d required=%d start_tile_z=%d floor_tile_z=%d " "ring_min_z=%d drop_depth_vox=%.3f"), ResolvedColumnTiles, RequiredColumnTiles, ExpectedLaunchSupportTile.Coord.Z, FloorChunkZ, LevelZeroRingMin.Z, LaunchLocalVoxelZ - SurfaceLocalVoxelZ); FVoxelTileKey WalkTarget; const TCHAR* WalkUnavailableReason = nullptr; const bool bWalkResolved = IsCollisionReadyFromSupportTile( ExpectedLaunchSupportTile, &WalkTarget, &WalkUnavailableReason); const bool bWalkTargetInsideRenderRing = WalkTarget.Coord.X >= LevelZeroRingMin.X && WalkTarget.Coord.X <= LevelZeroRingMax.X && WalkTarget.Coord.Y >= LevelZeroRingMin.Y && WalkTarget.Coord.Y <= LevelZeroRingMax.Y && WalkTarget.Coord.Z >= LevelZeroRingMin.Z && WalkTarget.Coord.Z <= LevelZeroRingMax.Z; if (bWalkResolved || WalkUnavailableReason != nullptr || WalkTarget.Coord.Z != FloorChunkZ || bWalkTargetInsideRenderRing) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("deep_drop_support_walk_target_invalid"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed " "reason=deep_drop_support_walk_target_invalid resolved=%d " "unavailable=%s walk_target=(%d,%d,%d) floor_tile_z=%d " "render_ring_desired=%d gate_retained=%d ring_min_z=%d"), bWalkResolved ? 1 : 0, WalkUnavailableReason != nullptr ? WalkUnavailableReason : TEXT("none"), WalkTarget.Coord.X, WalkTarget.Coord.Y, WalkTarget.Coord.Z, FloorChunkZ, bWalkTargetInsideRenderRing ? 1 : 0, CollisionGateDesiredTiles.Contains(WalkTarget) ? 1 : 0, LevelZeroRingMin.Z); return; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] deep_drop_support_walk " "start=(%d,%d,%d) target=(%d,%d,%d) render_ring_desired=0 " "gate_retained=%d desired_now=%d ring_min_z=%d floor_tile_z=%d"), ExpectedLaunchSupportTile.Coord.X, ExpectedLaunchSupportTile.Coord.Y, ExpectedLaunchSupportTile.Coord.Z, WalkTarget.Coord.X, WalkTarget.Coord.Y, WalkTarget.Coord.Z, CollisionGateDesiredTiles.Contains(WalkTarget) ? 1 : 0, IsDesired(WalkTarget) ? 1 : 0, LevelZeroRingMin.Z, FloorChunkZ); } const bool bPlaced = PlayerPawn->SetActorLocation( RequestedLaunchWorld, false, nullptr, ETeleportType::TeleportPhysics); if (!bPlaced) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("stress_launch_placement_failed"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed " "reason=stress_launch_placement_failed requested=(%.2f,%.2f,%.2f)"), RequestedLaunchWorld.X, RequestedLaunchWorld.Y, RequestedLaunchWorld.Z); return; } const FVector ActualPosition = PlayerPawn->GetActorLocation(); const FVector ActualLaunchVoxel = WorldToLocalVoxel(ActualPosition); // Keep streaming centred on the launch footprint while the character drops and later // moves horizontally. The support tile below the capsule is still not cooked at launch. bHeadlessStreamingTestCenterOverride = true; HeadlessStreamingTestCenterVoxel = ActualLaunchVoxel; InOutPlayerPosition = ActualPosition; InOutPlayerHeading = FVector::ZeroVector; HeadlessCollisionGateStressTestLastActualPosition = ActualPosition; HeadlessCollisionGateStressTestStartPosition = ActualPosition; HeadlessCollisionGateStressTestDirection = FVector::ZeroVector; bHeadlessCollisionGateStressTestLanded = false; FVoxelTileKey FootTile; const bool bHaveFootTile = GetPlayerSupportTile(PlayerPawn, ActualPosition, FootTile); FVoxelTileKey GateSupportTile; const TCHAR* SupportUnavailableReason = nullptr; const bool bSupportReady = IsPlayerSupportCollisionReady( PlayerPawn, ActualPosition, &GateSupportTile, &SupportUnavailableReason); const bool bFootTileReady = bHaveFootTile && IsTileCollisionReady(FootTile); if (!bHaveFootTile) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("stress_launch_support_missing"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] launch_setup_failed " "reason=stress_launch_support_missing")); return; } HeadlessCollisionGateStressTestLaunchSupportTile = FootTile; bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured = true; HeadlessCollisionGateStressTestLaunchSupportCheckSeconds = Now; if (bFootTileReady) { HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = Now; HeadlessCollisionGateStressTestLaunchSupportReadySeconds = Now; } else if (PendingTiles.Contains(FootTile) || LoadedTiles.Contains(FootTile)) { HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = Now; } bHeadlessCollisionGateStressTestStartPlaced = true; HeadlessCollisionGateStressTestStartSeconds = Now; HeadlessCollisionGateStressTestBeginSeconds = Now; bHeadlessCollisionGateStressTestStarted = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] drop_started position=(%.1f,%.1f,%.1f) " "support=(%d,%d,%d)"), ActualPosition.X, ActualPosition.Y, ActualPosition.Z, FootTile.Coord.X, FootTile.Coord.Y, FootTile.Coord.Z); if (ACharacter* Character = Cast(PlayerPawn)) { if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) { CharacterMovement->MaxWalkSpeed = HeadlessCollisionGateStressTestSpeedCmPerSecond; CharacterMovement->MaxAcceleration = FMath::Max( CharacterMovement->MaxAcceleration, HeadlessCollisionGateStressTestSpeedCmPerSecond * 8.0f); CharacterMovement->BrakingDecelerationWalking = FMath::Max( CharacterMovement->BrakingDecelerationWalking, HeadlessCollisionGateStressTestSpeedCmPerSecond * 8.0f); CharacterMovement->StopMovementImmediately(); CharacterMovement->SetMovementMode(MOVE_Falling); // Exercise the gate's actual hazard: a pawn descending onto a just-streamed // floor fast enough to cross the surface in one movement frame if uncooked. CharacterMovement->Velocity = FVector( 0.0f, 0.0f, -HeadlessCollisionGateStressTestSpeedCmPerSecond); CharacterMovement->UpdateComponentVelocity(); } } float FeetDensity = 0.0f; float CenterDensity = 0.0f; float ActualSurfaceLocalVoxelZ = 0.0f; float ClearanceCm = 0.0f; const bool bLaunchSurfaceFound = MeasureHeadlessCollisionGateStressTerrain( ActualPosition, CapsuleHalfHeightCm, FeetDensity, CenterDensity, ActualSurfaceLocalVoxelZ, ClearanceCm); if (bLaunchSurfaceFound) { HeadlessCollisionGateStressTestMinClearanceCm = ClearanceCm; HeadlessCollisionGateStressTestMinClearancePosition = ActualPosition; HeadlessCollisionGateStressTestMinFeetDensity = FeetDensity; HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ = ActualSurfaceLocalVoxelZ; ++HeadlessCollisionGateStressTestTerrainSampleCount; } else { bHeadlessCollisionGateStressTestTerrainMeasurementMissing = true; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] start_placed moved=%d " "support=(%d,%d,%d) tile_collision_ready=%d gate_support=(%d,%d,%d) " "gate_support_ready=%d unavailable=%s surface_vox_z=%.4f " "spawn_height_vox=%.2f position=(%.1f,%.1f,%.1f) " "launch_vox=(%.3f,%.3f,%.3f)"), bPlaced ? 1 : 0, FootTile.Coord.X, FootTile.Coord.Y, FootTile.Coord.Z, bFootTileReady ? 1 : 0, GateSupportTile.Coord.X, GateSupportTile.Coord.Y, GateSupportTile.Coord.Z, bSupportReady ? 1 : 0, SupportUnavailableReason != nullptr ? SupportUnavailableReason : TEXT("none"), SurfaceLocalVoxelZ, LaunchHeightAboveGroundVoxels, ActualPosition.X, ActualPosition.Y, ActualPosition.Z, ActualLaunchVoxel.X, ActualLaunchVoxel.Y, ActualLaunchVoxel.Z); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] start_density feet=%.6f center=%.6f " "surface_found=%d surface_vox_z=%.4f clearance_cm=%.3f " "half_height_cm=%.2f probe_feet_density=%.6f"), FeetDensity, CenterDensity, bLaunchSurfaceFound ? 1 : 0, ActualSurfaceLocalVoxelZ, ClearanceCm, CapsuleHalfHeightCm, ProbeFeetDensity); return; } // The measured interval starts with the vertical drop. The gate must also stop a small // horizontal probe while support collision is unresolved, then resume normal swept movement. if (!bHeadlessCollisionGateStressTestLanded) { if (bHeadlessCollisionGateStressDeepDrop && bPawnCollisionGateEngaged && HeadlessCollisionGateStressTestGateDistanceCm < 50.0) { if (ACharacter* Character = Cast(PlayerPawn)) { if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) { FVector Velocity = CharacterMovement->Velocity; Velocity.X = 100.0f; CharacterMovement->Velocity = Velocity; CharacterMovement->UpdateComponentVelocity(); if (!bHeadlessCollisionGateStressTestFullStopProbeApplied) { bHeadlessCollisionGateStressTestFullStopProbeApplied = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] full_stop_probe " "applied=1 velocity_x_cm_s=%.1f gate=1"), Velocity.X); } } } } InOutPlayerHeading = FVector::ZeroVector; return; } const FVector WorldDirection = GetActorTransform().TransformVectorNoScale( HeadlessCollisionGateStressTestDirection).GetSafeNormal(); if (WorldDirection.IsNearlyZero()) { InOutPlayerHeading = FVector::ZeroVector; return; } if (ACharacter* Character = Cast(PlayerPawn)) { if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) { // Do not undo MOVE_None while the collision gate is engaged; the gate owns the hold. if (!bPawnCollisionGateEngaged && CharacterMovement->MovementMode == MOVE_None) { CharacterMovement->SetMovementMode(MOVE_Walking); } PlayerPawn->AddMovementInput(WorldDirection, 1.0f, false); } } else { PlayerPawn->AddMovementInput(WorldDirection, 1.0f, false); } InOutPlayerHeading = WorldDirection * HeadlessCollisionGateStressTestSpeedCmPerSecond; } void AVoxelWorld::ObserveHeadlessCollisionGateStressTest( const FVector& PlayerPosition, APawn* PlayerPawn) { if (!bHeadlessCollisionGateStressTest || !bHeadlessCollisionGateStressTestStarted || !IsValid(PlayerPawn) || bHeadlessCollisionGateStressTestExitRequested || bHeadlessCollisionGateStressTestFailed) { return; } const double Now = FPlatformTime::Seconds(); const FVector PreviousPosition = HeadlessCollisionGateStressTestLastActualPosition; const float HorizontalDelta = FVector::Dist2D( PreviousPosition, PlayerPosition); HeadlessCollisionGateStressTestDistanceCm += HorizontalDelta; HeadlessCollisionGateStressTestLastActualPosition = PlayerPosition; const bool bGateEngaged = bPawnCollisionGateEngaged; if (bGateEngaged || bHeadlessCollisionGateStressTestGateWasEngaged) { HeadlessCollisionGateStressTestGateDistanceCm += HorizontalDelta; } if (bGateEngaged && !bHeadlessCollisionGateStressTestGateWasEngaged) { ++HeadlessCollisionGateStressTestGateHoldCount; HeadlessCollisionGateStressTestGateStartSeconds = Now; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] gate=engaged elapsed_s=%.6f"), Now - HeadlessCollisionGateStressTestStartSeconds); } else if (!bGateEngaged && bHeadlessCollisionGateStressTestGateWasEngaged) { const double GateDuration = FMath::Max( 0.0, Now - HeadlessCollisionGateStressTestGateStartSeconds); HeadlessCollisionGateStressTestTotalGateDurationSeconds += GateDuration; HeadlessCollisionGateStressTestMaxGateDurationSeconds = FMath::Max( HeadlessCollisionGateStressTestMaxGateDurationSeconds, GateDuration); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] gate=released duration_s=%.6f " "total_s=%.6f"), GateDuration, HeadlessCollisionGateStressTestTotalGateDurationSeconds); } bHeadlessCollisionGateStressTestGateWasEngaged = bGateEngaged; // Confirm the injected horizontal velocity was blocked for a sustained interval. Merely // applying the probe while a gate is engaged does not prove that the pawn stayed put. constexpr double ProbeStopConfirmationSeconds = 0.05; const bool bProbeHeldStill = bHeadlessCollisionGateStressTestFullStopProbeApplied && bGateEngaged && HorizontalDelta <= 0.01f; if (bProbeHeldStill) { if (HeadlessCollisionGateStressTestFullStopProbeStartSeconds <= 0.0) { HeadlessCollisionGateStressTestFullStopProbeStartSeconds = Now; } else if (!bHeadlessCollisionGateStressTestFullStopProbeObserved && Now - HeadlessCollisionGateStressTestFullStopProbeStartSeconds >= ProbeStopConfirmationSeconds) { bHeadlessCollisionGateStressTestFullStopProbeObserved = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] full_stop_probe " "observed=1 gate=1 held_s=%.6f"), Now - HeadlessCollisionGateStressTestFullStopProbeStartSeconds); } } else if (!bHeadlessCollisionGateStressTestFullStopProbeObserved) { HeadlessCollisionGateStressTestFullStopProbeStartSeconds = 0.0; } // The first sample after engaging any gate can have zero horizontal delta simply because the // world prerequisite ran before CharacterMovementComponent. Require a sustained no-progress // interval before calling it a full stop; a permanent stop still fails through the distance // shortfall check below, while repeated gate holds remain visible in gate_holds/total_s. constexpr double FullStopConfirmationSeconds = 0.05; const bool bNoHorizontalProgress = bGateEngaged && bHeadlessCollisionGateStressTestLanded && HorizontalDelta <= 0.01f; if (bNoHorizontalProgress) { if (HeadlessCollisionGateStressTestFullStopStartSeconds <= 0.0) { HeadlessCollisionGateStressTestFullStopStartSeconds = Now; } if (!bHeadlessCollisionGateStressTestFullStop && Now - HeadlessCollisionGateStressTestFullStopStartSeconds >= FullStopConfirmationSeconds) { ++HeadlessCollisionGateStressTestFullStopCount; bHeadlessCollisionGateStressTestFullStop = true; } } else { if (bHeadlessCollisionGateStressTestFullStop && HeadlessCollisionGateStressTestFullStopStartSeconds > 0.0) { HeadlessCollisionGateStressTestFullStopDurationSeconds += FMath::Max( 0.0, Now - HeadlessCollisionGateStressTestFullStopStartSeconds); } bHeadlessCollisionGateStressTestFullStop = false; HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0; } float CapsuleHalfHeightCm = 0.0f; FVector PawnVelocity = PlayerPawn->GetVelocity(); if (const ACharacter* Character = Cast(PlayerPawn)) { if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent()) { CapsuleHalfHeightCm = Capsule->GetScaledCapsuleHalfHeight(); } if (const UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) { PawnVelocity = CharacterMovement->Velocity; } } const FVoxelTileKey& LaunchSupportTile = HeadlessCollisionGateStressTestLaunchSupportTile; const bool bLaunchTileMatches = bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured && LaunchSupportTile.Level == 0; if (bLaunchTileMatches) { const bool bPending = PendingTiles.Contains(LaunchSupportTile); const bool bLoaded = LoadedTiles.Contains(LaunchSupportTile); if (HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds <= 0.0 && (bPending || bLoaded)) { HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = Now; } if (HeadlessCollisionGateStressTestLaunchSupportReadySeconds <= 0.0 && IsTileCollisionReady(LaunchSupportTile)) { if (HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds <= 0.0) { HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = Now; } HeadlessCollisionGateStressTestLaunchSupportReadySeconds = Now; } } if (!bHeadlessCollisionGateStressTestLanded) { if (const ACharacter* Character = Cast(PlayerPawn)) { if (const UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement(); CharacterMovement != nullptr && CharacterMovement->IsMovingOnGround()) { bHeadlessCollisionGateStressTestLanded = true; HeadlessCollisionGateStressTestLandedSeconds = Now; HeadlessCollisionGateStressTestDirection = FVector::XAxisVector; // Follow the character after landing so the horizontal diagnostic exercises the // normal desired-set path while the support gate remains active. bHeadlessStreamingTestCenterOverride = false; UPrimitiveComponent* MovementBase = CharacterMovement->GetMovementBase(); const FVector MovementBaseLocation = IsValid(MovementBase) ? MovementBase->GetComponentLocation() : FVector::ZeroVector; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] landed elapsed_s=%.6f " "position=(%.2f,%.2f,%.2f) velocity=(%.2f,%.2f,%.2f) " "movement_mode=%d base=%s base_location=(%.2f,%.2f,%.2f) " "launch_support_ready=%d"), Now - HeadlessCollisionGateStressTestBeginSeconds, PlayerPosition.X, PlayerPosition.Y, PlayerPosition.Z, PawnVelocity.X, PawnVelocity.Y, PawnVelocity.Z, static_cast(CharacterMovement->MovementMode), *GetNameSafe(MovementBase), MovementBaseLocation.X, MovementBaseLocation.Y, MovementBaseLocation.Z, bLaunchTileMatches && IsTileCollisionReady(LaunchSupportTile) ? 1 : 0); } } } float FeetDensity = 0.0f; float CenterDensity = 0.0f; float SurfaceLocalVoxelZ = 0.0f; float ClearanceCm = 0.0f; const bool bSurfaceFound = MeasureHeadlessCollisionGateStressTerrain( PlayerPosition, CapsuleHalfHeightCm, FeetDensity, CenterDensity, SurfaceLocalVoxelZ, ClearanceCm); const FVector LocalPosition = WorldToLocalVoxel(PlayerPosition); FVoxelTileKey FootTile; const bool bHaveFootTile = GetPlayerSupportTile(PlayerPawn, PlayerPosition, FootTile); const bool bFootTileCollisionReady = bHaveFootTile && IsTileCollisionReady(FootTile); FVoxelTileKey GateSupportTile; const TCHAR* GateSupportUnavailableReason = nullptr; const bool bGateSupportReady = IsPlayerSupportCollisionReady( PlayerPawn, PlayerPosition, &GateSupportTile, &GateSupportUnavailableReason); if (bSurfaceFound) { ++HeadlessCollisionGateStressTestTerrainSampleCount; if (ClearanceCm < HeadlessCollisionGateStressTestMinClearanceCm) { HeadlessCollisionGateStressTestMinClearanceCm = ClearanceCm; HeadlessCollisionGateStressTestMinClearancePosition = PlayerPosition; HeadlessCollisionGateStressTestMinFeetDensity = FeetDensity; HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ = SurfaceLocalVoxelZ; } } else { bHeadlessCollisionGateStressTestTerrainMeasurementMissing = true; } if (!bHeadlessCollisionGateStressTestStartHeightCrossed && PlayerPosition.Z < HeadlessCollisionGateStressTestStartPosition.Z - 0.5f) { bHeadlessCollisionGateStressTestStartHeightCrossed = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] start_height_crossed elapsed_s=%.6f " "position=(%.2f,%.2f,%.2f) velocity=(%.2f,%.2f,%.2f) gate=%d " "foot_tile=(%d,%d,%d) foot_tile_ready=%d gate_support=(%d,%d,%d) " "gate_support_ready=%d unavailable=%s feet_density=%.6f " "surface_found=%d surface_vox_z=%.4f clearance_cm=%.3f"), Now - HeadlessCollisionGateStressTestStartSeconds, PlayerPosition.X, PlayerPosition.Y, PlayerPosition.Z, PawnVelocity.X, PawnVelocity.Y, PawnVelocity.Z, bPawnCollisionGateEngaged ? 1 : 0, FootTile.Coord.X, FootTile.Coord.Y, FootTile.Coord.Z, bFootTileCollisionReady ? 1 : 0, GateSupportTile.Coord.X, GateSupportTile.Coord.Y, GateSupportTile.Coord.Z, bGateSupportReady ? 1 : 0, GateSupportUnavailableReason != nullptr ? GateSupportUnavailableReason : TEXT("none"), FeetDensity, bSurfaceFound ? 1 : 0, SurfaceLocalVoxelZ, ClearanceCm); } constexpr double AllowedSurfacePenetrationCm = 5.0; bool bSweptIntoSolid = false; FVector FirstSolidSample = PlayerPosition; float FirstSolidDensity = FeetDensity; const float SegmentLengthCm = FVector::Distance(PreviousPosition, PlayerPosition); const int32 SegmentSamples = FMath::Clamp( FMath::CeilToInt(SegmentLengthCm / (VOXEL_SIZE * 0.5f)), 1, 256); for (int32 SampleIndex = 1; SampleIndex < SegmentSamples; ++SampleIndex) { const float Alpha = static_cast(SampleIndex) / static_cast(SegmentSamples); const FVector SampleWorldPosition = FMath::Lerp(PreviousPosition, PlayerPosition, Alpha); float SampleFeetDensity = 0.0f; float SampleCenterDensity = 0.0f; float SampleSurfaceLocalVoxelZ = 0.0f; float SampleClearanceCm = 0.0f; const bool bSampleSurfaceFound = MeasureHeadlessCollisionGateStressTerrain( SampleWorldPosition, CapsuleHalfHeightCm, SampleFeetDensity, SampleCenterDensity, SampleSurfaceLocalVoxelZ, SampleClearanceCm); if (bSampleSurfaceFound) { ++HeadlessCollisionGateStressTestTerrainSampleCount; if (SampleClearanceCm < HeadlessCollisionGateStressTestMinClearanceCm) { HeadlessCollisionGateStressTestMinClearanceCm = SampleClearanceCm; HeadlessCollisionGateStressTestMinClearancePosition = SampleWorldPosition; HeadlessCollisionGateStressTestMinFeetDensity = SampleFeetDensity; HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ = SampleSurfaceLocalVoxelZ; } } else { bHeadlessCollisionGateStressTestTerrainMeasurementMissing = true; } if ((bSampleSurfaceFound && SampleClearanceCm < -AllowedSurfacePenetrationCm) || (!bSampleSurfaceFound && SampleFeetDensity < -0.25f)) { bSweptIntoSolid = true; FirstSolidSample = SampleWorldPosition; FirstSolidDensity = SampleFeetDensity; break; } } const bool bBelowSurface = bSurfaceFound ? ClearanceCm < -AllowedSurfacePenetrationCm : FeetDensity < -0.25f; FVector ViolationPosition = bSweptIntoSolid ? FirstSolidSample : PlayerPosition; float ViolationFeetDensity = bSweptIntoSolid ? FirstSolidDensity : FeetDensity; float ViolationCenterDensity = CenterDensity; float ViolationSurfaceLocalVoxelZ = SurfaceLocalVoxelZ; float ViolationClearanceCm = ClearanceCm; const bool bViolationSurfaceFound = bSweptIntoSolid ? MeasureHeadlessCollisionGateStressTerrain( ViolationPosition, CapsuleHalfHeightCm, ViolationFeetDensity, ViolationCenterDensity, ViolationSurfaceLocalVoxelZ, ViolationClearanceCm) : bSurfaceFound; const bool bSweptPenetrationExceedsTolerance = bSweptIntoSolid && (!bViolationSurfaceFound || ViolationClearanceCm < -AllowedSurfacePenetrationCm); if (bBelowSurface || bSweptPenetrationExceedsTolerance) { if (bViolationSurfaceFound && ViolationClearanceCm < HeadlessCollisionGateStressTestMinClearanceCm) { HeadlessCollisionGateStressTestMinClearanceCm = ViolationClearanceCm; HeadlessCollisionGateStressTestMinClearancePosition = ViolationPosition; HeadlessCollisionGateStressTestMinFeetDensity = ViolationFeetDensity; HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ = ViolationSurfaceLocalVoxelZ; } bHeadlessCollisionGateStressTestTerrainViolation = true; bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("terrain_penetration"); FVoxelTileKey ViolationFootTile; const bool bHaveViolationFootTile = GetPlayerSupportTile( PlayerPawn, ViolationPosition, ViolationFootTile); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCollisionGateStressTest] first_terrain_penetration " "elapsed_s=%.6f position=(%.2f,%.2f,%.2f) local_vox=(%.3f,%.3f,%.3f) " "velocity=(%.2f,%.2f,%.2f) gate=%d foot_tile=(%d,%d,%d) " "foot_tile_ready=%d feet_density=%.6f surface_found=%d " "surface_vox_z=%.4f clearance_cm=%.3f swept=%d"), Now - HeadlessCollisionGateStressTestStartSeconds, ViolationPosition.X, ViolationPosition.Y, ViolationPosition.Z, WorldToLocalVoxel(ViolationPosition).X, WorldToLocalVoxel(ViolationPosition).Y, WorldToLocalVoxel(ViolationPosition).Z, PawnVelocity.X, PawnVelocity.Y, PawnVelocity.Z, bPawnCollisionGateEngaged ? 1 : 0, ViolationFootTile.Coord.X, ViolationFootTile.Coord.Y, ViolationFootTile.Coord.Z, bHaveViolationFootTile && IsTileCollisionReady(ViolationFootTile) ? 1 : 0, ViolationFeetDensity, bViolationSurfaceFound ? 1 : 0, ViolationSurfaceLocalVoxelZ, ViolationClearanceCm, bSweptIntoSolid ? 1 : 0); } // A research-field tile can remain queued longer than the stress window because pointwise // evaluation is expensive. Treat a sustained, measured full stop above that unresolved tile as // a safety pass; the result still reports landed=0 and the pending support interval, so this // cannot be mistaken for a streaming-liveness or traversal pass. constexpr double SafePendingHoldSeconds = 0.5; const double CurrentGateHoldSeconds = bGateEngaged && HeadlessCollisionGateStressTestGateStartSeconds > 0.0 ? FMath::Max(0.0, Now - HeadlessCollisionGateStressTestGateStartSeconds) : 0.0; if (!bHeadlessCollisionGateStressTestPassed && !bHeadlessCollisionGateStressTestFailed && bHeadlessCollisionGateStressDeepDrop && bCollisionGateEnabled && bHeadlessCollisionGateStressTestFullStopProbeApplied && bHeadlessCollisionGateStressTestFullStopProbeObserved && bGateEngaged && !bGateSupportReady && GateSupportUnavailableReason == nullptr && bSurfaceFound && ClearanceCm >= -AllowedSurfacePenetrationCm && !bHeadlessCollisionGateStressTestTerrainViolation && HeadlessCollisionGateStressTestTerrainSampleCount > 0 && CurrentGateHoldSeconds >= SafePendingHoldSeconds) { bHeadlessCollisionGateStressTestPassed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("safe_pending_support_hold"); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] safety_hold_pass elapsed_s=%.6f " "hold_s=%.6f clearance_cm=%.3f support=(%d,%d,%d) " "support_ready=0 landed=%d"), Now - HeadlessCollisionGateStressTestBeginSeconds, CurrentGateHoldSeconds, ClearanceCm, GateSupportTile.Coord.X, GateSupportTile.Coord.Y, GateSupportTile.Coord.Z, bHeadlessCollisionGateStressTestLanded ? 1 : 0); } ++HeadlessCollisionGateStressTestTraceFrame; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] trace frame=%d elapsed_s=%.6f " "position=(%.2f,%.2f,%.2f) local_vox=(%.3f,%.3f,%.3f) " "velocity=(%.2f,%.2f,%.2f) gate=%d foot_tile=(%d,%d,%d) " "foot_tile_ready=%d gate_support=(%d,%d,%d) gate_support_ready=%d " "feet_density=%.6f center_density=%.6f surface_found=%d " "surface_vox_z=%.4f clearance_cm=%.3f landed=%d segment_cm=%.2f"), HeadlessCollisionGateStressTestTraceFrame, Now - HeadlessCollisionGateStressTestStartSeconds, PlayerPosition.X, PlayerPosition.Y, PlayerPosition.Z, LocalPosition.X, LocalPosition.Y, LocalPosition.Z, PawnVelocity.X, PawnVelocity.Y, PawnVelocity.Z, bPawnCollisionGateEngaged ? 1 : 0, FootTile.Coord.X, FootTile.Coord.Y, FootTile.Coord.Z, bFootTileCollisionReady ? 1 : 0, GateSupportTile.Coord.X, GateSupportTile.Coord.Y, GateSupportTile.Coord.Z, bGateSupportReady ? 1 : 0, FeetDensity, CenterDensity, bSurfaceFound ? 1 : 0, SurfaceLocalVoxelZ, ClearanceCm, bHeadlessCollisionGateStressTestLanded ? 1 : 0, SegmentLengthCm); if (bHeadlessCollisionGateStressTestLanded && Now - HeadlessCollisionGateStressTestLandedSeconds >= static_cast(HeadlessCollisionGateStressTestDurationSeconds)) { return; } if (Now - HeadlessCollisionGateStressTestBeginSeconds >= static_cast(HeadlessCollisionGateStressTestTimeoutSeconds)) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("stress_timeout"); } } void AVoxelWorld::AdvanceHeadlessSurfaceFallTest( FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn) { if (!bHeadlessSurfaceFallTest || bHeadlessSurfaceFallTestFailed || bHeadlessSurfaceFallTestPassed || !IsValid(PlayerPawn) || HeadlessSurfaceFallTestBeginSeconds <= 0.0 || !Generator || !StrateManager) { return; } const double Now = FPlatformTime::Seconds(); if (!bHeadlessSurfaceFallTestStartPlaced) { if (StrateManager->GetNumStrates() != 1 || StrateManager->GetGeneratorTypeForChunk(FIntVector(0, 0, 0)) != ECaveGeneratorType::SurfaceWorld) { bHeadlessSurfaceFallTestFailed = true; HeadlessSurfaceFallTestFailureReason = TEXT("layout_is_not_one_surface_strate"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeSurfaceFallTest] result=FAIL reason=%s strates=%d"), *HeadlessSurfaceFallTestFailureReason, StrateManager->GetNumStrates()); return; } float HeightfieldTerrainZ = 0.0f; float CeilingZ = 0.0f; if (!Generator->GetSurfaceHeightAt( 0.0f, 0.0f, 0, HeightfieldTerrainZ, CeilingZ)) { bHeadlessSurfaceFallTestFailed = true; HeadlessSurfaceFallTestFailureReason = TEXT("surface_height_oracle_unavailable"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeSurfaceFallTest] result=FAIL reason=%s"), *HeadlessSurfaceFallTestFailureReason); return; } if (const ACharacter* Character = Cast(PlayerPawn)) { if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent()) { const float HalfHeightCm = Capsule->GetScaledCapsuleHalfHeight(); if (VoxelMath::IsFinite(HalfHeightCm) && HalfHeightCm > 0.0f) { HeadlessSurfaceFallTestPlayerHalfHeightVoxels = HalfHeightCm / VOXEL_SIZE; } } } int32 TopChunkZ = 0; int32 BottomChunkZ = 0; if (!StrateManager->GetStrateChunkZBounds(0, TopChunkZ, BottomChunkZ)) { bHeadlessSurfaceFallTestFailed = true; HeadlessSurfaceFallTestFailureReason = TEXT("surface_strate_bounds_unavailable"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeSurfaceFallTest] result=FAIL reason=%s"), *HeadlessSurfaceFallTestFailureReason); return; } // The authored surface entry is at the top of the one-strate band. The procedural // heightfield can be far below that opening, so use the higher of the two as the spawn // floor; this reproduces a character falling through the surface-only world instead of // accidentally starting next to the eventual terrain. const float SurfaceEntryTopZ = static_cast(TopChunkZ + 1) * CHUNK_SIZE; if (!StrateManager->GetLayout().IsValidIndex(0) || StrateManager->GetLayout()[0].Definition == nullptr || Settings == nullptr) { bHeadlessSurfaceFallTestFailed = true; HeadlessSurfaceFallTestFailureReason = TEXT("surface_entry_geometry_unavailable"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeSurfaceFallTest] result=FAIL reason=%s"), *HeadlessSurfaceFallTestFailureReason); return; } const UVoxelStrateDefinition* TopDefinition = StrateManager->GetLayout()[0].Definition; const float TopSeal = TopDefinition->SurfaceParams.BoundarySealThickness; const float OriginRadius = Settings->GetEffectiveOriginSpineRadius(); const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding = VoxelPassageGeometry::BuildOriginLandingGeometry( SurfaceEntryTopZ, static_cast(BottomChunkZ) * CHUNK_SIZE, TopSeal, OriginRadius); if (!OriginLanding.bValid) { bHeadlessSurfaceFallTestFailed = true; HeadlessSurfaceFallTestFailureReason = TEXT("surface_entry_landing_unavailable"); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeSurfaceFallTest] result=FAIL reason=%s"), *HeadlessSurfaceFallTestFailureReason); return; } HeadlessSurfaceFallTestTerrainZ = OriginLanding.FloorZ; const float GroundHeightfieldZ = HeightfieldTerrainZ; const float SpawnBaseZ = FMath::Max(HeightfieldTerrainZ, SurfaceEntryTopZ); const FVector StartVoxel( 0.0f, 0.0f, SpawnBaseZ + HeadlessSurfaceFallTestPlayerHalfHeightVoxels + HeadlessSurfaceFallTestSpawnHeightVoxels); HeadlessSurfaceFallTestStartPosition = LocalVoxelToWorld(StartVoxel); const bool bPlaced = PlayerPawn->SetActorLocation( HeadlessSurfaceFallTestStartPosition, false, nullptr, ETeleportType::TeleportPhysics); const FVector ActualPosition = PlayerPawn->GetActorLocation(); InOutPlayerPosition = ActualPosition; InOutPlayerHeading = FVector::ZeroVector; HeadlessSurfaceFallTestStartPosition = ActualPosition; HeadlessSurfaceFallTestLastActualPosition = ActualPosition; HeadlessSurfaceFallTestDistanceCm = 0.0; bHeadlessSurfaceFallTestStartPlaced = true; HeadlessSurfaceFallTestStartSeconds = Now; if (ACharacter* Character = Cast(PlayerPawn)) { if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) { CharacterMovement->StopMovementImmediately(); CharacterMovement->SetMovementMode(MOVE_Falling); } } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeSurfaceFallTest] start_placed moved=%d ground_vox=%.3f " "heightfield_vox=%.3f entry_top_vox=%.3f " "spawn_vox=(%.2f,%.2f,%.2f) world=(%.1f,%.1f,%.1f)"), bPlaced ? 1 : 0, HeadlessSurfaceFallTestTerrainZ, GroundHeightfieldZ, SurfaceEntryTopZ, StartVoxel.X, StartVoxel.Y, StartVoxel.Z, ActualPosition.X, ActualPosition.Y, ActualPosition.Z); return; } // Reproduce the gate's original stale-prediction path while the pawn is still above the // landing: the current support is valid, but a deliberately fast one-frame horizontal probe // points at a tile outside the current desired set. The old gate remembers that probe after // stopping the pawn and never re-derives it. The probe is cleared on the next tick only when // the gate has correctly released, after which ordinary gravity continues the fall. if (!bHeadlessSurfaceFallTestPredictionProbeInjected && Now - HeadlessSurfaceFallTestStartSeconds >= 0.25) { FVoxelTileKey ProbeSupportTile; const FVector LocalPosition = WorldToLocalVoxel(InOutPlayerPosition); const bool bProbeSupportReady = IsPlayerSupportCollisionReady(PlayerPawn, InOutPlayerPosition, &ProbeSupportTile); int32 ProbeTopChunkZ = 0; int32 ProbeBottomChunkZ = 0; const bool bProbeSupportInLayout = bProbeSupportReady && StrateManager->GetStrateChunkZBounds( ProbeSupportTile.Coord.Z, ProbeTopChunkZ, ProbeBottomChunkZ); if (bProbeSupportReady && bProbeSupportInLayout && LocalPosition.Z - HeadlessSurfaceFallTestPlayerHalfHeightVoxels > HeadlessSurfaceFallTestTerrainZ + 0.5f * 100.0f / VOXEL_SIZE) { bHeadlessSurfaceFallTestPredictionProbeInjected = true; HeadlessSurfaceFallTestPredictionProbePosition = InOutPlayerPosition; if (ACharacter* Character = Cast(PlayerPawn)) { if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) { CharacterMovement->Velocity = FVector(1000000.0f, 0.0f, 0.0f); CharacterMovement->SetMovementMode(MOVE_Falling); CharacterMovement->UpdateComponentVelocity(); } } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeSurfaceFallTest] prediction_probe injected=1 " "current_support=(%d,%d,%d) local=(%.2f,%.2f,%.2f) " "velocity_cm_s=1000000 pawn_velocity=(%.1f,%.1f,%.1f)"), ProbeSupportTile.Coord.X, ProbeSupportTile.Coord.Y, ProbeSupportTile.Coord.Z, LocalPosition.X, LocalPosition.Y, LocalPosition.Z, PlayerPawn->GetVelocity().X, PlayerPawn->GetVelocity().Y, PlayerPawn->GetVelocity().Z); // Use a stable test-frame delta so this command-line probe exercises the same // prediction branch even when an uncapped -nullrhi tick reports a near-zero delta. UpdatePawnCollisionGate(PlayerPawn, InOutPlayerPosition, 1.0f / 60.0f); } } else if (bHeadlessSurfaceFallTestPredictionProbeInjected && !bHeadlessSurfaceFallTestPredictionProbeCleared && !bPawnCollisionGateEngaged) { const bool bRestored = PlayerPawn->SetActorLocation( HeadlessSurfaceFallTestPredictionProbePosition, false, nullptr, ETeleportType::TeleportPhysics); InOutPlayerPosition = PlayerPawn->GetActorLocation(); InOutPlayerHeading = FVector::ZeroVector; bHeadlessSurfaceFallTestPredictionProbeCleared = true; if (ACharacter* Character = Cast(PlayerPawn)) { if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) { CharacterMovement->StopMovementImmediately(); CharacterMovement->SetMovementMode(MOVE_Falling); } } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeSurfaceFallTest] prediction_probe cleared=1 restored=%d"), bRestored ? 1 : 0); } HeadlessSurfaceFallTestDistanceCm += FVector::Dist( HeadlessSurfaceFallTestLastActualPosition, InOutPlayerPosition); HeadlessSurfaceFallTestLastActualPosition = InOutPlayerPosition; const FVector LocalPosition = WorldToLocalVoxel(InOutPlayerPosition); const float FeetZ = LocalPosition.Z - HeadlessSurfaceFallTestPlayerHalfHeightVoxels; bool bOnGround = false; if (const ACharacter* Character = Cast(PlayerPawn)) { if (const UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) { bOnGround = CharacterMovement->IsMovingOnGround(); } } if (bOnGround && bHeadlessSurfaceFallTestPredictionProbeCleared && FeetZ <= HeadlessSurfaceFallTestTerrainZ + HeadlessSurfaceFallTestGroundToleranceVoxels && FeetZ >= HeadlessSurfaceFallTestTerrainZ - HeadlessSurfaceFallTestGroundToleranceVoxels) { bHeadlessSurfaceFallTestPassed = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeSurfaceFallTest] reached_ground elapsed_s=%.6f " "distance_m=%.6f feet_vox=%.3f terrain_vox=%.3f gate=%d probe=1"), Now - HeadlessSurfaceFallTestStartSeconds, HeadlessSurfaceFallTestDistanceCm / 100.0, FeetZ, HeadlessSurfaceFallTestTerrainZ, bPawnCollisionGateEngaged ? 1 : 0); } } void AVoxelWorld::MaybeFinishHeadlessSurfaceFallTest() { if (!bHeadlessSurfaceFallTest || bHeadlessSurfaceFallTestExitRequested || HeadlessSurfaceFallTestBeginSeconds <= 0.0) { return; } const double Now = FPlatformTime::Seconds(); if (!bHeadlessSurfaceFallTestStartPlaced && Now - HeadlessSurfaceFallTestBeginSeconds >= static_cast(HeadlessSurfaceFallTestTimeoutSeconds)) { bHeadlessSurfaceFallTestFailed = true; HeadlessSurfaceFallTestFailureReason = TEXT("pawn_start_not_observed"); } else if (!bHeadlessSurfaceFallTestPredictionProbeInjected && !bHeadlessSurfaceFallTestPassed && !bHeadlessSurfaceFallTestFailed && Now - HeadlessSurfaceFallTestStartSeconds >= static_cast(HeadlessSurfaceFallTestTimeoutSeconds)) { bHeadlessSurfaceFallTestFailed = true; HeadlessSurfaceFallTestFailureReason = TEXT("surface_prediction_probe_not_exercised"); } else if (bHeadlessSurfaceFallTestPredictionProbeInjected && !bHeadlessSurfaceFallTestPredictionProbeCleared && !bHeadlessSurfaceFallTestPassed && !bHeadlessSurfaceFallTestFailed && Now - HeadlessSurfaceFallTestStartSeconds >= static_cast(HeadlessSurfaceFallTestTimeoutSeconds)) { bHeadlessSurfaceFallTestFailed = true; HeadlessSurfaceFallTestFailureReason = TEXT("collision_gate_prediction_deadlock"); } else if (!bHeadlessSurfaceFallTestPassed && !bHeadlessSurfaceFallTestFailed && Now - HeadlessSurfaceFallTestStartSeconds >= static_cast(HeadlessSurfaceFallTestTimeoutSeconds)) { bHeadlessSurfaceFallTestFailed = true; HeadlessSurfaceFallTestFailureReason = TEXT("surface_ground_timeout"); } if (!bHeadlessSurfaceFallTestPassed && !bHeadlessSurfaceFallTestFailed) { return; } bHeadlessSurfaceFallTestExitRequested = true; const FVector FinalPosition = HeadlessSurfaceFallTestLastActualPosition; const bool bPassed = bHeadlessSurfaceFallTestPassed && !bHeadlessSurfaceFallTestFailed; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeSurfaceFallTest] result=%s reason=%s elapsed_s=%.6f " "distance_m=%.6f final_position=(%.1f,%.1f,%.1f) terrain_vox=%.3f"), bPassed ? TEXT("PASS") : TEXT("FAIL"), bPassed ? TEXT("reached_surface_ground") : *HeadlessSurfaceFallTestFailureReason, Now - HeadlessSurfaceFallTestBeginSeconds, HeadlessSurfaceFallTestDistanceCm / 100.0, FinalPosition.X, FinalPosition.Y, FinalPosition.Z, HeadlessSurfaceFallTestTerrainZ); FPlatformMisc::RequestExitWithStatus( false, bPassed ? 0 : 1, TEXT("VoxelForge surface fall test complete")); } void AVoxelWorld::MaybeFinishHeadlessCollisionGateStressTest() { if (!bHeadlessCollisionGateStressTest || bHeadlessCollisionGateStressTestExitRequested || HeadlessCollisionGateStressTestBeginSeconds <= 0.0) { return; } const double Now = FPlatformTime::Seconds(); const double SinceBegin = Now - HeadlessCollisionGateStressTestBeginSeconds; if (!bHeadlessCollisionGateStressTestStartPlaced && !bHeadlessCollisionGateStressTestPassed && !bHeadlessCollisionGateStressTestFailed && SinceBegin >= static_cast(HeadlessCollisionGateStressTestTimeoutSeconds)) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("stress_start_not_observed"); } else if (bHeadlessCollisionGateStressTestStarted && bHeadlessCollisionGateStressTestLanded && !bHeadlessCollisionGateStressTestPassed && !bHeadlessCollisionGateStressTestFailed && Now - HeadlessCollisionGateStressTestLandedSeconds >= static_cast(HeadlessCollisionGateStressTestDurationSeconds)) { // The gate's job is to protect the density surface. Travel, holds, and support timing // stay in the report, but only measured capsule clearance decides PASS/FAIL. if (bHeadlessCollisionGateStressTestTerrainViolation) { // The first penetration was recorded at the sample that observed it. } else if (HeadlessCollisionGateStressTestTerrainSampleCount == 0 || bHeadlessCollisionGateStressTestTerrainMeasurementMissing) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("terrain_surface_unavailable"); } else if (HeadlessCollisionGateStressTestMinClearanceCm < -5.0) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("terrain_clearance_below_tolerance"); } else { bHeadlessCollisionGateStressTestPassed = true; } } else if (!bHeadlessCollisionGateStressTestPassed && !bHeadlessCollisionGateStressTestFailed && SinceBegin >= static_cast(HeadlessCollisionGateStressTestTimeoutSeconds)) { bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("stress_timeout"); } if (bHeadlessCollisionGateStressTestPassed && !bHeadlessCollisionGateStressTestFailed && bHeadlessCollisionGateStressDeepDrop && bCollisionGateEnabled && (!bHeadlessCollisionGateStressTestFullStopProbeApplied || !bHeadlessCollisionGateStressTestFullStopProbeObserved)) { bHeadlessCollisionGateStressTestPassed = false; bHeadlessCollisionGateStressTestFailed = true; HeadlessCollisionGateStressTestFailureReason = TEXT("gate_full_stop_probe_not_observed"); } if (!bHeadlessCollisionGateStressTestPassed && !bHeadlessCollisionGateStressTestFailed) { return; } if (bHeadlessCollisionGateStressTestGateWasEngaged && HeadlessCollisionGateStressTestGateStartSeconds > 0.0) { const double GateDuration = FMath::Max( 0.0, Now - HeadlessCollisionGateStressTestGateStartSeconds); HeadlessCollisionGateStressTestTotalGateDurationSeconds += GateDuration; HeadlessCollisionGateStressTestMaxGateDurationSeconds = FMath::Max( HeadlessCollisionGateStressTestMaxGateDurationSeconds, GateDuration); } if (bHeadlessCollisionGateStressTestFullStop && HeadlessCollisionGateStressTestFullStopStartSeconds > 0.0) { HeadlessCollisionGateStressTestFullStopDurationSeconds += FMath::Max( 0.0, Now - HeadlessCollisionGateStressTestFullStopStartSeconds); } bHeadlessCollisionGateStressTestExitRequested = true; const bool bPassed = bHeadlessCollisionGateStressTestPassed && !bHeadlessCollisionGateStressTestFailed; const double ExpectedDistanceCm = static_cast(HeadlessCollisionGateStressTestSpeedCmPerSecond) * static_cast(HeadlessCollisionGateStressTestDurationSeconds); const double SupportSubmitLatencySeconds = bStartupCollisionGateTimingCaptured && StartupCollisionGateSubmittedSeconds > 0.0 ? FMath::Max(0.0, StartupCollisionGateSubmittedSeconds - StartupCollisionGateFirstCheckSeconds) : -1.0; const double SupportReadyLatencySeconds = bStartupCollisionGateTimingCaptured && StartupCollisionGateReadySeconds > 0.0 ? FMath::Max(0.0, StartupCollisionGateReadySeconds - StartupCollisionGateFirstCheckSeconds) : -1.0; const double StressSupportSubmitLatencySeconds = bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured && HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds > 0.0 ? FMath::Max(0.0, HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds - HeadlessCollisionGateStressTestLaunchSupportCheckSeconds) : -1.0; const double StressSupportReadyLatencySeconds = bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured && HeadlessCollisionGateStressTestLaunchSupportReadySeconds > 0.0 ? FMath::Max(0.0, HeadlessCollisionGateStressTestLaunchSupportReadySeconds - HeadlessCollisionGateStressTestLaunchSupportCheckSeconds) : -1.0; float FinalFeetDensity = 0.0f; float FinalCenterDensity = 0.0f; float FinalSurfaceLocalVoxelZ = 0.0f; float FinalClearanceCm = 0.0f; float FinalHalfHeightCm = 0.0f; FVector IgnoredCurrentPosition; APawn* StressPawn = nullptr; TryGetPlayerPosition(IgnoredCurrentPosition, &StressPawn); if (const ACharacter* Character = Cast(StressPawn)) { if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent()) { FinalHalfHeightCm = Capsule->GetScaledCapsuleHalfHeight(); } } const bool bFinalSurfaceFound = MeasureHeadlessCollisionGateStressTerrain( HeadlessCollisionGateStressTestLastActualPosition, FinalHalfHeightCm, FinalFeetDensity, FinalCenterDensity, FinalSurfaceLocalVoxelZ, FinalClearanceCm); const FVector FinalLocalPosition = WorldToLocalVoxel( HeadlessCollisionGateStressTestLastActualPosition); const float ColumnCenterZ = bFinalSurfaceFound ? FinalSurfaceLocalVoxelZ : FinalLocalPosition.Z - FinalHalfHeightCm / VOXEL_SIZE; FString DensityColumnSamples; for (int32 Offset = 12; Offset >= -12; --Offset) { const float SampleZ = ColumnCenterZ + static_cast(Offset); const float Density = Generator != nullptr ? Generator->GetDensityAt(FinalLocalPosition.X, FinalLocalPosition.Y, SampleZ) : 0.0f; if (!DensityColumnSamples.IsEmpty()) { DensityColumnSamples += TEXT(","); } DensityColumnSamples += FString::Printf(TEXT("%.2f:%.5f"), SampleZ, Density); } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] density_column world_xy=(%.2f,%.2f) " "local_xy_vox=(%.4f,%.4f) surface_found=%d surface_vox_z=%.4f " "feet_density=%.6f clearance_cm=%.3f samples_zvox_density=%s"), HeadlessCollisionGateStressTestLastActualPosition.X, HeadlessCollisionGateStressTestLastActualPosition.Y, FinalLocalPosition.X, FinalLocalPosition.Y, bFinalSurfaceFound ? 1 : 0, FinalSurfaceLocalVoxelZ, FinalFeetDensity, FinalClearanceCm, *DensityColumnSamples); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] result=%s reason=%s elapsed_s=%.6f " "distance_m=%.6f expected_distance_m=%.6f speed_cm_s=%.1f spawn_height_vox=%.2f deep_drop=%d " "gate_holds=%d gate_total_s=%.6f max_gate_s=%.6f gate_progress_m=%.6f " "full_stop_probe_applied=%d full_stop_probe_observed=%d " "full_stops=%d full_stop_total_s=%.6f collision_gate=%d " "support_tile=(%d,%d,%d) support_first_check_to_submit_s=%.6f " "support_first_check_to_collision_ready_s=%.6f " "test_support_tile=(%d,%d,%d) " "test_support_first_check_to_submit_s=%.6f " "test_support_first_check_to_collision_ready_s=%.6f landed=%d " "terrain_samples=%d terrain_measurement_missing=%d " "start_height_crossed=%d min_clearance_cm=%.3f " "min_clearance_position=(%.2f,%.2f,%.2f) min_feet_density=%.6f " "min_surface_vox_z=%.4f final_position=(%.1f,%.1f,%.1f)"), bPassed ? TEXT("PASS") : TEXT("FAIL"), bPassed && HeadlessCollisionGateStressTestFailureReason.IsEmpty() ? TEXT("capsule_never_below_density_surface") : *HeadlessCollisionGateStressTestFailureReason, Now - HeadlessCollisionGateStressTestBeginSeconds, HeadlessCollisionGateStressTestDistanceCm / 100.0, ExpectedDistanceCm / 100.0, HeadlessCollisionGateStressTestSpeedCmPerSecond, HeadlessCollisionGateStressTestSpawnHeightVoxels, bHeadlessCollisionGateStressDeepDrop ? 1 : 0, HeadlessCollisionGateStressTestGateHoldCount, HeadlessCollisionGateStressTestTotalGateDurationSeconds, HeadlessCollisionGateStressTestMaxGateDurationSeconds, HeadlessCollisionGateStressTestGateDistanceCm / 100.0, bHeadlessCollisionGateStressTestFullStopProbeApplied ? 1 : 0, bHeadlessCollisionGateStressTestFullStopProbeObserved ? 1 : 0, HeadlessCollisionGateStressTestFullStopCount, HeadlessCollisionGateStressTestFullStopDurationSeconds, bCollisionGateEnabled ? 1 : 0, StartupCollisionGateSupportTile.Coord.X, StartupCollisionGateSupportTile.Coord.Y, StartupCollisionGateSupportTile.Coord.Z, SupportSubmitLatencySeconds, SupportReadyLatencySeconds, HeadlessCollisionGateStressTestLaunchSupportTile.Coord.X, HeadlessCollisionGateStressTestLaunchSupportTile.Coord.Y, HeadlessCollisionGateStressTestLaunchSupportTile.Coord.Z, StressSupportSubmitLatencySeconds, StressSupportReadyLatencySeconds, bHeadlessCollisionGateStressTestLanded ? 1 : 0, HeadlessCollisionGateStressTestTerrainSampleCount, bHeadlessCollisionGateStressTestTerrainMeasurementMissing ? 1 : 0, bHeadlessCollisionGateStressTestStartHeightCrossed ? 1 : 0, HeadlessCollisionGateStressTestMinClearanceCm, HeadlessCollisionGateStressTestMinClearancePosition.X, HeadlessCollisionGateStressTestMinClearancePosition.Y, HeadlessCollisionGateStressTestMinClearancePosition.Z, HeadlessCollisionGateStressTestMinFeetDensity, HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ, HeadlessCollisionGateStressTestLastActualPosition.X, HeadlessCollisionGateStressTestLastActualPosition.Y, HeadlessCollisionGateStressTestLastActualPosition.Z); FPlatformMisc::RequestExitWithStatus( false, bPassed ? 0 : 1, TEXT("VoxelForge collision-gate stress test complete")); } void AVoxelWorld::MaybeFinishHeadlessStreamingTest() { if (GVoxelForgeStep1ExitAfterFrames > 0 && !bVoxelForgeStep1ExitRequested && GFrameCounter - GVoxelForgeStep1ExitStartFrame >= static_cast(GVoxelForgeStep1ExitAfterFrames)) { bVoxelForgeStep1ExitRequested = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStep1Exit] target_frames=%d elapsed_frames=%llu; requesting clean exit"), GVoxelForgeStep1ExitAfterFrames, static_cast(GFrameCounter - GVoxelForgeStep1ExitStartFrame)); FPlatformMisc::RequestExit(false, TEXT("VoxelForge step 1 frame-count capture complete")); return; } const double ExitDiagnosticNowSeconds = FPlatformTime::Seconds(); const double ExitDiagnosticElapsedSeconds = HeadlessStreamingTestBeginSeconds > 0.0 ? ExitDiagnosticNowSeconds - HeadlessStreamingTestBeginSeconds : -1.0; static double LastExitDiagnosticElapsedSeconds = -30.0; if (ExitDiagnosticElapsedSeconds >= 0.0 && ExitDiagnosticElapsedSeconds - LastExitDiagnosticElapsedSeconds >= 30.0) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStreamingExitDiag] elapsed_s=%.3f threshold_s=%.3f " "exit_requested=%d surface_fall=%d crossing=%d gate_stress=%d"), ExitDiagnosticElapsedSeconds, HeadlessStreamingTestExitSeconds, bHeadlessStreamingTestExitRequested ? 1 : 0, bHeadlessSurfaceFallTest ? 1 : 0, bHeadlessStrateCrossingTest ? 1 : 0, bHeadlessCollisionGateStressTest ? 1 : 0); LastExitDiagnosticElapsedSeconds = ExitDiagnosticElapsedSeconds; } if (bHeadlessStrateCrossingTest) { MaybeFinishHeadlessStrateCrossingTest(); return; } if (bHeadlessSurfaceFallTest) { MaybeFinishHeadlessSurfaceFallTest(); return; } if (bHeadlessCollisionGateStressTest) { MaybeFinishHeadlessCollisionGateStressTest(); return; } if (bHeadlessStreamingTestExitRequested || HeadlessStreamingTestExitSeconds <= 0.0f || HeadlessStreamingTestBeginSeconds <= 0.0) { return; } if (FPlatformTime::Seconds() - HeadlessStreamingTestBeginSeconds < static_cast(HeadlessStreamingTestExitSeconds)) { return; } bHeadlessStreamingTestExitRequested = true; LogVisualStalenessAudit(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStreamingTest] complete; moves=%d distance_m=%.6f last_position=(%.1f,%.1f,%.1f); requesting graceful exit after clean session"), HeadlessStreamingTestMoveAttempts, HeadlessStreamingTestDistanceCm / 100.0, HeadlessStreamingTestLastActualPosition.X, HeadlessStreamingTestLastActualPosition.Y, HeadlessStreamingTestLastActualPosition.Z); FPlatformMisc::RequestExit(false, TEXT("VoxelForge headless streaming test complete")); } static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile, const FVector& BoundsMin, const FVector& BoundsMax) { const FIntVector Origin = Tile.OriginVoxels(); const FVector TileMin( static_cast(Origin.X), static_cast(Origin.Y), static_cast(Origin.Z)); const FVector TileMax = TileMin + FVector(static_cast(Tile.ExtentVoxels())); return TileMin.X < BoundsMax.X && TileMax.X > BoundsMin.X && TileMin.Y < BoundsMax.Y && TileMax.Y > BoundsMin.Y && TileMin.Z < BoundsMax.Z && TileMax.Z > BoundsMin.Z; } static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B) { const FIntVector AOrigin = A.OriginVoxels(); const FIntVector BOrigin = B.OriginVoxels(); const FVector AMin( static_cast(AOrigin.X), static_cast(AOrigin.Y), static_cast(AOrigin.Z)); const FVector BMin( static_cast(BOrigin.X), static_cast(BOrigin.Y), static_cast(BOrigin.Z)); const float AExtent = static_cast(A.ExtentVoxels()); const float BExtent = static_cast(B.ExtentVoxels()); return AMin.X < BMin.X + BExtent && BMin.X < AMin.X + AExtent && AMin.Y < BMin.Y + BExtent && BMin.Y < AMin.Y + AExtent && AMin.Z < BMin.Z + BExtent && BMin.Z < AMin.Z + AExtent; } template static void VF_AppendTouchingAncestors( const FVoxelTileKey& Tile, int32 AncestorLevel, CandidateArray& OutCandidates) { if (AncestorLevel <= Tile.Level) { return; } const int32 Divisor = 1 << (AncestorLevel - Tile.Level); const auto FloorDiv = [Divisor](int32 Value) { return Value >= 0 ? Value / Divisor : -(((-Value) + Divisor - 1) / Divisor); }; const auto AxisCandidates = [&FloorDiv, Divisor](int32 Value) { TArray> Candidates; int32 First = FloorDiv(Value); if ((Value % Divisor) == 0) { --First; } const int32 Last = FloorDiv(Value + 1); for (int32 Coord = First; Coord <= Last; ++Coord) { Candidates.Add(Coord); } return Candidates; }; const TArray> X = AxisCandidates(Tile.Coord.X); const TArray> Y = AxisCandidates(Tile.Coord.Y); const TArray> Z = AxisCandidates(Tile.Coord.Z); for (const int32 XCoord : X) { for (const int32 YCoord : Y) { for (const int32 ZCoord : Z) { OutCandidates.AddUnique(FVoxelTileKey(FIntVector(XCoord, YCoord, ZCoord), AncestorLevel)); } } } } void AVoxelWorld::UpdateVisualStalenessAudit() { FVisualGapAuditState* GapAudit = GVoxelForgeVisualGapAudits.Find(this); if (!bVisualStalenessAudit && (!GapAudit || !GapAudit->bEnabled)) { return; } const double Now = FPlatformTime::Seconds(); if (GapAudit && GapAudit->bEnabled) { FVector GapViewLocation = FVector::ZeroVector; FRotator GapViewRotation = FRotator::ZeroRotator; float GapHorizontalTanHalfFov = 0.0f; float GapVerticalTanHalfFov = 0.0f; bool bGapAuditHasView = false; if (UWorld* World = GetWorld()) { if (APlayerController* PC = World->GetFirstPlayerController()) { PC->GetPlayerViewPoint(GapViewLocation, GapViewRotation); float HorizontalFovDegrees = 90.0f; if (PC->PlayerCameraManager) { HorizontalFovDegrees = PC->PlayerCameraManager->GetFOVAngle(); } FVector2D ViewportSize(16.0f, 9.0f); if (GEngine && GEngine->GameViewport) { GEngine->GameViewport->GetViewportSize(ViewportSize); } const float AspectRatio = ViewportSize.Y > 0.0f ? ViewportSize.X / ViewportSize.Y : (16.0f / 9.0f); GapHorizontalTanHalfFov = FMath::Tan( FMath::DegreesToRadians(HorizontalFovDegrees * 0.5f)); GapVerticalTanHalfFov = GapHorizontalTanHalfFov / FMath::Max(AspectRatio, 0.1f); bGapAuditHasView = true; } } ++GapAudit->Frame; if (!GapAudit->bCollectionStarted && bHeadlessStreamingTestMovement && HeadlessStreamingTestBeginSeconds > 0.0 && Now - HeadlessStreamingTestBeginSeconds >= static_cast(HeadlessStreamingTestStartSeconds)) { VF_StartVisualGapAuditWindow(*GapAudit, Now); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeVisualGapAudit] collection_begin=1 reason=fast_walk start_frame=%llu"), static_cast(GapAudit->Frame)); } if (URealtimeMeshComponent* Injected = GapAudit->InjectedComponent.Get()) { if (GapAudit->Frame >= GapAudit->InjectionRestoreFrame) { Injected->SetVisibility(true); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeVisualGapAudit] injected_early_hide_restored=1 frame=%llu tile=(%d,%d,%d,%d)"), static_cast(GapAudit->Frame), GapAudit->InjectedTile.Level, GapAudit->InjectedTile.Coord.X, GapAudit->InjectedTile.Coord.Y, GapAudit->InjectedTile.Coord.Z); GapAudit->InjectedComponent.Reset(); } } const auto CandidateTilesAtLevel = [this]( const FVector& WorldPosition, int32 Level, auto& OutCandidates) { const FVector LocalVoxel = WorldToLocalVoxel(WorldPosition); const float TileExtentVoxels = static_cast(CHUNK_SIZE * (1 << Level)); TArray> XCandidates; TArray> YCandidates; TArray> ZCandidates; const auto AddAxisCandidates = [TileExtentVoxels](float Value, auto& Candidates) { const float ExactTileCoord = Value / TileExtentVoxels; const int32 BaseTileCoord = FMath::FloorToInt(ExactTileCoord); Candidates.Add(BaseTileCoord); const float NearestBoundary = FMath::RoundToFloat(ExactTileCoord); if (FMath::Abs(ExactTileCoord - NearestBoundary) <= 1.0e-4f) { Candidates.AddUnique(static_cast(NearestBoundary) - 1); } }; AddAxisCandidates(LocalVoxel.X, XCandidates); AddAxisCandidates(LocalVoxel.Y, YCandidates); AddAxisCandidates(LocalVoxel.Z, ZCandidates); for (const int32 X : XCandidates) { for (const int32 Y : YCandidates) { for (const int32 Z : ZCandidates) { OutCandidates.AddUnique(FVoxelTileKey(FIntVector(X, Y, Z), Level)); } } } }; const auto IsPointInCameraView = [bGapAuditHasView, &GapViewLocation, &GapViewRotation, GapHorizontalTanHalfFov, GapVerticalTanHalfFov]( const FVector& WorldPosition) { if (!bGapAuditHasView) { return true; } const FVector ViewLocal = GapViewRotation.UnrotateVector(WorldPosition - GapViewLocation); return ViewLocal.X > 0.0f && FMath::Abs(ViewLocal.Y) <= ViewLocal.X * GapHorizontalTanHalfFov && FMath::Abs(ViewLocal.Z) <= ViewLocal.X * GapVerticalTanHalfFov; }; const auto IsEditedPoint = [this, GapAudit](const FVector& WorldPosition) { const FVector LocalVoxel = WorldToLocalVoxel(WorldPosition); for (const TPair& Bounds : GapAudit->EditedBounds) { if (LocalVoxel.X >= Bounds.Key.X && LocalVoxel.X <= Bounds.Value.X && LocalVoxel.Y >= Bounds.Key.Y && LocalVoxel.Y <= Bounds.Value.Y && LocalVoxel.Z >= Bounds.Key.Z && LocalVoxel.Z <= Bounds.Value.Z) { return true; } } return false; }; const auto IsDesiredPoint = [this, &CandidateTilesAtLevel](const FVector& WorldPosition) { for (int32 Level = 0; Level <= DesiredTransitionMaxLevel; ++Level) { TArray> Candidates; CandidateTilesAtLevel(WorldPosition, Level, Candidates); for (const FVoxelTileKey& Candidate : Candidates) { if (IsDesired(Candidate)) { return true; } } } return false; }; const auto IsCoveredByVisibleTile = [this, GapAudit, &CandidateTilesAtLevel]( const FVector& WorldPosition, const FVoxelTileKey* ExcludedTile) { for (int32 Level = 0; Level <= DesiredTransitionMaxLevel; ++Level) { TArray> Candidates; CandidateTilesAtLevel(WorldPosition, Level, Candidates); for (const FVoxelTileKey& Candidate : Candidates) { if (ExcludedTile && Candidate == *ExcludedTile) { continue; } const URealtimeMeshComponent* const* Component = TileComponents.Find(Candidate); const TArray* Witnesses = GapAudit->WitnessesByTile.Find(Candidate); if (Component && *Component && (*Component)->IsVisible() && Witnesses && !Witnesses->IsEmpty()) { return true; } } } return false; }; if (GapAudit->bCollecting && GapAudit->BeginSeconds <= 0.0) { for (const TPair& Pair : TileComponents) { const TArray* Witnesses = GapAudit->WitnessesByTile.Find(Pair.Key); if (Pair.Value && Pair.Value->IsVisible() && Witnesses && !Witnesses->IsEmpty()) { GapAudit->BeginSeconds = Now; GapAudit->LastSampleSeconds = Now; break; } } } if (GapAudit->bInjectEarlyHide && !GapAudit->bInjectionDone && GapAudit->BeginSeconds > 0.0 && Now - GapAudit->BeginSeconds >= 2.0) { TArray CandidateTiles; TileComponents.GetKeys(CandidateTiles); CandidateTiles.Sort([](const FVoxelTileKey& A, const FVoxelTileKey& B) { if (A.Level != B.Level) { return A.Level < B.Level; } if (A.Coord.Z != B.Coord.Z) { return A.Coord.Z < B.Coord.Z; } if (A.Coord.Y != B.Coord.Y) { return A.Coord.Y < B.Coord.Y; } return A.Coord.X < B.Coord.X; }); for (const FVoxelTileKey& CandidateTile : CandidateTiles) { URealtimeMeshComponent* Component = TileComponents.FindRef(CandidateTile); const TArray* Witnesses = GapAudit->WitnessesByTile.Find(CandidateTile); if (!Component || !Component->IsVisible() || !Witnesses || Witnesses->IsEmpty()) { continue; } for (int32 WitnessIndex = 0; WitnessIndex < Witnesses->Num(); ++WitnessIndex) { const FVector& Point = (*Witnesses)[WitnessIndex]; if (!IsPointInCameraView(Point) || !IsDesiredPoint(Point) || IsEditedPoint(Point) || IsCoveredByVisibleTile(Point, &CandidateTile)) { continue; } Component->SetVisibility(false); GapAudit->bInjectionDone = true; GapAudit->InjectedTile = CandidateTile; GapAudit->InjectedWitnessKey.Tile = CandidateTile; GapAudit->InjectedWitnessKey.Index = WitnessIndex; GapAudit->InjectedComponent = Component; GapAudit->InjectionRestoreFrame = GapAudit->Frame + 3; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeVisualGapAudit] injected_early_hide=1 frame=%llu tile=(%d,%d,%d,%d) witness=(%.2f,%.2f,%.2f) restore_frame=%llu"), static_cast(GapAudit->Frame), CandidateTile.Level, CandidateTile.Coord.X, CandidateTile.Coord.Y, CandidateTile.Coord.Z, Point.X, Point.Y, Point.Z, static_cast(GapAudit->InjectionRestoreFrame)); break; } if (GapAudit->bInjectionDone) { break; } } } TArray CurrentVisibleWitnesses; for (const TPair& Pair : TileComponents) { if (!Pair.Value || !Pair.Value->IsVisible()) { continue; } const TArray* Witnesses = GapAudit->WitnessesByTile.Find(Pair.Key); if (!Witnesses) { continue; } for (int32 Index = 0; Index < Witnesses->Num(); ++Index) { if (!IsPointInCameraView((*Witnesses)[Index])) { continue; } FVisualGapWitness& Sample = CurrentVisibleWitnesses.AddDefaulted_GetRef(); Sample.Key.Tile = Pair.Key; Sample.Key.Index = Index; Sample.WorldPosition = (*Witnesses)[Index]; } } if (GapAudit->bCollecting) { ++GapAudit->CollectionFrames; if (bGapAuditHasView) { ++GapAudit->CameraViewFrames; } TSet MissingThisFrame; for (const FVisualGapWitness& Previous : GapAudit->PreviousVisibleWitnesses) { const bool bExpected = IsPointInCameraView(Previous.WorldPosition) && IsDesiredPoint(Previous.WorldPosition) && !IsEditedPoint(Previous.WorldPosition); if (!bExpected || IsCoveredByVisibleTile(Previous.WorldPosition, nullptr)) { GapAudit->MissingSinceSeconds.Remove(Previous.Key); continue; } FVisualGapMissingSample* MissingSince = GapAudit->MissingSinceSeconds.Find(Previous.Key); if (!MissingSince) { FVisualGapMissingSample& Added = GapAudit->MissingSinceSeconds.Add(Previous.Key); Added.BeginSeconds = Now; Added.BeginFrame = GapAudit->CollectionFrames; Added.WorldPosition = Previous.WorldPosition; MissingSince = &Added; ++GapAudit->GapEvents; if (GapAudit->LoggedGapEvents < 24) { ++GapAudit->LoggedGapEvents; FVoxelTileKey ExpectedTile; bool bFoundExpectedTile = false; for (int32 Level = 0; Level <= DesiredTransitionMaxLevel; ++Level) { TArray> Candidates; CandidateTilesAtLevel(Previous.WorldPosition, Level, Candidates); const FVoxelTileKey* Candidate = Candidates.FindByPredicate( [this](const FVoxelTileKey& Tile) { return IsDesired(Tile); }); if (Candidate) { ExpectedTile = *Candidate; bFoundExpectedTile = true; break; } } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeVisualGapAudit] point_gap_begin=1 source=(%d,%d,%d,%d) " "expected_found=%d expected=(%d,%d,%d,%d) point=(%.2f,%.2f,%.2f) frame=%llu"), Previous.Key.Tile.Level, Previous.Key.Tile.Coord.X, Previous.Key.Tile.Coord.Y, Previous.Key.Tile.Coord.Z, bFoundExpectedTile ? 1 : 0, ExpectedTile.Level, ExpectedTile.Coord.X, ExpectedTile.Coord.Y, ExpectedTile.Coord.Z, Previous.WorldPosition.X, Previous.WorldPosition.Y, Previous.WorldPosition.Z, static_cast(GapAudit->Frame)); } if (GapAudit->bInjectionDone && GapAudit->Frame < GapAudit->InjectionRestoreFrame && Previous.Key == GapAudit->InjectedWitnessKey) { GapAudit->bInjectionCaught = true; } } MissingThisFrame.Add(Previous.Key); } // A hidden source component stops contributing fresh witnesses, but its previously // visible points remain expected for as long as a desired tile covers that location. for (auto It = GapAudit->MissingSinceSeconds.CreateIterator(); It; ++It) { if (MissingThisFrame.Contains(It.Key())) { continue; } const FVector& Point = It.Value().WorldPosition; if (!IsDesiredPoint(Point) || IsEditedPoint(Point) || IsCoveredByVisibleTile(Point, nullptr)) { It.RemoveCurrent(); continue; } MissingThisFrame.Add(It.Key()); } const int32 MissingPointCount = MissingThisFrame.Num(); GapAudit->GapPointFrames += MissingPointCount; for (const TPair& Missing : GapAudit->MissingSinceSeconds) { const uint64 GapFrames = GapAudit->CollectionFrames - Missing.Value.BeginFrame + 1; GapAudit->LongestGapFrames = FMath::Max(GapAudit->LongestGapFrames, GapFrames); GapAudit->LongestGapSeconds = FMath::Max( GapAudit->LongestGapSeconds, FMath::Max(0.0, Now - Missing.Value.BeginSeconds)); } if (MissingPointCount > 0) { ++GapAudit->GapFrames; GapAudit->MaxGapPoints = FMath::Max(GapAudit->MaxGapPoints, MissingPointCount); } GapAudit->MissingSamples = GapAudit->MissingSinceSeconds.Num(); GapAudit->Samples += GapAudit->PreviousVisibleWitnesses.Num(); GapAudit->LastSampleSeconds = Now; } GapAudit->PreviousVisibleWitnesses = MoveTemp(CurrentVisibleWitnesses); if (GapAudit->bCollecting) { if (const FDigMotionTestState* DigTest = GVoxelForgeDigMotionTests.Find(this)) { if (DigTest->bResultLogged) { GapAudit->bCollecting = false; } } } } if (VisualStalenessAuditMode == 1 && VisualAuditEditEpoch != 0 && VisualAuditEditBeginSeconds > 0.0) { int32 StaleVisibleTiles = 0; for (const TPair& Pair : TileComponents) { if (!Pair.Value || !Pair.Value->IsVisible() || !VF_TileOverlapsVoxelBounds( Pair.Key, VisualAuditEditMinVoxel, VisualAuditEditMaxVoxel)) { continue; } if (AppliedModificationEpoch.FindRef(Pair.Key) != VisualAuditEditEpoch) { ++StaleVisibleTiles; } } if (StaleVisibleTiles > 0) { bVisualAuditEditSawStale = true; VisualAuditEditMaxStaleTiles = FMath::Max( VisualAuditEditMaxStaleTiles, StaleVisibleTiles); } else if (!bVisualAuditEditResolved) { bVisualAuditEditResolved = true; VisualAuditEditStaleDurationSeconds = FMath::Max( 0.0, Now - VisualAuditEditBeginSeconds); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeVisualStaleness] edit_resolved=1 duration_s=%.6f stale_tiles_max=%d"), VisualAuditEditStaleDurationSeconds, VisualAuditEditMaxStaleTiles); } } if (VisualStalenessAuditMode == 2 && bHeadlessStreamingTestMovement && HeadlessStreamingTestBeginSeconds > 0.0 && Now - HeadlessStreamingTestBeginSeconds >= static_cast(HeadlessStreamingTestStartSeconds)) { TArray VisibleTiles; VisibleTiles.Reserve(TileComponents.Num()); for (const TPair& Pair : TileComponents) { if (Pair.Value && Pair.Value->IsVisible()) { VisibleTiles.Add(Pair.Key); } } int32 OverlapPairs = 0; for (int32 OldIndex = 0; OldIndex < VisibleTiles.Num(); ++OldIndex) { const FVoxelTileKey& OldTile = VisibleTiles[OldIndex]; if (IsDesired(OldTile)) { continue; } for (int32 NewIndex = 0; NewIndex < VisibleTiles.Num(); ++NewIndex) { const FVoxelTileKey& NewTile = VisibleTiles[NewIndex]; if (OldTile.Level == NewTile.Level || !IsDesired(NewTile) || !VF_TileFootprintsOverlap(OldTile, NewTile)) { continue; } ++OverlapPairs; } } if (OverlapPairs > 0) { if (VisualAuditLODOverlapStartSeconds <= 0.0) { VisualAuditLODOverlapStartSeconds = Now; ++VisualAuditLODOverlapEvents; } VisualAuditLODOverlapMaxPairs = FMath::Max( VisualAuditLODOverlapMaxPairs, OverlapPairs); } else if (VisualAuditLODOverlapStartSeconds > 0.0) { VisualAuditLODOverlapMaxSeconds = FMath::Max( VisualAuditLODOverlapMaxSeconds, FMath::Max(0.0, Now - VisualAuditLODOverlapStartSeconds)); VisualAuditLODOverlapStartSeconds = 0.0; } } } void AVoxelWorld::LogVisualStalenessAudit() { FVisualGapAuditState* GapAudit = GVoxelForgeVisualGapAudits.Find(this); const bool bNeedStalenessReport = bVisualStalenessAudit && !bVisualStalenessAuditExitLogged; const bool bNeedGapReport = GapAudit && GapAudit->bEnabled && !GapAudit->bReportLogged; if (!bNeedStalenessReport && !bNeedGapReport) { return; } const double Now = FPlatformTime::Seconds(); if (bNeedGapReport) { if (URealtimeMeshComponent* Injected = GapAudit->InjectedComponent.Get()) { Injected->SetVisibility(true); GapAudit->InjectedComponent.Reset(); } const bool bCameraViewComplete = GapAudit->CollectionFrames > 0 && GapAudit->CameraViewFrames == static_cast(GapAudit->CollectionFrames); const bool bPassed = bCameraViewComplete && (GapAudit->bInjectionCaught || (!GapAudit->bInjectEarlyHide && GapAudit->Samples > 0 && GapAudit->GapPointFrames == 0)); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeVisualGapAudit] result=%s window_s=%.3f window_frames=%llu camera_view_frames=%d surface_point_frames=%d " "gap_count=%d gap_frames=%d gap_point_frames=%d max_missing_points=%d " "longest_gap_frames=%llu longest_gap_ms=%.3f injected_early_hide=%d injection_caught=%d"), bPassed ? TEXT("PASS") : TEXT("FAIL"), GapAudit->BeginSeconds > 0.0 ? FMath::Max(0.0, GapAudit->LastSampleSeconds - GapAudit->BeginSeconds) : 0.0, static_cast(GapAudit->CollectionFrames), GapAudit->CameraViewFrames, GapAudit->Samples, GapAudit->GapEvents, GapAudit->GapFrames, GapAudit->GapPointFrames, GapAudit->MaxGapPoints, static_cast(GapAudit->LongestGapFrames), GapAudit->LongestGapSeconds * 1000.0, GapAudit->bInjectionDone ? 1 : 0, GapAudit->bInjectionCaught ? 1 : 0); GapAudit->bReportLogged = true; } if (!bNeedStalenessReport) { return; } bVisualStalenessAuditExitLogged = true; if (VisualAuditLODOverlapStartSeconds > 0.0) { VisualAuditLODOverlapMaxSeconds = FMath::Max( VisualAuditLODOverlapMaxSeconds, FMath::Max(0.0, Now - VisualAuditLODOverlapStartSeconds)); VisualAuditLODOverlapStartSeconds = 0.0; } if (VisualAuditEditEpoch != 0 && VisualAuditEditBeginSeconds > 0.0 && !bVisualAuditEditResolved) { VisualAuditEditStaleDurationSeconds = FMath::Max( 0.0, Now - VisualAuditEditBeginSeconds); } const bool bPassed = VisualStalenessAuditMode == 1 ? (VisualAuditEditEpoch != 0 && bVisualAuditEditResolved) : (VisualStalenessAuditMode == 2 && VisualAuditLODOverlapMaxSeconds <= 0.050); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeVisualStaleness] result=%s mode=%d " "edit_applied=%d edit_resolved=%d edit_stale_duration_s=%.6f " "edit_stale_tiles_max=%d lod_overlap_max_s=%.6f " "lod_overlap_events=%d lod_overlap_pairs_max=%d"), bPassed ? TEXT("PASS") : TEXT("FAIL"), VisualStalenessAuditMode, VisualAuditEditEpoch != 0 ? 1 : 0, bVisualAuditEditResolved ? 1 : 0, VisualAuditEditStaleDurationSeconds, VisualAuditEditMaxStaleTiles, VisualAuditLODOverlapMaxSeconds, VisualAuditLODOverlapEvents, VisualAuditLODOverlapMaxPairs); } void AVoxelWorld::LogReadyTransitionVisibilityMeasure() { if (!bMeasureReadyTransitionVisibility) { return; } if (!ReadyTransitionVisibilitySamples.IsEmpty()) { TArray Sorted = ReadyTransitionVisibilitySamples; Sorted.Sort([](const FReadyTransitionVisibilitySample& A, const FReadyTransitionVisibilitySample& B) { return A.Cycles < B.Cycles; }); const int32 P50Index = Sorted.Num() / 2; const int32 P95Index = FMath::Clamp( FMath::CeilToInt(static_cast(Sorted.Num()) * 0.95f) - 1, 0, Sorted.Num() - 1); const FReadyTransitionVisibilitySample& P95 = Sorted[P95Index]; const FReadyTransitionVisibilitySample& Max = Sorted.Last(); int32 MinComponents = MAX_int32, MaxComponents = 0; int32 MinDesired = MAX_int32, MaxDesired = 0; int64 TotalComponents = 0, TotalDesired = 0; for (const FReadyTransitionVisibilitySample& Sample : Sorted) { MinComponents = FMath::Min(MinComponents, Sample.TileComponentCount); MaxComponents = FMath::Max(MaxComponents, Sample.TileComponentCount); MinDesired = FMath::Min(MinDesired, Sample.DesiredTileCount); MaxDesired = FMath::Max(MaxDesired, Sample.DesiredTileCount); TotalComponents += Sample.TileComponentCount; TotalDesired += Sample.DesiredTileCount; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeReadyVisibilityMeasure] samples=%d p50_ms=%.6f p95_ms=%.6f max_ms=%.6f " "p95_components=%d p95_desired=%d components_range=%d..%d desired_range=%d..%d " "components_mean=%.1f desired_mean=%.1f"), Sorted.Num(), FPlatformTime::ToSeconds64(Sorted[P50Index].Cycles) * 1000.0, FPlatformTime::ToSeconds64(P95.Cycles) * 1000.0, FPlatformTime::ToSeconds64(Max.Cycles) * 1000.0, P95.TileComponentCount, P95.DesiredTileCount, MinComponents, MaxComponents, MinDesired, MaxDesired, static_cast(TotalComponents) / Sorted.Num(), static_cast(TotalDesired) / Sorted.Num()); constexpr int32 ComponentBucketWidth = 128; TMap> MillisecondsByComponentBucket; for (const FReadyTransitionVisibilitySample& Sample : Sorted) { const int32 BucketMin = (Sample.TileComponentCount / ComponentBucketWidth) * ComponentBucketWidth; MillisecondsByComponentBucket.FindOrAdd(BucketMin).Add( FPlatformTime::ToSeconds64(Sample.Cycles) * 1000.0); } TArray BucketMins; MillisecondsByComponentBucket.GetKeys(BucketMins); BucketMins.Sort(); for (const int32 BucketMin : BucketMins) { TArray& Times = MillisecondsByComponentBucket.FindChecked(BucketMin); Times.Sort(); double TotalMs = 0.0; for (const double TimeMs : Times) { TotalMs += TimeMs; } const int32 P50BucketIndex = Times.Num() / 2; const int32 P95BucketIndex = FMath::Clamp( FMath::CeilToInt(static_cast(Times.Num()) * 0.95f) - 1, 0, Times.Num() - 1); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeReadyVisibilityBucket] components=%d..%d samples=%d " "p50_ms=%.6f p95_ms=%.6f mean_ms=%.6f"), BucketMin, BucketMin + ComponentBucketWidth - 1, Times.Num(), Times[P50BucketIndex], Times[P95BucketIndex], TotalMs / Times.Num()); } } if (!FarFieldSubmitSamples.IsEmpty()) { TArray Sorted = FarFieldSubmitSamples; Sorted.Sort([](const FFarFieldSubmitSample& A, const FFarFieldSubmitSample& B) { return A.Cycles < B.Cycles; }); const int32 P50Index = Sorted.Num() / 2; const int32 P95Index = FMath::Clamp( FMath::CeilToInt(static_cast(Sorted.Num()) * 0.95f) - 1, 0, Sorted.Num() - 1); const FFarFieldSubmitSample& P95 = Sorted[P95Index]; const FFarFieldSubmitSample& Max = Sorted.Last(); int32 MinDesired = MAX_int32, MaxDesired = 0; int32 MinLevel = MAX_int32, MaxLevel = 0; for (const FFarFieldSubmitSample& Sample : Sorted) { MinDesired = FMath::Min(MinDesired, Sample.DesiredTileCount); MaxDesired = FMath::Max(MaxDesired, Sample.DesiredTileCount); MinLevel = FMath::Min(MinLevel, Sample.MaxDesiredLevel); MaxLevel = FMath::Max(MaxLevel, Sample.MaxDesiredLevel); } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeFarFieldSubmitMeasure] samples=%d p50_ms=%.6f p95_ms=%.6f max_ms=%.6f " "p95_desired=%d p95_max_level=%d desired_range=%d..%d levels=%d..%d"), Sorted.Num(), FPlatformTime::ToSeconds64(Sorted[P50Index].Cycles) * 1000.0, FPlatformTime::ToSeconds64(P95.Cycles) * 1000.0, FPlatformTime::ToSeconds64(Max.Cycles) * 1000.0, P95.DesiredTileCount, P95.MaxDesiredLevel, MinDesired, MaxDesired, MinLevel, MaxLevel); } } void AVoxelWorld::Tick(float DeltaTime) { Super::Tick(DeltaTime); TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_Tick); // game-thread streaming orchestration breakdown if (bUseVoxelForgeCore && VoxelForgeCoreState.IsValid() && VoxelForgeCoreState->bInitialFailed.load(std::memory_order_acquire)) { FallBackToLegacyAfterCoreInitializationFailure( VoxelForgeCoreState->InitialFailureReason); } #if WITH_EDITOR if (bTestResearchProfileSwitchConfigured && !bTestResearchProfileSwitchApplied && FPlatformTime::Seconds() - TestResearchProfileSwitchBeginSeconds >= static_cast(TestResearchProfileSwitchAfterSeconds)) { const int32 PreviousProfile = VoxelForgeRedefinedDensityField::GetParameterSet(); const int32 TargetProfile = FMath::Clamp( TestResearchProfileSwitchTo, 0, VoxelForgeRedefinedDensityField::GetParameterSetCount() - 1); VoxelForgeRedefinedDensityField::SetParameterSet(TargetProfile); bTestResearchProfileSwitchApplied = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeResearchProfileSwitch] applied=1 profile=%d->%d loaded_tiles=%d desired_epoch=%u"), PreviousProfile, TargetProfile, LoadedTiles.Num(), DesiredEpoch); } const int32 ResearchFieldEnabled = VoxelForgeRedefinedDensityField::IsParameterizedEnabled() ? 1 : 0; const int32 ResearchFieldProfile = VoxelForgeRedefinedDensityField::GetParameterSet(); if (AppliedResearchFieldEnabled != INDEX_NONE && (AppliedResearchFieldEnabled != ResearchFieldEnabled || AppliedResearchFieldProfile != ResearchFieldProfile)) { UE_LOG(LogTemp, Display, TEXT("[VoxelWorld] research_field_selection_changed enabled=%d->%d profile=%d->%d; rebuilding loaded world"), AppliedResearchFieldEnabled, ResearchFieldEnabled, AppliedResearchFieldProfile, ResearchFieldProfile); AppliedResearchFieldEnabled = ResearchFieldEnabled; AppliedResearchFieldProfile = ResearchFieldProfile; RegenerateAllChunks(); } #endif if (DensityVolume && (!Settings || !Settings->bEnableDensityVolume)) { DensityVolume->NotifyShutdown(); DensityVolume = nullptr; SetDensityVolumeMaterialParamsDisabled(); } FVector PlayerLastPos = FVector::ZeroVector; APawn* PlayerPawn = nullptr; const bool bHasPlayer = TryGetPlayerPosition(PlayerLastPos, &PlayerPawn); if (GVoxelForgeStep1PauseGenerationAfterSeconds > 0.0f && !bVoxelForgeStep1GenerationPauseApplied) { const double NowSeconds = FPlatformTime::Seconds(); if (GVoxelForgeStep1StartSeconds <= 0.0) { GVoxelForgeStep1StartSeconds = NowSeconds; } else if (NowSeconds - GVoxelForgeStep1StartSeconds >= static_cast(GVoxelForgeStep1PauseGenerationAfterSeconds)) { GVoxelForgeStep1GenerationCap = 0; bVoxelForgeStep1GenerationPauseApplied = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStep1] generation_paused=1 elapsed_s=%.3f pending=%d held_results=%d"), NowSeconds - GVoxelForgeStep1StartSeconds, PendingTiles.Num(), Step1HeldApplyResults.Num()); if (GVoxelForgeStep1CoreCookBenchmark != 0 && Step1PauseReplayStreams.IsValid() && !bStep1PauseReplayQueued) { if (FQueuedThreadPool* CookPool = GVoxelForgeStep1CoreCookPool.GetOrCreate()) { CookPool->AddQueuedWork( new FVoxelStep1CoreCookWork( Step1PauseReplayStreams, Step1PauseReplayTile, Step1PauseReplayTriangles, true, true, FPlatformTime::Cycles64()), EQueuedWorkPriority::High); bStep1PauseReplayQueued = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStep1CoreCook] stage=paused_replay_queued " "tile=(%d,%d,%d) triangles=%d"), Step1PauseReplayTile.Coord.X, Step1PauseReplayTile.Coord.Y, Step1PauseReplayTile.Coord.Z, Step1PauseReplayTriangles); Step1PauseReplayStreams.Reset(); } } } } if (bHasPlayer) { // Capture velocity before the collision gate can deliberately zero it while the support // tile is cooking. The scheduler uses this as the movement heading for its next-tile // critical prefix; a stationary pawn falls back to actor forward in BuildDesiredTiles. FVector PlayerHeading = PlayerPawn ? PlayerPawn->GetVelocity() : FVector::ZeroVector; AdvanceHeadlessStreamingTest(PlayerLastPos, PlayerHeading, PlayerPawn); AdvanceHeadlessStrateCrossingTest(PlayerLastPos, PlayerHeading, PlayerPawn); AdvanceHeadlessSurfaceFallTest(PlayerLastPos, PlayerHeading, PlayerPawn); AdvanceHeadlessCollisionGateStressTest(PlayerLastPos, PlayerHeading, PlayerPawn); AdvanceHeadlessDigMotionTest(PlayerLastPos, PlayerPawn); PeakObservedPawnSpeedCmPerSecond = FMath::Max( PeakObservedPawnSpeedCmPerSecond, PlayerHeading.Size()); StartupTraceSpeedCmPerSecond = PlayerHeading.Size(); UpdatePawnCollisionGate(PlayerPawn, PlayerLastPos, DeltaTime); RecordHeadlessDigMotionFrame(PlayerPawn, PlayerLastPos, DeltaTime); ObserveHeadlessStrateCrossingTest(PlayerLastPos, PlayerPawn); ObserveHeadlessCollisionGateStressTest(PlayerLastPos, PlayerPawn); const FVector StreamingCenterPosition = bHeadlessStreamingTestCenterOverride ? LocalVoxelToWorld(HeadlessStreamingTestCenterVoxel) : PlayerLastPos; UpdateChunksAroundPosition(StreamingCenterPosition, PlayerPawn, PlayerHeading); if (AtmosphereManager) { AtmosphereManager->UpdateForPlayer(PlayerLastPos); } if (ContentManager) { // Distance-based decoration streaming (no LOD pop). Cheap no-op unless the player crosses // a decoration cell boundary or changes strate; otherwise just drains the spawn budget. { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateDecorations); ContentManager->UpdateDecorations(PlayerLastPos); } // Rare hash-lattice landmarks (the "mini-suns") — cheap at any radius (scales with count, not area). // Landmarks now cover F7 set-pieces too (AnchorMode HashLattice/PassageMouth + exclusion). { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateLandmarks); ContentManager->UpdateLandmarks(PlayerLastPos); } // One strate-global ocean plane following the player (water at every LOD, to the horizon). { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateWater); ContentManager->UpdateWater(PlayerLastPos); } } if (DensityVolume) { // Density clipmap for mini-sun shadows: recentre + queue/launch/drain worker fills. // Cheap unless the player crossed a level-0 cell boundary or a carve dirtied cells. TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateDensityVolume); DensityVolume->Update(PlayerLastPos); UpdateTerrainMaterialParams(); } // Bounded-directional mini-sun lighting: stream the nearest orbs into the Light Function MPC. // Independent of the density volume (self-guards on OrbLightMPC); this is the replacement path. UpdateOrbLightMPC(); } else { if (bUseVoxelForgeCore) { // Headless/server worlds without a local pawn still keep the Core fallback region // streamed; this is the same explicit anchor path used by BuildHost validation. UpdateChunksAroundPosition( GetVoxelForgeCoreDefaultCenterWorld(), nullptr, FVector::ZeroVector); } ReleasePawnCollisionGate(); if (APawn* PreviousPawn = PawnTickPrerequisite.Get()) { PreviousPawn->RemoveTickPrerequisiteActor(this); } if (UPawnMovementComponent* PreviousMovement = PawnMovementTickPrerequisite.Get()) { PreviousMovement->RemoveTickPrerequisiteActor(this); } PawnTickPrerequisite.Reset(); PawnMovementTickPrerequisite.Reset(); } // The diagnostic harness uses an explicit command-line token rather than a console command: // Unreal processes startup ExecCmds before this runtime module's console registrations are // guaranteed to exist. Reading the command line here is order-independent, and the actual edit // still waits for a loaded tile and goes through the ordinary modification path below. if (!GVoxelForgeTestModificationCommandLineConsumed) { GVoxelForgeTestModificationCommandLineConsumed = true; const FString CommandLine(FCommandLine::Get()); if (CommandLine.Contains(TEXT("-voxel.TestModification=1"), ESearchCase::IgnoreCase)) { GVoxelForgePendingModificationRadius = 3.0f; GVoxelForgePendingModificationStrength = 10.0f; FString ModificationTarget; if (FParse::Value( *CommandLine, TEXT("voxel.TestModificationTarget="), ModificationTarget) && ModificationTarget.Equals(TEXT("loaded-proof"), ESearchCase::IgnoreCase)) { GVoxelForgeTestModificationLoadedProofTarget = true; } else if (ModificationTarget.Equals(TEXT("loaded-geometry"), ESearchCase::IgnoreCase)) { GVoxelForgeTestModificationLoadedGeometryTarget = true; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestModification] queued radius=%.2f strength=%.2f target=%s"), GVoxelForgePendingModificationRadius, GVoxelForgePendingModificationStrength, GVoxelForgeTestModificationLoadedProofTarget ? TEXT("loaded-proof") : (GVoxelForgeTestModificationLoadedGeometryTarget ? TEXT("loaded-geometry") : TEXT("player"))); GVoxelForgePendingTestModification = 1; } } ProcessPendingChunks(); PollStep1SupportSweepProbes(); ReconcileReadyTransitionVisibility(); PollStep1VisibleApplyProbes(); // Near-field evidence is independent of the full horizon settle point. Record one completion // for each desired-set epoch after this frame's result drain, including moving crossings. RecordLOD0RingReadyIfComplete(); // The startup trace stops at the same point the streaming policy declares itself settled: // every desired key is loaded, no generation task remains, and the game-thread result drain // has run for the frame. This is intentionally after ProcessPendingChunks so request-to-apply // includes the final submission; the separate collision_ready events carry cook latency. if (VoxelForgeStartupTrace::IsActive() && bAllChunksLoaded && PendingTiles.Num() == 0 && bStartupTraceDesiredRecorded && PendingCollisionCooks.Num() == 0 && !bStartupTraceUntilEndPlay && (!bStartupTraceThroughCrossing || !bHeadlessStrateCrossingTest || bHeadlessStrateCrossingTestPassed || bHeadlessStrateCrossingTestFailed) // The desired set being applied is not the player-ready point: the feet tile must have // received its completed LOD0 RMC body first. If no pawn exists, there is no gate to wait // for and the headless trace keeps its old completion behavior. && (!bHasPlayer || bHeadlessStreamingTestCenterOverride || IsPlayerSupportCollisionReady(PlayerPawn, PlayerLastPos))) { TMap SteadyLevelCounts; TMap SteadyLevelMins; TMap SteadyLevelMaxs; TMap LoadedLevelCounts; TMap LoadedLevelVertices; TMap LoadedLevelTriangles; TMap LoadedLevelGeometryBytes; for (const FVoxelTileKey& Key : DesiredSorted) { ++SteadyLevelCounts.FindOrAdd(Key.Level); FIntVector& Min = SteadyLevelMins.FindOrAdd(Key.Level, FIntVector(MAX_int32, MAX_int32, MAX_int32)); FIntVector& Max = SteadyLevelMaxs.FindOrAdd(Key.Level, FIntVector(MIN_int32, MIN_int32, MIN_int32)); Min.X = FMath::Min(Min.X, Key.Coord.X); Min.Y = FMath::Min(Min.Y, Key.Coord.Y); Min.Z = FMath::Min(Min.Z, Key.Coord.Z); Max.X = FMath::Max(Max.X, Key.Coord.X); Max.Y = FMath::Max(Max.Y, Key.Coord.Y); Max.Z = FMath::Max(Max.Z, Key.Coord.Z); } for (const FVoxelTileKey& Key : LoadedTiles) { ++LoadedLevelCounts.FindOrAdd(Key.Level); if (const FLoadedTileGeometryStats* Geometry = LoadedTileGeometry.Find(Key)) { LoadedLevelVertices.FindOrAdd(Key.Level) += Geometry->Vertices; LoadedLevelTriangles.FindOrAdd(Key.Level) += Geometry->Triangles; LoadedLevelGeometryBytes.FindOrAdd(Key.Level) += Geometry->GeometryBytes; } } VoxelForgeStartupTrace::RecordEvent(TEXT("steady_state"), FString::Printf( TEXT("\"desired_tiles\":%d,\"loaded_tiles\":%d,\"collision_ready_tiles\":%d," "\"collision_not_required_tiles\":%d,\"pending_collision_cooks\":%d," "\"pawn_collision_gate_engaged\":%d," "\"obsolete_tile_aborts\":%d,\"desired_epoch\":%u," "\"pending_tiles\":%d,\"pending_unload\":%d,\"generation_epoch\":%u"), DesiredSorted.Num(), LoadedTiles.Num(), CollisionReadyTiles.Num(), CollisionNotRequiredTiles.Num(), PendingCollisionCooks.Num(), bPawnCollisionGateEngaged ? 1 : 0, ObsoleteTileAbortCount, DesiredEpoch, PendingTiles.Num(), PendingUnload.Num(), GenerationEpoch)); TArray SteadyLevels; SteadyLevelCounts.GetKeys(SteadyLevels); SteadyLevels.Sort(); for (const int32 Level : SteadyLevels) { const FIntVector Min = SteadyLevelMins.FindChecked(Level); const FIntVector Max = SteadyLevelMaxs.FindChecked(Level); const int32 ExtentVoxels = CHUNK_SIZE << Level; const double TileExtentMetres = static_cast(ExtentVoxels) * VOXEL_SIZE / 100.0; const double SpanX = static_cast(Max.X - Min.X + 1) * TileExtentMetres; const double SpanY = static_cast(Max.Y - Min.Y + 1) * TileExtentMetres; const double SpanZ = static_cast(Max.Z - Min.Z + 1) * TileExtentMetres; VoxelForgeStartupTrace::RecordEvent(TEXT("steady_desired_level"), FString::Printf( TEXT("\"level\":%d,\"tiles\":%d,\"min_coord\":[%d,%d,%d],\"max_coord\":[%d,%d,%d]," "\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s]"), Level, SteadyLevelCounts.FindChecked(Level), Min.X, Min.Y, Min.Z, Max.X, Max.Y, Max.Z, *FString::SanitizeFloat(TileExtentMetres), *FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ))); VoxelForgeStartupTrace::RecordEvent(TEXT("steady_loaded_level"), FString::Printf( TEXT("\"level\":%d,\"tiles\":%d,\"vertices\":%llu,\"triangles\":%llu,\"geometry_bytes\":%llu"), Level, LoadedLevelCounts.FindRef(Level), static_cast(LoadedLevelVertices.FindRef(Level)), static_cast(LoadedLevelTriangles.FindRef(Level)), static_cast(LoadedLevelGeometryBytes.FindRef(Level)))); } const bool bExitOnSteady = bHeadlessStreamingTestExitOnSteady; VoxelForgeStartupTrace::Finish(TEXT("desired_set_satisfied_and_queue_drained")); if (bExitOnSteady && !bHeadlessStreamingTestExitRequested) { bHeadlessStreamingTestExitRequested = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStreamingTest] steady_exit_requested=1")); FPlatformMisc::RequestExit(false, TEXT("VoxelForge steady streaming measurement complete")); } } // Complete the one-shot diagnostic request only after the player's level-0 centre tile is // loaded and idle. The edit fixture selects an already-visible level-0 geometry tile, so it // measures the normal synchronous centre remesh without depending on a coarse overlay being // visible. No pawn is distinct from a pawn at (0,0,0); retain the actor-location fallback for // headless/editor use without making the world-origin pawn skip streaming. FVector ModificationPosition = bHasPlayer ? PlayerLastPos : GetActorLocation(); FVoxelTileKey ModificationCenterTile( GetTileCoordAtLocalCm(WorldToLocalCm(ModificationPosition)), 0); bool bModificationTargetSelected = false; if (GVoxelForgePendingTestModification != 0 && (GVoxelForgeTestModificationLoadedProofTarget || GVoxelForgeTestModificationLoadedGeometryTarget) && Generator) { // The diagnostic target is deliberately selected from the applied set. That makes the // edit exercise the normal loaded-tile synchronous remesh, while the proof query here is // the same public predicate used by GenerateTileResult before the diff exists. for (const FVoxelTileKey& CandidateTile : LoadedTiles) { if (CandidateTile.Level != 0 || PendingTiles.Contains(CandidateTile)) { continue; } if (GVoxelForgeTestModificationLoadedGeometryTarget && !LoadedTileGeometry.Contains(CandidateTile)) { continue; } if (GVoxelForgeTestModificationLoadedGeometryTarget) { URealtimeMeshComponent* CandidateComponent = TileComponents.FindRef(CandidateTile); if (!CandidateComponent || !CandidateComponent->IsVisible()) { continue; } } const FIntVector CandidateOrigin( CandidateTile.Coord.X * CHUNK_SIZE, CandidateTile.Coord.Y * CHUNK_SIZE, CandidateTile.Coord.Z * CHUNK_SIZE); if (GVoxelForgeTestModificationLoadedProofTarget && !Generator->TryProveSealedSolidTile(CandidateOrigin, 1, CHUNK_SIZE)) { continue; } const FIntVector CandidateCenter( CandidateOrigin.X + CHUNK_SIZE / 2, CandidateOrigin.Y + CHUNK_SIZE / 2, CandidateOrigin.Z + CHUNK_SIZE / 2); ModificationPosition = LocalVoxelToWorld(FVector(CandidateCenter)); ModificationCenterTile = CandidateTile; bModificationTargetSelected = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestModification] player_mine_target tile=(%d,%d,%d) " "local_center=(%d,%d,%d) before_proof=%d"), CandidateTile.Coord.X, CandidateTile.Coord.Y, CandidateTile.Coord.Z, CandidateCenter.X, CandidateCenter.Y, CandidateCenter.Z, GVoxelForgeTestModificationLoadedProofTarget ? 1 : 0); break; } } if (GVoxelForgePendingTestModification != 0 && DiffLayer && ((!GVoxelForgeTestModificationLoadedProofTarget && !GVoxelForgeTestModificationLoadedGeometryTarget) || bModificationTargetSelected) && LoadedTiles.Contains(ModificationCenterTile) && !PendingTiles.Contains(ModificationCenterTile)) { GVoxelForgePendingTestModification = 0; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestModification] applying player_mine=1 tile=(%d,%d,%d) " "position=(%.2f,%.2f,%.2f) radius=%.2f strength=%.2f"), ModificationCenterTile.Coord.X, ModificationCenterTile.Coord.Y, ModificationCenterTile.Coord.Z, ModificationPosition.X, ModificationPosition.Y, ModificationPosition.Z, GVoxelForgePendingModificationRadius, GVoxelForgePendingModificationStrength); CarveAtPosition( ModificationPosition, GVoxelForgePendingModificationRadius, GVoxelForgePendingModificationStrength); const FIntVector ModifiedOrigin( ModificationCenterTile.Coord.X * CHUNK_SIZE, ModificationCenterTile.Coord.Y * CHUNK_SIZE, ModificationCenterTile.Coord.Z * CHUNK_SIZE); const bool bProofSurvivedCarve = Generator && Generator->TryProveSealedSolidTile(ModifiedOrigin, 1, CHUNK_SIZE); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestModification] player_mine_complete tile=(%d,%d,%d) " "after_proof=%d"), ModificationCenterTile.Coord.X, ModificationCenterTile.Coord.Y, ModificationCenterTile.Coord.Z, bProofSurvivedCarve ? 1 : 0); } ProcessUnloadQueue(); UpdateVisualStalenessAudit(); MaybeFinishHeadlessStreamingTest(); if (bTestCeilingViewEnabled) { UWorld* World = GetWorld(); APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; if (PC && !TestCeilingCamera.IsValid()) { FActorSpawnParameters SpawnParams; SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; ACameraActor* Camera = World->SpawnActor( ACameraActor::StaticClass(), TestCeilingCameraWorldPosition, TestCeilingCameraRotation, SpawnParams); if (IsValid(Camera)) { TestCeilingCamera = Camera; PC->SetViewTarget(Camera); UPointLightComponent* CaptureHeadlamp = NewObject(Camera); if (IsValid(CaptureHeadlamp)) { Camera->AddInstanceComponent(CaptureHeadlamp); CaptureHeadlamp->SetupAttachment(Camera->GetRootComponent()); CaptureHeadlamp->SetMobility(EComponentMobility::Movable); CaptureHeadlamp->SetIntensity(6500.0f); CaptureHeadlamp->SetAttenuationRadius(1600.0f); CaptureHeadlamp->SetUseInverseSquaredFalloff(true); CaptureHeadlamp->SetSourceRadius(8.0f); CaptureHeadlamp->SetLightColor(FLinearColor(1.0f, 0.88f, 0.72f)); CaptureHeadlamp->SetCastShadows(true); CaptureHeadlamp->RegisterComponent(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestCeiling] camera_headlamp=1 intensity_lm=6500 radius_cm=1600 " "inverse_square=1 source_radius_cm=8 shadows=1")); } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestCeiling] camera_installed=1 applied_visible_tiles=%llu"), static_cast(AppliedVisibleTileCount)); } } const double CameraNow = FPlatformTime::Seconds(); const double CameraElapsedSeconds = CameraNow - TestCeilingViewBeginSeconds; const bool bCameraMoveWindow = TestCeilingCameraMoveSpeedCmPerSecond > 0.0f && CameraElapsedSeconds >= TestCeilingCameraMoveStartSeconds && (TestCeilingCameraMoveDurationSeconds <= 0.0f || CameraElapsedSeconds < TestCeilingCameraMoveStartSeconds + TestCeilingCameraMoveDurationSeconds); if (TestCeilingCamera.IsValid() && TestCeilingCameraMoveSpeedCmPerSecond > 0.0f) { if (bCameraMoveWindow) { if (TestCeilingCameraLastMoveSeconds > 0.0) { const double MoveDeltaSeconds = FMath::Max( 0.0, CameraNow - TestCeilingCameraLastMoveSeconds); TestCeilingCameraWorldPosition += TestCeilingCameraMoveDirection * TestCeilingCameraMoveSpeedCmPerSecond * static_cast(MoveDeltaSeconds); if (ACameraActor* Camera = TestCeilingCamera.Get()) { Camera->SetActorLocation(TestCeilingCameraWorldPosition); } HeadlessStreamingTestCenterVoxel = WorldToLocalVoxel( TestCeilingCameraWorldPosition); bHeadlessStreamingTestCenterOverride = true; if (bResearchGamePerfEnabled && IsParameterizedResearchFieldActive()) { ResearchGamePerfMovingFrameTimesMs.Add(MoveDeltaSeconds * 1000.0); ResearchGamePerfMovingSeconds += MoveDeltaSeconds; } } TestCeilingCameraLastMoveSeconds = CameraNow; } else { TestCeilingCameraLastMoveSeconds = 0.0; } } const bool bFirstCaptureDue = !bTestCeilingCaptureRequested && CameraElapsedSeconds >= static_cast(TestCeilingCaptureDelaySeconds); const bool bSecondCaptureDue = bTestCeilingCaptureRequested && !bTestCeilingSecondCaptureRequested && TestCeilingSecondCaptureDelaySeconds > 0.0f && CameraElapsedSeconds >= static_cast(TestCeilingSecondCaptureDelaySeconds) && (!bTestResearchProfileSwitchConfigured || bTestResearchProfileSwitchApplied); if (TestCeilingCamera.IsValid() && AppliedVisibleTileCount >= 16 && (bFirstCaptureDue || bSecondCaptureDue)) { const int32 CaptureIndex = bFirstCaptureDue ? 1 : 2; if (CaptureIndex == 1) { bTestCeilingCaptureRequested = true; } else { bTestCeilingSecondCaptureRequested = true; } FVector ActiveViewWorldPosition = FVector::ZeroVector; FRotator ActiveViewRotation = FRotator::ZeroRotator; PC->GetPlayerViewPoint(ActiveViewWorldPosition, ActiveViewRotation); const FVector ActiveViewVoxel = WorldToLocalVoxel(ActiveViewWorldPosition); const float ActiveViewDensity = Generator != nullptr ? Generator->GetDensityAt(ActiveViewVoxel.X, ActiveViewVoxel.Y, ActiveViewVoxel.Z) : 0.0f; int32 ActiveResearchEnabled = 0; int32 ActiveResearchProfile = -1; #if WITH_EDITOR ActiveResearchEnabled = VoxelForgeRedefinedDensityField::IsParameterizedEnabled() ? 1 : 0; ActiveResearchProfile = VoxelForgeRedefinedDensityField::GetParameterSet(); #endif UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestCeiling] active_view_target=%s active_view_world_cm=(%.3f,%.3f,%.3f) " "rotation=(%.3f,%.3f,%.3f) local_vox=(%.3f,%.3f,%.3f) density=%.3f research=%d profile=%d"), *GetNameSafe(PC->GetViewTarget()), ActiveViewWorldPosition.X, ActiveViewWorldPosition.Y, ActiveViewWorldPosition.Z, ActiveViewRotation.Pitch, ActiveViewRotation.Yaw, ActiveViewRotation.Roll, ActiveViewVoxel.X, ActiveViewVoxel.Y, ActiveViewVoxel.Z, ActiveViewDensity, ActiveResearchEnabled, ActiveResearchProfile); // UEngine::Exec is not routed through the game viewport in the command-line game // target, so HighResShot returns false there even with a live render viewport. Call // the viewport API directly; it schedules the same one-frame high-res capture. const bool bCaptureAccepted = GEngine && GEngine->GameViewport && GEngine->GameViewport->GetGameViewport() && GEngine->GameViewport->GetGameViewport()->TakeHighResScreenShot(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestCeiling] capture_index=%d capture_requested=%d elapsed_s=%.3f " "visible_tiles=%llu triangles=%llu"), CaptureIndex, bCaptureAccepted ? 1 : 0, CameraElapsedSeconds, static_cast(AppliedVisibleTileCount), static_cast(AppliedTriangleCount)); } } #if ENABLE_DRAW_DEBUG // Density-volume overlay (step 1a): cyan boxes for solid level-0 cells near the player. if (DensityVolume) { DensityVolume->DebugDraw(); } // Inter-strate passage overlay (cyan path, green=upper / red=lower endpoints). // Points are in voxel coords → world units (×VOXEL_SIZE) → actor space. if (bDebugDrawPassages && StrateManager) { const FTransform Xf = GetActorTransform(); auto ToWorld = [&](const FVector& VoxelPt) { return Xf.TransformPosition(VoxelPt * VOXEL_SIZE); }; auto DrawSeg = [&](const FVector& A, const FVector& B) { DrawDebugLine(GetWorld(), ToWorld(A), ToWorld(B), FColor::Cyan, false, -1.0f, 0, 30.0f); }; for (const FVoxelPassage& P : StrateManager->GetPassages()) { if (P.ControlPoints.Num() >= 2) { for (int32 j = 0; j < P.ControlPoints.Num() - 1; ++j) DrawSeg(P.ControlPoints[j], P.ControlPoints[j + 1]); } else { DrawSeg(P.UpperPoint, P.LowerPoint); } DrawDebugSphere(GetWorld(), ToWorld(P.UpperPoint), P.Radius * VOXEL_SIZE, 12, FColor::Green, false, -1.0f, 0, 4.0f); DrawDebugSphere(GetWorld(), ToWorld(P.LowerPoint), P.Radius * VOXEL_SIZE, 12, FColor::Red, false, -1.0f, 0, 4.0f); } } #endif } //============================================================================= // ESPACE MONDE <-> ESPACE ACTEUR / WORLD <-> ACTOR SPACE //============================================================================= // Le champ de densite est en espace ACTEUR. Ces trois fonctions sont la SEULE frontiere // autorisee entre les coordonnees Unreal et les coordonnees voxel (cf. VoxelWorld.h). // The density field is in ACTOR space. These three are the ONLY sanctioned boundary between // Unreal coordinates and voxel coordinates (see VoxelWorld.h). FVector AVoxelWorld::WorldToLocalCm(FVector WorldPos) const { return GetActorTransform().InverseTransformPosition(WorldPos); } float AVoxelWorld::GetActiveVoxelSizeCm() const { if (!bUseVoxelForgeCore) return VOXEL_SIZE; const float Requested = FMath::IsFinite(CoreVoxelSizeCm) ? CoreVoxelSizeCm : 2.0f * VOXEL_SIZE; const float Clamped = FMath::Clamp(Requested, VOXEL_SIZE, 2.0f * VOXEL_SIZE); return static_cast(FMath::RoundToInt(Clamped * 10000.0f)) / 10000.0f; } float AVoxelWorld::GetLegacyVoxelToActiveVoxelScale() const { // Preserve the physical size of the pre-Core Blueprint brush API in either voxel scale. // Conserver la taille physique des brosses Blueprint historiques dans les deux échelles. return VOXEL_SIZE / GetActiveVoxelSizeCm(); } FIntVector AVoxelWorld::GetTileCoordAtLocalCm(const FVector& LocalPositionCm) const { const double VoxelSize = static_cast(GetActiveVoxelSizeCm()); return FIntVector( FMath::FloorToInt(LocalPositionCm.X / VoxelSize / CHUNK_SIZE), FMath::FloorToInt(LocalPositionCm.Y / VoxelSize / CHUNK_SIZE), FMath::FloorToInt(LocalPositionCm.Z / VoxelSize / CHUNK_SIZE)); } FIntVector AVoxelWorld::GetTileCoordAtVoxelPosition(const FVector& VoxelPosition) const { return FIntVector( FMath::FloorToInt(VoxelPosition.X / CHUNK_SIZE), FMath::FloorToInt(VoxelPosition.Y / CHUNK_SIZE), FMath::FloorToInt(VoxelPosition.Z / CHUNK_SIZE)); } FVector AVoxelWorld::WorldToLocalVoxel(FVector WorldPos) const { return WorldToLocalCm(WorldPos) / GetActiveVoxelSizeCm(); } FVector AVoxelWorld::LocalVoxelToWorld(FVector VoxelPos) const { return GetActorTransform().TransformPosition(VoxelPos * GetActiveVoxelSizeCm()); } bool AVoxelWorld::IsVoxelForgeCoreReady() const { return bUseVoxelForgeCore && VoxelForgeCoreState.IsValid() && VoxelForgeCoreState->bReady.load(std::memory_order_acquire) && !VoxelForgeCoreState->bShuttingDown.load(std::memory_order_acquire); } FVector AVoxelWorld::GetVoxelForgeCoreDefaultCenterWorld() const { return GetActorTransform().TransformPosition(CoreFallbackCenterLocalCm); } bool AVoxelWorld::GetVoxelForgeCoreWorldAssembly( FString& OutCanonicalJson, FString& OutContentHash) const { OutCanonicalJson = VoxelForgeCoreWorldAssemblyJson; OutContentHash = VoxelForgeCoreWorldAssemblyHash; return bUseVoxelForgeCore && !OutCanonicalJson.IsEmpty() && !OutContentHash.IsEmpty(); } bool AVoxelWorld::QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const { OutDensity = 0.0f; if (!IsVoxelForgeCoreReady()) return false; const FVector VoxelPosition = WorldToLocalVoxel(WorldLocation); const TSharedPtr State = VoxelForgeCoreState; if (!State.IsValid()) return false; FReadScopeLock Lock(State->WorldLock); if (!State->World) return false; OutDensity = State->World->densityAt({VoxelPosition.X, VoxelPosition.Y, VoxelPosition.Z}); return FMath::IsFinite(OutDensity); } bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason) { OutFailureReason.Reset(); VoxelForgeCoreWorldAssemblyJson.Reset(); VoxelForgeCoreWorldAssemblyHash.Reset(); if (!bUseVoxelForgeCore) return true; if (!Settings || !StrateManager) { OutFailureReason = TEXT("Settings or the resolved strate layout is missing."); return false; } const int32 ActiveCoreClipRadius = FMath::Clamp(CoreClipRadius, 1, 64); const int32 ActiveCoreMaxClipLevel = FMath::Clamp(CoreMaxClipLevel, 0, 8); const float ActiveCoreVoxelSizeCm = GetActiveVoxelSizeCm(); FString Json; FString ExportError; if (!VoxelForgeCoreAdapter::SerializeResolvedWorld( Settings, StrateManager, ActiveCoreClipRadius, ActiveCoreMaxClipLevel, ActiveCoreVoxelSizeCm, Json, ExportError)) { OutFailureReason = FString::Printf(TEXT("snapshot export failed: %s"), *ExportError); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason); return false; } FTCHARToUTF8 Utf8Json(*Json); std::string CoreError; std::optional Snapshot = VoxelForgeCore::ResolvedWorldSnapshot::FromJson( std::string_view(Utf8Json.Get(), static_cast(Utf8Json.Length())), CoreError); if (!Snapshot) { OutFailureReason = FString::Printf(TEXT("schema-v1 snapshot resolve failed: %s"), UTF8_TO_TCHAR(CoreError.c_str())); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason); return false; } if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly()) { const std::string CanonicalAssembly = Assembly->ToJson(); if (CanonicalAssembly.empty()) { OutFailureReason = TEXT("the generated world assembly could not be serialized canonically."); return false; } VoxelForgeCoreWorldAssemblyJson = UTF8_TO_TCHAR(CanonicalAssembly.c_str()); VoxelForgeCoreWorldAssemblyHash = FString::Printf(TEXT("%016llx"), static_cast(Assembly->contentHash)); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCoreRuntime] world_assembly_hash=%016llx recipe_hash=%016llx plan_hash=%016llx surface_strates=%d island_strates=%d"), static_cast(Assembly->contentHash), static_cast(Assembly->recipe.contentHash), static_cast(Assembly->plan.contentHash), static_cast(Assembly->plan.strates.size()), static_cast(Assembly->floatingIslands.size())); } TSharedPtr State = MakeShared(); State->World = std::make_unique(std::move(*Snapshot)); State->FullResolutionLodCount = State->World->configuration().fullResolutionLodCount; FVector CameraVoxels = FVector::ZeroVector; FVector InitialPlayerPosition = FVector::ZeroVector; if (TryGetPlayerPosition(InitialPlayerPosition)) { CameraVoxels = WorldToLocalCm(InitialPlayerPosition) / GetActiveVoxelSizeCm(); } else if (State->World->configuration().viewerStartFound) { const VoxelForgeCore::Vec3d Start = State->World->configuration().viewerStartVoxels; CameraVoxels = FVector(Start.x, Start.y, Start.z); } CoreFallbackCenterLocalCm = CameraVoxels * GetActiveVoxelSizeCm(); const std::vector InitialDesired = State->World->desiredTiles( {CameraVoxels.X, CameraVoxels.Y, CameraVoxels.Z}); if (InitialDesired.empty()) { OutFailureReason = TEXT("Core produced an empty initial clipmap."); return false; } const int32 IntMax = MAX_int32; const int32 IntMin = MIN_int32; VoxelForgeCore::Vec3i FineMin{IntMax, IntMax, IntMax}; VoxelForgeCore::Vec3i FineMax{IntMin, IntMin, IntMin}; VoxelForgeCore::Vec3i ClipMin{IntMax, IntMax, IntMax}; VoxelForgeCore::Vec3i ClipMax{IntMin, IntMin, IntMin}; auto IncludeExtent = [](int32& Min, int32& Max, int32 Origin, int32 Extent) { Min = FMath::Min(Min, Origin); Max = FMath::Max(Max, Origin + Extent); }; for (const VoxelForgeCore::TileKey& Key : InitialDesired) { const int32 Extent = 32 << Key.lod; const int32 OriginX = Key.coordinate.x * Extent; const int32 OriginY = Key.coordinate.y * Extent; const int32 OriginZ = Key.coordinate.z * Extent; IncludeExtent(ClipMin.x, ClipMax.x, OriginX, Extent); IncludeExtent(ClipMin.y, ClipMax.y, OriginY, Extent); IncludeExtent(ClipMin.z, ClipMax.z, OriginZ, Extent); if (Key.lod < State->FullResolutionLodCount) { IncludeExtent(FineMin.x, FineMax.x, OriginX, Extent); IncludeExtent(FineMin.y, FineMax.y, OriginY, Extent); IncludeExtent(FineMin.z, FineMax.z, OriginZ, Extent); } } if (FineMin.x == IntMax || ClipMin.x == IntMax) { OutFailureReason = TEXT("Core produced invalid initial clipmap bounds."); return false; } VoxelForgeCore::ShapeBuildRequest Request; Request.minVoxel = FineMin; Request.maxVoxel = FineMax; Request.clipMinVoxel = ClipMin; Request.clipMaxVoxel = ClipMax; Request.workerLimit = static_cast(FMath::Clamp(GetMaxConcurrentTasks(), 1, 16)); Request.detail = VoxelForgeCore::DetailVariant::coherent; Request.includeFarWorld = true; VoxelForgeCoreState = State; State->bSnapshotReady.store(true, std::memory_order_release); ActiveTaskCount.fetch_add(1, std::memory_order_relaxed); UE::Tasks::Launch(TEXT("VoxelForgeCoreCompile"), [this, State, Request, InitialDesired, ActiveCoreClipRadius, ActiveCoreMaxClipLevel, ActiveCoreVoxelSizeCm]() { struct FTaskGuard { std::atomic& Counter; ~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_release); } } Guard{ActiveTaskCount}; if (bShuttingDown.load(std::memory_order_acquire) || State->bShuttingDown.load(std::memory_order_acquire)) { return; } std::string Error; bool bCompiled = false; bCompiled = State->World->compile(Request, State->Scheduler, Error); if (bCompiled) { // The initial compile builds into local state and atomically publishes an immutable // shape. Proven native-only tiles may hold shared WorldLock readers during this work; // take the writer only for region retention after publication. // La compilation initiale construit un etat local puis publie une forme immuable. // Les tuiles natives validees gardent le verrou lecteur partage; le verrou ecrivain // ne sert qu'a retenir les regions apres publication. FWriteScopeLock Lock(State->WorldLock); if (State->World) { State->World->retainTileRegions(InitialDesired); FScopeLock RetainedLock(&State->RetainedTilesLock); for (const VoxelForgeCore::TileKey& Key : InitialDesired) { State->RetainedTiles.insert(Key); } } } if (!bCompiled) { State->InitialFailureReason = FString::Printf( TEXT("Core shape compilation failed: %s"), UTF8_TO_TCHAR(Error.c_str())); State->bInitialFailed.store(true, std::memory_order_release); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] shape compile failed: %s"), UTF8_TO_TCHAR(Error.c_str())); return; } State->bReady.store(true, std::memory_order_release); State->bRebuildDesired.store(true, std::memory_order_release); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCoreRuntime] compiled=1 tiles=%d clip_radius=%d max_level=%d voxel_cm=%.4f"), static_cast(InitialDesired.size()), ActiveCoreClipRadius, ActiveCoreMaxClipLevel, ActiveCoreVoxelSizeCm); }, UE::Tasks::ETaskPriority::BackgroundNormal); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCoreRuntime] snapshot queued seed=%d clip_radius=%d max_level=%d initial_tiles=%d"), StrateManager->GetWorldSeed(), ActiveCoreClipRadius, ActiveCoreMaxClipLevel, static_cast(InitialDesired.size())); return true; } void AVoxelWorld::FallBackToLegacyAfterCoreInitializationFailure(const FString& FailureReason) { if (!bUseVoxelForgeCore) return; const FString Reason = FailureReason.IsEmpty() ? TEXT("Core initialization failed without a diagnostic reason.") : FailureReason; if (VoxelForgeCoreState.IsValid()) { VoxelForgeCoreState->bShuttingDown.store(true, std::memory_order_release); VoxelForgeCoreState.Reset(); } VoxelForgeCoreWorldAssemblyJson.Reset(); VoxelForgeCoreWorldAssemblyHash.Reset(); bUseVoxelForgeCore = false; // The Core adapter sets this before validation; legacy generation must not inherit it. // L'adaptateur Core l'active avant validation ; le générateur historique ne doit pas en hériter. if (StrateManager != nullptr) { StrateManager->SetHardTransitionFallbackForAssembly(false); } CoreFallbackCenterLocalCm = FVector::ZeroVector; if (Mesher != nullptr) { Mesher->SetGeometryVoxelSizeCm(VOXEL_SIZE); } if (ContentManager != nullptr && Settings != nullptr) { ContentManager->Initialize(this, StrateManager, Generator, Settings, Settings->GetEffectiveWorldSeed()); ContentManager->ClearAll(); } if (AtmosphereManager != nullptr) { AtmosphereManager->Reset(); } RegenerateAllChunks(); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] Core initialization failed; falling back to the legacy generator. reason=%s"), *Reason); if (GEngine != nullptr) { const FString ScreenMessage = FString::Printf( TEXT("VoxelForge Core could not start: %s Legacy voxel generation is active."), *Reason); GEngine->AddOnScreenDebugMessage(INDEX_NONE, 20.0f, FColor::Red, ScreenMessage); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCoreRuntime] fallback_notice=on_screen seconds=20 reason=%s"), *Reason); } } bool AVoxelWorld::TryGetPlayerPosition(FVector& OutPosition, APawn** OutPawn) const { OutPosition = FVector::ZeroVector; if (OutPawn != nullptr) { *OutPawn = nullptr; } UWorld* World = GetWorld(); APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; APawn* Pawn = PC ? PC->GetPawn() : nullptr; if (!IsValid(Pawn)) { return false; } const FVector Position = Pawn->GetActorLocation(); if (!VoxelMath::IsFinite(Position.X) || !VoxelMath::IsFinite(Position.Y) || !VoxelMath::IsFinite(Position.Z)) { return false; } OutPosition = Position; if (OutPawn != nullptr) { *OutPawn = Pawn; } return true; } FVector AVoxelWorld::GetPlayerPosition() const { FVector Position = FVector::ZeroVector; TryGetPlayerPosition(Position); return Position; } bool AVoxelWorld::GetPlayerSupportTile(APawn* Pawn, const FVector& PlayerPosition, FVoxelTileKey& OutSupportTile) const { if (!IsValid(Pawn)) { return false; } float HalfHeightCm = 0.0f; if (const ACharacter* Character = Cast(Pawn)) { if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent()) { HalfHeightCm = Capsule->GetScaledCapsuleHalfHeight(); } } else { // APawn is not required to be a character. Use its colliding component bounds as a // conservative feet estimate so custom pawn movement still gets the same safety gate. FVector BoundsOrigin = FVector::ZeroVector; FVector BoundsExtent = FVector::ZeroVector; Pawn->GetActorBounds(/*bOnlyCollidingComponents*/ true, BoundsOrigin, BoundsExtent); HalfHeightCm = FMath::Max(0.0f, BoundsExtent.Z); } if (!VoxelMath::IsFinite(HalfHeightCm)) { return false; } // Probe one centimetre below the feet. At an exact chunk boundary this selects the tile // containing the solid surface below, rather than the empty tile immediately above it. FVector LocalFeet = WorldToLocalCm(PlayerPosition); LocalFeet.Z -= HalfHeightCm + 1.0f; OutSupportTile = FVoxelTileKey(GetTileCoordAtLocalCm(LocalFeet), 0); return true; } bool AVoxelWorld::IsTileCollisionReady(const FVoxelTileKey& Tile) const { if (Tile.Level != 0 || !IsTileCollisionDesired(Tile) || !CollisionReadyTiles.Contains(Tile) || !LoadedTiles.Contains(Tile)) { return false; } URealtimeMeshComponent* MeshComp = TileComponents.FindRef(Tile); if (!IsValid(MeshComp)) { return false; } URealtimeMeshSimple* Mesh = MeshComp->GetRealtimeMeshAs(); return Mesh != nullptr && Mesh->GetBodySetup() != nullptr; } bool AVoxelWorld::IsTileCollisionDesired(const FVoxelTileKey& Tile) const { return Tile.Level == 0 && (CollisionDesiredTiles.Contains(Tile) || CollisionGateDesiredTiles.Contains(Tile)); } bool AVoxelWorld::IsCollisionTileUnavailable(const FVoxelTileKey& Tile, const TCHAR*& OutReason) const { OutReason = nullptr; // A tile outside every authored strate is open air. The streamer deliberately does not // request it, so treating its absent collision as pending would make a one-strate world an // impossible gate target. Inter-strate gap chunks are different: they are authored solid // bedrock and must remain blocking until their collision is ready. if (StrateManager != nullptr) { int32 TopChunkZ = 0; int32 BottomChunkZ = 0; const bool bInStrate = StrateManager->GetStrateChunkZBounds( Tile.Coord.Z, TopChunkZ, BottomChunkZ); if (!bInStrate && !StrateManager->IsGapChunk(Tile.Coord)) { OutReason = TEXT("out_of_layout"); return true; } } return false; } bool AVoxelWorld::IsCollisionReadyFromSupportTile( const FVoxelTileKey& InitialTile, FVoxelTileKey* OutSupportTile, const TCHAR** OutUnavailableReason) const { if (OutUnavailableReason != nullptr) { *OutUnavailableReason = nullptr; } FVoxelTileKey CandidateTile = InitialTile; const int32 Direction = CollisionSolidTiles.Contains(InitialTile) ? 1 : -1; // A pawn above an all-air/empty band searches down. If its probe lands inside an all-solid // tile (including an exact chunk-boundary case), search up instead: the MC boundary can be // emitted by the first non-solid tile above it. Never cross an unresolved tile. constexpr int32 MaxSupportSearchTiles = 64; for (int32 Depth = 0; Depth < MaxSupportSearchTiles; ++Depth) { if (IsTileCollisionReady(CandidateTile)) { if (OutSupportTile != nullptr) { *OutSupportTile = CandidateTile; } return true; } const TCHAR* UnavailableReason = nullptr; if (IsCollisionTileUnavailable(CandidateTile, UnavailableReason)) { if (OutSupportTile != nullptr) { *OutSupportTile = CandidateTile; } if (OutUnavailableReason != nullptr) { *OutUnavailableReason = UnavailableReason; } return true; } if (!CollisionNotRequiredTiles.Contains(CandidateTile)) { if (OutSupportTile != nullptr) { *OutSupportTile = CandidateTile; } return false; } CandidateTile.Coord.Z += Direction; } if (OutSupportTile != nullptr) { *OutSupportTile = CandidateTile; } return false; } bool AVoxelWorld::IsPlayerSupportCollisionReady(APawn* Pawn, const FVector& PlayerPosition, FVoxelTileKey* OutSupportTile, const TCHAR** OutUnavailableReason) const { if (OutUnavailableReason != nullptr) { *OutUnavailableReason = nullptr; } FVoxelTileKey CandidateTile; if (!GetPlayerSupportTile(Pawn, PlayerPosition, CandidateTile)) { return false; } return IsCollisionReadyFromSupportTile( CandidateTile, OutSupportTile, OutUnavailableReason); } bool AVoxelWorld::IsPawnCapsuleOverlappingPendingFloor( APawn* Pawn, const FVector& PlayerPosition, const FVector& NextPlayerPosition, FVoxelTileKey* OutPendingTile) const { if (OutPendingTile != nullptr) { *OutPendingTile = FVoxelTileKey(); } if (!IsValid(Pawn)) { return false; } FVoxelTileKey SupportTile; const TCHAR* SupportUnavailableReason = nullptr; const bool bCurrentSupportReady = IsPlayerSupportCollisionReady( Pawn, PlayerPosition, &SupportTile, &SupportUnavailableReason); if (!bCurrentSupportReady && (SupportUnavailableReason != nullptr || SupportTile.Level != 0 || CollisionNotRequiredTiles.Contains(SupportTile))) { return false; } if (bCurrentSupportReady) { const TCHAR* NextSupportUnavailableReason = nullptr; if (IsPlayerSupportCollisionReady( Pawn, NextPlayerPosition, &SupportTile, &NextSupportUnavailableReason) || NextSupportUnavailableReason != nullptr || SupportTile.Level != 0 || CollisionNotRequiredTiles.Contains(SupportTile)) { return false; } } float HalfHeightCm = 0.0f; float RadiusCm = 0.0f; if (const ACharacter* Character = Cast(Pawn)) { if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent()) { HalfHeightCm = Capsule->GetScaledCapsuleHalfHeight(); RadiusCm = Capsule->GetScaledCapsuleRadius(); } } else { FVector BoundsOrigin = FVector::ZeroVector; FVector BoundsExtent = FVector::ZeroVector; Pawn->GetActorBounds(/*bOnlyCollidingComponents*/ true, BoundsOrigin, BoundsExtent); HalfHeightCm = FMath::Max(0.0f, BoundsExtent.Z); RadiusCm = FMath::Max(BoundsExtent.X, BoundsExtent.Y); } if (!VoxelMath::IsFinite(HalfHeightCm) || !VoxelMath::IsFinite(RadiusCm) || HalfHeightCm <= 0.0f || RadiusCm < 0.0f) { return false; } const FVector LocalPosition = WorldToLocalVoxel(PlayerPosition); const FVector LocalNextPosition = WorldToLocalVoxel(NextPlayerPosition); const float HalfHeightVoxels = HalfHeightCm / GetActiveVoxelSizeCm(); const float RadiusVoxels = RadiusCm / GetActiveVoxelSizeCm(); const float TileMinX = static_cast(SupportTile.Coord.X * CHUNK_SIZE); const float TileMaxX = static_cast((SupportTile.Coord.X + 1) * CHUNK_SIZE); const float TileMinY = static_cast(SupportTile.Coord.Y * CHUNK_SIZE); const float TileMaxY = static_cast((SupportTile.Coord.Y + 1) * CHUNK_SIZE); const float TileMinZ = static_cast(SupportTile.Coord.Z * CHUNK_SIZE); const float TileMaxZ = static_cast((SupportTile.Coord.Z + 1) * CHUNK_SIZE); const float CapsuleMinZ = FMath::Min(LocalPosition.Z, LocalNextPosition.Z) - HalfHeightVoxels; const float CapsuleMaxZ = FMath::Max(LocalPosition.Z, LocalNextPosition.Z) + HalfHeightVoxels; const float CapsuleMinX = FMath::Min(LocalPosition.X, LocalNextPosition.X) - RadiusVoxels; const float CapsuleMaxX = FMath::Max(LocalPosition.X, LocalNextPosition.X) + RadiusVoxels; const float CapsuleMinY = FMath::Min(LocalPosition.Y, LocalNextPosition.Y) - RadiusVoxels; const float CapsuleMaxY = FMath::Max(LocalPosition.Y, LocalNextPosition.Y) + RadiusVoxels; const bool bHorizontalOverlap = CapsuleMaxX > TileMinX && CapsuleMinX < TileMaxX && CapsuleMaxY > TileMinY && CapsuleMinY < TileMaxY; const bool bVerticalOverlap = CapsuleMaxZ > TileMinZ && CapsuleMinZ < TileMaxZ; if (!bHorizontalOverlap || !bVerticalOverlap) { return false; } if (OutPendingTile != nullptr) { *OutPendingTile = SupportTile; } return true; } void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTile) { const double RequestCheckSeconds = FPlatformTime::Seconds(); const bool bIsStressLaunchSupport = bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured && HeadlessCollisionGateStressTestLaunchSupportTile.Coord == SupportTile.Coord && HeadlessCollisionGateStressTestLaunchSupportTile.Level == SupportTile.Level; if (bIsStressLaunchSupport) { const bool bPending = PendingTiles.Contains(SupportTile); const bool bLoaded = LoadedTiles.Contains(SupportTile); if (HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds <= 0.0 && (bPending || bLoaded || IsTileCollisionReady(SupportTile))) { HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = RequestCheckSeconds; } if (HeadlessCollisionGateStressTestLaunchSupportReadySeconds <= 0.0 && IsTileCollisionReady(SupportTile)) { HeadlessCollisionGateStressTestLaunchSupportReadySeconds = RequestCheckSeconds; } } if (SupportTile.Level != 0 || IsTileCollisionReady(SupportTile) || CollisionNotRequiredTiles.Contains(SupportTile)) { return; } if (!bStartupCollisionGateTimingCaptured) { StartupCollisionGateSupportTile = SupportTile; bStartupCollisionGateTimingCaptured = true; StartupCollisionGateFirstCheckSeconds = RequestCheckSeconds; } if (!bPawnGateSupportRequestReported || PawnGateRequestedSupportTile.Coord != SupportTile.Coord || PawnGateRequestedSupportTile.Level != SupportTile.Level) { PawnGateRequestedSupportTile = SupportTile; bPawnGateSupportRequestReported = false; } // A gate check can run in BeginPlay, before the first desired set, and before the rebuild on a // teleport/crossing tick. Submit the level-0 collision tile immediately; the desired rebuild // below adopts the same key through PendingTiles and IsDesired, so it is neither duplicated nor // cancelled when the full clipmap arrives. const bool bDesiredAtRequest = DesiredStamp != 0 && IsDesired(SupportTile); if (bHeadlessCollisionGateStressDeepDrop && SupportTile.Coord.Z < 0 && !bPawnGateSupportRequestReported) { FIntVector RingMin = FIntVector::ZeroValue; FIntVector RingMax = FIntVector::ZeroValue; VoxelClipmapDesiredTiles::AlignedRingBounds( CurrentCenterChunk, Settings ? FMath::Max(1, Settings->ClipRadius) : 6, 2, RingMin, RingMax); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGateStressTest] gate_context target=(%d,%d,%d) " "desired=%d center=(%d,%d,%d) center_override=%d override_vox=(%.1f,%.1f,%.1f) " "ring_min=(%d,%d,%d) ring_max=(%d,%d,%d) anchors=%d"), SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z, bDesiredAtRequest ? 1 : 0, CurrentCenterChunk.X, CurrentCenterChunk.Y, CurrentCenterChunk.Z, bHeadlessStreamingTestCenterOverride ? 1 : 0, HeadlessStreamingTestCenterVoxel.X, HeadlessStreamingTestCenterVoxel.Y, HeadlessStreamingTestCenterVoxel.Z, RingMin.X, RingMin.Y, RingMin.Z, RingMax.X, RingMax.Y, RingMax.Z, StreamingAnchors.Num()); } bool bNeedsDesiredRebuild = !bDesiredAtRequest; constexpr int32 CollisionGatePrefetchDownTiles = 4; for (auto It = CollisionGateDesiredTiles.CreateIterator(); It; ++It) { const FVoxelTileKey& ExistingTile = *It; const int32 RelativeZ = SupportTile.Coord.Z - ExistingTile.Coord.Z; if (ExistingTile.Coord.X != SupportTile.Coord.X || ExistingTile.Coord.Y != SupportTile.Coord.Y || RelativeZ < 0 || RelativeZ >= CollisionGatePrefetchDownTiles) { bNeedsDesiredRebuild |= IsDesired(ExistingTile); It.RemoveCurrent(); } } for (int32 Offset = 0; Offset < CollisionGatePrefetchDownTiles; ++Offset) { FVoxelTileKey ColumnTile = SupportTile; ColumnTile.Coord.Z -= Offset; const TCHAR* UnavailableReason = nullptr; if (IsTileCollisionReady(ColumnTile) || CollisionNotRequiredTiles.Contains(ColumnTile) || IsCollisionTileUnavailable(ColumnTile, UnavailableReason)) { continue; } if (!CollisionGateDesiredTiles.Contains(ColumnTile)) { CollisionGateDesiredTiles.Add(ColumnTile); bNeedsDesiredRebuild |= !IsDesired(ColumnTile); } if (LoadedTiles.Contains(ColumnTile) && !PendingCollisionCooks.Contains(ColumnTile)) { // A render tile may already be loaded without a BodySetup. Upgrade it through the // normal generation/apply path so the gate never mistakes LoadedTiles for collision. DirtyRemeshQueue.Add(ColumnTile); bAllChunksLoaded = false; } } bForceDesiredRebuild |= bNeedsDesiredRebuild; // Core surface tiles must enter through the desired-set region compile before collision work. // Les tuiles de surface Core doivent passer par la compilation de région avant la collision. if (bUseVoxelForgeCore && !bDesiredAtRequest) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGate] support_request_deferred tile=(%d,%d,%d) " "reason=core_region_admission desired=0"), SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z); bPawnGateSupportRequestReported = true; return; } // Preserve the established critical floor ordering when it already belongs to this desired // epoch. Before the first build there is no array to promote into; LoadTile below still uses // the same CollisionCritical work class and priority queue. if (bDesiredAtRequest) { CriticalDesiredTiles.Remove(SupportTile); CriticalDesiredTiles.Insert(SupportTile, 0); } if (PendingTiles.Contains(SupportTile) || LoadedTiles.Contains(SupportTile)) { if (bIsStressLaunchSupport && HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds <= 0.0) { HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = RequestCheckSeconds; } if (bStartupCollisionGateTimingCaptured && StartupCollisionGateSupportTile.Coord == SupportTile.Coord && StartupCollisionGateSupportTile.Level == SupportTile.Level && StartupCollisionGateSubmittedSeconds <= 0.0) { // A prior normal streaming submit is already an accepted request. The exact earlier // submission is outside the gate's sample, so report zero gate-to-submit latency. StartupCollisionGateSubmittedSeconds = RequestCheckSeconds; } if (!bPawnGateSupportRequestReported) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGate] support_request_present tile=(%d,%d,%d) " "pending=%d loaded=%d desired=%d first_check_to_submit_s=%.6f"), SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z, PendingTiles.Contains(SupportTile) ? 1 : 0, LoadedTiles.Contains(SupportTile) ? 1 : 0, bDesiredAtRequest ? 1 : 0, bStartupCollisionGateTimingCaptured && StartupCollisionGateSupportTile.Coord == SupportTile.Coord && StartupCollisionGateSupportTile.Level == SupportTile.Level && StartupCollisionGateSubmittedSeconds > 0.0 ? FMath::Max(0.0, StartupCollisionGateSubmittedSeconds - StartupCollisionGateFirstCheckSeconds) : -1.0); bPawnGateSupportRequestReported = true; } return; } const int32 PendingBefore = PendingTiles.Num(); LoadTile(SupportTile, EVoxelTileWorkPriority::CollisionCritical); const bool bSubmitted = PendingTiles.Num() > PendingBefore || PendingTiles.Contains(SupportTile); if (bSubmitted) { bAllChunksLoaded = false; if (bIsStressLaunchSupport && HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds <= 0.0) { HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = FPlatformTime::Seconds(); } } if (!bPawnGateSupportRequestReported || bSubmitted) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGate] support_request tile=(%d,%d,%d) " "submitted=%d desired=%d priority=collision_critical " "first_check_to_submit_s=%.6f"), SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z, bSubmitted ? 1 : 0, bDesiredAtRequest ? 1 : 0, bStartupCollisionGateTimingCaptured && StartupCollisionGateSupportTile.Coord == SupportTile.Coord && StartupCollisionGateSupportTile.Level == SupportTile.Level && StartupCollisionGateSubmittedSeconds > 0.0 ? FMath::Max(0.0, StartupCollisionGateSubmittedSeconds - StartupCollisionGateFirstCheckSeconds) : -1.0); bPawnGateSupportRequestReported = true; } } void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition, float DeltaTime) { if (!IsValid(Pawn) || bShuttingDown.load(std::memory_order_relaxed)) { return; } // This is a startup snapshot of voxel.CollisionGate. The hot path never reads the CVar. if (!bCollisionGateEnabled) { ReleasePawnCollisionGate(); return; } // Tick prerequisites are kept even while the gate is open. That makes the next movement // decision happen before pawn physics, including the first frame after a tile-boundary crossing. if (PawnTickPrerequisite.Get() != Pawn) { if (APawn* PreviousPawn = PawnTickPrerequisite.Get()) { PreviousPawn->RemoveTickPrerequisiteActor(this); } if (UPawnMovementComponent* PreviousMovement = PawnMovementTickPrerequisite.Get()) { PreviousMovement->RemoveTickPrerequisiteActor(this); } PawnTickPrerequisite = Pawn; Pawn->AddTickPrerequisiteActor(this); bPawnGateUnsupportedReported = false; bPawnGateUnavailableReported = false; } // The actor prerequisite covers the pawn tick; attach the same prerequisite directly to its // movement component because CharacterMovementComponent has its own component tick function. // Re-check this even for the same pawn: possession/setup code can replace the component late. UPawnMovementComponent* CurrentMovement = Pawn->GetMovementComponent(); if (PawnMovementTickPrerequisite.Get() != CurrentMovement) { if (UPawnMovementComponent* PreviousMovement = PawnMovementTickPrerequisite.Get()) { PreviousMovement->RemoveTickPrerequisiteActor(this); } if (CurrentMovement != nullptr) { CurrentMovement->AddTickPrerequisiteActor(this); } PawnMovementTickPrerequisite = CurrentMovement; } if (bPawnCollisionGateEngaged && CollisionGatedPawn.Get() == Pawn && CollisionGatedCharacterMovement.Get() != CurrentMovement && CollisionGatedPawnMovement.Get() != CurrentMovement) { // The component was replaced while the pawn was held. Restore the old component (if it // still exists) and let the normal path engage the replacement against the same readiness // decision below. ReleasePawnCollisionGate(); } if (bPawnCollisionGateEngaged && CollisionGatedPawn.Get() != Pawn) { ReleasePawnCollisionGate(); } FVoxelTileKey CurrentSupportTile; const TCHAR* CurrentSupportUnavailableReason = nullptr; const bool bCurrentSupportReady = IsPlayerSupportCollisionReady( Pawn, PlayerPosition, &CurrentSupportTile, &CurrentSupportUnavailableReason); if (CurrentSupportUnavailableReason != nullptr) { if (!bPawnGateUnavailableReported) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGate] nonblocking pawn=%s reason=%s " "support=(%d,%d,%d); tile will not be produced, no collision to wait for"), *Pawn->GetName(), CurrentSupportUnavailableReason, CurrentSupportTile.Coord.X, CurrentSupportTile.Coord.Y, CurrentSupportTile.Coord.Z); } bPawnGateUnavailableReported = true; ReleasePawnCollisionGate(); return; } else { bPawnGateUnavailableReported = false; } if (!bCurrentSupportReady && !bStartupCollisionGateTimingCaptured) { StartupCollisionGateSupportTile = CurrentSupportTile; bStartupCollisionGateTimingCaptured = true; StartupCollisionGateFirstCheckSeconds = FPlatformTime::Seconds(); } // Predict one movement step before releasing a ready current tile. This requests a neighboring // support tile before horizontal movement can carry the capsule across its boundary. FVector NextPlayerPosition = PlayerPosition; FVector Velocity = Pawn->GetVelocity(); float GravityZ = 0.0f; if (const ACharacter* Character = Cast(Pawn)) { if (const UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) { Velocity = CharacterMovement->Velocity; GravityZ = CharacterMovement->GetGravityZ(); } } if (DeltaTime > 0.0f) { NextPlayerPosition += Velocity * DeltaTime; if (Velocity.Z <= KINDA_SMALL_NUMBER && GravityZ < 0.0f) { NextPlayerPosition.Z += 0.5f * GravityZ * DeltaTime * DeltaTime; } } FVoxelTileKey PredictedSupportTile; const TCHAR* PredictedSupportUnavailableReason = nullptr; const bool bPredictedSupportReady = IsPlayerSupportCollisionReady( Pawn, NextPlayerPosition, &PredictedSupportTile, &PredictedSupportUnavailableReason); if (!bCurrentSupportReady) { // Request collision even when the pawn is moving up and the gate will not hold it. A spawn // or teleport should not have to wait for downward velocity before its floor is queued. RequestCollisionGateSupportTile(CurrentSupportTile); } else if (!bPredictedSupportReady && PredictedSupportUnavailableReason == nullptr && PredictedSupportTile.Level == 0 && !CollisionNotRequiredTiles.Contains(PredictedSupportTile)) { RequestCollisionGateSupportTile(PredictedSupportTile); } // An unresolved tile above a rising pawn is not a landing hazard. Keep its request warm, but // let upward movement continue without entering the temporary collision-hold movement mode. if (Velocity.Z > KINDA_SMALL_NUMBER) { ReleasePawnCollisionGate(); return; } FVoxelTileKey PendingFloorTile; if (!IsPawnCapsuleOverlappingPendingFloor( Pawn, PlayerPosition, NextPlayerPosition, &PendingFloorTile)) { ReleasePawnCollisionGate(); return; } if (!bPawnCollisionGateEngaged) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGate] waiting pawn=%s current_support=(%d,%d,%d) ready=%d " "hold_movement=1"), *Pawn->GetName(), CurrentSupportTile.Coord.X, CurrentSupportTile.Coord.Y, CurrentSupportTile.Coord.Z, bCurrentSupportReady ? 1 : 0); } EngagePawnCollisionGate(Pawn); } void AVoxelWorld::EngagePawnCollisionGate(APawn* Pawn) { if (!IsValid(Pawn)) { return; } if (bPawnCollisionGateEngaged) { // Keep the whole pawn stopped until the support tile has collision. Saving/restoring the // pre-gate velocity lets normal movement resume once the tile is ready. if (UCharacterMovementComponent* CharacterMovement = CollisionGatedCharacterMovement.Get()) { if (CharacterMovement->MovementMode != MOVE_None) { CharacterMovement->SetMovementMode(MOVE_None); } CharacterMovement->GravityScale = 0.0f; CharacterMovement->StopMovementImmediately(); } else if (UPawnMovementComponent* PawnMovement = CollisionGatedPawnMovement.Get()) { if (bSavedPawnMovementWasActive && PawnMovement->IsActive()) { PawnMovement->Deactivate(); } PawnMovement->StopMovementImmediately(); } return; } CollisionGatedPawn = Pawn; if (ACharacter* Character = Cast(Pawn)) { if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) { CollisionGatedCharacterMovement = CharacterMovement; SavedCharacterGravityScale = CharacterMovement->GravityScale; bSavedCharacterGravityScale = true; SavedCharacterMovementMode = static_cast(CharacterMovement->MovementMode); SavedCharacterCustomMovementMode = CharacterMovement->CustomMovementMode; bSavedCharacterMovementMode = true; SavedGateVelocity = CharacterMovement->Velocity; bSavedGateVelocity = true; CharacterMovement->SetMovementMode(MOVE_None); CharacterMovement->GravityScale = 0.0f; CharacterMovement->StopMovementImmediately(); CharacterMovement->UpdateComponentVelocity(); bPawnCollisionGateEngaged = true; PawnGateEngagedAtSeconds = FPlatformTime::Seconds(); return; } } if (UPawnMovementComponent* PawnMovement = Pawn->GetMovementComponent()) { CollisionGatedPawnMovement = PawnMovement; SavedGateVelocity = PawnMovement->Velocity; bSavedGateVelocity = true; bSavedPawnMovementWasActive = PawnMovement->IsActive(); if (bSavedPawnMovementWasActive) { PawnMovement->Deactivate(); } PawnMovement->StopMovementImmediately(); PawnMovement->UpdateComponentVelocity(); bPawnCollisionGateEngaged = true; PawnGateEngagedAtSeconds = FPlatformTime::Seconds(); return; } if (!bPawnGateUnsupportedReported) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeCollisionGate] pawn=%s has no movement component; cannot gate it " "while support collision cooks"), *Pawn->GetName()); bPawnGateUnsupportedReported = true; } } void AVoxelWorld::ReleasePawnCollisionGate() { if (!bPawnCollisionGateEngaged) { CollisionGatedPawn.Reset(); CollisionGatedPawnMovement.Reset(); CollisionGatedCharacterMovement.Reset(); bSavedCharacterGravityScale = false; bSavedCharacterMovementMode = false; bSavedGateVelocity = false; SavedGateVelocity = FVector::ZeroVector; bSavedPawnMovementWasActive = false; bPawnGateSupportRequestReported = false; PawnGateEngagedAtSeconds = 0.0; return; } if (UCharacterMovementComponent* CharacterMovement = CollisionGatedCharacterMovement.Get()) { if (bSavedCharacterMovementMode) { CharacterMovement->SetMovementMode( static_cast(SavedCharacterMovementMode), SavedCharacterCustomMovementMode); } if (bSavedCharacterGravityScale) { CharacterMovement->GravityScale = SavedCharacterGravityScale; } if (bSavedGateVelocity) { CharacterMovement->Velocity = SavedGateVelocity; } CharacterMovement->UpdateComponentVelocity(); } else if (UPawnMovementComponent* PawnMovement = CollisionGatedPawnMovement.Get()) { if (bSavedPawnMovementWasActive && !PawnMovement->IsActive()) { PawnMovement->Activate(); } if (bSavedGateVelocity) { PawnMovement->Velocity = SavedGateVelocity; } PawnMovement->UpdateComponentVelocity(); } if (APawn* Pawn = CollisionGatedPawn.Get()) { const double HoldSeconds = PawnGateEngagedAtSeconds > 0.0 ? FMath::Max(0.0, FPlatformTime::Seconds() - PawnGateEngagedAtSeconds) : 0.0; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGate] released pawn=%s hold_s=%.6f"), *Pawn->GetName(), HoldSeconds); } CollisionGatedPawn.Reset(); CollisionGatedPawnMovement.Reset(); CollisionGatedCharacterMovement.Reset(); bSavedCharacterGravityScale = false; bSavedCharacterMovementMode = false; bSavedGateVelocity = false; SavedGateVelocity = FVector::ZeroVector; bSavedPawnMovementWasActive = false; bPawnCollisionGateEngaged = false; PawnGateEngagedAtSeconds = 0.0; bPawnGateSupportRequestReported = false; } void AVoxelWorld::BindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh) { if (Mesh == nullptr || CollisionBodyUpdatedHandles.Contains(Mesh)) { return; } CollisionBodyUpdatedHandles.Add( Mesh, Mesh->OnCollisionBodyUpdated().AddUObject( this, &AVoxelWorld::HandleRealtimeMeshCollisionBodyUpdated)); } void AVoxelWorld::UnbindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh) { if (Mesh == nullptr) { return; } if (FDelegateHandle* Handle = CollisionBodyUpdatedHandles.Find(Mesh)) { Mesh->OnCollisionBodyUpdated().Remove(*Handle); CollisionBodyUpdatedHandles.Remove(Mesh); } } void AVoxelWorld::HandleRealtimeMeshCollisionBodyUpdated(URealtimeMesh* Mesh, UBodySetup* BodySetup) { // RMC dispatches this event on the game thread after installing BodySetup. Keep the future as // the per-request authority, but use the native delegate as an independent completion witness // and diagnostic boundary (it has no tile/request serial of its own). if (!IsInGameThread()) { return; } for (TPair& Pair : PendingCollisionCooks) { FPendingCollisionCook& Pending = Pair.Value; if (Pending.Mesh.Get() == Mesh && BodySetup != nullptr) { Pending.bBodyUpdatedEventSeen = true; Pending.BodyUpdatedEventCycles = FPlatformTime::Cycles64(); Pending.BodyUpdatedEventFrame = GFrameCounter; if (GVoxelForgeStep1CollisionProbe != 0) { APawn* Pawn = nullptr; FVector PlayerPosition = FVector::ZeroVector; FVoxelTileKey CurrentSupportTile; const bool bHaveCurrentSupport = TryGetPlayerPosition(PlayerPosition, &Pawn) && GetPlayerSupportTile(Pawn, PlayerPosition, CurrentSupportTile); if ((bHaveCurrentSupport && CurrentSupportTile == Pair.Key) || Pending.bStep1SupportCohort) { const URealtimeMeshComponent* Component = TileComponents.FindRef(Pair.Key); Pending.bPhysicsStateReadyAtBodyUpdate = IsValid(Component) && Component->IsPhysicsStateCreated(); if (Pending.bPhysicsStateReadyAtBodyUpdate) { Pending.PhysicsStateReadyCycles = FPlatformTime::Cycles64(); } FStep1SupportSweepProbe& Probe = Step1SupportSweepProbes.Add(Pair.Key); Probe.CollisionRequestCycles = Pending.CollisionSubmittedCycles; Probe.BodyUpdatedCycles = Pending.BodyUpdatedEventCycles; Probe.PhysicsStateReadyCycles = Pending.PhysicsStateReadyCycles; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionProbe] stage=body_updated tile=(%d,%d,%d) " "request_to_body_ms=%.3f physics_ready=%d body_to_physics_ms=%.3f"), Pair.Key.Coord.X, Pair.Key.Coord.Y, Pair.Key.Coord.Z, FPlatformTime::ToSeconds64( Probe.BodyUpdatedCycles - Probe.CollisionRequestCycles) * 1000.0, Pending.bPhysicsStateReadyAtBodyUpdate ? 1 : 0, Pending.bPhysicsStateReadyAtBodyUpdate ? FPlatformTime::ToSeconds64( Probe.PhysicsStateReadyCycles - Probe.BodyUpdatedCycles) * 1000.0 : -1.0); PollStep1SupportSweepProbes(); } } return; } } } void AVoxelWorld::PollStep1SupportSweepProbes() { if (GVoxelForgeStep1CollisionProbe == 0 || Step1SupportSweepProbes.IsEmpty()) { return; } APawn* Pawn = nullptr; FVector PlayerPosition = FVector::ZeroVector; FVoxelTileKey CurrentSupportTile; if (!TryGetPlayerPosition(PlayerPosition, &Pawn)) { return; } const bool bHaveCurrentSupportTile = GetPlayerSupportTile( Pawn, PlayerPosition, CurrentSupportTile); const uint64 NowCycles = FPlatformTime::Cycles64(); for (auto It = Step1SupportSweepProbes.CreateIterator(); It; ++It) { FStep1SupportSweepProbe& Probe = It.Value(); URealtimeMeshComponent* Component = TileComponents.FindRef(It.Key()); if (!IsValid(Component)) { It.RemoveCurrent(); continue; } if (Component->IsPhysicsStateCreated() && Probe.PhysicsStateReadyCycles == 0) { Probe.PhysicsStateReadyCycles = NowCycles; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionProbe] stage=physics_ready tile=(%d,%d,%d) " "request_to_physics_ms=%.3f body_to_physics_ms=%.3f"), It.Key().Coord.X, It.Key().Coord.Y, It.Key().Coord.Z, FPlatformTime::ToSeconds64( Probe.PhysicsStateReadyCycles - Probe.CollisionRequestCycles) * 1000.0, FPlatformTime::ToSeconds64( Probe.PhysicsStateReadyCycles - Probe.BodyUpdatedCycles) * 1000.0); } ACharacter* Character = Cast(Pawn); UCapsuleComponent* Capsule = Character ? Character->GetCapsuleComponent() : nullptr; UWorld* World = GetWorld(); if (Probe.PhysicsStateReadyCycles != 0 && IsValid(Capsule) && World != nullptr) { const float Radius = Capsule->GetScaledCapsuleRadius(); const float HalfHeight = Capsule->GetScaledCapsuleHalfHeight(); const FVector Start = Pawn->GetActorLocation() + FVector(0.0, 0.0, 5.0); const FVector End = Start - FVector(0.0, 0.0, 200.0); const FCollisionShape CapsuleShape = FCollisionShape::MakeCapsule( Radius, FMath::Max(Radius, HalfHeight - 2.0f)); FCollisionQueryParams QueryParams( SCENE_QUERY_STAT(VoxelForgeStep1SupportSweep), false, Pawn); FHitResult Hit; const bool bHit = World->SweepSingleByChannel( Hit, Start, End, FQuat::Identity, ECC_Pawn, CapsuleShape, QueryParams); FHitResult ObjectHit; FCollisionObjectQueryParams ObjectQueryParams; ObjectQueryParams.AddObjectTypesToQuery(Component->GetCollisionObjectType()); const bool bObjectHit = World->SweepSingleByObjectType( ObjectHit, Start, End, FQuat::Identity, ObjectQueryParams, CapsuleShape, QueryParams); if (!Probe.bFirstSweepAttemptLogged) { const UPrimitiveComponent* HitComponent = Hit.GetComponent(); const UPrimitiveComponent* ObjectHitComponent = ObjectHit.GetComponent(); const FBoxSphereBounds ComponentBounds = Component->Bounds; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionProbe] stage=first_sweep_attempt tile=(%d,%d,%d) " "pawn_channel_hit=%d pawn_channel_target=%d pawn_hit_component=%s " "object_query_hit=%d object_query_target=%d object_hit_component=%s " "collision_enabled=%d pawn_response=%d object_type=%d physics_state=%d " "pawn=(%.1f,%.1f,%.1f) sweep_start_z=%.1f sweep_end_z=%.1f " "component_bounds=(%.1f,%.1f,%.1f;%.1f,%.1f,%.1f) " "current_support_match=%d"), It.Key().Coord.X, It.Key().Coord.Y, It.Key().Coord.Z, bHit ? 1 : 0, (bHit && HitComponent == Component) ? 1 : 0, HitComponent ? *HitComponent->GetName() : TEXT("none"), bObjectHit ? 1 : 0, (bObjectHit && ObjectHitComponent == Component) ? 1 : 0, ObjectHitComponent ? *ObjectHitComponent->GetName() : TEXT("none"), static_cast(Component->GetCollisionEnabled()), static_cast(Component->GetCollisionResponseToChannel(ECC_Pawn)), static_cast(Component->GetCollisionObjectType()), Component->IsPhysicsStateCreated() ? 1 : 0, PlayerPosition.X, PlayerPosition.Y, PlayerPosition.Z, Start.Z, End.Z, ComponentBounds.Origin.X, ComponentBounds.Origin.Y, ComponentBounds.Origin.Z, ComponentBounds.BoxExtent.X, ComponentBounds.BoxExtent.Y, ComponentBounds.BoxExtent.Z, bHaveCurrentSupportTile && CurrentSupportTile == It.Key() ? 1 : 0); Probe.bFirstSweepAttemptLogged = true; } if (bHit && Hit.GetComponent() == Component) { const UPrimitiveComponent* ExactHitComponent = Hit.GetComponent(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionProbe] stage=first_support_capsule_sweep tile=(%d,%d,%d) " "pawn_channel_target=1 target_component=%s hit_component=%s " "current_support_match=%d request_to_sweep_ms=%.3f " "body_to_sweep_ms=%.3f hit_z=%.3f"), It.Key().Coord.X, It.Key().Coord.Y, It.Key().Coord.Z, *Component->GetName(), ExactHitComponent ? *ExactHitComponent->GetName() : TEXT("none"), bHaveCurrentSupportTile && CurrentSupportTile == It.Key() ? 1 : 0, FPlatformTime::ToSeconds64(NowCycles - Probe.CollisionRequestCycles) * 1000.0, FPlatformTime::ToSeconds64(NowCycles - Probe.BodyUpdatedCycles) * 1000.0, Hit.ImpactPoint.Z); It.RemoveCurrent(); continue; } } if (Probe.BodyUpdatedCycles != 0 && NowCycles >= Probe.BodyUpdatedCycles && FPlatformTime::ToSeconds64(NowCycles - Probe.BodyUpdatedCycles) > 30.0) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeCollisionProbe] stage=sweep_timeout tile=(%d,%d,%d) " "physics_ready=%d support_match=%d body_to_timeout_ms=%.3f"), It.Key().Coord.X, It.Key().Coord.Y, It.Key().Coord.Z, Component->IsPhysicsStateCreated() ? 1 : 0, bHaveCurrentSupportTile && CurrentSupportTile == It.Key() ? 1 : 0, FPlatformTime::ToSeconds64(NowCycles - Probe.BodyUpdatedCycles) * 1000.0); It.RemoveCurrent(); } } } void AVoxelWorld::PollStep1VisibleApplyProbes() { if (GVoxelForgeStep1PrequeueVisibleLOD0 <= 0 || Step1PendingVisibleApplyProbes.IsEmpty()) { return; } const uint64 NowCycles = FPlatformTime::Cycles64(); for (auto It = Step1PendingVisibleApplyProbes.CreateIterator(); It; ++It) { URealtimeMeshComponent* Component = TileComponents.FindRef(It.Key()); if (!IsValid(Component)) { It.RemoveCurrent(); continue; } const float CurrentLastRenderTime = Component->GetLastRenderTime(); if (Component->IsVisible() && CurrentLastRenderTime > It.Value().LastRenderTimeBeforeApply + SMALL_NUMBER) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeFirstVisible] tile=(%d,%d,%d) level=0 " "applied_frame=%llu visible_frame=%llu frame_delta=%llu apply_to_visible_ms=%.3f"), It.Key().Coord.X, It.Key().Coord.Y, It.Key().Coord.Z, static_cast(It.Value().AppliedFrame), static_cast(GFrameCounter), static_cast(GFrameCounter - It.Value().AppliedFrame), FPlatformTime::ToSeconds64(NowCycles - It.Value().AppliedCycles) * 1000.0); It.RemoveCurrent(); continue; } if (NowCycles >= It.Value().AppliedCycles && FPlatformTime::ToSeconds64(NowCycles - It.Value().AppliedCycles) > 10.0) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeFirstVisible] tile=(%d,%d,%d) level=0 state=not_seen " "applied_frame=%llu waited_ms=%.3f visible=%d"), It.Key().Coord.X, It.Key().Coord.Y, It.Key().Coord.Z, static_cast(It.Value().AppliedFrame), FPlatformTime::ToSeconds64(NowCycles - It.Value().AppliedCycles) * 1000.0, Component->IsVisible() ? 1 : 0); It.RemoveCurrent(); } } } void AVoxelWorld::RecordLOD0ReadySample( uint64 RequestStartCycles, uint64 GenerationStartCycles, uint64 GenerationEndCycles, uint64 ApplyStartCycles, uint64 ReadyCycles, uint64 CollisionSubmittedCycles) { if (RequestStartCycles == 0 || ReadyCycles < RequestStartCycles) { return; } const auto SecondsBetween = [](uint64 Later, uint64 Earlier) -> double { return Later >= Earlier ? FPlatformTime::ToSeconds64(Later - Earlier) : 0.0; }; FStreamingLatencySample Sample; Sample.RequestToReadySeconds = SecondsBetween(ReadyCycles, RequestStartCycles); Sample.GenerationSeconds = (GenerationStartCycles != 0 && GenerationEndCycles >= GenerationStartCycles) ? SecondsBetween(GenerationEndCycles, GenerationStartCycles) : 0.0; // Queue wait is deliberately only the time before generation plus the result wait before the // game-thread apply. Mesh upload and the RMC collision cook stay in request-to-ready but are not // mislabeled as scheduler time. const double WorkerQueueSeconds = GenerationStartCycles != 0 ? SecondsBetween(GenerationStartCycles, RequestStartCycles) : 0.0; const double ResultQueueSeconds = GenerationEndCycles != 0 && ApplyStartCycles != 0 ? SecondsBetween(ApplyStartCycles, GenerationEndCycles) : (GenerationStartCycles == 0 ? SecondsBetween(ApplyStartCycles, RequestStartCycles) : 0.0); Sample.QueueWaitSeconds = WorkerQueueSeconds + ResultQueueSeconds; Sample.CollisionCookSeconds = CollisionSubmittedCycles != 0 ? SecondsBetween(ReadyCycles, CollisionSubmittedCycles) : 0.0; LOD0ReadySamples.Add(Sample); } void AVoxelWorld::RecordLOD0RingReadyIfComplete() { if (!VoxelForgeStartupTrace::IsActive() || DesiredEpoch == 0 || LastLOD0RingReadyEpoch == DesiredEpoch) { return; } int32 DesiredLOD0Count = 0; for (const FVoxelTileKey& Key : DesiredSorted) { if (Key.Level != 0) continue; ++DesiredLOD0Count; if (!LoadedTiles.Contains(Key)) { return; } } if (DesiredLOD0Count == 0) { return; } // LoadedTiles remains populated during an async remesh, so also require that no current // level-0 request is still in flight. Obsolete requests from an older desired epoch do not // hold the current ring back; their cancellation/epoch fence is handled by the normal drain. for (const FVoxelTileKey& Key : PendingTiles) { if (Key.Level == 0 && IsDesired(Key)) { return; } } LastLOD0RingReadyEpoch = DesiredEpoch; const uint64 ReadyCycles = FPlatformTime::Cycles64(); const double DurationSeconds = DesiredEpochStartCycles != 0 && ReadyCycles >= DesiredEpochStartCycles ? FPlatformTime::ToSeconds64(ReadyCycles - DesiredEpochStartCycles) : 0.0; VoxelForgeStartupTrace::RecordEvent(TEXT("lod0_ring_ready"), FString::Printf( TEXT("\"desired_epoch\":%u,\"center_chunk\":[%d,%d,%d],\"lod0_tiles\":%d," "\"duration_s\":%s"), DesiredEpoch, DesiredEpochCenterChunk.X, DesiredEpochCenterChunk.Y, DesiredEpochCenterChunk.Z, DesiredLOD0Count, *FString::SanitizeFloat(DurationSeconds))); } void AVoxelWorld::LogStreamingLatencySummary() const { TArray RequestToReady; TArray QueueWait; TArray Generation; TArray CollisionCook; RequestToReady.Reserve(LOD0ReadySamples.Num()); QueueWait.Reserve(LOD0ReadySamples.Num()); Generation.Reserve(LOD0ReadySamples.Num()); CollisionCook.Reserve(LOD0ReadySamples.Num()); for (const FStreamingLatencySample& Sample : LOD0ReadySamples) { RequestToReady.Add(Sample.RequestToReadySeconds); QueueWait.Add(Sample.QueueWaitSeconds); Generation.Add(Sample.GenerationSeconds); CollisionCook.Add(Sample.CollisionCookSeconds); } const auto Percentile = [](TArray Values, double Fraction) -> double { if (Values.Num() == 0) return 0.0; Values.Sort(); const int32 Index = FMath::Clamp( FMath::CeilToInt(Fraction * static_cast(Values.Num() - 1)), 0, Values.Num() - 1); return Values[Index]; }; const auto Maximum = [](const TArray& Values) -> double { double Result = 0.0; for (const double Value : Values) { Result = FMath::Max(Result, Value); } return Result; }; const double RequestP95 = Percentile(RequestToReady, 0.95); const double TravelP95Metres = RequestP95 * static_cast(PeakObservedPawnSpeedCmPerSecond) / 100.0; FString ClassifierVerdictSummary; for (int32 LOD = 0; LOD < TrackedClassifierLODCount; ++LOD) { if (LOD > 0) { ClassifierVerdictSummary += TEXT(";"); } ClassifierVerdictSummary += FString::Printf( TEXT("l%d[c=%llu,m=%llu,s=%llu,a=%llu]"), LOD, static_cast(OuterClassifierCallsByLOD[LOD].load(std::memory_order_relaxed)), static_cast(OuterClassifierVerdictsByLOD[LOD][0].load(std::memory_order_relaxed)), static_cast(OuterClassifierVerdictsByLOD[LOD][1].load(std::memory_order_relaxed)), static_cast(OuterClassifierVerdictsByLOD[LOD][2].load(std::memory_order_relaxed))); } FString AppliedLevelSummary; FString BandLevelSummary; for (int32 LOD = 0; LOD < TrackedClassifierLODCount; ++LOD) { if (LOD > 0) { AppliedLevelSummary += TEXT(";"); BandLevelSummary += TEXT(";"); } AppliedLevelSummary += FString::Printf( TEXT("l%d[t=%llu,tri=%llu]"), LOD, static_cast(AppliedVisibleTileCountByLevel[LOD]), static_cast(AppliedTriangleCountByLevel[LOD])); BandLevelSummary += FString::Printf( TEXT("l%d[t=%llu]"), LOD, static_cast(AppliedBandTileCountByLevel[LOD])); } const uint64 WorkerGenerationCycles = TotalWorkerGenerationCycles.load(std::memory_order_relaxed); const uint64 ObsoleteWorkerCycles = TotalObsoleteWorkerCycles.load(std::memory_order_relaxed); const uint64 SealedSolidProofCandidates = TotalSealedSolidProofCandidates.load(std::memory_order_relaxed); const uint64 SealedSolidProofSkips = TotalSealedSolidProofSkips.load(std::memory_order_relaxed); const uint64 OutOfLayoutAirProofSkips = TotalOutOfLayoutAirProofSkips.load(std::memory_order_relaxed); const bool bDiagnosticsEnabled = GVoxelForgeProfileTileGeneration != 0 || VoxelForgeStartupTrace::IsActive() || VoxelDensityProfile::GetMode() != VoxelDensityProfile::EMode::Disabled; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStreamingLatency] mode=%s diagnostics=%s lod0_ready_samples=%d " "request_to_ready_s[p50=%.6f p95=%.6f max=%.6f] " "queue_wait_s[p50=%.6f p95=%.6f max=%.6f] " "generation_s[p50=%.6f p95=%.6f max=%.6f] " "collision_cook_s[p50=%.6f p95=%.6f max=%.6f] " "peak_speed_cm_s=%.3f p95_travel_m=%.6f obsolete_tile_aborts=%d " "outer_classifier_mode=%d nested_block_early_out=%d " "applied_tiles=%llu applied_visible_tiles=%llu applied_triangles=%llu " "worker_generation_tasks=%llu worker_generation_sum_s=%.6f " "validation_density_calls=%llu mesher_density_calls=%llu " "sealed_solid_proof_candidates=%llu sealed_solid_proof_skips=%llu " "out_of_layout_air_proof_skips=%llu " "obsolete_worker_tasks=%llu obsolete_worker_sum_s=%.6f " "outer_classifier_verdicts=%s applied_level_summary=%s band_level_summary=%s"), bDiagnosticsEnabled ? TEXT("profiled") : TEXT("clean"), bDiagnosticsEnabled ? TEXT("on") : TEXT("off"), LOD0ReadySamples.Num(), Percentile(RequestToReady, 0.50), RequestP95, Maximum(RequestToReady), Percentile(QueueWait, 0.50), Percentile(QueueWait, 0.95), Maximum(QueueWait), Percentile(Generation, 0.50), Percentile(Generation, 0.95), Maximum(Generation), Percentile(CollisionCook, 0.50), Percentile(CollisionCook, 0.95), Maximum(CollisionCook), PeakObservedPawnSpeedCmPerSecond, TravelP95Metres, ObsoleteTileAbortCount, GVoxelForgeOuterClassifierMode, GVoxelForgeUseBlockEarlyOut, static_cast(AppliedTileCount), static_cast(AppliedVisibleTileCount), static_cast(AppliedTriangleCount), static_cast(TotalWorkerGenerationTasks.load(std::memory_order_relaxed)), FPlatformTime::ToSeconds64(WorkerGenerationCycles), static_cast(TotalValidationDensityCalls.load(std::memory_order_relaxed)), static_cast(TotalMesherDensityCalls.load(std::memory_order_relaxed)), static_cast(SealedSolidProofCandidates), static_cast(SealedSolidProofSkips), static_cast(OutOfLayoutAirProofSkips), static_cast(TotalObsoleteWorkerTasks.load(std::memory_order_relaxed)), FPlatformTime::ToSeconds64(ObsoleteWorkerCycles), *ClassifierVerdictSummary, *AppliedLevelSummary, *BandLevelSummary); } void AVoxelWorld::HandleTileCollisionCookComplete( const FVoxelTileKey& Tile, uint64 SubmissionId, URealtimeMesh* Mesh, uint8 Result, uint64 FutureCallbackCycles) { const uint64 CompletionGameThreadCycles = FPlatformTime::Cycles64(); if (!IsInGameThread()) { TWeakObjectPtr WeakWorld(this); AsyncTask(ENamedThreads::GameThread, [WeakWorld, Tile, SubmissionId, Mesh, Result, FutureCallbackCycles]() { if (AVoxelWorld* World = WeakWorld.Get()) { World->HandleTileCollisionCookComplete( Tile, SubmissionId, Mesh, Result, FutureCallbackCycles); } }); return; } if (bShuttingDown.load(std::memory_order_relaxed)) { return; } FPendingCollisionCook* Pending = PendingCollisionCooks.Find(Tile); URealtimeMesh* CompletedMesh = Pending ? Pending->Mesh.Get() : nullptr; if (Pending == nullptr || Pending->SubmissionId != SubmissionId || CompletedMesh == nullptr || CompletedMesh != Mesh) { // A remesh, unload, or pool reuse superseded this future. It must not change the newer // tile state, even if RMC finishes the old cook after that transition. return; } Pending->FutureCallbackCycles = FutureCallbackCycles; Pending->CompletionGameThreadCycles = CompletionGameThreadCycles; const uint64 MeasuredMeshSubmittedCycles = Pending->MeshSubmittedCycles; const uint64 MeasuredCollisionSubmittedCycles = Pending->CollisionSubmittedCycles; const bool bBodyUpdatedEventSeen = Pending->bBodyUpdatedEventSeen; const uint64 BodyUpdatedEventCycles = Pending->BodyUpdatedEventCycles; const uint64 BodyUpdatedEventFrame = Pending->BodyUpdatedEventFrame; const bool bPhysicsStateReadyAtBodyUpdate = Pending->bPhysicsStateReadyAtBodyUpdate; const uint64 PhysicsStateReadyCycles = Pending->PhysicsStateReadyCycles; const uint64 MeasuredCollisionSubmittedFrame = Pending->CollisionSubmittedFrame; const uint64 MeasuredRequestStartCycles = Pending->RequestStartCycles; const uint64 MeasuredGenerationStartCycles = Pending->GenerationStartCycles; const uint64 MeasuredGenerationEndCycles = Pending->GenerationEndCycles; const uint64 MeasuredApplyStartCycles = Pending->ApplyStartCycles; const EVoxelTileWorkPriority WorkPriority = Pending->WorkPriority; const bool bMeasuredSupportCohort = Pending->bStep1SupportCohort; const int32 NumVertices = Pending->NumVertices; const int32 NumTriangles = Pending->NumTriangles; const int32 NumSkirtTriangles = Pending->NumSkirtTriangles; const bool bPooledComponent = Pending->bPooledComponent; const int32 PendingCooksAtSubmit = Pending->PendingCooksAtSubmit; const bool bUseAsyncCook = Pending->bUseAsyncCook; const bool bUseComplexAsSimpleCollision = Pending->bUseComplexAsSimpleCollision; const bool bShouldFastCookMeshes = Pending->bShouldFastCookMeshes; const bool bMergeAllMeshes = Pending->bMergeAllMeshes; const uint64 SuppressedAtCycles = Pending->SuppressedAtCycles; const bool bRecordStreamingLatency = Pending->bRecordStreamingLatency; PendingCollisionCooks.Remove(Tile); const ERealtimeMeshCollisionUpdateResult CollisionResult = static_cast(Result); const bool bBodyInstalled = CollisionResult == ERealtimeMeshCollisionUpdateResult::Updated && CompletedMesh->GetBodySetup() != nullptr; const bool bCollisionStillDesired = IsTileCollisionDesired(Tile); if (bBodyInstalled && bCollisionStillDesired) { CollisionSuppressedUntilCook.Remove(Tile); CollisionSuppressedAtCycles.Remove(Tile); if (URealtimeMeshComponent* Comp = TileComponents.FindRef(Tile)) { // RMC caches local bounds on pooled components; rebinding after the async cook // invalidates that cache and rebuilds scene-query bounds from the installed mesh. // RMC met les bornes locales en cache sur les composants reutilises ; le rebranchement // apres la cuisson invalide ce cache et recalcule les bornes des traces. if (bPooledComponent && Comp->GetRealtimeMesh() == CompletedMesh) { Comp->SetRealtimeMesh(nullptr); Comp->SetRealtimeMesh(CompletedMesh); } Comp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); } CollisionReadyTiles.Add(Tile); if (bStartupCollisionGateTimingCaptured && StartupCollisionGateSupportTile.Coord == Tile.Coord && StartupCollisionGateSupportTile.Level == Tile.Level && StartupCollisionGateReadySeconds <= 0.0) { StartupCollisionGateReadySeconds = FPlatformTime::Seconds(); const double FirstCheckToSubmitSeconds = StartupCollisionGateSubmittedSeconds > 0.0 ? FMath::Max(0.0, StartupCollisionGateSubmittedSeconds - StartupCollisionGateFirstCheckSeconds) : -1.0; const double FirstCheckToReadySeconds = FMath::Max( 0.0, StartupCollisionGateReadySeconds - StartupCollisionGateFirstCheckSeconds); const double SubmitToReadySeconds = StartupCollisionGateSubmittedSeconds > 0.0 ? FMath::Max(0.0, StartupCollisionGateReadySeconds - StartupCollisionGateSubmittedSeconds) : -1.0; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGate] support_collision_ready tile=(%d,%d,%d) " "first_check_to_submit_s=%.6f first_check_to_collision_ready_s=%.6f " "submit_to_ready_s=%.6f"), Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, FirstCheckToSubmitSeconds, FirstCheckToReadySeconds, SubmitToReadySeconds); } } else { CollisionReadyTiles.Remove(Tile); if (!bCollisionStillDesired) { CollisionSuppressedUntilCook.Remove(Tile); CollisionSuppressedAtCycles.Remove(Tile); } if (URealtimeMeshComponent* Comp = TileComponents.FindRef(Tile)) { Comp->SetCollisionEnabled(ECollisionEnabled::NoCollision); } if (bCollisionStillDesired) { // A failed/ignored current cook is not a loaded guarantee. Leaving the tile unloaded // makes the normal submit loop retry it while the pawn remains gated. LoadedTiles.Remove(Tile); LoadedTileGeometry.Remove(Tile); bAllChunksLoaded = false; } } const uint64 ReadyCycles = FPlatformTime::Cycles64(); if (bBodyInstalled && Tile.Level == 0 && bRecordStreamingLatency) { RecordLOD0ReadySample(MeasuredRequestStartCycles, MeasuredGenerationStartCycles, MeasuredGenerationEndCycles, MeasuredApplyStartCycles, ReadyCycles, MeasuredCollisionSubmittedCycles); } const double MeshToCookSeconds = FPlatformTime::ToSeconds64( ReadyCycles - MeasuredMeshSubmittedCycles); const double CollisionRequestToCookSeconds = FPlatformTime::ToSeconds64( ReadyCycles - MeasuredCollisionSubmittedCycles); const double BodyEventToFutureSeconds = bBodyUpdatedEventSeen && ReadyCycles >= BodyUpdatedEventCycles ? FPlatformTime::ToSeconds64(ReadyCycles - BodyUpdatedEventCycles) : -1.0; const double BodyEventToPhysicsReadySeconds = bPhysicsStateReadyAtBodyUpdate && PhysicsStateReadyCycles >= BodyUpdatedEventCycles ? FPlatformTime::ToSeconds64(PhysicsStateReadyCycles - BodyUpdatedEventCycles) : -1.0; const auto SecondsBetween = [](uint64 EndCycles, uint64 StartCycles) -> double { return StartCycles != 0 && EndCycles >= StartCycles ? FPlatformTime::ToSeconds64(EndCycles - StartCycles) : -1.0; }; const double MeshGeneratedToReadySeconds = SecondsBetween( ReadyCycles, MeasuredGenerationEndCycles); const double RequestToWorkerStartSeconds = SecondsBetween( MeasuredGenerationStartCycles, MeasuredRequestStartCycles); const double WorkerGenerationSeconds = SecondsBetween( MeasuredGenerationEndCycles, MeasuredGenerationStartCycles); const double WorkerResultToApplySeconds = SecondsBetween( MeasuredApplyStartCycles, MeasuredGenerationEndCycles); const double ApplyToMeshSubmitSeconds = SecondsBetween( MeasuredMeshSubmittedCycles, MeasuredApplyStartCycles); const double MeshSubmitToCollisionSubmitSeconds = SecondsBetween( MeasuredCollisionSubmittedCycles, MeasuredMeshSubmittedCycles); const double RMCSubmitToBodyUpdateSeconds = bBodyUpdatedEventSeen ? SecondsBetween(BodyUpdatedEventCycles, MeasuredCollisionSubmittedCycles) : -1.0; const double BodyUpdateToFutureCallbackSeconds = bBodyUpdatedEventSeen ? SecondsBetween(FutureCallbackCycles, BodyUpdatedEventCycles) : -1.0; const double FutureCallbackToGameThreadSeconds = SecondsBetween( CompletionGameThreadCycles, FutureCallbackCycles); const double SuppressedToReadySeconds = SecondsBetween(ReadyCycles, SuppressedAtCycles); const int64 FramesToBodyUpdate = bBodyUpdatedEventSeen && BodyUpdatedEventFrame >= MeasuredCollisionSubmittedFrame ? static_cast(BodyUpdatedEventFrame - MeasuredCollisionSubmittedFrame) : -1; const TCHAR* ResultName = TEXT("Unknown"); switch (CollisionResult) { case ERealtimeMeshCollisionUpdateResult::Updated: ResultName = TEXT("Updated"); break; case ERealtimeMeshCollisionUpdateResult::Ignored: ResultName = TEXT("Ignored"); break; case ERealtimeMeshCollisionUpdateResult::Error: ResultName = TEXT("Error"); break; default: break; } if (GVoxelForgeProfileTileGeneration != 0 || GVoxelForgeProfileCollisionCookStages != 0 || VoxelForgeStartupTrace::IsActive() || !bBodyInstalled) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionReady] tile=(%d,%d,%d) level=%d result=%s body_setup=%d " "ready=%d body_event=%d priority=%d critical=%d support_cohort=%d " "vertices=%d triangles=%d " "skirt_triangles=%d pending_cooks=%d async=%d complex_as_simple=%d fast_cook=%d " "merge=%d request_to_worker_s=%.6f worker_gen_s=%.6f worker_to_apply_s=%.6f " "apply_to_mesh_submit_s=%.6f mesh_to_collision_submit_s=%.6f " "rmc_submit_to_body_update_s=%.6f submit_to_body_frames=%lld " "physics_ready_at_body=%d body_update_to_physics_ready_s=%.6f " "body_update_to_future_callback_s=%.6f future_callback_to_gt_s=%.6f " "suppressed_to_ready_s=%.6f mesh_submit_to_ready=%.6f " "mesh_generated_to_ready_s=%.6f " "collision_request_to_ready=%.6f body_event_to_future=%.6f"), Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, ResultName, CompletedMesh->GetBodySetup() != nullptr ? 1 : 0, bBodyInstalled ? 1 : 0, bBodyUpdatedEventSeen ? 1 : 0, static_cast(WorkPriority), WorkPriority == EVoxelTileWorkPriority::CollisionCritical ? 1 : 0, bMeasuredSupportCohort ? 1 : 0, NumVertices, NumTriangles, NumSkirtTriangles, PendingCooksAtSubmit, bUseAsyncCook ? 1 : 0, bUseComplexAsSimpleCollision ? 1 : 0, bShouldFastCookMeshes ? 1 : 0, bMergeAllMeshes ? 1 : 0, RequestToWorkerStartSeconds, WorkerGenerationSeconds, WorkerResultToApplySeconds, ApplyToMeshSubmitSeconds, MeshSubmitToCollisionSubmitSeconds, RMCSubmitToBodyUpdateSeconds, FramesToBodyUpdate, bPhysicsStateReadyAtBodyUpdate ? 1 : 0, BodyEventToPhysicsReadySeconds, BodyUpdateToFutureCallbackSeconds, FutureCallbackToGameThreadSeconds, SuppressedToReadySeconds, MeshToCookSeconds, MeshGeneratedToReadySeconds, CollisionRequestToCookSeconds, BodyEventToFutureSeconds); } if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("collision_ready"), FString::Printf( TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"result\":\"%s\",\"body_setup\":%d," "\"ready\":%d,\"body_update_event\":%d," "\"priority\":%d,\"critical\":%d,\"vertices\":%d,\"triangles\":%d," "\"skirt_triangles\":%d,\"pending_cooks_at_submit\":%d," "\"request_to_worker_s\":%s,\"worker_generation_s\":%s," "\"worker_result_to_apply_s\":%s,\"apply_to_mesh_submit_s\":%s," "\"mesh_to_collision_submit_s\":%s,\"rmc_submit_to_body_update_s\":%s," "\"submit_to_body_frames\":%lld,\"body_update_to_future_callback_s\":%s," "\"future_callback_to_gt_s\":%s,\"suppressed_to_ready_s\":%s," "\"mesh_submit_to_ready_s\":%s,\"collision_request_to_ready_s\":%s"), Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, ResultName, CompletedMesh->GetBodySetup() != nullptr ? 1 : 0, bBodyInstalled ? 1 : 0, bBodyUpdatedEventSeen ? 1 : 0, static_cast(WorkPriority), WorkPriority == EVoxelTileWorkPriority::CollisionCritical ? 1 : 0, NumVertices, NumTriangles, NumSkirtTriangles, PendingCooksAtSubmit, *FString::SanitizeFloat(RequestToWorkerStartSeconds), *FString::SanitizeFloat(WorkerGenerationSeconds), *FString::SanitizeFloat(WorkerResultToApplySeconds), *FString::SanitizeFloat(ApplyToMeshSubmitSeconds), *FString::SanitizeFloat(MeshSubmitToCollisionSubmitSeconds), *FString::SanitizeFloat(RMCSubmitToBodyUpdateSeconds), FramesToBodyUpdate, *FString::SanitizeFloat(BodyUpdateToFutureCallbackSeconds), *FString::SanitizeFloat(FutureCallbackToGameThreadSeconds), *FString::SanitizeFloat(SuppressedToReadySeconds), *FString::SanitizeFloat(MeshToCookSeconds), *FString::SanitizeFloat(CollisionRequestToCookSeconds))); } // A completion can occur between world ticks. Refresh immediately so the pawn is released on // the same game-thread turn once its feet tile is truly collidable. FVector PlayerPosition = FVector::ZeroVector; APawn* PlayerPawn = nullptr; if (TryGetPlayerPosition(PlayerPosition, &PlayerPawn)) { UpdatePawnCollisionGate(PlayerPawn, PlayerPosition, 0.0f); } } void AVoxelWorld::ProcessPendingChunks() { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ProcessPending); // This runs on the game thread, called from Tick. // // STEPS: // 1. Try to dequeue a result from ProcessQueue // TQueue has a Dequeue(OutItem) method that returns true if it got something // // 2. If we got a result: // a. Store the chunk data in our Chunks map // b. Apply the mesh so it becomes visible // c. Remove the coord from PendingChunkCoord (it's no longer "in progress") // // 3. You can process multiple results per frame with a while loop, // or limit to a few per frame to avoid stutters (e.g., max 4 per tick) // // TOOLS: // - ProcessQueue.Dequeue(Result) — returns bool, fills Result if true // - Chunks.Add(Key, Value) // - ApplyMeshToChunk(ChunkCoord, MeshData) // - PendingChunkCoord.Remove(ChunkCoord) // Drain the process queue up to the per-frame budget. // This prevents stutters from applying too many meshes in one frame. // Budget limits how many VISIBLE mesh applies we do per frame (GPU upload cost). // Empty meshes and stale results are free to drain — don't count them. const bool bProfileApply = GVoxelForgeProfileTileGeneration != 0; if (!bStartupApplyConfigTraced) { bStartupApplyConfigTraced = true; if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("startup_apply_config"), FString::Printf( TEXT("\"startup_apply_boost_cvar\":%d,\"step1_max_applies\":%d," "\"asset_max_applies\":%d,\"asset_budget_ms\":%.3f," "\"boost_max_applies\":%d,\"boost_budget_ms\":%.3f," "\"boost_max_duration_s\":%.3f"), GVoxelForgeStreamingStartupApplyBoost, GVoxelForgeStep1MaxMeshApplies, Settings ? Settings->MaxMeshAppliesPerFrame : 4, Settings ? FMath::Max(Settings->MaxMeshApplyMilliseconds, 0.0f) : 2.0f, StreamingStartupMaxMeshApplies, StreamingStartupApplyBudgetMilliseconds, StreamingStartupApplyMaxSeconds)); } } const uint64 ApplyFrameStartCycles = FPlatformTime::Cycles64(); const double FrameSeconds = LastApplyFrameCycles != 0 && ApplyFrameStartCycles >= LastApplyFrameCycles ? FPlatformTime::ToSeconds64(ApplyFrameStartCycles - LastApplyFrameCycles) : 0.0; LastApplyFrameCycles = ApplyFrameStartCycles; const double StartupElapsedSeconds = DesiredEpochStartCycles != 0 && ApplyFrameStartCycles >= DesiredEpochStartCycles ? FPlatformTime::ToSeconds64(ApplyFrameStartCycles - DesiredEpochStartCycles) : -1.0; const bool bInitialDesiredSetReady = DesiredEpoch == 1 && bAllChunksLoaded && PendingTiles.Num() == 0 && PendingCollisionCooks.Num() == 0 && LastLOD0RingReadyEpoch == DesiredEpoch; if (bStartupApplyBoostStarted && !bStartupApplyBoostEnded && (bInitialDesiredSetReady || DesiredEpoch != 1 || GVoxelForgeStreamingStartupApplyBoost <= 0 || GVoxelForgeStep1MaxMeshApplies > 0 || StartupElapsedSeconds >= StreamingStartupApplyMaxSeconds)) { bStartupApplyBoostEnded = true; if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("startup_apply_budget"), FString::Printf( TEXT("\"state\":\"normal\",\"reason\":\"%s\",\"elapsed_s\":%s"), bInitialDesiredSetReady ? TEXT("initial_set_and_walkable_ring_ready") : (StartupElapsedSeconds >= StreamingStartupApplyMaxSeconds ? TEXT("time_limit") : (DesiredEpoch != 1 ? TEXT("desired_epoch_changed") : TEXT("override_disabled"))), *FString::SanitizeFloat(StartupElapsedSeconds))); } } const bool bUseStartupApplyBoost = GVoxelForgeStreamingStartupApplyBoost > 0 && GVoxelForgeStep1MaxMeshApplies <= 0 && !bStartupApplyBoostEnded && DesiredEpoch == 1 && StartupElapsedSeconds >= 0.0 && !bInitialDesiredSetReady && StartupElapsedSeconds < StreamingStartupApplyMaxSeconds; if (bUseStartupApplyBoost && !bStartupApplyBoostStarted) { bStartupApplyBoostStarted = true; if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("startup_apply_budget"), FString::Printf( TEXT("\"state\":\"boosted\",\"max_applies\":%d,\"budget_ms\":%.3f," "\"max_duration_s\":%.3f,\"desired_epoch\":%u"), StreamingStartupMaxMeshApplies, StreamingStartupApplyBudgetMilliseconds, StreamingStartupApplyMaxSeconds, DesiredEpoch)); } } uint64 DrainStartCycles = ApplyFrameStartCycles; const int32 ConfiguredMaxApplies = FMath::Max( Settings ? Settings->MaxMeshAppliesPerFrame : 4, 1); const int32 MaxApplies = GVoxelForgeStep1MaxMeshApplies > 0 ? GVoxelForgeStep1MaxMeshApplies : (bUseStartupApplyBoost ? StreamingStartupMaxMeshApplies : ConfiguredMaxApplies); const float ApplyBudgetMilliseconds = bUseStartupApplyBoost ? StreamingStartupApplyBudgetMilliseconds : (Settings ? FMath::Max(Settings->MaxMeshApplyMilliseconds, 0.0f) : 2.0f); const double ApplyBudgetSeconds = static_cast(ApplyBudgetMilliseconds) * 0.001; int32 MeshesApplied = 0; int32 ResultsDrained = 0; int32 ApplyResultsByLevelTotal = 0; double ApplySeconds = 0.0; TStaticArray ApplyResultsByLevel{}; TStaticArray MeshesAppliedByLevel{}; TStaticArray ApplySecondsByLevel{}; bool bTimeBudgetHit = false; const auto RecordApplyFrame = [&]() { if (!VoxelForgeStartupTrace::IsActive()) return; VoxelForgeStartupTrace::FApplyFrameSample Sample; Sample.Frame = GFrameCounter; Sample.FrameSeconds = FrameSeconds; Sample.SpeedCmPerSecond = StartupTraceSpeedCmPerSecond; Sample.ApplySeconds = ApplySeconds; Sample.DrainSeconds = FPlatformTime::ToSeconds64( FPlatformTime::Cycles64() - DrainStartCycles); // `GetActiveGenerationTaskCount` includes completed tiles until the game-thread apply removes // their PendingTiles entry. Use the task guard's counter here so the difference estimates // results waiting in the MPSC queues instead of reporting zero by construction. // `GetActiveGenerationTaskCount` inclut les resultats termines jusqu'a leur application. // Utiliser le compteur RAII pour mesurer les resultats en attente dans les files MPSC. Sample.ActiveTasks = ActiveTaskCount.load(std::memory_order_relaxed); Sample.RunningWorkerTasks = ActiveGenerationWorkerCount.load(std::memory_order_relaxed); Sample.TaskCap = GetMaxConcurrentTasks(); Sample.PendingTiles = PendingTiles.Num(); Sample.PendingResultsEstimate = FMath::Max(0, Sample.PendingTiles - Sample.ActiveTasks); Sample.ResultsDrained = ResultsDrained; Sample.ApplyResultsByLevelTotal = ApplyResultsByLevelTotal; Sample.MeshesApplied = MeshesApplied; Sample.MaxApplies = MaxApplies; Sample.ApplyBudgetMilliseconds = ApplyBudgetMilliseconds; Sample.bCountLimitReached = MeshesApplied >= MaxApplies; Sample.bTimeLimitReached = bTimeBudgetHit; Sample.ApplyResultsByLevel = ApplyResultsByLevel; Sample.MeshesAppliedByLevel = MeshesAppliedByLevel; Sample.ApplySecondsByLevel = ApplySecondsByLevel; for (const FVoxelTileKey& PendingTile : PendingTiles) { if (PendingTile.Level >= 0 && PendingTile.Level < 9) { ++Sample.PendingByLevel[PendingTile.Level]; } } VoxelForgeStartupTrace::RecordApplyFrame(Sample); VoxelForgeStartupTrace::RecordEvent(TEXT("result_queue_frame"), FString::Printf( TEXT("\"frame\":%llu,\"queued_results\":%d,\"queued_bytes\":%llu," "\"active_tile_tasks\":%d,\"held_results\":%d,\"backlog_count\":%d," "\"backlog_cap\":%d"), static_cast(GFrameCounter), QueuedStreamingResultCount.load(std::memory_order_relaxed), static_cast(QueuedStreamingResultBytes.load(std::memory_order_relaxed)), ActiveTileTaskCount.load(std::memory_order_relaxed), Step1HeldApplyTiles.Num(), GetStreamingResultBacklogCount(), GVoxelForgeStreamingResultBacklogCap)); }; FChunkResult DequeuedChunk; const auto DequeueNextResult = [&]() -> bool { if (bStep1UploadCorpusReleased && Step1HeldApplyCursor < Step1HeldApplyResults.Num()) { DequeuedChunk = MoveTemp(Step1HeldApplyResults[Step1HeldApplyCursor++]); Step1HeldApplyTiles.Remove(DequeuedChunk.Tile); return true; } if (bStep1UploadCorpusReleased && Step1HeldApplyResults.Num() > 0) { Step1HeldApplyResults.Empty(); Step1HeldApplyCursor = 0; } // Mirror the admission order. A horizon result that completed earlier must not consume // the game-thread apply budget ahead of collision support, an edit, or ordinary LOD0. if (CriticalProcessQueue.Dequeue(DequeuedChunk) || EditedProcessQueue.Dequeue(DequeuedChunk) || NearProcessQueue.Dequeue(DequeuedChunk) || ProcessQueue.Dequeue(DequeuedChunk)) { RecordStreamingResultDequeued(DequeuedChunk); return true; } return false; }; if (GVoxelForgeStep1PrequeueVisibleLOD0 > 0 && !bStep1UploadCorpusReleased) { while (DequeueNextResult()) { const TSharedPtr Cancellation = PendingTileCancellation.FindRef(DequeuedChunk.Tile); const bool bRequestObsolete = DequeuedChunk.bObsolete || (Cancellation.IsValid() && Cancellation->bObsolete.load(std::memory_order_acquire)); if (DequeuedChunk.bAborted || bRequestObsolete || DequeuedChunk.Epoch != GenerationEpoch || !IsDesired(DequeuedChunk.Tile)) { if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("tile_discard"), FString::Printf( TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d," "\"request_ordinal\":%llu,\"epoch\":%u,\"current_epoch\":%u," "\"reason\":\"%s\""), DequeuedChunk.Tile.Coord.X, DequeuedChunk.Tile.Coord.Y, DequeuedChunk.Tile.Coord.Z, DequeuedChunk.Tile.Level, static_cast(DequeuedChunk.WorkPriority), static_cast(DequeuedChunk.RequestOrdinal), DequeuedChunk.Epoch, GenerationEpoch, DequeuedChunk.bAborted ? TEXT("aborted") : (bRequestObsolete ? TEXT("obsolete") : (DequeuedChunk.Epoch != GenerationEpoch ? TEXT("stale_epoch") : TEXT("not_desired"))))); } PendingTileCancellation.Remove(DequeuedChunk.Tile); PendingTiles.Remove(DequeuedChunk.Tile); if (bRequestObsolete) { ++ObsoleteTileAbortCount; } continue; } if (!Step1HeldApplyTiles.Contains(DequeuedChunk.Tile)) { Step1HeldApplyTiles.Add(DequeuedChunk.Tile); if (DequeuedChunk.Tile.Level == 0 && !DequeuedChunk.bEmpty && DequeuedChunk.Streams.IsValid() && DequeuedChunk.NumTriangles > 0) { ++Step1HeldVisibleLOD0Count; } Step1HeldApplyResults.Add(MoveTemp(DequeuedChunk)); if (Step1HeldVisibleLOD0Count >= GVoxelForgeStep1PrequeueVisibleLOD0) { break; } } } if (Step1HeldVisibleLOD0Count >= GVoxelForgeStep1PrequeueVisibleLOD0) { bStep1UploadCorpusReleased = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeUploadPrequeue] state=released target_visible_l0=%d " "held_visible_l0=%d held_results=%d active_generation=%d pending=%d frame=%llu"), GVoxelForgeStep1PrequeueVisibleLOD0, Step1HeldVisibleLOD0Count, Step1HeldApplyResults.Num(), GetActiveGenerationTaskCount(), PendingTiles.Num(), static_cast(GFrameCounter)); CSV_EVENT_GLOBAL(TEXT("VFStep1UploadCorpusReleased")); DrainStartCycles = FPlatformTime::Cycles64(); } else { if (Step1LastLoggedPrequeueCount < 0 || Step1HeldVisibleLOD0Count >= Step1LastLoggedPrequeueCount + 50) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeUploadPrequeue] state=collecting target_visible_l0=%d " "held_visible_l0=%d held_results=%d active_generation=%d pending=%d"), GVoxelForgeStep1PrequeueVisibleLOD0, Step1HeldVisibleLOD0Count, Step1HeldApplyResults.Num(), GetActiveGenerationTaskCount(), PendingTiles.Num()); Step1LastLoggedPrequeueCount = Step1HeldVisibleLOD0Count; } RecordApplyFrame(); return; } } while (DequeueNextResult()) { ++ResultsDrained; const TSharedPtr Cancellation = PendingTileCancellation.FindRef(DequeuedChunk.Tile); const bool bRequestObsolete = DequeuedChunk.bObsolete || (Cancellation.IsValid() && Cancellation->bObsolete.load(std::memory_order_acquire)); PendingTileCancellation.Remove(DequeuedChunk.Tile); PendingTiles.Remove(DequeuedChunk.Tile); // A worker can observe shutdown after entering the mesher. Do not turn that partial // result into an all-air tile: it must remain eligible for a future generation epoch. // Read the token here as well as on the worker: cancellation can race the final worker // check after it has already enqueued a complete result. This also drops a stale result // if the tile leaves and re-enters the desired set before the queue is drained. if (DequeuedChunk.bAborted || bRequestObsolete) { if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("tile_discard"), FString::Printf( TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d," "\"request_ordinal\":%llu,\"epoch\":%u,\"current_epoch\":%u," "\"reason\":\"%s\""), DequeuedChunk.Tile.Coord.X, DequeuedChunk.Tile.Coord.Y, DequeuedChunk.Tile.Coord.Z, DequeuedChunk.Tile.Level, static_cast(DequeuedChunk.WorkPriority), static_cast(DequeuedChunk.RequestOrdinal), DequeuedChunk.Epoch, GenerationEpoch, DequeuedChunk.bAborted ? TEXT("aborted") : TEXT("obsolete"))); } if (bRequestObsolete) { ++ObsoleteTileAbortCount; } continue; } // ApplyTileResult does epoch check, capture ingest, empty-release / mesh submission, and // only then records the tile as loaded. Only a real (visible) upload counts against the // per-frame budget — stale/empty drains are free. const int32 ResultLevel = DequeuedChunk.Tile.Level; const uint64 TileApplyStartCycles = FPlatformTime::Cycles64(); const bool bMeshApplied = ApplyTileResult(DequeuedChunk); const uint64 TileApplyEndCycles = FPlatformTime::Cycles64(); const double TileApplySeconds = TileApplyEndCycles >= TileApplyStartCycles ? FPlatformTime::ToSeconds64(TileApplyEndCycles - TileApplyStartCycles) : 0.0; ApplySeconds += TileApplySeconds; if (ResultLevel >= 0 && ResultLevel < 9) { ++ApplyResultsByLevelTotal; ++ApplyResultsByLevel[ResultLevel]; ApplySecondsByLevel[ResultLevel] += TileApplySeconds; } if (bMeshApplied) { ++MeshesApplied; if (ResultLevel >= 0 && ResultLevel < 9) { ++MeshesAppliedByLevel[ResultLevel]; } } // Time-box the entire game-thread drain, not only visible uploads. Empty releases and // stale-result cleanup normally cost nothing, but a pooled component teardown or a // backend callback can still be the one result that produces the player's hitch. if (ApplyBudgetSeconds > 0.0 && FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - DrainStartCycles) >= ApplyBudgetSeconds) { bTimeBudgetHit = true; break; } if (MeshesApplied >= MaxApplies) { break; } } if (bProfileApply && ResultsDrained > 0) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeApplyFrame] drained=%d meshes=%d elapsed=%.6f " "max_meshes=%d budget_ms=%.3f budget_hit=%d"), ResultsDrained, MeshesApplied, FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - DrainStartCycles), MaxApplies, ApplyBudgetMilliseconds, bTimeBudgetHit ? 1 : 0); } RecordApplyFrame(); } // Game-thread apply for one gen result. Shared by ProcessPendingChunks (async drain) and // SyncRemeshTile (synchronous carve). Returns true iff a visible mesh was uploaded (budget). bool AVoxelWorld::ApplyTileResult(FChunkResult& Result) { // Discard results from a previous generation epoch (stale), or for a tile that has since left // the desired set. ProcessPendingChunks also reads the per-request token before removing it, // closing the race where the worker enqueues just after CancelObsoleteTileWork flips the flag. if (Result.bAborted || Result.Epoch != GenerationEpoch || !IsDesired(Result.Tile)) { if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("tile_discard"), FString::Printf( TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d," "\"request_ordinal\":%llu,\"epoch\":%u,\"current_epoch\":%u," "\"reason\":\"%s\""), Result.Tile.Coord.X, Result.Tile.Coord.Y, Result.Tile.Coord.Z, Result.Tile.Level, static_cast(Result.WorkPriority), static_cast(Result.RequestOrdinal), Result.Epoch, GenerationEpoch, Result.bAborted ? TEXT("aborted") : (Result.Epoch != GenerationEpoch ? TEXT("stale_epoch") : TEXT("not_desired")))); } return false; } // Retain one approximately 4k-triangle support stream only for the opt-in paused cook // replay. The replay is queued after the timed generation pause, so it measures the dedicated // cook path without concurrent tile generation and still uses this exact generated mesh. if (GVoxelForgeStep1CoreCookBenchmark != 0 && Result.bStep1SupportCohort && !Step1PauseReplayStreams.IsValid() && Result.Streams.IsValid() && Result.NumTriangles >= 3500 && Result.NumTriangles <= 4300) { Step1PauseReplayStreams = MakeShared( *Result.Streams); Step1PauseReplayTile = Result.Tile; Step1PauseReplayTriangles = Result.NumTriangles; } // Store only lattices still within the local pawn's fixed 3×3×3 neighborhood. The cache is // independent of DiffLayer revisions because it contains only the procedural pre-edit field. if (Result.PreEditDensityGrid.Samples.Num() > 0 && VoxelForgePreEditDensityCache::IsPawnRadiusTile(Result.Tile, CurrentCenterChunk)) { if (PreEditDensityCache.Num() < VoxelForgePreEditDensityCache::MaxCachedTiles || PreEditDensityCache.Contains(Result.Tile)) { PreEditDensityCache.Add(Result.Tile, MoveTemp(Result.PreEditDensityGrid)); } } // A worker may have captured the density field before one or more edits. Do not let that // result overwrite the current visual/collision state when any later revision touches this // tile's footprint. Keep the old state in place and admit one post-edit request through the // normal budgeted edited queue; this is the edit equivalent of load-before-unload. if (Result.ModificationEpoch < ModificationEpoch) { bool bAffectedByLaterEdit = false; for (const FModificationRevision& Revision : ModificationRevisions) { if (Revision.Epoch > Result.ModificationEpoch && VF_TileOverlapsVoxelBounds( Result.Tile, Revision.MinVoxel, Revision.MaxVoxel)) { bAffectedByLaterEdit = true; break; } } if (bAffectedByLaterEdit) { DirtyRemeshQueue.Add(Result.Tile); bAllChunksLoaded = false; return false; } } if (Result.bHasCoreHashes) { FVoxelForgeCoreTileHashRecord& Record = VoxelForgeCoreTileHashes.FindOrAdd(Result.Tile); Record.Density = Result.CoreDensityHash; Record.Mesh = Result.CoreMeshHash; Record.Payload = Result.CorePayloadHash; Record.Topology = Result.CoreTopologyHash; Record.Triangles = Result.NumTriangles; Record.bEmpty = Result.bEmpty; Record.bHasHashes = true; } // Applied is emitted after the game-thread submission, not when the worker result is dequeued. // It deliberately does NOT mean collision-ready: RMC's cook future records that later boundary // in HandleTileCollisionCookComplete. const bool bResultEmpty = Result.bEmpty || !Result.Streams; const bool bTraceTile = VoxelForgeStartupTrace::IsActive(); const uint64 ApplyStartCycles = FPlatformTime::Cycles64(); Result.ApplyStartCycles = ApplyStartCycles; const auto LogTileApplied = [&]() { ++AppliedTileCount; if (Result.Tile.Level >= 0 && Result.Tile.Level < TrackedClassifierLODCount && Result.BandChunkLo != MIN_int32) { ++AppliedBandTileCountByLevel[Result.Tile.Level]; } if (!bResultEmpty) { ++AppliedVisibleTileCount; const uint64 Triangles = static_cast(FMath::Max(0, Result.NumTriangles)); AppliedTriangleCount += Triangles; if (Result.Tile.Level >= 0 && Result.Tile.Level < TrackedClassifierLODCount) { ++AppliedVisibleTileCountByLevel[Result.Tile.Level]; AppliedTriangleCountByLevel[Result.Tile.Level] += Triangles; } } if ((GVoxelForgeProfileTileGeneration != 0 || bTraceTile) && Result.RequestStartCycles != 0) { const uint64 AppliedCycles = FPlatformTime::Cycles64(); const bool bHaveWorkerTiming = Result.GenerationStartCycles != 0 && Result.GenerationEndCycles >= Result.GenerationStartCycles; const bool bHaveWorkerTaskTiming = Result.WorkerTaskStartCycles != 0 && Result.WorkerTaskEndCycles >= Result.WorkerTaskStartCycles; const double RequestToApply = FPlatformTime::ToSeconds64( AppliedCycles - Result.RequestStartCycles); const double ApplySeconds = ApplyStartCycles != 0 ? FPlatformTime::ToSeconds64(AppliedCycles - ApplyStartCycles) : 0.0; const double WorkerTaskSeconds = bHaveWorkerTaskTiming ? FPlatformTime::ToSeconds64( Result.WorkerTaskEndCycles - Result.WorkerTaskStartCycles) : (bHaveWorkerTiming ? FPlatformTime::ToSeconds64( Result.GenerationEndCycles - Result.GenerationStartCycles) : 0.0); const double GenerationSeconds = bHaveWorkerTiming ? FPlatformTime::ToSeconds64( Result.GenerationEndCycles - Result.GenerationStartCycles) : 0.0; const double WorkerQueueSeconds = bHaveWorkerTiming ? FPlatformTime::ToSeconds64( (bHaveWorkerTaskTiming ? Result.WorkerTaskStartCycles : Result.GenerationStartCycles) - Result.RequestStartCycles) : 0.0; const uint64 WorkerTaskEndCycles = bHaveWorkerTaskTiming ? Result.WorkerTaskEndCycles : Result.GenerationEndCycles; const double ResultQueueSeconds = bHaveWorkerTiming && ApplyStartCycles != 0 && ApplyStartCycles >= WorkerTaskEndCycles ? FPlatformTime::ToSeconds64(ApplyStartCycles - WorkerTaskEndCycles) : (bHaveWorkerTiming ? FMath::Max(0.0, RequestToApply - WorkerQueueSeconds - WorkerTaskSeconds - ApplySeconds) : FMath::Max(0.0, RequestToApply - ApplySeconds)); if (GVoxelForgeProfileTileGeneration != 0) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTileApplied] tile=(%d,%d,%d) level=%d empty=%d triangles=%d " "band=(%d,%d) " "request_to_apply=%.6f worker_queue=%.6f worker_task=%.6f generation=%.6f " "result_queue=%.6f apply=%.6f"), Result.Tile.Coord.X, Result.Tile.Coord.Y, Result.Tile.Coord.Z, Result.Tile.Level, bResultEmpty ? 1 : 0, Result.NumTriangles, Result.BandChunkLo, Result.BandChunkHi, RequestToApply, WorkerQueueSeconds, WorkerTaskSeconds, GenerationSeconds, ResultQueueSeconds, ApplySeconds); } 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.WorkPriority = static_cast(Result.WorkPriority); Sample.RequestOrdinal = Result.RequestOrdinal; Sample.GenerationOrdinal = Result.GenerationOrdinal; Sample.RequestStartCycles = Result.RequestStartCycles; Sample.WorkerTaskStartCycles = Result.WorkerTaskStartCycles; Sample.WorkerTaskEndCycles = Result.WorkerTaskEndCycles; Sample.WorkerThreadId = Result.WorkerThreadId; Sample.bResidentAtRequest = Result.bResidentAtRequest; Sample.Verdict = Result.ClassifyVerdict; Sample.bSealedSolidProof = Result.bSealedSolidProof; Sample.bEmpty = bResultEmpty; Sample.Vertices = Result.NumVertices; Sample.Triangles = Result.NumTriangles; Sample.GeometryBytes = Result.GeometryBytes; Sample.RequestToApplySeconds = RequestToApply; Sample.WorkerQueueSeconds = WorkerQueueSeconds; Sample.ResultQueueSeconds = ResultQueueSeconds; Sample.QueueWaitSeconds = WorkerQueueSeconds + ResultQueueSeconds; Sample.WorkerTaskSeconds = WorkerTaskSeconds; Sample.GenerationSeconds = GenerationSeconds; Sample.ClassifySeconds = Result.ClassifySeconds; Sample.MeshSeconds = Result.MeshSeconds; Sample.StreamSeconds = Result.StreamSeconds; Sample.ApplySeconds = ApplySeconds; VoxelForgeStartupTrace::RecordTile(Sample); } } }; // CAPTURE-DURING-MESHING: hand the mesher's captured density grid to the clipmap BEFORE the // empty-tile early-out — all-air / all-solid tiles are exactly the uniform cells the volume // needs, and they carry a valid CaptureGrid even though they render nothing. if (DensityVolume && Settings && Settings->bEnableDensityVolume && Result.CaptureGrid.Num() > 0) { DensityVolume->IngestTileCapture(Result.Tile.Coord, MoveTemp(Result.CaptureGrid)); } // Empty mesh = no-surface tile — nothing to render, but still "loaded". if (Result.bEmpty || !Result.Streams) { // An empty re-gen invalidates any previous body and any completion future for the old // geometry. Empty tiles are loaded bookkeeping, never collision-ready. CollisionReadyTiles.Remove(Result.Tile); CollisionSuppressedUntilCook.Remove(Result.Tile); CollisionSuppressedAtCycles.Remove(Result.Tile); CollisionSolidTiles.Remove(Result.Tile); if (Result.Tile.Level == 0) { CollisionNotRequiredTiles.Add(Result.Tile); if (Result.ClassifyVerdict == static_cast(EVoxelTileClass::AllSolid)) { CollisionSolidTiles.Add(Result.Tile); } } PendingCollisionCooks.Remove(Result.Tile); // Une RE-GEN (BandRemeshQueue / RemeshDirtyChunks) peut passer de "contenu" à "vide" : // bande déplacée hors de la tuile, ou skip cellule-plus-haute-que-la-bande après un // changement de strate (LoadTile). L'ancien composant doit tomber, sinon sa vieille // géométrie (l'autre strate !) reste affichée. Première gen vide : Find rate, no-op. const uint64 EmptyReleaseStartCycles = GVoxelForgeProfileTileGeneration != 0 ? FPlatformTime::Cycles64() : 0; bool bReleasedComponent = false; if (URealtimeMeshComponent** OldComp = TileComponents.Find(Result.Tile)) { if (*OldComp) { ReleaseTileComponent(*OldComp); bReleasedComponent = true; } TileComponents.Remove(Result.Tile); } if (GVoxelForgeProfileTileGeneration != 0) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeApplyProfile] level=%d empty=1 component_release=%d total=%.6f"), Result.Tile.Level, bReleasedComponent ? 1 : 0, FPlatformTime::ToSeconds64( FPlatformTime::Cycles64() - EmptyReleaseStartCycles)); } LoadedTiles.Add(Result.Tile); AppliedModificationEpoch.Add(Result.Tile, Result.ModificationEpoch); LoadedTileGeometry.Remove(Result.Tile); if (FVisualGapAuditState* GapAudit = GVoxelForgeVisualGapAudits.Find(this)) { GapAudit->WitnessesByTile.Remove(Result.Tile); } LogTileApplied(); if (Result.Tile.Level == 0 && Result.bRecordStreamingLatency) { RecordLOD0ReadySample(Result.RequestStartCycles, Result.GenerationStartCycles, Result.GenerationEndCycles, Result.ApplyStartCycles, FPlatformTime::Cycles64(), 0); } return false; } // Apply mesh (GPU upload). The vertex/index buffers were already built (T1.f, on the worker for // the async path or inline for the sync carve path); the game thread only uploads them here. const bool bApplied = ApplyMeshToTile(Result); if (!bApplied) { // A component/mesh initialisation failure is recoverable. Do not poison LoadedTiles: the // ordinary submit loop will retry this desired tile on a later frame. CollisionReadyTiles.Remove(Result.Tile); CollisionNotRequiredTiles.Remove(Result.Tile); CollisionSolidTiles.Remove(Result.Tile); PendingCollisionCooks.Remove(Result.Tile); LoadedTiles.Remove(Result.Tile); LoadedTileGeometry.Remove(Result.Tile); if (FVisualGapAuditState* GapAudit = GVoxelForgeVisualGapAudits.Find(this)) { GapAudit->WitnessesByTile.Remove(Result.Tile); } bAllChunksLoaded = false; LogTileApplied(); return false; } if (GVoxelForgeStep1PrequeueVisibleLOD0 > 0 && Result.Tile.Level == 0) { if (URealtimeMeshComponent* Component = TileComponents.FindRef(Result.Tile)) { FStep1VisibleApplyProbe& Probe = Step1PendingVisibleApplyProbes.Add(Result.Tile); Probe.AppliedCycles = FPlatformTime::Cycles64(); Probe.AppliedFrame = GFrameCounter; Probe.LastRenderTimeBeforeApply = Component->GetLastRenderTime(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeUploadApplied] tile=(%d,%d,%d) triangles=%d frame=%llu " "apply_begin_to_submit_ms=%.3f"), Result.Tile.Coord.X, Result.Tile.Coord.Y, Result.Tile.Coord.Z, Result.NumTriangles, static_cast(Probe.AppliedFrame), FPlatformTime::ToSeconds64(Probe.AppliedCycles - ApplyStartCycles) * 1000.0); } } LoadedTiles.Add(Result.Tile); AppliedModificationEpoch.Add(Result.Tile, Result.ModificationEpoch); LoadedTileGeometry.Add(Result.Tile, FLoadedTileGeometryStats{ static_cast(FMath::Max(0, Result.NumVertices)), static_cast(FMath::Max(0, Result.NumTriangles)), Result.GeometryBytes}); if (FVisualGapAuditState* GapAudit = GVoxelForgeVisualGapAudits.Find(this)) { if (Result.SurfaceWitnesses.IsEmpty()) { GapAudit->WitnessesByTile.Remove(Result.Tile); } else { GapAudit->WitnessesByTile.Add(Result.Tile, MoveTemp(Result.SurfaceWitnesses)); } } LogTileApplied(); return true; } // Same-frame level-0 re-mesh on the game thread (see header). Mirrors LoadTile's level-0 parameters // (Cells = CHUNK_SIZE, Step = 1) + the strate content band; skips density-volume capture (the volume // is refilled from the diff via MarkDirtyVoxelBox in RemeshDirtyChunks). void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile) { if (!Generator || !Mesher || ShouldAbortWork()) return; const bool bProfileModification = GVoxelForgeProfileTileGeneration != 0; const uint64 SyncStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0; const FIntVector OriginVoxels = Tile.OriginVoxels(); const int32 Cells = CHUNK_SIZE; // level 0 is always full-res (level 0 < FullResClipLevels) const int32 Step = 1; // Extent(=CHUNK_SIZE) / Cells // STRATE CONTENT CUT — identical to LoadTile. The effective minimum is at least 1, so a // level-0 re-mesh always remains a complete collision tile; only LOD1+ can receive the band. int32 BandVoxLo = INT32_MIN, BandVoxHi = INT32_MAX; int32 BandChunkLo = MIN_int32, BandChunkHi = MAX_int32; const int32 CutMin = Settings ? Settings->GetEffectiveStrateContentCutMinLevel() : 9; if (Tile.Level >= CutMin && MeshBandChunkLo != MIN_int32) { BandChunkLo = MeshBandChunkLo; BandChunkHi = MeshBandChunkHi; BandVoxLo = MeshBandChunkLo * CHUNK_SIZE; BandVoxHi = (MeshBandChunkHi + 1) * CHUNK_SIZE - 1; } FChunkResult Result; Result.DesiredEpoch = DesiredEpoch; Result.ModificationEpoch = ModificationEpoch; Result.bRecordStreamingLatency = false; Result.bCaptureSurfaceWitnesses = GVoxelForgeVisualGapAudits.Contains(this); Result.RequestStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0; const uint64 GenerateStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0; Result.GenerationStartCycles = GenerateStartCycles; FVoxelSharedDensityGrid EditedPreEditGrid; const FVoxelSharedDensityGrid* SharedDensityOverride = nullptr; const FVoxelSharedDensityGrid* CachedPreEditGrid = PreEditDensityCache.Find(Tile); const bool bCacheHit = CachedPreEditGrid != nullptr && VoxelForgePreEditDensityCache::BuildEditedGrid( *CachedPreEditGrid, DiffLayer, EditedPreEditGrid); if (bCacheHit) { SharedDensityOverride = &EditedPreEditGrid; } const bool bCapturePreEditDensityGrid = !bCacheHit && VoxelForgePreEditDensityCache::IsPawnRadiusTile(Tile, CurrentCenterChunk); GenerateTileResult(Tile, OriginVoxels, Step, Cells, GenerationEpoch, /*bWantCapture*/ false, BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi, Result, nullptr, bCapturePreEditDensityGrid, SharedDensityOverride); Result.GenerationEndCycles = GenerateStartCycles != 0 ? FPlatformTime::Cycles64() : 0; const double GenerateSeconds = bProfileModification ? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - GenerateStartCycles) : 0.0; const uint64 ApplyStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0; const bool bResultEmpty = Result.bEmpty || !Result.Streams; ApplyTileResult(Result); if (bProfileModification) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeSyncRemeshProfile] tile=(%d,%d,%d) empty=%d generation=%.6f " "apply=%.6f total=%.6f cache_hit=%d cache_tiles=%d cache_bytes=%llu"), Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, bResultEmpty ? 1 : 0, GenerateSeconds, FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ApplyStartCycles), FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - SyncStartCycles), bCacheHit ? 1 : 0, PreEditDensityCache.Num(), static_cast(PreEditDensityCache.Num() * (CHUNK_SIZE + 3) * (CHUNK_SIZE + 3) * (CHUNK_SIZE + 3) * sizeof(float))); } } void AVoxelWorld::ProcessUnloadQueue() { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ProcessUnload); // Budgeted teardown: destroy at most a few tiles' components + content actors per frame, so a // fast traversal's whole-shell cull (dozens of UnloadTile in one frame) doesn't spike the game // thread. The budget auto-scales with the backlog (PendingUnload/4) so it never falls far behind. if (PendingUnload.Num() == 0) return; // Drain the floor budget, scaling up with the backlog so we never fall far behind, but capped // at 4× the floor so a huge backlog (extreme speed) can't itself become a one-frame spike — the // excess just lingers a few more frames (it's all behind the player, out of view). const int32 Floor = Settings ? FMath::Max(1, Settings->MaxUnloadsPerFrame) : 6; int32 DestroyBudget = FMath::Clamp(PendingUnload.Num() / 4, Floor, Floor * 4); TArray Dequeued; // removed from the queue this frame (destroyed OR cancelled) for (const FVoxelTileKey& T : PendingUnload) { if (IsDesired(T)) { // Re-desired before its turn (player reversed) — keep it; it's still loaded, just drop // it from the queue. Doesn't count against the destroy budget. if (URealtimeMeshComponent* Comp = TileComponents.FindRef(T)) { Comp->SetVisibility(!CollisionOnlyTiles.Contains(T)); } Dequeued.Add(T); continue; } UnloadTile(T); Dequeued.Add(T); if (--DestroyBudget <= 0) break; } for (const FVoxelTileKey& T : Dequeued) PendingUnload.Remove(T); } void AVoxelWorld::QueuePendingUnload(const FVoxelTileKey& Tile) { if (IsDesired(Tile)) { return; } HideOverlappingDesiredVisuals(Tile); PendingUnload.Add(Tile); TransitionHold.Remove(Tile); // Teardown remains budgeted in ProcessUnloadQueue. Hiding here is the cheap, same-frame visual // handoff; collision is deliberately left untouched until UnloadTile drains the queue. if (URealtimeMeshComponent* Comp = TileComponents.FindRef(Tile)) { Comp->SetVisibility(false); } } void AVoxelWorld::HideOverlappingDesiredVisuals(const FVoxelTileKey& RetiredTile) { for (const TPair& Pair : TileComponents) { if (Pair.Key == RetiredTile || !Pair.Value || !IsDesired(Pair.Key) || CollisionOnlyTiles.Contains(Pair.Key)) { continue; } if (VF_TileFootprintsOverlap(RetiredTile, Pair.Key)) { Pair.Value->SetVisibility(false); } } } void AVoxelWorld::ReconcileReadyTransitionVisibility() { const FVisualGapAuditState* GapAudit = GVoxelForgeVisualGapAudits.Find(this); const bool bUseLegacyVisualHandoff = GapAudit && GapAudit->bLegacyVisualHandoff; const bool bMeasure = bMeasureReadyTransitionVisibility; const uint64 MeasureStartCycles = bMeasure ? FPlatformTime::Cycles64() : 0; const int32 ComponentCountAtStart = bMeasure ? TileComponents.Num() : 0; const int32 DesiredCountAtStart = bMeasure ? DesiredSorted.Num() : 0; const auto RecordMeasure = [this, bMeasure, MeasureStartCycles, ComponentCountAtStart, DesiredCountAtStart]() { if (!bMeasure) { return; } FReadyTransitionVisibilitySample& Sample = ReadyTransitionVisibilitySamples.AddDefaulted_GetRef(); Sample.Cycles = FPlatformTime::Cycles64() - MeasureStartCycles; Sample.TileComponentCount = ComponentCountAtStart; Sample.DesiredTileCount = DesiredCountAtStart; }; if (bAllChunksLoaded && TransitionHold.Num() == 0 && PendingUnload.Num() == 0) { RecordMeasure(); return; } if (DesiredSorted.Num() == 0 || TileComponents.Num() == 0) { RecordMeasure(); return; } // This is a visibility-only pass. It runs while streaming is unsettled and after result // application, so an old component is hidden in the same game-thread turn in which the full // desired covering set becomes loaded. Destruction and collision removal stay in the budgeted // queue below. TArray ReadyToHide; ReadyToHide.Reserve(TileComponents.Num()); auto GetAncestorAtLevel = [](const FVoxelTileKey& Tile, int32 AncestorLevel) { const int32 Divisor = 1 << (AncestorLevel - Tile.Level); return FVoxelTileKey(VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord, Divisor), AncestorLevel); }; for (const TPair& Pair : TileComponents) { const FVoxelTileKey& OldTile = Pair.Key; if (!Pair.Value || IsDesired(OldTile) || PendingUnload.Contains(OldTile)) { continue; } const bool bOldTileInClipRange = IsTileInClipRange(OldTile, CurrentCenterChunk); if (bUseLegacyVisualHandoff && !bOldTileInClipRange) { ReadyToHide.Add(OldTile); continue; } bool bHasReplacement = false; bool bAllReplacementsLoaded = true; // A coarser desired tile can only overlap this tile by containing it. Test those exact // ancestors directly, then test desired finer tiles from the prebuilt ancestor index. for (int32 Level = OldTile.Level + 1; Level <= DesiredTransitionMaxLevel; ++Level) { TArray> CandidateAncestors; VF_AppendTouchingAncestors(OldTile, Level, CandidateAncestors); for (const FVoxelTileKey& NewTile : CandidateAncestors) { if (!IsDesired(NewTile) || CollisionOnlyTiles.Contains(NewTile)) { continue; } bHasReplacement = true; if (!LoadedTiles.Contains(NewTile)) { bAllReplacementsLoaded = false; break; } } if (!bAllReplacementsLoaded) { break; } } if (bAllReplacementsLoaded) { if (const TArray* NewDescendants = DesiredTransitionDescendants.Find(OldTile)) { for (const FVoxelTileKey& NewTile : *NewDescendants) { if (CollisionOnlyTiles.Contains(NewTile)) { continue; } bHasReplacement = true; if (!LoadedTiles.Contains(NewTile)) { bAllReplacementsLoaded = false; break; } } } } if ((bHasReplacement && bAllReplacementsLoaded) || (!bUseLegacyVisualHandoff && !bOldTileInClipRange && !bHasReplacement)) { // Even a tile outside the current shell may still be the only rendered cover for a // surface that remains desired. Keep it visible until every overlapping replacement // is loaded; cull immediately only when no desired tile overlaps it. ReadyToHide.Add(OldTile); } } for (const FVoxelTileKey& Tile : ReadyToHide) { QueuePendingUnload(Tile); } // Replacement components may have been uploaded hidden while a retired tile still covered // only part of the transition. Reveal them only after every overlapping retired component has // entered PendingUnload (and was therefore hidden above). Collision visibility is independent: // the component remains queryable until the budgeted UnloadTile call. TSet VisibleRetiredTiles; TSet VisibleRetiredAncestors; int32 MaxVisibleRetiredLevel = DesiredTransitionMaxLevel; for (const TPair& OldPair : TileComponents) { const FVoxelTileKey& OldTile = OldPair.Key; URealtimeMeshComponent* OldComp = OldPair.Value; if (IsDesired(OldTile) || PendingUnload.Contains(OldTile) || !OldComp || !OldComp->IsVisible()) { continue; } VisibleRetiredTiles.Add(OldTile); MaxVisibleRetiredLevel = FMath::Max(MaxVisibleRetiredLevel, OldTile.Level); for (int32 Level = OldTile.Level + 1; Level <= DesiredTransitionMaxLevel; ++Level) { VisibleRetiredAncestors.Add(GetAncestorAtLevel(OldTile, Level)); } } for (const TPair& NewPair : TileComponents) { const FVoxelTileKey& NewTile = NewPair.Key; URealtimeMeshComponent* NewComp = NewPair.Value; if (!NewComp || !IsDesired(NewTile) || CollisionOnlyTiles.Contains(NewTile)) { continue; } bool bBlockedByVisibleRetiredTile = VisibleRetiredAncestors.Contains(NewTile); for (int32 Level = NewTile.Level + 1; !bBlockedByVisibleRetiredTile && Level <= MaxVisibleRetiredLevel; ++Level) { if (VisibleRetiredTiles.Contains(GetAncestorAtLevel(NewTile, Level))) { bBlockedByVisibleRetiredTile = true; } } if (!bBlockedByVisibleRetiredTile) { NewComp->SetVisibility(true); } } RecordMeasure(); } // Integer floor-division (correct for negatives), scalar + vector. static FORCEINLINE int32 VF_FloorDiv(int32 V, int32 D) { return V >= 0 ? (V / D) : -(((-V) + D - 1) / D); } static FORCEINLINE FIntVector VF_FloorDiv(const FIntVector& V, int32 D) { return FIntVector(VF_FloorDiv(V.X, D), VF_FloorDiv(V.Y, D), VF_FloorDiv(V.Z, D)); } // The outer shell is the MaxClipLevel ring stretched to RenderDistanceChunks. Shared by desired // selection and range culling so both use the same outer horizon. static FORCEINLINE void VF_OuterShell(const UVoxelSettings* Settings, int32 R, int32 MaxLevel, int32& OutLevel, int32& OutRadius) { VoxelClipmapDesiredTiles::FParameters Parameters; Parameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0; VoxelClipmapDesiredTiles::OuterShell(Parameters, R, MaxLevel, OutLevel, OutRadius); } void AVoxelWorld::BuildDesiredTransitionIndex() { DesiredTransitionDescendants.Reset(); DesiredTransitionMaxLevel = 0; for (const FVoxelTileKey& Key : DesiredSorted) { DesiredTransitionMaxLevel = FMath::Max(DesiredTransitionMaxLevel, Key.Level); } for (const TPair& Pair : TileComponents) { DesiredTransitionMaxLevel = FMath::Max(DesiredTransitionMaxLevel, Pair.Key.Level); } constexpr int32 MaxSupportedTileLevel = 8; const int32 DescendantIndexMaxLevel = FMath::Max( DesiredTransitionMaxLevel, MaxSupportedTileLevel); for (const FVoxelTileKey& Key : DesiredSorted) { for (int32 Level = Key.Level + 1; Level <= DescendantIndexMaxLevel; ++Level) { TArray> TouchingAncestors; VF_AppendTouchingAncestors(Key, Level, TouchingAncestors); for (const FVoxelTileKey& Ancestor : TouchingAncestors) { DesiredTransitionDescendants.FindOrAdd(Ancestor).Add(Key); } } } } FIntVector AVoxelWorld::GetStreamingHeadingStep(const FVector& PlayerHeading, const APawn* PlayerPawn) const { FVector Heading = GetActorTransform().InverseTransformVectorNoScale(PlayerHeading); if (Heading.IsNearlyZero() && IsValid(PlayerPawn)) { Heading = GetActorTransform().InverseTransformVectorNoScale( PlayerPawn->GetActorForwardVector()); } const float AbsX = FMath::Abs(Heading.X); const float AbsY = FMath::Abs(Heading.Y); const float AbsZ = FMath::Abs(Heading.Z); if (AbsX <= KINDA_SMALL_NUMBER && AbsY <= KINDA_SMALL_NUMBER && AbsZ <= KINDA_SMALL_NUMBER) { return FIntVector::ZeroValue; } if (AbsX >= AbsY && AbsX >= AbsZ) { return FIntVector(Heading.X >= 0.0f ? 1 : -1, 0, 0); } if (AbsY >= AbsZ) { return FIntVector(0, Heading.Y >= 0.0f ? 1 : -1, 0); } return FIntVector(0, 0, Heading.Z >= 0.0f ? 1 : -1); } int32 AVoxelWorld::GetStreamingCollisionLookaheadTiles( const FVector& PlayerHeading, const FIntVector& HeadingStep) const { if (GVoxelForgeStreamingCollisionLookahead == 0 || HeadingStep == FIntVector::ZeroValue || PlayerHeading.Size() < 50.0f) { return 0; } // Aim 350 ms ahead, round up to a whole level-0 tile, and cap the lead at two tiles. // Viser 350 ms d'avance, arrondir au tile L0 superieur et limiter l'avance a deux tiles. const float TileWidthCm = static_cast(CHUNK_SIZE) * GetActiveVoxelSizeCm(); const int32 RequestedTiles = FMath::CeilToInt( PlayerHeading.Size() * 0.35f / FMath::Max(TileWidthCm, 1.0f)); return FMath::Clamp(RequestedTiles, 1, 2); } void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition, APawn* PlayerPawn, const FVector& PlayerHeading, TArray& OutLeavers) { DesiredSorted.Reset(); CriticalDesiredTiles.Reset(); OutLeavers.Reset(); CollisionOnlyTiles.Reset(); CollisionDesiredTiles.Reset(); ++DesiredStamp; ++DesiredEpoch; DesiredEpochStartCycles = FPlatformTime::Cycles64(); DesiredEpochCenterChunk = Center; const TSharedPtr State = VoxelForgeCoreState; if (!State.IsValid() || !State->bReady.load(std::memory_order_acquire)) return; const FVector CameraVoxels = WorldToLocalCm(PlayerPosition) / GetActiveVoxelSizeCm(); std::vector CoreDesired; { FReadScopeLock Lock(State->WorldLock); if (!State->World) return; CoreDesired = State->World->desiredTiles( {CameraVoxels.X, CameraVoxels.Y, CameraVoxels.Z}); } const FIntVector LookaheadHeadingStep = GetStreamingHeadingStep(PlayerHeading, PlayerPawn); const int32 LookaheadTiles = GetStreamingCollisionLookaheadTiles( PlayerHeading, LookaheadHeadingStep); int32 LookaheadCollisionTilesAdded = 0; int32 LookaheadDesiredTilesMissing = 0; TArray CandidateKeys; TSet CandidateSet; TSet BaseVisualTiles; TSet FullVisualAnchorTiles; TSet CollisionAnchorTiles; auto AddCandidate = [&](const FVoxelTileKey& Key) { if (!CandidateSet.Contains(Key)) { CandidateSet.Add(Key); CandidateKeys.Add(Key); } DesiredStamped.FindOrAdd(Key) = DesiredStamp; }; for (const VoxelForgeCore::TileKey& Key : CoreDesired) { const FVoxelTileKey RuntimeKey( FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod); AddCandidate(RuntimeKey); BaseVisualTiles.Add(RuntimeKey); } const int32 PawnCollisionRadius = FMath::Clamp( Settings ? Settings->PawnCollisionRadiusChunks : 2, 1, 4); VoxelCollisionDesiredTiles::AddBox( CollisionDesiredTiles, Center, PawnCollisionRadius, PawnCollisionRadius, PawnCollisionRadius); if (GVoxelForgeStep1CoreCookBenchmark != 0 && Step1SupportCohortTiles.IsEmpty()) { VoxelCollisionDesiredTiles::AddBox( Step1SupportCohortTiles, Center, PawnCollisionRadius, PawnCollisionRadius, PawnCollisionRadius); } for (const FVoxelStreamingAnchor& Anchor : StreamingAnchors) { if (!Anchor.Actor.IsValid()) continue; const FIntVector AnchorCenter = Anchor.LastChunk; const int32 XYRadius = FMath::Clamp(Anchor.XYRadiusChunks, 0, 4); const int32 ZBelow = FMath::Clamp(Anchor.ZBelowChunks, 0, 4); const int32 ZAbove = FMath::Clamp(Anchor.ZAboveChunks, 0, 4); VoxelCollisionDesiredTiles::AddBox( CollisionDesiredTiles, AnchorCenter, XYRadius, ZBelow, ZAbove); for (int32 Dz = -ZBelow; Dz <= ZAbove; ++Dz) for (int32 Dy = -XYRadius; Dy <= XYRadius; ++Dy) for (int32 Dx = -XYRadius; Dx <= XYRadius; ++Dx) { const FVoxelTileKey Key(AnchorCenter + FIntVector(Dx, Dy, Dz), 0); AddCandidate(Key); if (Anchor.Policy == EVoxelAnchorPolicy::FullVisual) { FullVisualAnchorTiles.Add(Key); } else { CollisionAnchorTiles.Add(Key); } } } // Preserve unresolved support requests from the existing collision gate. They remain in Core // tile coordinates, and level-0 bounds use the Core voxel scale through GetPlayerSupportTile. for (auto It = CollisionGateDesiredTiles.CreateIterator(); It; ++It) { const FVoxelTileKey& Key = *It; const TCHAR* UnavailableReason = nullptr; if (IsTileCollisionReady(Key) || CollisionNotRequiredTiles.Contains(Key) || IsCollisionTileUnavailable(Key, UnavailableReason)) { It.RemoveCurrent(); continue; } AddCandidate(Key); CollisionDesiredTiles.Add(Key); CollisionAnchorTiles.Add(Key); } const FVoxelTileKey PlayerTile(GetTileCoordAtLocalCm(WorldToLocalCm(PlayerPosition)), 0); FVoxelTileKey OccupiedTile = PlayerTile; FVoxelTileKey SupportTile; if (GetPlayerSupportTile(PlayerPawn, PlayerPosition, SupportTile)) { OccupiedTile = SupportTile; } // Reserve the future 3x3 collision rings inside the existing LOD0 clipmap. // Reserver les prochains anneaux 3x3 dans le clipmap LOD0 deja present. for (int32 Ahead = 1; Ahead <= LookaheadTiles; ++Ahead) { const FIntVector AheadCenter = OccupiedTile.Coord + LookaheadHeadingStep * Ahead; for (int32 Dy = -1; Dy <= 1; ++Dy) for (int32 Dx = -1; Dx <= 1; ++Dx) { const FVoxelTileKey Key(AheadCenter + FIntVector(Dx, Dy, 0), 0); if (!CandidateSet.Contains(Key)) { ++LookaheadDesiredTilesMissing; continue; } if (!CollisionDesiredTiles.Contains(Key)) { CollisionDesiredTiles.Add(Key); ++LookaheadCollisionTilesAdded; } if (!BaseVisualTiles.Contains(Key)) CollisionAnchorTiles.Add(Key); } } if (LookaheadTiles > 0 && VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("collision_lookahead"), FString::Printf( TEXT("\"center\":[%d,%d,%d],\"walkable_center\":[%d,%d,%d]," "\"heading_step\":[%d,%d,%d],\"speed_cm_s\":%s," "\"lookahead_tiles\":%d,\"collision_tiles_added\":%d,\"desired_tiles_missing\":%d"), Center.X, Center.Y, Center.Z, OccupiedTile.Coord.X, OccupiedTile.Coord.Y, OccupiedTile.Coord.Z, LookaheadHeadingStep.X, LookaheadHeadingStep.Y, LookaheadHeadingStep.Z, *FString::SanitizeFloat(PlayerHeading.Size()), LookaheadTiles, LookaheadCollisionTilesAdded, LookaheadDesiredTilesMissing)); } for (const FVoxelTileKey& Key : CollisionAnchorTiles) { if (!BaseVisualTiles.Contains(Key) && !FullVisualAnchorTiles.Contains(Key)) { CollisionOnlyTiles.Add(Key); } } std::vector PriorityInput; PriorityInput.reserve(static_cast(CandidateKeys.Num())); for (const FVoxelTileKey& Key : CandidateKeys) { PriorityInput.push_back({{Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}); } std::vector PriorityOrder = VoxelForgeCore::World::prioritizeTiles( PriorityInput, {Center.X, Center.Y, Center.Z}); DesiredSorted.Reserve(static_cast(PriorityOrder.size())); for (const VoxelForgeCore::TileKey& Key : PriorityOrder) { DesiredSorted.Emplace( FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod); } ReconcileCollisionDesiredTiles(); for (auto It = DesiredStamped.CreateIterator(); It; ++It) { if (It.Value() != DesiredStamp) { OutLeavers.Add(It.Key()); It.RemoveCurrent(); } } CancelObsoleteTileWork(); auto AddCriticalTile = [this](const FVoxelTileKey& Key) { if (IsDesired(Key) && !CriticalDesiredTiles.Contains(Key)) { CriticalDesiredTiles.Add(Key); } }; if (CollisionGateDesiredTiles.Contains(PawnGateRequestedSupportTile)) { AddCriticalTile(PawnGateRequestedSupportTile); } for (const FVoxelTileKey& Key : DesiredSorted) { if (CollisionGateDesiredTiles.Contains(Key)) AddCriticalTile(Key); } AddCriticalTile(OccupiedTile); const FIntVector HeadingStep = LookaheadHeadingStep; if (HeadingStep != FIntVector::ZeroValue) { FVoxelTileKey NextTile = OccupiedTile; NextTile.Coord += HeadingStep; AddCriticalTile(NextTile); } AddCriticalTile(PlayerTile); // The local 3x3 XY collision ring is the walkable gate; admit it with the support tile. // L'anneau de collision XY 3x3 forme le seuil praticable; il passe avec la tuile de support. for (int32 Dy = -1; Dy <= 1; ++Dy) for (int32 Dx = -1; Dx <= 1; ++Dx) { const FVoxelTileKey WalkableKey(OccupiedTile.Coord + FIntVector(Dx, Dy, 0), 0); if (CollisionDesiredTiles.Contains(WalkableKey)) AddCriticalTile(WalkableKey); } for (int32 Ahead = 1; Ahead <= LookaheadTiles; ++Ahead) { const FIntVector AheadCenter = OccupiedTile.Coord + HeadingStep * Ahead; for (int32 Dy = -1; Dy <= 1; ++Dy) for (int32 Dx = -1; Dx <= 1; ++Dx) { const FVoxelTileKey WalkableKey(AheadCenter + FIntVector(Dx, Dy, 0), 0); if (CollisionDesiredTiles.Contains(WalkableKey)) AddCriticalTile(WalkableKey); } } for (int32 Index = CriticalDesiredTiles.Num() - 1; Index >= 0; --Index) { const int32 ExistingIndex = DesiredSorted.Find(CriticalDesiredTiles[Index]); if (ExistingIndex > 0) { const FVoxelTileKey Key = DesiredSorted[ExistingIndex]; DesiredSorted.RemoveAt(ExistingIndex, 1, EAllowShrinking::No); DesiredSorted.Insert(Key, 0); } } BuildDesiredTransitionIndex(); bool bRetainedSetChanged = false; int32 RetainedTileCount = 0; TSet DesiredSet; for (const FVoxelTileKey& Key : DesiredSorted) DesiredSet.Add(Key); TArray MissingRetainedFineTiles; { FScopeLock Lock(&State->RetainedTilesLock); RetainedTileCount = static_cast(State->RetainedTiles.size()); bRetainedSetChanged = DesiredSet.Num() != RetainedTileCount; for (const FVoxelTileKey& Key : DesiredSorted) { const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}; const bool bAlreadyRetained = State->RetainedTiles.contains(CoreKey); if (!bAlreadyRetained) bRetainedSetChanged = true; if (!bAlreadyRetained && Key.Level < State->FullResolutionLodCount) { MissingRetainedFineTiles.Add(Key); } } } TArray MissingRegionTiles; { FReadScopeLock Lock(State->WorldLock); if (State->World) { for (const FVoxelTileKey& Key : MissingRetainedFineTiles) { const VoxelForgeCore::TileKey CoreKey{ {Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}; if (State->World->needsTileRegion(CoreKey)) { MissingRegionTiles.Add(Key); } } } } if (bRetainedSetChanged && !State->bRegionCompileFailed.load(std::memory_order_acquire)) { const double Now = FPlatformTime::Seconds(); const bool bHadPreviousCenter = bHasLastCoreRegionRequestCenter; const bool bCenterMoved = bHadPreviousCenter && LastCoreRegionRequestCenter != Center; const FIntVector PreviousCenter = LastCoreRegionRequestCenter; if (!bCoreRegionCompilePending) { CoreRegionPendingSinceSeconds = Now; } PendingCoreRegionRequestCenter = Center; LastCoreRegionRequestCenter = Center; bHasLastCoreRegionRequestCenter = true; PendingCoreRegionTiles = MoveTemp(MissingRegionTiles); PendingCoreRegionTileSet.Reset(); for (const FVoxelTileKey& Key : PendingCoreRegionTiles) { PendingCoreRegionTileSet.Add(Key); } PendingCoreRetainedTiles = DesiredSorted; bCoreRegionCompilePending = true; ++CoreRegionCompileRequestSerial; if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("core_region_compile_request"), FString::Printf( TEXT("\"request_serial\":%d,\"reason\":\"%s\",\"center\":[%d,%d,%d]," "\"previous_center\":[%d,%d,%d],\"center_delta\":[%d,%d,%d]," "\"desired_tiles\":%d,\"retained_tiles\":%d,\"missing_region_tiles\":%d," "\"desired_epoch\":%u,\"active_tile_tasks\":%d"), CoreRegionCompileRequestSerial, !bHadPreviousCenter ? TEXT("initial_desired_region_set") : (bCenterMoved ? TEXT("player_center_moved") : TEXT("desired_set_changed_same_center")), Center.X, Center.Y, Center.Z, PreviousCenter.X, PreviousCenter.Y, PreviousCenter.Z, bCenterMoved ? Center.X - PreviousCenter.X : 0, bCenterMoved ? Center.Y - PreviousCenter.Y : 0, bCenterMoved ? Center.Z - PreviousCenter.Z : 0, DesiredSorted.Num(), RetainedTileCount, PendingCoreRegionTiles.Num(), DesiredEpoch, ActiveTileTaskCount.load(std::memory_order_relaxed))); } StartPendingCoreRegionCompile(); } } void AVoxelWorld::StartPendingCoreRegionCompile() { if (!bCoreRegionCompilePending || (GVoxelForgeCoreRegionConcurrentAdmission == 0 && ActiveTaskCount.load(std::memory_order_acquire) != 0)) { return; } const TSharedPtr State = VoxelForgeCoreState; if (!State.IsValid() || !State->bReady.load(std::memory_order_acquire) || State->bRegionCompileFailed.load(std::memory_order_acquire)) { return; } bool bExpectedIdle = false; if (!State->bRegionCompileInFlight.compare_exchange_strong( bExpectedIdle, true, std::memory_order_acq_rel)) { return; } State->bRegionCompileFinished.store(false, std::memory_order_release); State->bRegionCompileSucceeded.store(false, std::memory_order_release); const TArray MissingTiles = PendingCoreRegionTiles; const TArray RetainedTiles = PendingCoreRetainedTiles; const FIntVector CompileCenter = PendingCoreRegionRequestCenter; const bool bAllowConcurrentGeneration = GVoxelForgeCoreRegionConcurrentAdmission != 0; const uint32 TaskEpoch = GenerationEpoch; const uint32 TaskDesiredEpoch = DesiredEpoch; const uint32 WorkerLimit = static_cast(FMath::Clamp(GetMaxConcurrentTasks(), 1, 16)); ActiveCoreRegionCompileCenter = CompileCenter; ActiveCoreRegionCompileRequestSerial = CoreRegionCompileRequestSerial; ActiveCoreRegionCompileStartSeconds = FPlatformTime::Seconds(); ActiveCoreRegionCompileStartCycles = FPlatformTime::Cycles64(); CoreRegionCompileTilesInFlight.Reset(); for (const FVoxelTileKey& Key : MissingTiles) { CoreRegionCompileTilesInFlight.Add(Key); } const int32 CompileSerial = State->RegionCompileSerial.fetch_add( 1, std::memory_order_relaxed) + 1; State->RegionCompileStartCycles.store(ActiveCoreRegionCompileStartCycles, std::memory_order_release); State->RegionCompileEndCycles.store(0, std::memory_order_release); State->RegionCompileTileCount.store(0, std::memory_order_release); State->RegionCompileWorkerSeconds.store(0.0, std::memory_order_release); if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("core_region_compile_start"), FString::Printf( TEXT("\"compile_serial\":%d,\"request_serial\":%d,\"center\":[%d,%d,%d]," "\"missing_region_tiles\":%d,\"retained_tiles\":%d,\"concurrent_admission\":%d," "\"active_tile_tasks\":%d,\"active_task_guard\":%d"), CompileSerial, CoreRegionCompileRequestSerial, CompileCenter.X, CompileCenter.Y, CompileCenter.Z, MissingTiles.Num(), RetainedTiles.Num(), bAllowConcurrentGeneration ? 1 : 0, ActiveTileTaskCount.load(std::memory_order_relaxed), ActiveTaskCount.load(std::memory_order_relaxed))); } ActiveTaskCount.fetch_add(1, std::memory_order_relaxed); UE::Tasks::Launch(TEXT("VoxelForgeCoreTileRegion"), [this, State, MissingTiles, RetainedTiles, TaskEpoch, TaskDesiredEpoch, WorkerLimit, bAllowConcurrentGeneration, CompileSerial]() { struct FTaskGuard { std::atomic& Counter; ~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_release); } } Guard{ActiveTaskCount}; if (bShuttingDown.load(std::memory_order_acquire) || State->bShuttingDown.load(std::memory_order_acquire)) { State->RegionCompileEndCycles.store(FPlatformTime::Cycles64(), std::memory_order_release); State->bRegionCompileInFlight.store(false, std::memory_order_release); State->bRegionCompileFinished.store(true, std::memory_order_release); return; } std::vector Missing; std::vector Retained; Missing.reserve(static_cast(MissingTiles.Num())); Retained.reserve(static_cast(RetainedTiles.Num())); for (const FVoxelTileKey& Key : MissingTiles) { Missing.push_back({{Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}); } for (const FVoxelTileKey& Key : RetainedTiles) { Retained.push_back({{Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}); } std::string Error; bool bCompiled = false; std::size_t CompiledTileCount = 0; VoxelForgeCore::TileRegionCompileStats CompileStats; const auto CompileAndRetain = [&]() { bCompiled = State->World->compileTileRegion( Missing, WorkerLimit, State->Scheduler, Error, &CompiledTileCount, &CompileStats); if (bCompiled) { State->World->retainTileRegions(Retained); } }; if (bAllowConcurrentGeneration) { // Tile generation also takes a shared WorldLock. Core's region map publishes immutable // shared_ptr pages under its own lock, so readers can finish with the prior page safely. // Le maillage prend aussi ce verrou en lecture. Core publie des pages immuables partagees; // les lecteurs deja partis gardent leur page precedente jusqu'a la fin. FReadScopeLock Lock(State->WorldLock); CompileAndRetain(); } else { FWriteScopeLock Lock(State->WorldLock); CompileAndRetain(); } State->RegionCompileWorkerSeconds.store(CompileStats.workerSeconds, std::memory_order_release); State->bRegionCompileSucceeded.store(bCompiled, std::memory_order_release); if (!bCompiled) { State->bRegionCompileFailed.store(true, std::memory_order_release); UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] tile-region compile failed epoch=%u desired_epoch=%u: %s"), TaskEpoch, TaskDesiredEpoch, UTF8_TO_TCHAR(Error.c_str())); } else { { FScopeLock RetainedLock(&State->RetainedTilesLock); State->RetainedTiles.clear(); for (const VoxelForgeCore::TileKey& Key : Retained) { State->RetainedTiles.insert(Key); } } State->RegionCompileTileCount.store( static_cast(CompiledTileCount), std::memory_order_release); State->bRebuildDesired.store(true, std::memory_order_release); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCoreRuntime] tile_regions_compiled=%d retained=%d epoch=%u desired_epoch=%u " "wall_ms=%.3f worker_s=%.3f serial=%d"), static_cast(CompiledTileCount), RetainedTiles.Num(), TaskEpoch, TaskDesiredEpoch, CompileStats.wallSeconds * 1000.0, CompileStats.workerSeconds, CompileSerial); } State->RegionCompileEndCycles.store(FPlatformTime::Cycles64(), std::memory_order_release); State->bRegionCompileInFlight.store(false, std::memory_order_release); State->bRegionCompileFinished.store(true, std::memory_order_release); }, UE::Tasks::ETaskPriority::BackgroundNormal); } void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition, APawn* PlayerPawn, const FVector& PlayerHeading, TArray& OutLeavers) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_BuildDesiredTiles); if (bUseVoxelForgeCore) { BuildCoreDesiredTiles(Center, PlayerPosition, PlayerPawn, PlayerHeading, OutLeavers); return; } DesiredSorted.Reset(); CriticalDesiredTiles.Reset(); OutLeavers.Reset(); CollisionOnlyTiles.Reset(); // §9.4 — rebuilt by AddAnchorDesiredTiles below CollisionDesiredTiles.Reset(); ++DesiredStamp; // les upserts ci-dessous marquent le crossing courant ++DesiredEpoch; DesiredEpochStartCycles = FPlatformTime::Cycles64(); DesiredEpochCenterChunk = Center; const FIntVector LookaheadHeadingStep = GetStreamingHeadingStep(PlayerHeading, PlayerPawn); const int32 LookaheadTiles = GetStreamingCollisionLookaheadTiles( PlayerHeading, LookaheadHeadingStep); int32 LookaheadCollisionTilesAdded = 0; int32 LookaheadDesiredTilesMissing = 0; const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 6; const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4; // Render-distance tiles expand horizontally. // IsTileInClipRange shares the outer-shell calculation so culling sees this same horizon. int32 StrateTopZ = 0, StrateBottomZ = 0; bool bHasStrate = false; if (StrateManager) { bHasStrate = StrateManager->GetStrateChunkZBounds(Center.Z, StrateTopZ, StrateBottomZ); } // Keep the clamp enabled, but cover the player's complete current strate plus its margin. // Only when no strate bounds exist (the inter-strate gap) does the small player window apply. int32 ZLo = MIN_int32, ZHi = MAX_int32; VoxelClipmapDesiredTiles::ResolveVerticalBounds( Center.Z, Settings ? Settings->ViewDistanceUp : 10, Settings ? Settings->ViewDistanceDown : 10, Settings ? Settings->StrateViewMarginChunks : 6, Settings && Settings->bClampViewToStrate, bHasStrate, StrateTopZ, StrateBottomZ, ZLo, ZHi); VoxelClipmapDesiredTiles::FParameters TileParameters; TileParameters.ClipRadius = R; TileParameters.MaxClipLevel = MaxLevel; TileParameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0; VoxelClipmapDesiredTiles::Build(Center, ZLo, ZHi, TileParameters, DesiredSorted); for (const FVoxelTileKey& Key : DesiredSorted) { DesiredStamped.FindOrAdd(Key) = DesiredStamp; } const int32 PawnCollisionRadius = FMath::Clamp( Settings ? Settings->PawnCollisionRadiusChunks : 2, 1, 4); VoxelCollisionDesiredTiles::AddBox( CollisionDesiredTiles, Center, PawnCollisionRadius, PawnCollisionRadius, PawnCollisionRadius); if (GVoxelForgeStep1CoreCookBenchmark != 0 && Step1SupportCohortTiles.IsEmpty()) { VoxelCollisionDesiredTiles::AddBox( Step1SupportCohortTiles, Center, PawnCollisionRadius, PawnCollisionRadius, PawnCollisionRadius); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStep1SupportCohort] stage=defined center=(%d,%d,%d) " "radius=%d tiles=%d"), Center.X, Center.Y, Center.Z, PawnCollisionRadius, Step1SupportCohortTiles.Num()); } // Streaming anchors (AI / remote players, §9.3): fold each one's small level-0 box into the SAME // desired set BEFORE the leaver sweep, so the delta cull releases an anchor's tiles automatically // once it moves away or is unregistered. No-op (zero cost) when there are no anchors. AddAnchorDesiredTiles(); // A pending support target and a few tiles below it are collision-only desired tiles. This // keeps an out-of-ring request alive across this rebuild and lets the worker/apply queues fill // the landing column while the pawn is still above it. for (auto It = CollisionGateDesiredTiles.CreateIterator(); It; ++It) { const FVoxelTileKey& Key = *It; const TCHAR* UnavailableReason = nullptr; if (IsTileCollisionReady(Key) || CollisionNotRequiredTiles.Contains(Key) || IsCollisionTileUnavailable(Key, UnavailableReason)) { It.RemoveCurrent(); continue; } uint32& Stamp = DesiredStamped.FindOrAdd(Key); const bool bNew = Stamp != DesiredStamp; if (bNew) { Stamp = DesiredStamp; DesiredSorted.Add(Key); CollisionOnlyTiles.Add(Key); } CollisionDesiredTiles.Add(Key); } const FVoxelTileKey PlayerTile( WorldToChunkCoord(WorldToLocalCm(PlayerPosition)), 0); FVoxelTileKey OccupiedTile = PlayerTile; FVoxelTileKey SupportTile; if (GetPlayerSupportTile(PlayerPawn, PlayerPosition, SupportTile)) { OccupiedTile = SupportTile; } // Reserve predictive collision inside the current clipmap so those tiles cook before arrival. // Reserver la collision predictive dans le clipmap courant pour cuire avant l'arrivee. for (int32 Ahead = 1; Ahead <= LookaheadTiles; ++Ahead) { const FIntVector AheadCenter = OccupiedTile.Coord + LookaheadHeadingStep * Ahead; for (int32 Dy = -1; Dy <= 1; ++Dy) for (int32 Dx = -1; Dx <= 1; ++Dx) { const FVoxelTileKey Key(AheadCenter + FIntVector(Dx, Dy, 0), 0); if (!IsDesired(Key)) { ++LookaheadDesiredTilesMissing; continue; } if (!CollisionDesiredTiles.Contains(Key)) { CollisionDesiredTiles.Add(Key); ++LookaheadCollisionTilesAdded; } } } if (LookaheadTiles > 0 && VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("collision_lookahead"), FString::Printf( TEXT("\"center\":[%d,%d,%d],\"walkable_center\":[%d,%d,%d]," "\"heading_step\":[%d,%d,%d],\"speed_cm_s\":%s," "\"lookahead_tiles\":%d,\"collision_tiles_added\":%d,\"desired_tiles_missing\":%d"), Center.X, Center.Y, Center.Z, OccupiedTile.Coord.X, OccupiedTile.Coord.Y, OccupiedTile.Coord.Z, LookaheadHeadingStep.X, LookaheadHeadingStep.Y, LookaheadHeadingStep.Z, *FString::SanitizeFloat(PlayerHeading.Size()), LookaheadTiles, LookaheadCollisionTilesAdded, LookaheadDesiredTilesMissing)); } if (GVoxelForgeProfileTileGeneration != 0) { int32 RenderLOD0Count = 0; for (const FVoxelTileKey& Key : DesiredSorted) { if (Key.Level == 0) { ++RenderLOD0Count; } } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionRadius] pawn_radius_chunks=%d collision_l0=%d " "render_l0=%d anchors=%d gate_column=%d"), PawnCollisionRadius, CollisionDesiredTiles.Num(), RenderLOD0Count, StreamingAnchors.Num(), CollisionGateDesiredTiles.Num()); } ReconcileCollisionDesiredTiles(); // Balayage UNIQUE de la map : les entrées à stamp périmé viennent de quitter le desired set — // ce sont les seuls candidats au cull de ce crossing (avec la TransitionHold). On les retire // ici même (RemoveCurrent est sûr en itérant), la map reste donc == desired set courant. for (auto It = DesiredStamped.CreateIterator(); It; ++It) { if (It.Value() != DesiredStamp) { OutLeavers.Add(It.Key()); It.RemoveCurrent(); } } if (bStartupCollisionGateTimingCaptured && !bStartupCollisionGateAdoptionReported && PendingTiles.Contains(StartupCollisionGateSupportTile)) { const bool bSupportAdopted = IsDesired(StartupCollisionGateSupportTile); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionGate] support_request_desired_reconcile tile=(%d,%d,%d) " "adopted=%d desired_epoch=%u pending=%d"), StartupCollisionGateSupportTile.Coord.X, StartupCollisionGateSupportTile.Coord.Y, StartupCollisionGateSupportTile.Coord.Z, bSupportAdopted ? 1 : 0, DesiredEpoch, PendingTiles.Contains(StartupCollisionGateSupportTile) ? 1 : 0); bStartupCollisionGateAdoptionReported = true; } // Cancel work that no longer belongs to this desired set before scheduling replacements. A // worker owns no mutable world state; it only observes its request token in GenerateTileResult. CancelObsoleteTileWork(); // Nearest-first (by tile-centre distance to the TRUE pawn position), so a large tile's // artificial chunk-centre approximation cannot put work behind the player. The explicit key // tie-break keeps equal-distance ordering deterministic across runs. const FVector PlayerVoxel = WorldToLocalCm(PlayerPosition) / VOXEL_SIZE; DesiredSorted.Sort([&PlayerVoxel](const FVoxelTileKey& A, const FVoxelTileKey& B) { const FVector CA = A.CenterCm() / VOXEL_SIZE; const FVector CB = B.CenterCm() / VOXEL_SIZE; const float DA = FVector::DistSquared(CA, PlayerVoxel); const float DB = FVector::DistSquared(CB, PlayerVoxel); if (DA != DB) { return DA < DB; } if (A.Level != B.Level) { return A.Level < B.Level; } if (A.Coord.X != B.Coord.X) { return A.Coord.X < B.Coord.X; } if (A.Coord.Y != B.Coord.Y) { return A.Coord.Y < B.Coord.Y; } return A.Coord.Z < B.Coord.Z; }); // Absolute floor priority. The support tile is the level-0 tile under the pawn's feet — the // one that can catch a falling pawn — followed by the adjacent tile in the dominant movement // direction. Keep the actor-centre tile as a fallback for capsule/offset edge cases. Only keys // in the current desired set are promoted, so this never expands the streaming footprint. auto AddCriticalTile = [this](const FVoxelTileKey& Key) { if (IsDesired(Key) && !CriticalDesiredTiles.Contains(Key)) { CriticalDesiredTiles.Add(Key); } }; if (CollisionGateDesiredTiles.Contains(PawnGateRequestedSupportTile)) { AddCriticalTile(PawnGateRequestedSupportTile); } for (const FVoxelTileKey& Key : DesiredSorted) { if (CollisionGateDesiredTiles.Contains(Key)) { AddCriticalTile(Key); } } AddCriticalTile(OccupiedTile); const FIntVector HeadingStep = LookaheadHeadingStep; if (HeadingStep != FIntVector::ZeroValue) { FVoxelTileKey NextTile = OccupiedTile; NextTile.Coord += HeadingStep; AddCriticalTile(NextTile); } AddCriticalTile(PlayerTile); // Prioritize exactly the 3x3 walkable collision neighborhood around the pawn. // Prioriser exactement le voisinage de collision praticable 3x3 autour du pion. for (int32 Dy = -1; Dy <= 1; ++Dy) for (int32 Dx = -1; Dx <= 1; ++Dx) { const FVoxelTileKey WalkableKey(OccupiedTile.Coord + FIntVector(Dx, Dy, 0), 0); if (CollisionDesiredTiles.Contains(WalkableKey)) AddCriticalTile(WalkableKey); } for (int32 Ahead = 1; Ahead <= LookaheadTiles; ++Ahead) { const FIntVector AheadCenter = OccupiedTile.Coord + HeadingStep * Ahead; for (int32 Dy = -1; Dy <= 1; ++Dy) for (int32 Dx = -1; Dx <= 1; ++Dx) { const FVoxelTileKey WalkableKey(AheadCenter + FIntVector(Dx, Dy, 0), 0); if (CollisionDesiredTiles.Contains(WalkableKey)) AddCriticalTile(WalkableKey); } } // Insert in reverse so CriticalDesiredTiles[0] remains the occupied floor, [1] the heading // tile, and all ordinary distance-sorted work follows both. for (int32 Index = CriticalDesiredTiles.Num() - 1; Index >= 0; --Index) { const int32 ExistingIndex = DesiredSorted.Find(CriticalDesiredTiles[Index]); if (ExistingIndex > 0) { const FVoxelTileKey Key = DesiredSorted[ExistingIndex]; DesiredSorted.RemoveAt(ExistingIndex, 1, EAllowShrinking::No); DesiredSorted.Insert(Key, 0); } } // Build a dyadic ancestor index once per desired-set epoch. Different-level tile overlap is // containment because every tile footprint is power-of-two aligned. This lets the per-frame // transition pass inspect only actual overlapping candidates. DesiredTransitionDescendants.Reset(); DesiredTransitionMaxLevel = 0; for (const FVoxelTileKey& Key : DesiredSorted) { DesiredTransitionMaxLevel = FMath::Max(DesiredTransitionMaxLevel, Key.Level); } for (const TPair& Pair : TileComponents) { DesiredTransitionMaxLevel = FMath::Max(DesiredTransitionMaxLevel, Pair.Key.Level); } // A worker from the previous desired epoch can finish late with a component at a level no // longer present in TileComponents when this index is built. Index through the highest // possible tile level (MaxClipLevel is clamped to 8) so that such a retired tile still // finds all of its desired descendants when the result is reconciled. constexpr int32 MaxSupportedTileLevel = 8; const int32 DescendantIndexMaxLevel = FMath::Max( DesiredTransitionMaxLevel, MaxSupportedTileLevel); for (const FVoxelTileKey& Key : DesiredSorted) { for (int32 Level = Key.Level + 1; Level <= DescendantIndexMaxLevel; ++Level) { TArray> TouchingAncestors; VF_AppendTouchingAncestors(Key, Level, TouchingAncestors); for (const FVoxelTileKey& Ancestor : TouchingAncestors) { DesiredTransitionDescendants.FindOrAdd(Ancestor).Add(Key); } } } if (VoxelForgeStartupTrace::IsActive()) { int32 LOD0Count = 0; for (const FVoxelTileKey& Key : DesiredSorted) { if (Key.Level == 0) ++LOD0Count; } VoxelForgeStartupTrace::RecordEvent(TEXT("lod0_ring_begin"), FString::Printf( TEXT("\"desired_epoch\":%u,\"center_chunk\":[%d,%d,%d],\"lod0_tiles\":%d"), DesiredEpoch, Center.X, Center.Y, Center.Z, LOD0Count)); } } void AVoxelWorld::CancelObsoleteTileWork() { for (const FVoxelTileKey& Tile : PendingTiles) { if (IsDesired(Tile)) { continue; } if (TSharedPtr* Cancellation = PendingTileCancellation.Find(Tile)) { (*Cancellation)->bObsolete.store(true, std::memory_order_release); } } } // Fold every registered anchor's small level-0 box into the current desired set (§9.3). Runs inside // BuildDesiredTiles after the player clipmap, keyed on the same DesiredStamp so the leaver // sweep + delta cull handle anchor tiles leaving. Level-0 only (collision lives on level-0 tiles); empty // tiles in the box are ~free (the trivial-tile reject skips gen). Dedup vs the player clipmap by stamp. // §9.4: a tile the player clipmap did NOT stamp (bNew) that only a CollisionOnly anchor wants goes into // CollisionOnlyTiles → hidden at apply. A FullVisual anchor (or the clipmap) forces it rendered. void AVoxelWorld::AddAnchorDesiredTiles() { for (const FVoxelStreamingAnchor& Anchor : StreamingAnchors) { if (!Anchor.Actor.IsValid()) continue; // dead ptr — pruned in UpdateChunksAroundPosition const FIntVector AC = Anchor.LastChunk; // set this Tick by the move-detection pass const int32 RXY = FMath::Clamp(Anchor.XYRadiusChunks, 0, 4); // guard the box small const int32 RZLo = FMath::Clamp(Anchor.ZBelowChunks, 0, 4); const int32 RZHi = FMath::Clamp(Anchor.ZAboveChunks, 0, 4); const bool bColl = (Anchor.Policy == EVoxelAnchorPolicy::CollisionOnly); VoxelCollisionDesiredTiles::AddBox(CollisionDesiredTiles, AC, RXY, RZLo, RZHi); for (int32 dz = -RZLo; dz <= RZHi; ++dz) for (int32 dy = -RXY; dy <= RXY; ++dy) for (int32 dx = -RXY; dx <= RXY; ++dx) { const FVoxelTileKey Key(AC + FIntVector(dx, dy, dz), 0); uint32& S = DesiredStamped.FindOrAdd(Key); const bool bNew = (S != DesiredStamp); // false ⇒ already desired (clipmap / earlier anchor) if (bNew) { S = DesiredStamp; DesiredSorted.Add(Key); } if (bColl) { // Collision-only only if NOTHING full-visual claimed this exact level-0 tile this // crossing (bNew). If the clipmap or a FullVisual anchor stamped it first, leave it rendered. if (bNew) { CollisionOnlyTiles.Add(Key); } } else { CollisionOnlyTiles.Remove(Key); // FullVisual anchor → force rendered (undo a prior coll mark) } } } } void AVoxelWorld::ReconcileCollisionDesiredTiles() { // A radius/anchor change takes effect on the same desired-set rebuild. Disable old far bodies // immediately; a late cook callback performs the same check before it can restore readiness. for (auto It = CollisionReadyTiles.CreateIterator(); It; ++It) { const FVoxelTileKey& Key = *It; if (IsTileCollisionDesired(Key)) { continue; } if (URealtimeMeshComponent* Comp = TileComponents.FindRef(Key)) { Comp->SetCollisionEnabled(ECollisionEnabled::NoCollision); } It.RemoveCurrent(); } // Tiles loaded for rendering before entering the collision radius need a collision remesh. // Proven uniform tiles have no surface/body and stay on the cheap resolved path. for (const FVoxelTileKey& Key : CollisionDesiredTiles) { if (!LoadedTiles.Contains(Key) || CollisionReadyTiles.Contains(Key) || CollisionNotRequiredTiles.Contains(Key) || PendingTiles.Contains(Key) || PendingCollisionCooks.Contains(Key)) { continue; } DirtyRemeshQueue.Add(Key); bAllChunksLoaded = false; } } // §9.4 — apply visibility flips to ALREADY-LOADED tiles when a tile changed render↔collision-only this // crossing (player walked toward/away from a CollisionOnly cluster). Bounded by the collision-only set // (small); a no-op when there are no CollisionOnly anchors. Newly-loaded tiles get their state at apply // (ApplyMeshToTile reads CollisionOnlyTiles). Called after BuildDesiredTiles rebuilt CollisionOnlyTiles. void AVoxelWorld::ReconcileAnchorTileVisibility() { if (CollisionOnlyTiles.Num() == 0 && PrevCollisionOnlyTiles.Num() == 0) return; // fast path // Became collision-only → hide (if loaded). for (const FVoxelTileKey& Key : CollisionOnlyTiles) { if (!PrevCollisionOnlyTiles.Contains(Key)) { if (URealtimeMeshComponent* Comp = TileComponents.FindRef(Key)) { Comp->SetVisibility(false); } } } // Stopped being collision-only → show, but only if still desired (else it's a leaver being culled — // don't flash it visible on its way out). for (const FVoxelTileKey& Key : PrevCollisionOnlyTiles) { if (!CollisionOnlyTiles.Contains(Key) && IsDesired(Key)) { if (URealtimeMeshComponent* Comp = TileComponents.FindRef(Key)) { Comp->SetVisibility(true); } } } PrevCollisionOnlyTiles = CollisionOnlyTiles; } void AVoxelWorld::RegisterStreamingAnchor(AActor* Actor, EVoxelAnchorPolicy Policy, int32 XYRadiusChunks, int32 ZBelowChunks, int32 ZAboveChunks) { if (!Actor) return; for (FVoxelStreamingAnchor& Existing : StreamingAnchors) { if (Existing.Actor.Get() == Actor) // already registered → update policy/box in place { Existing.Policy = Policy; Existing.XYRadiusChunks = XYRadiusChunks; Existing.ZBelowChunks = ZBelowChunks; Existing.ZAboveChunks = ZAboveChunks; bForceDesiredRebuild = true; return; } } FVoxelStreamingAnchor A; A.Actor = Actor; A.Policy = Policy; A.XYRadiusChunks = XYRadiusChunks; A.ZBelowChunks = ZBelowChunks; A.ZAboveChunks = ZAboveChunks; // LastChunk stays at its sentinel → next Tick's move detection sets it + triggers the rebuild. StreamingAnchors.Add(A); bForceDesiredRebuild = true; } void AVoxelWorld::UnregisterStreamingAnchor(AActor* Actor) { if (!Actor) return; for (int32 i = StreamingAnchors.Num() - 1; i >= 0; --i) { if (StreamingAnchors[i].Actor.Get() == Actor) { StreamingAnchors.RemoveAtSwap(i); bForceDesiredRebuild = true; // rebuild next Tick so its now-unwanted tiles become leavers } } } bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector& Center) const { // In range = the tile's centre falls within the OUTERMOST shell (VF_OuterShell — the // render-distance level+radius, same as BuildDesiredTiles). A loaded-but-not-desired tile can // still cover a desired surface outside this shell, so the caller checks replacement readiness // before hiding it in either case. const int32 R = bUseVoxelForgeCore ? FMath::Clamp(CoreClipRadius, 1, 64) : (Settings ? FMath::Max(1, Settings->ClipRadius) : 6); const int32 MaxLevelBase = bUseVoxelForgeCore ? FMath::Clamp(CoreMaxClipLevel, 0, 8) : (Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4); int32 MaxLevel = MaxLevelBase, ROuter = R; if (bUseVoxelForgeCore) { VoxelForgeCore::WorldConfiguration Configuration; const TSharedPtr State = VoxelForgeCoreState; if (State.IsValid()) { FReadScopeLock Lock(State->WorldLock); if (State->World) Configuration = State->World->configuration(); } VoxelClipmapDesiredTiles::FParameters Parameters; Parameters.RenderDistanceChunks = Configuration.renderDistanceChunks; VoxelClipmapDesiredTiles::OuterShell(Parameters, R, MaxLevelBase, MaxLevel, ROuter); } else { VF_OuterShell(Settings, R, MaxLevelBase, MaxLevel, ROuter); } const int32 PowMax = 1 << MaxLevel; const FIntVector CMax = VF_FloorDiv(Center, PowMax); FVector CV; if (bUseVoxelForgeCore) { const double TileExtentVoxels = static_cast(CHUNK_SIZE << Tile.Level); CV = FVector( (static_cast(Tile.Coord.X) + 0.5) * TileExtentVoxels, (static_cast(Tile.Coord.Y) + 0.5) * TileExtentVoxels, (static_cast(Tile.Coord.Z) + 0.5) * TileExtentVoxels); } else { CV = Tile.CenterCm() / VOXEL_SIZE; } const int32 SizeMax = CHUNK_SIZE * PowMax; const FIntVector TMax( VF_FloorDiv(FMath::FloorToInt(CV.X), SizeMax), VF_FloorDiv(FMath::FloorToInt(CV.Y), SizeMax), VF_FloorDiv(FMath::FloorToInt(CV.Z), SizeMax)); return FMath::Abs(TMax.X - CMax.X) <= ROuter && FMath::Abs(TMax.Y - CMax.Y) <= ROuter && FMath::Abs(TMax.Z - CMax.Z) <= ROuter; } int32 AVoxelWorld::GetMaxConcurrentTasks() const { // T2.d — both sides of the cap are authored in Voxel|Streaming. BackgroundNormal priority // (see LoadTile) yields to frame work; this cap avoids oversubscribing available workers. // La priorite BackgroundNormal cede la place au frame ; ce plafond evite de depasser les workers disponibles. const int32 Asset = Settings ? Settings->MaxConcurrentTasks : 16; const int32 LogicalCores = FMath::Max( 1, FPlatformMisc::NumberOfCoresIncludingHyperthreads()); const int32 AutomaticCap = FMath::Max(2, LogicalCores - 2); const int32 AuthoredCoreCap = Settings ? Settings->MaxGenerationWorkerCores : 0; const int32 CoreCap = AuthoredCoreCap > 0 ? FMath::Clamp(AuthoredCoreCap, 1, LogicalCores) : AutomaticCap; const int32 NormalCap = FMath::Clamp(Asset, 1, CoreCap); if (GVoxelForgeStep1GenerationCap == 0) { return 0; } return GVoxelForgeStep1GenerationCap > 0 ? FMath::Min(NormalCap, GVoxelForgeStep1GenerationCap) : NormalCap; } int32 AVoxelWorld::GetMaxConcurrentStreamingTasks() const { const int32 TaskCap = GetMaxConcurrentTasks(); if (TaskCap <= 0 || GVoxelForgeStreamingBackgroundWorkerCap == 0) { return TaskCap; } const int32 BackgroundWorkers = FTaskGraphInterface::Get().GetNumBackgroundThreads(); return BackgroundWorkers > 0 ? FMath::Min(TaskCap, BackgroundWorkers) : TaskCap; } int32 AVoxelWorld::GetActiveGenerationTaskCount() const { return FMath::Max(0, ActiveTaskCount.load(std::memory_order_relaxed)); } int32 AVoxelWorld::GetGenerationAdmissionCount() const { if (GVoxelForgeStreamingLegacyPendingResultCap == 0) { return GetActiveGenerationTaskCount(); } const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0 ? Step1HeldApplyTiles.Num() : 0; return FMath::Max(0, PendingTiles.Num() - HeldCompletedResults); } uint64 AVoxelWorld::GetStreamingResultPayloadBytes(const FChunkResult& Result) const { uint64 Bytes = Result.GeometryBytes; if (Bytes == 0 && Result.Streams.IsValid()) { Result.Streams->ForEach([&Bytes](const RealtimeMesh::FRealtimeMeshStream& Stream) { Bytes += static_cast(Stream.GetAllocatedSize()); }); } Bytes += static_cast(Result.CaptureGrid.GetAllocatedSize()); Bytes += static_cast(Result.PreEditDensityGrid.Samples.GetAllocatedSize()); Bytes += static_cast(Result.SurfaceWitnesses.GetAllocatedSize()); return Bytes; } int32 AVoxelWorld::GetStreamingResultBacklogCount() const { const int32 Queued = FMath::Max(0, QueuedStreamingResultCount.load(std::memory_order_acquire)); const int32 ActiveTiles = FMath::Max(0, ActiveTileTaskCount.load(std::memory_order_acquire)); const int32 HeldResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0 ? Step1HeldApplyTiles.Num() : 0; return Queued + ActiveTiles + HeldResults; } bool AVoxelWorld::IsStreamingResultBacklogCapped() const { const int32 Limit = FMath::Max(0, GVoxelForgeStreamingResultBacklogCap); return Limit > 0 && GetStreamingResultBacklogCount() >= Limit; } void AVoxelWorld::EnqueueStreamingResult(FChunkResult&& Result) { const uint64 Bytes = GetStreamingResultPayloadBytes(Result); const int32 Count = QueuedStreamingResultCount.fetch_add(1, std::memory_order_acq_rel) + 1; const uint64 TotalBytes = QueuedStreamingResultBytes.fetch_add(Bytes, std::memory_order_acq_rel) + Bytes; int32 PeakCount = PeakQueuedStreamingResultCount.load(std::memory_order_relaxed); while (Count > PeakCount && !PeakQueuedStreamingResultCount.compare_exchange_weak( PeakCount, Count, std::memory_order_relaxed)) { } uint64 PeakBytes = PeakQueuedStreamingResultBytes.load(std::memory_order_relaxed); while (TotalBytes > PeakBytes && !PeakQueuedStreamingResultBytes.compare_exchange_weak( PeakBytes, TotalBytes, std::memory_order_relaxed)) { } if (Result.WorkPriority == EVoxelTileWorkPriority::CollisionCritical) { CriticalProcessQueue.Enqueue(MoveTemp(Result)); } else if (Result.WorkPriority == EVoxelTileWorkPriority::Edited) { EditedProcessQueue.Enqueue(MoveTemp(Result)); } else if (Result.WorkPriority == EVoxelTileWorkPriority::LOD0) { NearProcessQueue.Enqueue(MoveTemp(Result)); } else { ProcessQueue.Enqueue(MoveTemp(Result)); } } void AVoxelWorld::RecordStreamingResultDequeued(const FChunkResult& Result) { QueuedStreamingResultCount.fetch_sub(1, std::memory_order_acq_rel); QueuedStreamingResultBytes.fetch_sub( GetStreamingResultPayloadBytes(Result), std::memory_order_acq_rel); } int32 AVoxelWorld::GetNearTaskReserve(int32 MaxTasks) const { const int32 AuthoredReserve = Settings ? Settings->NearTaskReserve : 4; // Keep at least one slot usable by coarse work even when an authored reserve is larger than // the effective task budget. A one-slot budget has no meaningful reservation to make. return FMath::Clamp(AuthoredReserve, 0, FMath::Max(0, MaxTasks - 1)); } void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APawn* PlayerPawn, const FVector& PlayerHeading) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateChunks); // Measure the scheduler update only in opt-in trace runs; keep the normal frame path unchanged. // Mesurer l'ordonnanceur uniquement dans les traces opt-in ; garder le chemin normal sans surcout. const bool bTraceStreamingScheduler = VoxelForgeStartupTrace::IsActive(); const double StreamingUpdateStartSeconds = bTraceStreamingScheduler ? FPlatformTime::Seconds() : 0.0; const int32 PendingTilesAtUpdateEntry = PendingTiles.Num(); const uint64 ResultBacklogCapStopsAtUpdateEntry = StreamingResultBacklogCapStops.load(std::memory_order_relaxed); double DesiredRebuildMilliseconds = 0.0; const auto RecordSchedulerSkip = [&](const TCHAR* Reason) { if (!bTraceStreamingScheduler) return; const double Now = FPlatformTime::Seconds(); int32 DesiredMissing = 0; int32 DesiredMissingByLevel[9] = {}; for (const FVoxelTileKey& Tile : DesiredSorted) { if (!PendingTiles.Contains(Tile) && !LoadedTiles.Contains(Tile)) { ++DesiredMissing; if (Tile.Level >= 0 && Tile.Level < UE_ARRAY_COUNT(DesiredMissingByLevel)) { ++DesiredMissingByLevel[Tile.Level]; } } } FString DesiredMissingByLevelJson = TEXT("["); for (int32 Level = 0; Level < UE_ARRAY_COUNT(DesiredMissingByLevel); ++Level) { if (Level > 0) DesiredMissingByLevelJson += TEXT(","); DesiredMissingByLevelJson += FString::FromInt(DesiredMissingByLevel[Level]); } DesiredMissingByLevelJson += TEXT("]"); const int32 ActiveTasks = ActiveTaskCount.load(std::memory_order_relaxed); const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0 ? Step1HeldApplyTiles.Num() : 0; const int32 PendingResultsEstimate = FMath::Max( 0, PendingTiles.Num() - ActiveTasks - HeldCompletedResults); const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); const int32 AdmissionOccupancy = GetGenerationAdmissionCount(); const uint64 TotalWorkerCycles = TotalGenerationWorkerTaskCycles.load(std::memory_order_relaxed); const uint64 WorkerCyclesDelta = TotalWorkerCycles >= LastStreamingSchedulerWorkerCycles ? TotalWorkerCycles - LastStreamingSchedulerWorkerCycles : 0; const double IntervalStart = LastStreamingSchedulerSampleSeconds > 0.0 ? LastStreamingSchedulerSampleSeconds : StreamingUpdateStartSeconds; const double IntervalSeconds = FMath::Max(0.0, Now - IntervalStart); const double BusySeconds = FPlatformTime::ToSeconds64(WorkerCyclesDelta); const double SpeedCmPerSecond = PlayerHeading.Size(); VoxelForgeStartupTrace::RecordEvent(TEXT("scheduler_skip"), FString::Printf( TEXT("\"frame\":%llu,\"reason\":\"%s\",\"desired_count\":%d," "\"desired_missing\":%d,\"desired_missing_by_lod\":%s," "\"moving\":%d,\"speed_cm_s\":%s," "\"pending_tiles\":%d,\"admission_occupancy\":%d," "\"active_task_guard\":%d,\"running_workers\":%d,\"pending_results_estimate\":%d," "\"queued_results\":%d,\"queued_result_bytes\":%llu," "\"result_backlog\":%d,\"result_backlog_cap\":%d," "\"task_cap\":%d,\"legacy_pending_result_cap\":%d," "\"worker_busy_s_interval\":%s,\"worker_busy_avg\":%s"), static_cast(GFrameCounter), Reason, DesiredSorted.Num(), DesiredMissing, *DesiredMissingByLevelJson, SpeedCmPerSecond >= 50.0 ? 1 : 0, *FString::SanitizeFloat(SpeedCmPerSecond), PendingTiles.Num(), AdmissionOccupancy, ActiveTasks, ActiveGenerationWorkerCount.load(std::memory_order_relaxed), PendingResultsEstimate, QueuedStreamingResultCount.load(std::memory_order_relaxed), static_cast(QueuedStreamingResultBytes.load(std::memory_order_relaxed)), GetStreamingResultBacklogCount(), GVoxelForgeStreamingResultBacklogCap, MaxTasks, GVoxelForgeStreamingLegacyPendingResultCap, *FString::SanitizeFloat(BusySeconds), *FString::SanitizeFloat(IntervalSeconds > 0.0 ? BusySeconds / IntervalSeconds : 0.0))); LastStreamingSchedulerSampleSeconds = Now; LastStreamingSchedulerWorkerCycles = TotalWorkerCycles; }; bool bBlockAllCoreAdmissionWhileCompiling = false; if (bUseVoxelForgeCore) { const TSharedPtr State = VoxelForgeCoreState; if (!State.IsValid() || State->bInitialFailed.load(std::memory_order_acquire) || !State->bReady.load(std::memory_order_acquire)) { RecordSchedulerSkip(TEXT("core_not_ready")); return; // Core mode never falls through to the legacy generator. } if (State->bRebuildDesired.exchange(false, std::memory_order_acq_rel)) { bForceDesiredRebuild = true; } if (bCoreRegionCompilePending) { if (State->bRegionCompileFinished.exchange(false, std::memory_order_acq_rel)) { const bool bRegionCompileSucceeded = State->bRegionCompileSucceeded.load(std::memory_order_acquire); const uint64 CompileStartCycles = State->RegionCompileStartCycles.load(std::memory_order_acquire); const uint64 CompileEndCycles = State->RegionCompileEndCycles.load(std::memory_order_acquire); const double CompileElapsedMilliseconds = CompileEndCycles >= CompileStartCycles ? FPlatformTime::ToSeconds64(CompileEndCycles - CompileStartCycles) * 1000.0 : 0.0; const double GateElapsedMilliseconds = CoreRegionPendingSinceSeconds > 0.0 ? FMath::Max(0.0, FPlatformTime::Seconds() - CoreRegionPendingSinceSeconds) * 1000.0 : 0.0; const int32 CompiledTileCount = State->RegionCompileTileCount.load(std::memory_order_acquire); const double CompileWorkerSeconds = State->RegionCompileWorkerSeconds.load(std::memory_order_acquire); const int32 CompileSerial = State->RegionCompileSerial.load(std::memory_order_acquire); const uint64 IndependentSubmissions = CoreRegionIndependentSubmissions; const uint64 DependentDeclines = CoreRegionDependentDeclines; if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("core_region_compile_complete"), FString::Printf( TEXT("\"compile_serial\":%d,\"request_serial\":%d,\"center\":[%d,%d,%d]," "\"success\":%d,\"compiled_tiles\":%d,\"compile_elapsed_ms\":%s," "\"compile_worker_s\":%s,\"pending_gate_elapsed_ms\":%s," "\"independent_submissions\":%llu," "\"dependent_declines\":%llu,\"concurrent_admission\":%d"), CompileSerial, ActiveCoreRegionCompileRequestSerial, ActiveCoreRegionCompileCenter.X, ActiveCoreRegionCompileCenter.Y, ActiveCoreRegionCompileCenter.Z, bRegionCompileSucceeded ? 1 : 0, CompiledTileCount, *FString::SanitizeFloat(CompileElapsedMilliseconds), *FString::SanitizeFloat(CompileWorkerSeconds), *FString::SanitizeFloat(GateElapsedMilliseconds), static_cast(IndependentSubmissions), static_cast(DependentDeclines), GVoxelForgeCoreRegionConcurrentAdmission != 0 ? 1 : 0)); } CoreRegionIndependentSubmissions = 0; CoreRegionDependentDeclines = 0; CoreRegionCompileTilesInFlight.Reset(); if (bRegionCompileSucceeded) { bForceDesiredRebuild = true; bool bRetainedSetMatchesDesired = false; TArray MissingRetainedFineTiles; { FScopeLock RetainedLock(&State->RetainedTilesLock); bRetainedSetMatchesDesired = DesiredSorted.Num() == static_cast(State->RetainedTiles.size()); for (const FVoxelTileKey& Key : DesiredSorted) { const VoxelForgeCore::TileKey CoreKey{ {Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}; if (!State->RetainedTiles.contains(CoreKey)) { bRetainedSetMatchesDesired = false; if (Key.Level < State->FullResolutionLodCount) { MissingRetainedFineTiles.Add(Key); } } } } TArray MissingRegionTiles; { FReadScopeLock WorldLock(State->WorldLock); if (State->World) { for (const FVoxelTileKey& Key : MissingRetainedFineTiles) { const VoxelForgeCore::TileKey CoreKey{ {Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}; if (State->World->needsTileRegion(CoreKey)) { MissingRegionTiles.Add(Key); } } } } if (!bRetainedSetMatchesDesired) { if (CoreRegionPendingSinceSeconds <= 0.0) { CoreRegionPendingSinceSeconds = FPlatformTime::Seconds(); } PendingCoreRegionTiles = MoveTemp(MissingRegionTiles); PendingCoreRegionTileSet.Reset(); for (const FVoxelTileKey& Key : PendingCoreRegionTiles) { PendingCoreRegionTileSet.Add(Key); } PendingCoreRetainedTiles = DesiredSorted; PendingCoreRegionRequestCenter = DesiredEpochCenterChunk; bCoreRegionCompilePending = true; StartPendingCoreRegionCompile(); } else { bCoreRegionCompilePending = false; PendingCoreRegionTiles.Reset(); PendingCoreRegionTileSet.Reset(); PendingCoreRetainedTiles.Reset(); CoreRegionPendingSinceSeconds = 0.0; } } else { bCoreRegionCompilePending = false; PendingCoreRegionTiles.Reset(); PendingCoreRegionTileSet.Reset(); PendingCoreRetainedTiles.Reset(); CoreRegionPendingSinceSeconds = 0.0; } } else { StartPendingCoreRegionCompile(); } bBlockAllCoreAdmissionWhileCompiling = bCoreRegionCompilePending && GVoxelForgeCoreRegionConcurrentAdmission == 0; } } const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); // Keep at most one worker-set of ready work so an admission pass can feed idle slots // without letting far work consume the near reserve. / Garder une file d'un ensemble // de workers pour nourrir les slots sans consommer la reserve proche. const int32 ReadyQueueDepth = FMath::Clamp( GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks); const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth; const int32 AuthoredNearTaskReserve = GetNearTaskReserve(MaxTasks); const FIntVector CenterChunk = GetTileCoordAtLocalCm(WorldToLocalCm(CenterPosition)); FVoxelTileKey WalkableSupportTile; const FIntVector WalkableCenter = GetPlayerSupportTile( PlayerPawn, CenterPosition, WalkableSupportTile) ? WalkableSupportTile.Coord : CenterChunk; bool bWalkableRingReadyForReserve = true; int32 WalkableRingReadyCount = 0; for (int32 Dy = -1; Dy <= 1; ++Dy) for (int32 Dx = -1; Dx <= 1; ++Dx) { const FVoxelTileKey WalkableKey(WalkableCenter + FIntVector(Dx, Dy, 0), 0); if (CollisionReadyTiles.Contains(WalkableKey) || CollisionNotRequiredTiles.Contains(WalkableKey)) { ++WalkableRingReadyCount; } else { bWalkableRingReadyForReserve = false; } } if (VoxelForgeStartupTrace::IsActive()) { const bool bGateCenterChanged = !bHasLastWalkableGateSample || LastWalkableGateCenter != WalkableCenter; const bool bGateStateChanged = !bHasLastWalkableGateSample || bLastWalkableGateReady != bWalkableRingReadyForReserve; if (bGateCenterChanged || bGateStateChanged) { const FIntVector PreviousGateCenter = LastWalkableGateCenter; VoxelForgeStartupTrace::RecordEvent(TEXT("walkable_gate"), FString::Printf( TEXT("\"support_center\":[%d,%d,%d],\"stream_center\":[%d,%d,%d]," "\"previous_support_center\":[%d,%d,%d],\"center_delta\":[%d,%d,%d]," "\"center_changed\":%d,\"ready\":%d,\"ready_tiles\":%d," "\"required_tiles\":9,\"speed_cm_s\":%s,\"desired_epoch\":%u"), WalkableCenter.X, WalkableCenter.Y, WalkableCenter.Z, CenterChunk.X, CenterChunk.Y, CenterChunk.Z, PreviousGateCenter.X, PreviousGateCenter.Y, PreviousGateCenter.Z, bGateCenterChanged ? WalkableCenter.X - PreviousGateCenter.X : 0, bGateCenterChanged ? WalkableCenter.Y - PreviousGateCenter.Y : 0, bGateCenterChanged ? WalkableCenter.Z - PreviousGateCenter.Z : 0, bGateCenterChanged ? 1 : 0, bWalkableRingReadyForReserve ? 1 : 0, WalkableRingReadyCount, *FString::SanitizeFloat(PlayerHeading.Size()), DesiredEpoch)); bHasLastWalkableGateSample = true; LastWalkableGateCenter = WalkableCenter; bLastWalkableGateReady = bWalkableRingReadyForReserve; } } if (bBlockAllCoreAdmissionWhileCompiling) { RecordSchedulerSkip(TEXT("core_region_compile_pending")); return; } const bool bPawnMoving = PlayerHeading.Size() >= 50.0f; bStreamingPlayerMoving = bPawnMoving; // Match the near reserve to the added far queue so queued horizon tiles cannot spend that headroom. // Aligner la reserve proche sur la file lointaine pour que l'horizon ne consomme pas cette marge. const int32 QueueAwareNearTaskReserve = FMath::Clamp( FMath::Max(AuthoredNearTaskReserve, ReadyQueueDepth), 0, FMath::Max(0, MaxTasks - 1)); EffectiveNearTaskReserve = GVoxelForgeStreamingDynamicNearReserve == 0 ? AuthoredNearTaskReserve : ((bPawnMoving || !bWalkableRingReadyForReserve) ? QueueAwareNearTaskReserve : 0); const int32 NearTaskReserve = EffectiveNearTaskReserve; const int32 NearAdmissionCap = bPawnMoving ? MaxTasks : TotalAdmissionCap; const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve) + ReadyQueueDepth; const int32 FirstCoarseLevel = FMath::Max( 2, Settings ? Settings->FullResClipLevels : 2); const bool bUseCoarseFirstOrder = GVoxelForgeStreamingCoarseFirst != 0 && bWalkableRingReadyForReserve; int32 SchedulerSubmittedByLevel[9] = {}; int32 SchedulerLoadAttemptsByLevel[9] = {}; int32 SchedulerLevelCapReturns[9] = {}; int32 SchedulerLevelCapBreaks[9] = {}; int32 SchedulerCriticalFullCapBreaks = 0; int32 SchedulerDirtyFullCapBreaks = 0; int32 SchedulerBandCoarseCapBreaks = 0; int32 SchedulerOrderStopLevel = -1; int32 SchedulerOrderStopCap = 0; if (GVoxelForgeProfileTileGeneration != 0 && !GVoxelForgeStreamingBudgetReported) { const int32 LogicalCores = FMath::Max( 1, FPlatformMisc::NumberOfCoresIncludingHyperthreads()); const int32 AutomaticCap = FMath::Max(2, LogicalCores - 2); const int32 BackgroundWorkerThreads = FTaskGraphInterface::Get().GetNumBackgroundThreads(); const int32 AssetCap = Settings ? Settings->MaxConcurrentTasks : 16; const int32 AuthoredCoreCap = Settings ? Settings->MaxGenerationWorkerCores : 0; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStreamingBudget] logical_cores=%d automatic_core_cap=%d " "authored_core_cap=%d asset_task_cap=%d effective_generation_cap=%d " "taskgraph_background_workers=%d background_worker_cap=%d " "ready_queue_depth=%d total_admission_cap=%d " "step1_generation_cap=%d step1_max_mesh_applies=%d " "near_task_reserve=%d dynamic_reserve=%d legacy_result_cap=%d " "moving=%d walkable_ring_ready=%d " "coarse_task_cap=%d coarse_first=%d " "max_mesh_applies=%d max_apply_ms=%.3f max_unloads=%d"), LogicalCores, AutomaticCap, AuthoredCoreCap, AssetCap, MaxTasks, BackgroundWorkerThreads, GVoxelForgeStreamingBackgroundWorkerCap, ReadyQueueDepth, TotalAdmissionCap, GVoxelForgeStep1GenerationCap, GVoxelForgeStep1MaxMeshApplies, NearTaskReserve, GVoxelForgeStreamingDynamicNearReserve, GVoxelForgeStreamingLegacyPendingResultCap, bPawnMoving ? 1 : 0, bWalkableRingReadyForReserve ? 1 : 0, CoarseTaskCap, GVoxelForgeStreamingCoarseFirst, Settings ? Settings->MaxMeshAppliesPerFrame : 4, Settings ? Settings->MaxMeshApplyMilliseconds : 2.0f, Settings ? Settings->MaxUnloadsPerFrame : 6); GVoxelForgeStreamingBudgetReported = true; } CurrentCenterChunk = CenterChunk; for (auto It = PreEditDensityCache.CreateIterator(); It; ++It) { if (!VoxelForgePreEditDensityCache::IsPawnRadiusTile(It.Key(), CenterChunk)) { It.RemoveCurrent(); } } // Streaming anchors (AI / remote players, §9.3): prune dead ones + detect chunk crossings so the // desired set rebuilds when an anchor moves (its box of collision tiles follows it). Cheap: a few // WorldToChunkCoord per anchor, and empty (no anchors) is a zero-iteration loop. bool bAnchorsMoved = false; for (int32 i = StreamingAnchors.Num() - 1; i >= 0; --i) { AActor* A = StreamingAnchors[i].Actor.Get(); if (!A) { StreamingAnchors.RemoveAtSwap(i); // destroyed → drop it; its tiles must be culled bAnchorsMoved = true; continue; } const FIntVector AC = GetTileCoordAtLocalCm(WorldToLocalCm(A->GetActorLocation())); if (AC != StreamingAnchors[i].LastChunk) { StreamingAnchors[i].LastChunk = AC; bAnchorsMoved = true; } } //========================================================================= // Rebuild the desired tile set when the player crosses a level-0 tile boundary — or when a // streaming anchor moved / (un)registered (bAnchorsMoved / bForceDesiredRebuild). //========================================================================= const bool bDesiredRebuiltThisUpdate = CenterChunk != LastUpdateCenter || bAnchorsMoved || bForceDesiredRebuild; if (bDesiredRebuiltThisUpdate) { LastUpdateCenter = CenterChunk; bAllChunksLoaded = false; bForceDesiredRebuild = false; // consumed // DELTA CULL: BuildDesiredTiles returns the LEAVERS (desired on the previous crossing, but // not now). Only those leavers + TransitionHold (retained across prior crossings) enter // the cull. Pending work that leaves desired is marked obsolete before replacement submit; // its aborted result is drained and dropped, while the settled cull remains the full-scan // safety net. TArray Leavers; const bool bTraceFirstDesired = VoxelForgeStartupTrace::IsActive() && !bStartupTraceDesiredRecorded; if (bTraceFirstDesired) { VoxelForgeStartupTrace::StageBegin(TEXT("BuildDesiredTiles.first")); } const double DesiredRebuildStartSeconds = bTraceStreamingScheduler ? FPlatformTime::Seconds() : 0.0; BuildDesiredTiles(CenterChunk, CenterPosition, PlayerPawn, PlayerHeading, Leavers); if (bTraceStreamingScheduler) { DesiredRebuildMilliseconds = (FPlatformTime::Seconds() - DesiredRebuildStartSeconds) * 1000.0; } if (bTraceFirstDesired) { VoxelForgeStartupTrace::StageEnd(TEXT("BuildDesiredTiles.first")); struct FDesiredLevelSummary { int32 Level = -1; int32 Count = 0; FIntVector Min = FIntVector(MAX_int32, MAX_int32, MAX_int32); FIntVector Max = FIntVector(MIN_int32, MIN_int32, MIN_int32); }; TArray LevelSummaries; for (const FVoxelTileKey& Key : DesiredSorted) { int32 SummaryIndex = INDEX_NONE; for (int32 Index = 0; Index < LevelSummaries.Num(); ++Index) { if (LevelSummaries[Index].Level == Key.Level) { SummaryIndex = Index; break; } } if (SummaryIndex == INDEX_NONE) { SummaryIndex = LevelSummaries.AddDefaulted(); LevelSummaries[SummaryIndex].Level = Key.Level; } FDesiredLevelSummary& Summary = LevelSummaries[SummaryIndex]; ++Summary.Count; Summary.Min.X = FMath::Min(Summary.Min.X, Key.Coord.X); Summary.Min.Y = FMath::Min(Summary.Min.Y, Key.Coord.Y); Summary.Min.Z = FMath::Min(Summary.Min.Z, Key.Coord.Z); Summary.Max.X = FMath::Max(Summary.Max.X, Key.Coord.X); Summary.Max.Y = FMath::Max(Summary.Max.Y, Key.Coord.Y); Summary.Max.Z = FMath::Max(Summary.Max.Z, Key.Coord.Z); } LevelSummaries.Sort([](const FDesiredLevelSummary& A, const FDesiredLevelSummary& B) { return A.Level < B.Level; }); VoxelForgeStartupTrace::RecordEvent(TEXT("desired_set"), FString::Printf( TEXT("\"center_chunk\":[%d,%d,%d],\"total\":%d,\"levels\":%d,\"leavers\":%d," "\"collision_only\":%d"), CenterChunk.X, CenterChunk.Y, CenterChunk.Z, DesiredSorted.Num(), LevelSummaries.Num(), Leavers.Num(), CollisionOnlyTiles.Num())); for (const FDesiredLevelSummary& Summary : LevelSummaries) { const int32 ExtentVoxels = CHUNK_SIZE << Summary.Level; const double TileExtentMetres = static_cast(ExtentVoxels) * VOXEL_SIZE / 100.0; const double SpanX = static_cast(Summary.Max.X - Summary.Min.X + 1) * TileExtentMetres; const double SpanY = static_cast(Summary.Max.Y - Summary.Min.Y + 1) * TileExtentMetres; const double SpanZ = static_cast(Summary.Max.Z - Summary.Min.Z + 1) * TileExtentMetres; VoxelForgeStartupTrace::RecordEvent(TEXT("desired_level"), FString::Printf( TEXT("\"level\":%d,\"tiles\":%d,\"min_coord\":[%d,%d,%d],\"max_coord\":[%d,%d,%d]," "\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s]"), Summary.Level, Summary.Count, Summary.Min.X, Summary.Min.Y, Summary.Min.Z, Summary.Max.X, Summary.Max.Y, Summary.Max.Z, *FString::SanitizeFloat(TileExtentMetres), *FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ))); } bStartupTraceDesiredRecorded = true; } // §9.4 — toggle visibility on already-loaded tiles that flipped render↔collision-only this // crossing (a CollisionOnly cluster the player just walked toward/away from). No-op w/o anchors. ReconcileAnchorTileVisibility(); // STRATE CONTENT CUT — the band coarse tiles are meshed to = the player strate's EXACT // chunk-Z bounds (no margin: that's the view clamp's job — selection vs content). In the // inter-strate gap: no band (full tiles, you can see both sides through the descent). // On band change (strate transition), re-queue the loaded coarse tiles whose mesh depends // on it — fully inside BOTH bands ⇒ identical either way; fully outside both ⇒ empty // either way; everything else re-gens in place via BandRemeshQueue (no visual pop). { const int32 CutMin = Settings ? Settings->GetEffectiveStrateContentCutMinLevel() : 9; int32 NewLo = MIN_int32, NewHi = MAX_int32; if (CutMin <= 8 && StrateManager) { int32 StrTopZ = 0, StrBotZ = 0; if (StrateManager->GetStrateChunkZBounds(CenterChunk.Z, StrTopZ, StrBotZ)) { NewLo = StrBotZ; NewHi = StrTopZ; } } if (NewLo != MeshBandChunkLo || NewHi != MeshBandChunkHi) { // Diagnostic volontairement VISIBLE : si cette ligne n'apparaît JAMAIS dans // l'Output Log, la coupe de contenu ne s'est jamais armée (bounds de strate // introuvables pour la position du PION → tout se maille plein, comme avant). UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] Strate content band -> chunks [%d..%d] (was [%d..%d]), CutMinLevel=%d, pawn chunk Z=%d"), NewLo, NewHi, MeshBandChunkLo, MeshBandChunkHi, CutMin, CenterChunk.Z); for (const FVoxelTileKey& T : LoadedTiles) { if (T.Level < CutMin) continue; const int32 CLo = T.Coord.Z << T.Level; const int32 CHi = ((T.Coord.Z + 1) << T.Level) - 1; const bool bInOld = CLo >= MeshBandChunkLo && CHi <= MeshBandChunkHi; const bool bInNew = CLo >= NewLo && CHi <= NewHi; const bool bOutOld = CHi < MeshBandChunkLo || CLo > MeshBandChunkHi; const bool bOutNew = CHi < NewLo || CLo > NewHi; if ((bInOld && bInNew) || (bOutOld && bOutNew)) continue; // même contenu if (!IsDesired(T)) continue; // sera cull, pas re-gen BandRemeshQueue.Add(T); } MeshBandChunkLo = NewLo; MeshBandChunkHi = NewHi; bAllChunksLoaded = false; // le drain de BandRemeshQueue vit dans le bloc submit } } // Cull rules — STRICT LOAD-BEFORE-UNLOAD so crossing a shell boundary NEVER leaves a hole: // cull only after EVERY desired tile that overlaps the retiring footprint is loaded, whether // the old tile is still inside the shell or has crossed the outer clip range. Otherwise it // stays in TransitionHold until the covering set is ready. { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_CullTiles); auto FootprintsOverlap = [](const FVoxelTileKey& A, const FVoxelTileKey& B) -> bool { const int32 ea = A.ExtentVoxels(), eb = B.ExtentVoxels(); const FIntVector aMin = A.OriginVoxels(), bMin = B.OriginVoxels(); return aMin.X <= bMin.X + eb && bMin.X <= aMin.X + ea && aMin.Y <= bMin.Y + eb && bMin.Y <= aMin.Y + ea && aMin.Z <= bMin.Z + eb && bMin.Z <= aMin.Z + ea; }; // "Every covering desired tile loaded" ⟺ "no unloaded desired tile overlaps T". // Built LAZILY: only needed if an in-range transition candidate actually exists. TArray DesiredPending; bool bPendingBuilt = false; auto EnsurePending = [&]() { if (bPendingBuilt) return; bPendingBuilt = true; for (const FVoxelTileKey& D : DesiredSorted) { if (!LoadedTiles.Contains(D)) DesiredPending.Add(D); } }; auto ReplacementsReady = [&](const FVoxelTileKey& T) -> bool { for (const FVoxelTileKey& D : DesiredPending) { if (CollisionOnlyTiles.Contains(D)) { continue; } if (FootprintsOverlap(T, D)) return false; // a covering tile isn't ready → keep T } return true; }; // true = la tuile doit être RETENUE (transition en attente de ses remplaçants) ; // false = rien à retenir (cullée, re-désirée, ou jamais chargée). auto NeedsHold = [&](const FVoxelTileKey& T) -> bool { if (IsDesired(T)) { return false; } // re-désirée if (!LoadedTiles.Contains(T) && !TileComponents.Contains(T)) { return false; // jamais chargée / rien d'appliqué → rien à cull } const FVisualGapAuditState* GapAudit = GVoxelForgeVisualGapAudits.Find(this); if (GapAudit && GapAudit->bLegacyVisualHandoff && !IsTileInClipRange(T, CurrentCenterChunk)) { QueuePendingUnload(T); return false; } // Quand le backlog est gros (sprint), sauter le test de recouvrement et RETENIR // est la direction hole-safe, y compris juste au-delà du shell extérieur. EnsurePending(); if (DesiredPending.Num() <= 48 && ReplacementsReady(T)) { QueuePendingUnload(T); return false; } HideOverlappingDesiredVisuals(T); return true; }; for (const FVoxelTileKey& T : Leavers) { if (NeedsHold(T)) { AddToTransitionHold(T); } } // Hold : re-évaluation à BUDGET tournant. Re-scanner toute la hold par crossing // redevient le vieux scan O(loaded) dès que le streaming ne settle jamais (mesuré // 2.47 ms/crossing packagé) ; retenir plus longtemps est hole-safe, chaque tuile // repasse sous le curseur en quelques crossings. int32 HoldBudget = FMath::Min(TransitionHoldQueue.Num(), 256); while (HoldBudget > 0 && TransitionHoldQueue.Num() > 0) { if (TransitionHoldCursor >= TransitionHoldQueue.Num()) { TransitionHoldCursor = 0; } const FVoxelTileKey T = TransitionHoldQueue[TransitionHoldCursor]; if (!TransitionHold.Contains(T)) { // Clé périmée (déchargée / settled-cullée) — retrait paresseux, ne consomme // pas le budget (la queue rétrécit ⇒ la boucle termine). TransitionHoldQueue.RemoveAtSwap(TransitionHoldCursor); continue; } --HoldBudget; if (!NeedsHold(T)) { TransitionHold.Remove(T); TransitionHoldQueue.RemoveAtSwap(TransitionHoldCursor); } else { ++TransitionHoldCursor; } } // Teardown différé — ProcessUnloadQueue étale les destructions sur plusieurs frames. } } //========================================================================= if (bUseVoxelForgeCore && bCoreRegionCompilePending && GVoxelForgeCoreRegionConcurrentAdmission == 0) { RecordSchedulerSkip(TEXT("core_region_compile_pending")); return; } // Submit pending work (collision-critical -> edits -> coarse shell -> fine detail). // Soumettre le travail (collision critique -> modifications -> enveloppe grossiere -> detail fin). // Coarse work is capped below the full task budget so later near requests keep admission. // Le travail grossier reste sous le budget total pour reserver des admissions aux tuiles proches. //========================================================================= const bool bSubmissionLoopRan = !bAllChunksLoaded; if (bSubmissionLoopRan) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_SubmitTiles); int32 Submitted = 0; const auto SubmitTileWithTraceCount = [&](const FVoxelTileKey& Tile, EVoxelTileWorkPriority Priority) { if (Tile.Level >= 0 && Tile.Level < 9) { ++SchedulerLoadAttemptsByLevel[Tile.Level]; } const uint64 OrdinalBefore = NextStreamingRequestOrdinal; LoadTile(Tile, Priority); if (NextStreamingRequestOrdinal != OrdinalBefore && Tile.Level >= 0 && Tile.Level < 9) { ++SchedulerSubmittedByLevel[Tile.Level]; } }; // FLOOR RESPONSIVENESS — the support tile and collision-required 3x3 XY ring lead ordinary work. // PRIORITE AU SOL — la tuile de support et l'anneau XY 3x3 requis pour la collision passent avant le reste. // They use the foreground task lane when enabled, above ordinary background streaming. // Ils utilisent la voie foreground lorsqu'elle est active, avant le streaming distant. for (const FVoxelTileKey& T : CriticalDesiredTiles) { if (GetGenerationAdmissionCount() >= TotalAdmissionCap) { ++SchedulerCriticalFullCapBreaks; break; } if (PendingTiles.Contains(T) || LoadedTiles.Contains(T)) continue; SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::CollisionCritical); ++Submitted; } // DIG RESPONSIVENESS — drain player-carve re-meshes next at BackgroundHigh. A tile that's // still in flight is KEPT queued (retried next frame) so its stale pre-carve result is corrected. for (auto It = DirtyRemeshQueue.CreateIterator(); It; ++It) { if (GetGenerationAdmissionCount() >= TotalAdmissionCap) { ++SchedulerDirtyFullCapBreaks; break; } const FVoxelTileKey T = *It; if (!IsDesired(T)) { It.RemoveCurrent(); continue; } // obsolete — drop if (PendingTiles.Contains(T)) { continue; } // in flight — retry after it lands It.RemoveCurrent(); SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::Edited); ++Submitted; } const uint64 FarFieldMeasureStartCycles = bMeasureReadyTransitionVisibility ? FPlatformTime::Cycles64() : 0; int32 MaxDesiredLevel = 0; for (const FVoxelTileKey& T : DesiredSorted) { MaxDesiredLevel = FMath::Max(MaxDesiredLevel, T.Level); } auto SubmitLevel = [&](int32 Level, EVoxelTileWorkPriority Priority, int32 AdmissionCap, bool bPreferTravelDirection) { if (GetGenerationAdmissionCount() >= AdmissionCap) { if (Level >= 0 && Level < 9) { ++SchedulerLevelCapReturns[Level]; } return; } TArray LevelTiles; for (const FVoxelTileKey& T : DesiredSorted) { if (T.Level == Level && !PendingTiles.Contains(T) && !LoadedTiles.Contains(T)) { LevelTiles.Add(T); } } // Sort only outstanding keys; re-sorting loaded or pending tiles on every tick inflated game-thread work. // Trier uniquement les cles restantes ; re-trier chaque tick les tuiles chargees ou en attente alourdissait le thread de jeu. if (bPreferTravelDirection && LevelTiles.Num() > 1) { FVector Heading = GetActorTransform().InverseTransformVectorNoScale(PlayerHeading); if (Heading.IsNearlyZero() && IsValid(PlayerPawn)) { Heading = GetActorTransform().InverseTransformVectorNoScale( PlayerPawn->GetActorForwardVector()); } Heading = Heading.GetSafeNormal(); const FVector PlayerLocalCm = WorldToLocalCm(CenterPosition); const double TileSizeCm = static_cast(CHUNK_SIZE << Level) * GetActiveVoxelSizeCm(); LevelTiles.Sort([&](const FVoxelTileKey& A, const FVoxelTileKey& B) { const FVector CenterA( (static_cast(A.Coord.X) + 0.5) * TileSizeCm, (static_cast(A.Coord.Y) + 0.5) * TileSizeCm, (static_cast(A.Coord.Z) + 0.5) * TileSizeCm); const FVector CenterB( (static_cast(B.Coord.X) + 0.5) * TileSizeCm, (static_cast(B.Coord.Y) + 0.5) * TileSizeCm, (static_cast(B.Coord.Z) + 0.5) * TileSizeCm); const FVector OffsetA = CenterA - PlayerLocalCm; const FVector OffsetB = CenterB - PlayerLocalCm; const bool bAheadA = !Heading.IsNearlyZero() && FVector::DotProduct(OffsetA, Heading) > 0.0; const bool bAheadB = !Heading.IsNearlyZero() && FVector::DotProduct(OffsetB, Heading) > 0.0; if (bAheadA != bAheadB) return bAheadA; const double DistanceA = OffsetA.SizeSquared(); const double DistanceB = OffsetB.SizeSquared(); if (DistanceA != DistanceB) return DistanceA < DistanceB; if (A.Coord.X != B.Coord.X) return A.Coord.X < B.Coord.X; if (A.Coord.Y != B.Coord.Y) return A.Coord.Y < B.Coord.Y; return A.Coord.Z < B.Coord.Z; }); } for (const FVoxelTileKey& T : LevelTiles) { if (GetGenerationAdmissionCount() >= AdmissionCap) { if (Level >= 0 && Level < 9) { ++SchedulerLevelCapBreaks[Level]; } break; } if (PendingTiles.Contains(T)) continue; if (LoadedTiles.Contains(T)) continue; SubmitTileWithTraceCount(T, Priority); ++Submitted; } }; // Coarse-first gives the far shell the first non-critical admissions. Keep the current // collision-ready 3x3 ring above it, then spend the remaining slots on full-resolution // refinement. The opt-out reproduces the S4 LOD0-to-LOD8 submission order for a control. // Le shell lointain passe apres l'anneau de collision, puis le raffinement fin. L'option // inverse restaure l'ordre LOD0-vers-LOD8 de S4 comme controle negatif. // Keep the walkable ring on the legacy near-first path; only spend its reserved task slots // on coarse work after all nine collision gates are ready. // Garder l'anneau praticable en priorite ; les taches grossieres commencent apres ses neuf collisions. if (bUseCoarseFirstOrder) { for (int32 Level = MaxDesiredLevel; Level >= FirstCoarseLevel; --Level) { SubmitLevel(Level, EVoxelTileWorkPriority::Horizon, CoarseTaskCap, true); } for (int32 Level = 0; Level < FirstCoarseLevel && Level <= MaxDesiredLevel; ++Level) { const EVoxelTileWorkPriority Priority = Level == 0 ? EVoxelTileWorkPriority::LOD0 : EVoxelTileWorkPriority::Horizon; const int32 LevelAdmissionCap = Priority == EVoxelTileWorkPriority::Horizon ? CoarseTaskCap : NearAdmissionCap; SubmitLevel(Level, Priority, LevelAdmissionCap, true); } } else { SubmitLevel(0, EVoxelTileWorkPriority::LOD0, NearAdmissionCap, false); for (int32 Level = 1; Level <= MaxDesiredLevel; ++Level) { SubmitLevel(Level, EVoxelTileWorkPriority::Horizon, CoarseTaskCap, false); if (GetGenerationAdmissionCount() >= CoarseTaskCap) { if (Level >= 0 && Level < 9) { ++SchedulerLevelCapBreaks[Level]; } SchedulerOrderStopLevel = Level; SchedulerOrderStopCap = CoarseTaskCap; break; } } } if (bMeasureReadyTransitionVisibility) { FFarFieldSubmitSample& Sample = FarFieldSubmitSamples.AddDefaulted_GetRef(); Sample.Cycles = FPlatformTime::Cycles64() - FarFieldMeasureStartCycles; Sample.DesiredTileCount = DesiredSorted.Num(); Sample.MaxDesiredLevel = MaxDesiredLevel; } // Bande de strate changée : re-gen budgétée des tuiles grossières concernées (le vieux // mesh reste visible jusqu'au résultat — même schéma que RemeshDirtyChunks). for (auto It = BandRemeshQueue.CreateIterator(); It; ++It) { if (GetGenerationAdmissionCount() >= CoarseTaskCap) { ++SchedulerBandCoarseCapBreaks; break; } const FVoxelTileKey T = *It; It.RemoveCurrent(); if (!IsDesired(T) || PendingTiles.Contains(T) || !LoadedTiles.Contains(T)) continue; SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::Horizon); ++Submitted; } if (GVoxelForgeProfileTileGeneration != 0 && PendingTiles.Num() > GVoxelForgeMaxPendingTilesObserved) { GVoxelForgeMaxPendingTilesObserved = PendingTiles.Num(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStreamingBudget] pending_max=%d effective=%d ready_queue=%d submitted=%d"), GVoxelForgeMaxPendingTilesObserved, MaxTasks, ReadyQueueDepth, Submitted); } if (GVoxelForgeProfileTileGeneration != 0 && GetGenerationAdmissionCount() >= CoarseTaskCap && !GVoxelForgeGenerationCapHitReported) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStreamingBudget] coarse_generation_cap_hit=1 pending=%d " "coarse_cap=%d effective=%d near_reserve=%d"), PendingTiles.Num(), CoarseTaskCap, MaxTasks, NearTaskReserve); GVoxelForgeGenerationCapHitReported = true; } if (Submitted == 0 && PendingTiles.Num() == 0 && BandRemeshQueue.Num() == 0 && DirtyRemeshQueue.Num() == 0) { // Everything desired is loaded → load-before-unload is satisfied: drop the // deferred (in-range, not-desired) transition tiles now. No holes. Full scan — // rare (once per settle), and the safety net behind the delta cull above. for (const auto& Pair : TileComponents) if (!IsDesired(Pair.Key)) QueuePendingUnload(Pair.Key); for (const FVoxelTileKey& T : LoadedTiles) if (!IsDesired(T) && !TileComponents.Contains(T)) QueuePendingUnload(T); // Teardown is drained by ProcessUnloadQueue (budgeted) — single spike-free path. bAllChunksLoaded = true; } } if (VoxelForgeStartupTrace::IsActive()) { const double TraceNow = FPlatformTime::Seconds(); if (TraceNow - LastStreamingTraceSampleSeconds >= 0.25) { int32 DesiredByLevel[9] = {}; int32 ResidentByLevel[9] = {}; int32 PendingByLevel[9] = {}; for (const FVoxelTileKey& Tile : DesiredSorted) { if (Tile.Level < 0 || Tile.Level > 8) continue; ++DesiredByLevel[Tile.Level]; if (LoadedTiles.Contains(Tile)) ++ResidentByLevel[Tile.Level]; if (PendingTiles.Contains(Tile)) ++PendingByLevel[Tile.Level]; } FString DesiredCounts = TEXT("["); FString ResidentCounts = TEXT("["); FString PendingCounts = TEXT("["); for (int32 Level = 0; Level <= 8; ++Level) { if (Level > 0) { DesiredCounts += TEXT(","); ResidentCounts += TEXT(","); PendingCounts += TEXT(","); } DesiredCounts += FString::FromInt(DesiredByLevel[Level]); ResidentCounts += FString::FromInt(ResidentByLevel[Level]); PendingCounts += FString::FromInt(PendingByLevel[Level]); } DesiredCounts += TEXT("]"); ResidentCounts += TEXT("]"); PendingCounts += TEXT("]"); const FVector LocalPawn = WorldToLocalCm(CenterPosition); const double StreamingUpdateMilliseconds = (TraceNow - StreamingUpdateStartSeconds) * 1000.0; VoxelForgeStartupTrace::RecordEvent(TEXT("streaming_sample"), FString::Printf( TEXT("\"scope\":\"desired_residency_not_visual_enclosure\",\"center\":[%d,%d,%d]," "\"position_cm\":[%s,%s,%s],\"speed_cm_s\":%s,\"walkable_ready\":%d," "\"desired_by_lod\":%s,\"resident_by_lod\":%s,\"pending_by_lod\":%s," "\"active_generation\":%d,\"admission_occupancy\":%d," "\"task_cap\":%d,\"near_reserve\":%d," "\"coarse_first\":%d,\"dynamic_near_reserve\":%d," "\"legacy_pending_result_cap\":%d,\"epoch\":%u," "\"update_chunks_ms\":%s,\"desired_rebuild_ms\":%s"), CenterChunk.X, CenterChunk.Y, CenterChunk.Z, *FString::SanitizeFloat(LocalPawn.X), *FString::SanitizeFloat(LocalPawn.Y), *FString::SanitizeFloat(LocalPawn.Z), *FString::SanitizeFloat(PlayerHeading.Size()), bWalkableRingReadyForReserve ? 1 : 0, *DesiredCounts, *ResidentCounts, *PendingCounts, GetActiveGenerationTaskCount(), GetGenerationAdmissionCount(), MaxTasks, NearTaskReserve, GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve, GVoxelForgeStreamingLegacyPendingResultCap, GenerationEpoch, *FString::SanitizeFloat(StreamingUpdateMilliseconds), *FString::SanitizeFloat(DesiredRebuildMilliseconds))); LastStreamingTraceSampleSeconds = TraceNow; } // Mesurer chaque decision d'admission avec l'etat du meme tick, y compris les resultats en attente. // Capture each admission decision from this tick, including completed results still awaiting apply. int32 DesiredMissingByLevel[9] = {}; int32 LoadedByLevel[9] = {}; int32 PendingByLevel[9] = {}; int32 AdmissionCapByLevel[9] = {}; int32 DeclinedRuleByLevel[9] = {}; for (const FVoxelTileKey& Tile : DesiredSorted) { if (Tile.Level < 0 || Tile.Level > 8) continue; if (PendingTiles.Contains(Tile)) { ++PendingByLevel[Tile.Level]; } else if (LoadedTiles.Contains(Tile)) { ++LoadedByLevel[Tile.Level]; } else { ++DesiredMissingByLevel[Tile.Level]; } } const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0 ? Step1HeldApplyTiles.Num() : 0; const int32 AdmissionOccupancy = GetGenerationAdmissionCount(); const int32 ActiveTaskGuard = ActiveTaskCount.load(std::memory_order_relaxed); const int32 PendingResultsEstimate = FMath::Max( 0, PendingTiles.Num() - ActiveTaskGuard - HeldCompletedResults); int32 FullCapDeclinedTiles = 0; int32 ReserveDeclinedTiles = 0; int32 InnerLoadDeclinedTiles = 0; int32 ResultBacklogCapDeclinedTiles = 0; const bool bResultBacklogCapHitThisUpdate = StreamingResultBacklogCapStops.load(std::memory_order_relaxed) > ResultBacklogCapStopsAtUpdateEntry; for (int32 Level = 0; Level < 9; ++Level) { AdmissionCapByLevel[Level] = bUseCoarseFirstOrder ? (Level >= FirstCoarseLevel || Level > 0 ? CoarseTaskCap : NearAdmissionCap) : (Level == 0 ? NearAdmissionCap : CoarseTaskCap); if (DesiredMissingByLevel[Level] <= 0) continue; if (bAllChunksLoaded) { DeclinedRuleByLevel[Level] = 3; // Loaded latch while unadmitted desired keys remain. // Verrou charge alors que des cles desirees restent non admises. } else if (SchedulerLevelCapReturns[Level] > 0 || SchedulerLevelCapBreaks[Level] > 0 || (SchedulerOrderStopLevel >= 0 && Level >= SchedulerOrderStopLevel)) { const int32 DeclinedAtCap = AdmissionCapByLevel[Level] > 0 ? AdmissionCapByLevel[Level] : SchedulerOrderStopCap; if (DeclinedAtCap < TotalAdmissionCap) { DeclinedRuleByLevel[Level] = 2; // Near reserve reached during this submit pass. // Reserve proche atteinte pendant cette passe d'admission. ReserveDeclinedTiles += DesiredMissingByLevel[Level]; } else { DeclinedRuleByLevel[Level] = 1; // Full task cap reached during this submit pass. // Capacite totale des taches atteinte pendant cette passe. FullCapDeclinedTiles += DesiredMissingByLevel[Level]; } } else if (AdmissionOccupancy >= TotalAdmissionCap) { DeclinedRuleByLevel[Level] = 1; // Full task admission cap. // Capacite totale d'admission des taches atteinte. FullCapDeclinedTiles += DesiredMissingByLevel[Level]; } else if (AdmissionOccupancy >= AdmissionCapByLevel[Level] && AdmissionCapByLevel[Level] < TotalAdmissionCap) { DeclinedRuleByLevel[Level] = 2; // Authored near reserve held back this LOD. // La reserve proche configuree retient ce niveau de detail. ReserveDeclinedTiles += DesiredMissingByLevel[Level]; } else if (bResultBacklogCapHitThisUpdate) { DeclinedRuleByLevel[Level] = 6; // Result backlog is at its independent safety cap. // Le plafond independant de resultats en attente est atteint. ResultBacklogCapDeclinedTiles += DesiredMissingByLevel[Level]; } else if (SchedulerLoadAttemptsByLevel[Level] > SchedulerSubmittedByLevel[Level]) { DeclinedRuleByLevel[Level] = 4; // LoadTile/LoadCoreTile declined despite a scheduler attempt. // LoadTile/LoadCoreTile a refuse la tuile apres la tentative du scheduler. InnerLoadDeclinedTiles += DesiredMissingByLevel[Level]; } else { DeclinedRuleByLevel[Level] = 5; // Candidate remains with apparent admission capacity. // La tuile desiree reste manquante avec une capacite d'admission apparente. } } const auto IntArrayJson = [](const int32 Values[9]) { FString Json = TEXT("["); for (int32 Index = 0; Index < 9; ++Index) { if (Index > 0) Json += TEXT(","); Json += FString::FromInt(Values[Index]); } Json += TEXT("]"); return Json; }; const double SchedulerTraceNow = FPlatformTime::Seconds(); const uint64 TotalWorkerCycles = TotalGenerationWorkerTaskCycles.load(std::memory_order_relaxed); const uint64 WorkerCyclesDelta = TotalWorkerCycles >= LastStreamingSchedulerWorkerCycles ? TotalWorkerCycles - LastStreamingSchedulerWorkerCycles : 0; const double IntervalStart = LastStreamingSchedulerSampleSeconds > 0.0 ? LastStreamingSchedulerSampleSeconds : StreamingUpdateStartSeconds; const double IntervalSeconds = FMath::Max(0.0, SchedulerTraceNow - IntervalStart); const double BusySeconds = FPlatformTime::ToSeconds64(WorkerCyclesDelta); VoxelForgeStartupTrace::RecordEvent(TEXT("scheduler_admission"), FString::Printf( TEXT("\"frame\":%llu,\"center\":[%d,%d,%d],\"desired_rebuilt\":%d," "\"submission_loop_ran\":%d,\"all_chunks_loaded\":%d," "\"walkable_ready\":%d,\"moving\":%d," "\"core_region_compile_pending\":%d,\"core_ready\":%d,\"coarse_first\":%d," "\"dynamic_near_reserve\":%d,\"legacy_pending_result_cap\":%d," "\"core_region_concurrent_admission\":%d," "\"max_tasks\":%d,\"ready_queue_depth\":%d," "\"total_admission_cap\":%d,\"near_admission_cap\":%d," "\"near_reserve\":%d," "\"coarse_cap\":%d,\"first_coarse_level\":%d," "\"submission_stop_level\":%d,\"submission_stop_cap\":%d," "\"pending_entry\":%d,\"pending_after\":%d,\"admission_occupancy\":%d," "\"active_task_guard\":%d,\"running_workers\":%d,\"pending_results_estimate\":%d," "\"queued_results\":%d,\"queued_result_bytes\":%llu," "\"result_backlog\":%d,\"result_backlog_cap\":%d," "\"region_independent_submissions\":%llu,\"region_dependent_declines\":%llu," "\"worker_busy_s_interval\":%s,\"worker_busy_avg\":%s," "\"desired_missing_by_lod\":%s,\"loaded_by_lod\":%s,\"pending_by_lod\":%s," "\"submitted_by_lod\":%s,\"load_attempts_by_lod\":%s," "\"admission_cap_by_lod\":%s,\"declined_rule_by_lod\":%s," "\"level_cap_returns_by_lod\":%s,\"level_cap_breaks_by_lod\":%s," "\"full_cap_declined_tiles\":%d,\"reserve_declined_tiles\":%d," "\"inner_load_declined_tiles\":%d,\"result_backlog_cap_declined_tiles\":%d," "\"critical_full_cap_breaks\":%d," "\"dirty_full_cap_breaks\":%d,\"band_coarse_cap_breaks\":%d"), static_cast(GFrameCounter), CenterChunk.X, CenterChunk.Y, CenterChunk.Z, bDesiredRebuiltThisUpdate ? 1 : 0, bSubmissionLoopRan ? 1 : 0, bAllChunksLoaded ? 1 : 0, bWalkableRingReadyForReserve ? 1 : 0, bPawnMoving ? 1 : 0, bCoreRegionCompilePending ? 1 : 0, IsVoxelForgeCoreReady() ? 1 : 0, GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve, GVoxelForgeStreamingLegacyPendingResultCap, GVoxelForgeCoreRegionConcurrentAdmission, MaxTasks, ReadyQueueDepth, TotalAdmissionCap, NearAdmissionCap, NearTaskReserve, CoarseTaskCap, FirstCoarseLevel, SchedulerOrderStopLevel, SchedulerOrderStopCap, PendingTilesAtUpdateEntry, PendingTiles.Num(), AdmissionOccupancy, ActiveTaskGuard, ActiveGenerationWorkerCount.load(std::memory_order_relaxed), PendingResultsEstimate, QueuedStreamingResultCount.load(std::memory_order_relaxed), static_cast(QueuedStreamingResultBytes.load(std::memory_order_relaxed)), GetStreamingResultBacklogCount(), GVoxelForgeStreamingResultBacklogCap, static_cast(CoreRegionIndependentSubmissions), static_cast(CoreRegionDependentDeclines), *FString::SanitizeFloat(BusySeconds), *FString::SanitizeFloat(IntervalSeconds > 0.0 ? BusySeconds / IntervalSeconds : 0.0), *IntArrayJson(DesiredMissingByLevel), *IntArrayJson(LoadedByLevel), *IntArrayJson(PendingByLevel), *IntArrayJson(SchedulerSubmittedByLevel), *IntArrayJson(SchedulerLoadAttemptsByLevel), *IntArrayJson(AdmissionCapByLevel), *IntArrayJson(DeclinedRuleByLevel), *IntArrayJson(SchedulerLevelCapReturns), *IntArrayJson(SchedulerLevelCapBreaks), FullCapDeclinedTiles, ReserveDeclinedTiles, InnerLoadDeclinedTiles, ResultBacklogCapDeclinedTiles, SchedulerCriticalFullCapBreaks, SchedulerDirtyFullCapBreaks, SchedulerBandCoarseCapBreaks)); LastStreamingSchedulerSampleSeconds = SchedulerTraceNow; LastStreamingSchedulerWorkerCycles = TotalWorkerCycles; } else if (bTraceStreamingScheduler) { // Report a loaded latch only if desired work is still unadmitted; the normal no-missing path stays quiet. // Signaler le verrou charge seulement s'il reste du travail desire non admis. for (const FVoxelTileKey& Tile : DesiredSorted) { if (!PendingTiles.Contains(Tile) && !LoadedTiles.Contains(Tile)) { RecordSchedulerSkip(TEXT("all_chunks_loaded_latch")); break; } } } } void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority WorkPriority) { if (bUseVoxelForgeCore) { LoadCoreTile(Tile, WorkPriority); return; } if (GVoxelForgeStep1GenerationCap == 0) return; if (PendingTiles.Contains(Tile)) return; if (IsStreamingResultBacklogCapped()) { StreamingResultBacklogCapStops.fetch_add(1, std::memory_order_relaxed); return; } const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); // Core and worker lane clamped. / Core et voie worker plafonnes. const int32 ReadyQueueDepth = FMath::Clamp( GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks); const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth; const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon; const int32 NearReserve = EffectiveNearTaskReserve >= 0 ? FMath::Clamp(EffectiveNearTaskReserve, 0, FMath::Max(0, MaxTasks - 1)) : GetNearTaskReserve(MaxTasks); const int32 NearAdmissionCap = bStreamingPlayerMoving ? MaxTasks : TotalAdmissionCap; const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve) + ReadyQueueDepth; const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon ? CoarseTaskCap : (WorkPriority == EVoxelTileWorkPriority::LOD0 ? NearAdmissionCap : TotalAdmissionCap); if (GetGenerationAdmissionCount() >= AdmissionCap) { if (GVoxelForgeProfileTileGeneration != 0 && !GVoxelForgeGenerationCapHitReported) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStreamingBudget] generation_cap_hit=1 pending=%d admission=%d " "effective=%d near_reserve=%d tile_level=%d"), PendingTiles.Num(), AdmissionCap, MaxTasks, NearReserve, Tile.Level); GVoxelForgeGenerationCapHitReported = true; } return; // Budget full — wait for a task to finish. } PendingTiles.Add(Tile); const uint64 RequestOrdinal = ++NextStreamingRequestOrdinal; const bool bResidentAtRequest = LoadedTiles.Contains(Tile); if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::FTileSample RequestSample; RequestSample.TileX = Tile.Coord.X; RequestSample.TileY = Tile.Coord.Y; RequestSample.TileZ = Tile.Coord.Z; RequestSample.Level = Tile.Level; RequestSample.WorkPriority = static_cast(WorkPriority); RequestSample.RequestOrdinal = RequestOrdinal; RequestSample.bResidentAtRequest = bResidentAtRequest; VoxelForgeStartupTrace::RecordRequest(RequestSample, GenerationEpoch); } if (bStartupCollisionGateTimingCaptured && StartupCollisionGateSupportTile.Coord == Tile.Coord && StartupCollisionGateSupportTile.Level == Tile.Level && StartupCollisionGateSubmittedSeconds <= 0.0) { StartupCollisionGateSubmittedSeconds = FPlatformTime::Seconds(); } const uint64 RequestStartCycles = FPlatformTime::Cycles64(); const FIntVector OriginVoxels = Tile.OriginVoxels(); // min corner, voxel coords // Coarse levels mesh with FEWER cells → much cheaper gen (blockier far field, which is far // away + skirts hide the seams). Near levels (< FullResClipLevels) stay full CHUNK_SIZE cells. // Extent stays CHUNK_SIZE<= 0 ? FMath::Clamp(GVoxelForgeTestFullResClipLevels, 1, 9) : (Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2); const int32 Extent = CHUNK_SIZE << Tile.Level; const int32 Cells = (Tile.Level < FullRes) ? CHUNK_SIZE : (Settings ? FMath::Clamp(Settings->CoarseTileCells, 4, CHUNK_SIZE) : 16); const int32 Step = FMath::Max(1, Extent / Cells); const uint32 TaskEpoch = GenerationEpoch; const uint32 TaskDesiredEpoch = DesiredEpoch; const uint32 TaskModificationEpoch = ModificationEpoch; if (GVoxelForgeStep1CoreCookBenchmark != 0 && Step1SupportCohortTiles.IsEmpty()) { FVector PlayerPosition = FVector::ZeroVector; if (TryGetPlayerPosition(PlayerPosition)) { const FIntVector CohortCenter = WorldToChunkCoord( WorldToLocalCm(PlayerPosition)); const int32 PawnRadius = FMath::Clamp( Settings ? Settings->PawnCollisionRadiusChunks : 2, 1, 4); VoxelCollisionDesiredTiles::AddBox( Step1SupportCohortTiles, CohortCenter, PawnRadius, PawnRadius, PawnRadius); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStep1SupportCohort] stage=defined center=(%d,%d,%d) " "radius=%d tiles=%d source=first_load"), CohortCenter.X, CohortCenter.Y, CohortCenter.Z, PawnRadius, Step1SupportCohortTiles.Num()); } } const bool bStep1SupportCohort = GVoxelForgeStep1CoreCookBenchmark != 0 && Step1SupportCohortTiles.Contains(Tile); // CAPTURE-DURING-MESHING: only level-0 full-res tiles map 1:1 onto a density-clipmap level // (Step == 1<bEnableDensityVolume && DensityVolume->IsTileCaptureUseful(Tile.Coord); const bool bWantSurfaceWitnesses = GVoxelForgeVisualGapAudits.Contains(this); const bool bWantPreEditDensityGrid = !PreEditDensityCache.Contains(Tile) && VoxelForgePreEditDensityCache::IsPawnRadiusTile(Tile, CurrentCenterChunk); // STRATE CONTENT CUT — LODs at/above the effective minimum mesh only the player-strate band // (see the band update in UpdateChunksAroundPosition + UVoxelSettings::StrateContentCutMinLevel). // Chunk band → voxels (inclusive). LOD0 is always full gameplay resolution; the independent // collision radius decides which level-0 results also create a Chaos body. Finer LODs below // the configured minimum and all LODs in the gap (no band) mesh full. int32 BandVoxLo = INT32_MIN, BandVoxHi = INT32_MAX; int32 BandChunkLo = MIN_int32, BandChunkHi = MAX_int32; const int32 CutMin = Settings ? Settings->GetEffectiveStrateContentCutMinLevel() : 9; if (Tile.Level >= CutMin && MeshBandChunkLo != MIN_int32) { BandChunkLo = MeshBandChunkLo; BandChunkHi = MeshBandChunkHi; BandVoxLo = MeshBandChunkLo * CHUNK_SIZE; BandVoxHi = (MeshBandChunkHi + 1) * CHUNK_SIZE - 1; // Résidu ultra-grossier (niveaux ≥7) : la coupe est à la granularité de la CELLULE. Si // UNE cellule (Step voxels de haut) est plus haute que la bande entière, toute cellule // qui chevauche la bande échantillonne quand même les airs des DEUX strates (mêmes trous // et mélanges de matériaux qu'avant la coupe) — la tuile ne peut rendre que des artefacts // ⇒ on n'émet RIEN. Résultat vide via ProcessQueue (bookkeeping normal : PendingTiles, // LoadedTiles, epoch) ; jamais figé — le changement de bande re-queue via BandRemeshQueue. if (Step > (BandChunkHi - BandChunkLo + 1) * CHUNK_SIZE) { FChunkResult Empty; Empty.Tile = Tile; Empty.Epoch = TaskEpoch; Empty.DesiredEpoch = TaskDesiredEpoch; Empty.ModificationEpoch = TaskModificationEpoch; Empty.RequestStartCycles = RequestStartCycles; Empty.RequestOrdinal = RequestOrdinal; Empty.bResidentAtRequest = bResidentAtRequest; Empty.BandChunkLo = BandChunkLo; Empty.BandChunkHi = BandChunkHi; Empty.WorkPriority = WorkPriority; Empty.bStep1SupportCohort = bStep1SupportCohort; EnqueueStreamingResult(MoveTemp(Empty)); return; } } const TSharedPtr Cancellation = MakeShared(); PendingTileCancellation.Add(Tile, Cancellation); ActiveTaskCount.fetch_add(1, std::memory_order_relaxed); ActiveTileTaskCount.fetch_add(1, std::memory_order_relaxed); // BackgroundNormal yields to foreground work; near tasks stay ahead at BackgroundHigh. // BackgroundNormal cede la priorite au premier plan ; les tuiles proches restent prioritaires en BackgroundHigh. const UE::Tasks::ETaskPriority TaskPriority = bNearPriority ? UE::Tasks::ETaskPriority::BackgroundHigh : UE::Tasks::ETaskPriority::BackgroundNormal; UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture, bWantPreEditDensityGrid, bWantSurfaceWitnesses, BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi, RequestStartCycles, RequestOrdinal, bResidentAtRequest, TaskDesiredEpoch, TaskModificationEpoch, Cancellation, WorkPriority, bStep1SupportCohort]() { // RAII: decrement the counter on every exit path. struct FTaskGuard { std::atomic& Counter; std::atomic& TileCounter; ~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); TileCounter.fetch_sub(1, std::memory_order_relaxed); } } Guard{ActiveTaskCount, ActiveTileTaskCount}; ActiveGenerationWorkerCount.fetch_add(1, std::memory_order_relaxed); const uint64 WorkerTaskStartCycles = FPlatformTime::Cycles64(); const uint32 WorkerThreadId = FPlatformTLS::GetCurrentThreadId(); struct FWorkerGuard { std::atomic& Counter; std::atomic& BusyCycles; uint64 StartedCycles; ~FWorkerGuard() { const uint64 EndedCycles = FPlatformTime::Cycles64(); if (EndedCycles >= StartedCycles) { BusyCycles.fetch_add(EndedCycles - StartedCycles, std::memory_order_relaxed); } Counter.fetch_sub(1, std::memory_order_relaxed); } } WorkerGuard{ActiveGenerationWorkerCount, TotalGenerationWorkerTaskCycles, WorkerTaskStartCycles}; FChunkResult Result; Result.WorkerTaskStartCycles = WorkerTaskStartCycles; Result.WorkerThreadId = WorkerThreadId; Result.RequestOrdinal = RequestOrdinal; Result.bResidentAtRequest = bResidentAtRequest; Result.WorkPriority = WorkPriority; auto EnqueueObsoleteResult = [&]() { if (bShuttingDown.load(std::memory_order_relaxed) || bGenerationPaused.load(std::memory_order_relaxed)) { return; } FChunkResult Canceled; Canceled.Tile = Tile; Canceled.Epoch = TaskEpoch; Canceled.DesiredEpoch = TaskDesiredEpoch; Canceled.ModificationEpoch = TaskModificationEpoch; Canceled.RequestStartCycles = RequestStartCycles; Canceled.RequestOrdinal = RequestOrdinal; Canceled.GenerationOrdinal = Result.GenerationOrdinal; Canceled.WorkerTaskStartCycles = Result.WorkerTaskStartCycles; Canceled.WorkerTaskEndCycles = FPlatformTime::Cycles64(); Canceled.WorkerThreadId = Result.WorkerThreadId; Canceled.GenerationStartCycles = Result.GenerationStartCycles; Canceled.GenerationEndCycles = FPlatformTime::Cycles64(); Canceled.bResidentAtRequest = bResidentAtRequest; Canceled.WorkPriority = WorkPriority; Canceled.bStep1SupportCohort = bStep1SupportCohort; Canceled.bAborted = true; Canceled.bObsolete = true; EnqueueStreamingResult(MoveTemp(Canceled)); }; if (ShouldAbortWork()) return; if (Cancellation->bObsolete.load(std::memory_order_relaxed)) { EnqueueObsoleteResult(); return; } FScopedVoxelStackRegistration StackRegistration(Tile.Level); Result.DesiredEpoch = TaskDesiredEpoch; Result.ModificationEpoch = TaskModificationEpoch; Result.WorkPriority = WorkPriority; Result.RequestOrdinal = RequestOrdinal; Result.bResidentAtRequest = bResidentAtRequest; Result.bStep1SupportCohort = bStep1SupportCohort; Result.bCaptureSurfaceWitnesses = bWantSurfaceWitnesses; const uint64 GenerationStartCycles = FPlatformTime::Cycles64(); Result.GenerationOrdinal = NextStreamingGenerationOrdinal.fetch_add(1, std::memory_order_relaxed) + 1; Result.GenerationStartCycles = GenerationStartCycles; GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture, BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi, Result, &Cancellation->bObsolete, bWantPreEditDensityGrid); Result.GenerationEndCycles = GenerationStartCycles != 0 ? FPlatformTime::Cycles64() : 0; Result.RequestStartCycles = RequestStartCycles; const uint64 WorkerGenerationCycles = Result.GenerationEndCycles >= GenerationStartCycles ? Result.GenerationEndCycles - GenerationStartCycles : 0; if (bStep1SupportCohort) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStep1SupportCohort] stage=mesh_ready tile=(%d,%d,%d) " "empty=%d triangles=%d priority=%d request_to_worker_ms=%.3f " "generation_ms=%.3f"), Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Result.bEmpty ? 1 : 0, Result.NumTriangles, static_cast(WorkPriority), FPlatformTime::ToSeconds64(GenerationStartCycles - RequestStartCycles) * 1000.0, FPlatformTime::ToSeconds64(WorkerGenerationCycles) * 1000.0); } TotalWorkerGenerationTasks.fetch_add(1, std::memory_order_relaxed); TotalWorkerGenerationCycles.fetch_add(WorkerGenerationCycles, std::memory_order_relaxed); const bool bObsoleteWork = Cancellation->bObsolete.load(std::memory_order_relaxed) || Result.bObsolete; if (bObsoleteWork) { TotalObsoleteWorkerTasks.fetch_add(1, std::memory_order_relaxed); TotalObsoleteWorkerCycles.fetch_add(WorkerGenerationCycles, std::memory_order_relaxed); } if (bObsoleteWork || Result.bAborted) { if (!ShouldAbortWork()) { EnqueueObsoleteResult(); } return; } if (!ShouldAbortWork()) { Result.WorkerTaskEndCycles = FPlatformTime::Cycles64(); EnqueueStreamingResult(MoveTemp(Result)); // move: don't copy the geometry payload } }, TaskPriority); } void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority WorkPriority) { if (GVoxelForgeStep1GenerationCap == 0) return; if (bCoreRegionCompilePending) { if (GVoxelForgeCoreRegionConcurrentAdmission == 0) { ++CoreRegionDependentDeclines; return; } if (PendingCoreRegionTileSet.Contains(Tile) || CoreRegionCompileTilesInFlight.Contains(Tile)) { ++CoreRegionDependentDeclines; return; } } if (PendingTiles.Contains(Tile)) return; const TSharedPtr State = VoxelForgeCoreState; if (!State.IsValid()||State->bInitialFailed.load(std::memory_order_acquire) ||State->bRegionCompileFailed.load(std::memory_order_acquire) ||State->bShuttingDown.load(std::memory_order_acquire))return; const bool bShapeReady=State->bReady.load(std::memory_order_acquire); if(!bShapeReady) { if(!State->bSnapshotReady.load(std::memory_order_acquire))return; const VoxelForgeCore::TileKey CoreKey{ {Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level}; bool bShapeFree=false; { FScopeLock ReadinessLock(&State->ShapeReadinessLock); if(State->ShapeFreeTiles.contains(CoreKey))bShapeFree=true; else if(State->ShapeDependentTiles.contains(CoreKey))return; else { FReadScopeLock WorldReadLock(State->WorldLock); if(!State->World)return; bShapeFree=State->World->isTileShapeFree(CoreKey); if(bShapeFree)State->ShapeFreeTiles.insert(CoreKey); else State->ShapeDependentTiles.insert(CoreKey); } } if(!bShapeFree)return; } if (IsStreamingResultBacklogCapped()) { StreamingResultBacklogCapStops.fetch_add(1, std::memory_order_relaxed); return; } const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); const int32 ReadyQueueDepth = FMath::Clamp( GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks); const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth; const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon; const int32 NearReserve = EffectiveNearTaskReserve >= 0 ? FMath::Clamp(EffectiveNearTaskReserve, 0, FMath::Max(0, MaxTasks - 1)) : GetNearTaskReserve(MaxTasks); const int32 NearAdmissionCap = bStreamingPlayerMoving ? MaxTasks : TotalAdmissionCap; const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve) + ReadyQueueDepth; const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon ? CoarseTaskCap : (WorkPriority == EVoxelTileWorkPriority::LOD0 ? NearAdmissionCap : TotalAdmissionCap); if (GetGenerationAdmissionCount() >= AdmissionCap) return; PendingTiles.Add(Tile); const TSharedPtr Cancellation = MakeShared(); PendingTileCancellation.Add(Tile, Cancellation); ActiveTaskCount.fetch_add(1, std::memory_order_relaxed); ActiveTileTaskCount.fetch_add(1, std::memory_order_relaxed); if (bCoreRegionCompilePending) { ++CoreRegionIndependentSubmissions; } const uint32 TaskEpoch = GenerationEpoch; const uint32 TaskDesiredEpoch = DesiredEpoch; const uint32 TaskModificationEpoch = ModificationEpoch; const uint64 RequestStartCycles = FPlatformTime::Cycles64(); const uint64 RequestOrdinal = ++NextStreamingRequestOrdinal; const bool bResidentAtRequest = LoadedTiles.Contains(Tile); if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::FTileSample RequestSample; RequestSample.TileX = Tile.Coord.X; RequestSample.TileY = Tile.Coord.Y; RequestSample.TileZ = Tile.Coord.Z; RequestSample.Level = Tile.Level; RequestSample.WorkPriority = static_cast(WorkPriority); RequestSample.RequestOrdinal = RequestOrdinal; RequestSample.bResidentAtRequest = bResidentAtRequest; VoxelForgeStartupTrace::RecordRequest(RequestSample, TaskEpoch); } const bool bCaptureHashes = bCaptureVoxelForgeCoreHashesForValidation; const bool bLegacyDirectMappedColumnCache=GVoxelForgeCoreColumnCacheDirectMapped!=0; const bool bMemoizeCoreMCGradients=GVoxelForgeCoreMCGradientMemoization!=0; const UE::Tasks::ETaskPriority TaskPriority = bNearPriority ? UE::Tasks::ETaskPriority::BackgroundHigh : UE::Tasks::ETaskPriority::BackgroundNormal; UE::Tasks::Launch(TEXT("VoxelForgeCoreTile"), [this, State, Cancellation, Tile, WorkPriority, TaskEpoch, TaskDesiredEpoch, TaskModificationEpoch, RequestStartCycles, RequestOrdinal, bResidentAtRequest, bCaptureHashes,bLegacyDirectMappedColumnCache,bMemoizeCoreMCGradients]() { struct FTaskGuard { std::atomic& Counter; std::atomic& TileCounter; ~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); TileCounter.fetch_sub(1, std::memory_order_relaxed); } } Guard{ActiveTaskCount, ActiveTileTaskCount}; ActiveGenerationWorkerCount.fetch_add(1, std::memory_order_relaxed); const uint64 WorkerTaskStartCycles = FPlatformTime::Cycles64(); const uint32 WorkerThreadId = FPlatformTLS::GetCurrentThreadId(); struct FWorkerGuard { std::atomic& Counter; std::atomic& BusyCycles; uint64 StartedCycles; ~FWorkerGuard() { const uint64 EndedCycles = FPlatformTime::Cycles64(); if (EndedCycles >= StartedCycles) { BusyCycles.fetch_add(EndedCycles - StartedCycles, std::memory_order_relaxed); } Counter.fetch_sub(1, std::memory_order_relaxed); } } WorkerGuard{ActiveGenerationWorkerCount, TotalGenerationWorkerTaskCycles, WorkerTaskStartCycles}; auto Enqueue = [this, WorkPriority](FChunkResult&& Result) { Result.WorkPriority = WorkPriority; Result.WorkerTaskEndCycles = FPlatformTime::Cycles64(); EnqueueStreamingResult(MoveTemp(Result)); }; if (bShuttingDown.load(std::memory_order_acquire) || State->bShuttingDown.load(std::memory_order_acquire)) { return; } FChunkResult Result; Result.WorkerTaskStartCycles = WorkerTaskStartCycles; Result.WorkerThreadId = WorkerThreadId; Result.Tile = Tile; Result.Epoch = TaskEpoch; Result.DesiredEpoch = TaskDesiredEpoch; Result.ModificationEpoch = TaskModificationEpoch; Result.RequestStartCycles = RequestStartCycles; Result.RequestOrdinal = RequestOrdinal; Result.bResidentAtRequest = bResidentAtRequest; Result.WorkPriority = WorkPriority; Result.GenerationStartCycles = FPlatformTime::Cycles64(); Result.GenerationOrdinal = NextStreamingGenerationOrdinal.fetch_add(1, std::memory_order_relaxed) + 1; auto EnqueueObsolete = [&]() { FChunkResult Canceled; Canceled.Tile = Tile; Canceled.Epoch = TaskEpoch; Canceled.DesiredEpoch = TaskDesiredEpoch; Canceled.ModificationEpoch = TaskModificationEpoch; Canceled.RequestStartCycles = RequestStartCycles; Canceled.RequestOrdinal = RequestOrdinal; Canceled.GenerationOrdinal = Result.GenerationOrdinal; Canceled.WorkerTaskStartCycles = Result.WorkerTaskStartCycles; Canceled.WorkerThreadId = Result.WorkerThreadId; Canceled.bResidentAtRequest = bResidentAtRequest; Canceled.GenerationStartCycles = Result.GenerationStartCycles; Canceled.GenerationEndCycles = FPlatformTime::Cycles64(); Canceled.WorkPriority = WorkPriority; Canceled.bAborted = true; Canceled.bObsolete = true; if (!bShuttingDown.load(std::memory_order_acquire) && !State->bShuttingDown.load(std::memory_order_acquire)) { Enqueue(MoveTemp(Canceled)); } }; if (Cancellation->bObsolete.load(std::memory_order_acquire)) { EnqueueObsolete(); return; } const VoxelForgeCore::TileKey CoreKey{{Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z}, Tile.Level}; VoxelForgeCore::TileGenerationOptions Options; switch (CoreMesherRecipe) { case EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1: Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1; break; case EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV2: Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV2; break; default: Options.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1; break; } Options.hashDensity = bCaptureHashes; Options.legacyDirectMappedColumnCache=bLegacyDirectMappedColumnCache; Options.memoizeMcGradients=bMemoizeCoreMCGradients; std::string CoreError; std::optional CoreMesh; { FReadScopeLock Lock(State->WorldLock); if (State->World) { CoreMesh = State->World->generateTile(CoreKey, Options, CoreError); } } Result.GenerationEndCycles = FPlatformTime::Cycles64(); if (!CoreMesh) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] tile generation failed key=(%d,%d,%d) lod=%d: %s"), Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, UTF8_TO_TCHAR(CoreError.c_str())); Result.bAborted = true; Enqueue(MoveTemp(Result)); return; } if (Cancellation->bObsolete.load(std::memory_order_acquire) || bShuttingDown.load(std::memory_order_acquire) || State->bShuttingDown.load(std::memory_order_acquire)) { EnqueueObsolete(); return; } Result.bEmpty = CoreMesh->indices.empty(); Result.NumVertices = static_cast(CoreMesh->vertices.size()); Result.NumTriangles = static_cast(CoreMesh->indices.size() / 3); Result.NumCeilingTriangles = static_cast(CoreMesh->groups.ceilingTriangleCount); Result.NumSkirtTriangles = static_cast(CoreMesh->groups.skirtTriangleCount); Result.bHasGroundTris = CoreMesh->groups.groundTriangleCount > 0; Result.bHasCeilingTris = CoreMesh->groups.ceilingTriangleCount > 0; Result.bHasCoreHashes = CoreMesh->hasDensityHash && CoreMesh->hasMeshHash && CoreMesh->hasPayloadHash && CoreMesh->hasTopologyHash; if (Result.bHasCoreHashes) { Result.CoreDensityHash = CoreMesh->densityHash; Result.CoreMeshHash = CoreMesh->meshHash; Result.CorePayloadHash = CoreMesh->payloadHash; Result.CoreTopologyHash = CoreMesh->topologyHash; } if (!Result.bEmpty) { FVoxelMeshData MeshData; MeshData.Vertices.Reserve(Result.NumVertices); MeshData.Normals.Reserve(Result.NumVertices); MeshData.UVs.Reserve(Result.NumVertices); MeshData.Colors.Reserve(Result.NumVertices); for (const VoxelForgeCore::MeshVertex& Vertex : CoreMesh->vertices) { MeshData.Vertices.Emplace(Vertex.position.x, Vertex.position.y, Vertex.position.z); MeshData.Normals.Emplace(Vertex.normal.x, Vertex.normal.y, Vertex.normal.z); MeshData.UVs.Emplace(Vertex.uv.x, Vertex.uv.y); MeshData.Colors.Emplace(Vertex.color.r, Vertex.color.g, Vertex.color.b, Vertex.color.a); } MeshData.Triangles.Reserve(static_cast(CoreMesh->indices.size())); for (const std::uint32_t Index : CoreMesh->indices) { MeshData.Triangles.Add(static_cast(Index)); } MeshData.NumSurfaceVertices = static_cast(CoreMesh->groups.surfaceVertexCount); MeshData.NumCeilingTriangles = Result.NumCeilingTriangles; MeshData.NumSkirtTriangles = Result.NumSkirtTriangles; Result.Streams = MakeShared(); BuildTileStreamSet(*Result.Streams, MeshData); } Result.GenerationEndCycles = FPlatformTime::Cycles64(); if (Cancellation->bObsolete.load(std::memory_order_acquire) || bShuttingDown.load(std::memory_order_acquire) || State->bShuttingDown.load(std::memory_order_acquire)) { EnqueueObsolete(); return; } Enqueue(MoveTemp(Result)); }, TaskPriority); } // Worker-side gen for one tile (shared by the async ChunkGen task and the synchronous carve path). // READS Generator/Mesher only — safe on a worker or the game thread. Fills Result; no enqueue. void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels, int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture, int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi, FChunkResult& Result, const std::atomic* ObsoleteFlag, bool bWantPreEditDensityGrid, const FVoxelSharedDensityGrid* SharedDensityOverride) { const bool bLogTileProfile = GVoxelForgeProfileTileGeneration != 0; const bool bMeasureTile = bLogTileProfile || VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Attribution || VoxelForgeStartupTrace::IsActive(); const double TileStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 ClassifyVerdict = -1; // Mixed = 0, AllSolid = 1, AllAir = 2 FVoxelTileClassificationStats ClassifierStats; const bool bProfileOps = bLogTileProfile && VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Full; const VoxelDensityProfile::FSnapshot TileProfileStart = bMeasureTile ? VoxelDensityProfile::SnapshotCurrentThread() : VoxelDensityProfile::FSnapshot(); auto EmitTileProfile = [&]() { if (bLogTileProfile) { const VoxelDensityProfile::FSnapshot TileProfileEnd = bMeasureTile ? VoxelDensityProfile::SnapshotCurrentThread() : VoxelDensityProfile::FSnapshot(); const auto CounterDelta = [&](VoxelDensityProfile::ECounter Counter) -> uint64 { const int32 Index = static_cast(Counter); return bMeasureTile && TileProfileEnd.Counters[Index] >= TileProfileStart.Counters[Index] ? TileProfileEnd.Counters[Index] - TileProfileStart.Counters[Index] : 0; }; const auto CycleDelta = [&](VoxelDensityProfile::EBucket Bucket) -> uint64 { const int32 Index = static_cast(Bucket); return bMeasureTile && TileProfileEnd.Cycles[Index] >= TileProfileStart.Cycles[Index] ? TileProfileEnd.Cycles[Index] - TileProfileStart.Cycles[Index] : 0; }; const double TileSeconds = FPlatformTime::Seconds() - TileStartSeconds; const double CacheBuildSeconds = FPlatformTime::ToSeconds64( CycleDelta(VoxelDensityProfile::EBucket::RoomGraphBuild)); const double EvaluationSeconds = FMath::Max( 0.0, TileSeconds - CacheBuildSeconds); const uint64 WormEligibleSamples = CounterDelta( VoxelDensityProfile::ECounter::WormEligibleSamples); const uint64 WormBlockProofs = CounterDelta( VoxelDensityProfile::ECounter::WormBlockProofs); const uint64 WormBlockSkippedSamples = CounterDelta( VoxelDensityProfile::ECounter::WormBlockSkippedSamples); const double WormBlockSkipRate = WormEligibleSamples > 0 ? static_cast(WormBlockSkippedSamples) / static_cast(WormEligibleSamples) : 0.0; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d aborted=%d empty=%d triangles=%d ceiling_triangles=%d " "band=(%d,%d) " "verdict=%d proof=%d air_proof=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f " "cache_build=%.6f evaluation=%.6f " "refine=%u stack=%u core_samples=%u final_samples=%u core_hits=%u final_hits=%u " "core_leaves=%u final_leaves=%u tail_queries=%u tail_eval=%u " "stack_work=%.6f core_work=%.6f final_work=%.6f tail_work=%.6f " "room_work=%.6f room_exact=%.6f cache_work=%.6f " "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 cache_builds=%llu " "cache_generator=%llu cache_tunnel_core=%llu cache_op_shared=%llu " "cache_op_local=%llu cache_classifier_shared=%llu cache_classifier_local=%llu " "cache_unknown=%llu room_candidates=%llu room_evaluated=%llu " "tunnel_candidates=%llu tunnel_evaluated=%llu tunnel_core_candidates=%llu " "tunnel_core_evaluated=%llu support_column_candidates=%llu " "worm_eligible=%llu worm_block_proofs=%llu worm_block_skipped=%llu " "worm_skip_rate=%.6f"), Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells, Result.bAborted ? 1 : 0, Result.bEmpty ? 1 : 0, Result.NumTriangles, Result.NumCeilingTriangles, BandChunkLo, BandChunkHi, ClassifyVerdict, Result.bSealedSolidProof ? 1 : 0, Result.bOutOfLayoutAirProof ? 1 : 0, ClassifySeconds, MeshSeconds, StreamSeconds, TileSeconds, CacheBuildSeconds, EvaluationSeconds, ClassifierStats.RefineNodes, ClassifierStats.StackBoxCalls, ClassifierStats.ExactCoreSamples, ClassifierStats.ExactFinalSamples, ClassifierStats.ExactCoreCacheHits, ClassifierStats.ExactFinalCacheHits, ClassifierStats.ExactCoreLeaves, ClassifierStats.ExactFinalLeaves, ClassifierStats.RoomTailQueries, ClassifierStats.RoomTailEvaluated, FPlatformTime::ToSeconds64(ClassifierStats.StackBoxCycles), FPlatformTime::ToSeconds64(ClassifierStats.ExactCoreCycles), FPlatformTime::ToSeconds64(ClassifierStats.ExactFinalCycles), FPlatformTime::ToSeconds64(ClassifierStats.RoomTailCycles), FPlatformTime::ToSeconds64(ClassifierStats.RoomPropagateCycles), FPlatformTime::ToSeconds64(ClassifierStats.RoomExactPrimitiveCycles), FPlatformTime::ToSeconds64(ClassifierStats.RoomCacheWindowCycles), ClassifierStats.RoomNumRooms, ClassifierStats.RoomNumTunnels, ClassifierStats.RoomNumRoomFloorJoins, ClassifierStats.RoomNumPits, ClassifierStats.RoomNumChimneys, ClassifierStats.WholeMixedNodes, ClassifierStats.WholeSolidNodes, ClassifierStats.WholeAirNodes, ClassifierStats.NeedsFinalFieldNodes, ClassifierStats.SplitNodes, ClassifierStats.MaxRefinementDepth, static_cast(CounterDelta(VoxelDensityProfile::ECounter::OpBlockBuilds)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::OpBlockSamples)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::OpBlockOperators)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::OpBlockActiveOperators)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::OpBlockPrunedOperators)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::SdfCacheBuild)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildGeneratorTile)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildGeneratorTunnelCore)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildOpShared)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildOpLocal)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildClassifierShared)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildClassifierLocal)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildUnknown)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::CaveRoomCandidates)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::CaveRoomEvaluated)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::CaveTunnelCandidates)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::CaveTunnelEvaluated)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::TunnelCoreCandidates)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::TunnelCoreEvaluated)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::TunnelSupportColumnCandidates)), static_cast(WormEligibleSamples), static_cast(WormBlockProofs), static_cast(WormBlockSkippedSamples), WormBlockSkipRate); if (bProfileOps) { static const VoxelDensityProfile::EBucket GameOpBuckets[] = { VoxelDensityProfile::EBucket::RoomGraphSource, VoxelDensityProfile::EBucket::SdfCarve, VoxelDensityProfile::EBucket::CaveRoughnessMod, VoxelDensityProfile::EBucket::CaveTerraceMod, VoxelDensityProfile::EBucket::LayerLineMod, VoxelDensityProfile::EBucket::RibbingMod, VoxelDensityProfile::EBucket::CaveOverhangMod, VoxelDensityProfile::EBucket::CaveCliffMod, VoxelDensityProfile::EBucket::ScallopMod, VoxelDensityProfile::EBucket::CaveArchMod, VoxelDensityProfile::EBucket::RoomColumnMod, VoxelDensityProfile::EBucket::DomeMod, VoxelDensityProfile::EBucket::PinchMod, VoxelDensityProfile::EBucket::FloorBiasMod, VoxelDensityProfile::EBucket::WormFieldSource, VoxelDensityProfile::EBucket::OriginSpineOp, VoxelDensityProfile::EBucket::BoundarySealOp, VoxelDensityProfile::EBucket::PassageCarveOp, VoxelDensityProfile::EBucket::XYEdgeSealOp, }; for (const VoxelDensityProfile::EBucket Bucket : GameOpBuckets) { const int32 Index = static_cast(Bucket); const uint64 Calls = TileProfileEnd.Calls[Index] >= TileProfileStart.Calls[Index] ? TileProfileEnd.Calls[Index] - TileProfileStart.Calls[Index] : 0; const uint64 Cycles = TileProfileEnd.WallCycles[Index] >= TileProfileStart.WallCycles[Index] ? TileProfileEnd.WallCycles[Index] - TileProfileStart.WallCycles[Index] : 0; if (Calls == 0) { continue; } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeGameOpProfile] tile=(%d,%d,%d) level=%d op=%s calls=%llu total_us=%.3f us_per_call=%.6f"), Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, VoxelDensityProfile::BucketName(Bucket), static_cast(Calls), FPlatformTime::ToSeconds64(Cycles) * 1.0e6, FPlatformTime::ToSeconds64(Cycles) * 1.0e6 / static_cast(Calls)); } } } }; Result.Tile = Tile; Result.Epoch = Epoch; Result.bAborted = false; Result.bObsolete = false; Result.bEmpty = true; Result.ClassifyVerdict = -1; Result.bSealedSolidProof = false; Result.bOutOfLayoutAirProof = false; Result.ClassifySeconds = 0.0; Result.MeshSeconds = 0.0; Result.StreamSeconds = 0.0; Result.NumTriangles = 0; Result.ValidationDensityCalls = 0; Result.MesherDensityCalls = 0; Result.Streams.Reset(); Result.CaptureGrid.Reset(); Result.PreEditDensityGrid.Reset(); Result.BandChunkLo = BandChunkLo; // strate content cut (MIN/MAX = uncut) Result.BandChunkHi = BandChunkHi; // Shared by async workers and the synchronous carve path. An interrupted result is empty but // explicitly discarded, never an all-air tile that gets marked loaded. auto AbortResult = [&]() { Result.bAborted = true; Result.bObsolete = ObsoleteFlag != nullptr && ObsoleteFlag->load(std::memory_order_relaxed); Result.bEmpty = true; Result.Streams.Reset(); Result.CaptureGrid.Reset(); Result.PreEditDensityGrid.Reset(); }; if (ShouldAbortWork(ObsoleteFlag)) { AbortResult(); EmitTileProfile(); return; } // T1.d — TRIVIAL-TILE REJECT: ~84 % des tuiles générées sortaient vides (tout-roc / // tout-air) en payant quand même le pré-échantillonnage complet. Le classifieur prouve // (bornes exactes sur le treillis du mesher + gardes conservatives) qu'une tuile est // uniforme → on saute GenerateMesh, Result reste bEmpty. Mixed = génération normale. // Les tuiles à capture (density volume) génèrent toujours : le volume veut la grille // même pour les cellules uniformes, et ces tuiles sont rares (fenêtre d'ombre). // (Gate IsoLevel == 0 : les verdicts du classifieur supposent l'iso MC à zéro exactement.) bool bTrivialEmpty = false; if (!IsParameterizedResearchFieldActive() && GVoxelForgeSealedSolidProof != 0 && !bWantCapture && SharedDensityOverride == nullptr && Generator && Mesher && Mesher->IsoLevel == 0.0f) { TotalSealedSolidProofCandidates.fetch_add(1, std::memory_order_relaxed); const double ProofStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0; if (Generator->TryProveSealedSolidTile(OriginVoxels, Step, Cells)) { ClassifySeconds = bMeasureTile ? FPlatformTime::Seconds() - ProofStartSeconds : 0.0; ClassifyVerdict = static_cast(EVoxelTileClass::AllSolid); Result.ClassifyVerdict = ClassifyVerdict; Result.bSealedSolidProof = true; TotalSealedSolidProofSkips.fetch_add(1, std::memory_order_relaxed); INC_DWORD_STAT(STAT_VoxelForgeTilesSkippedAllSolid); bTrivialEmpty = true; } else if (Generator->TryProveOutOfLayoutAirTile(OriginVoxels, Step, Cells)) { ClassifySeconds = bMeasureTile ? FPlatformTime::Seconds() - ProofStartSeconds : 0.0; ClassifyVerdict = static_cast(EVoxelTileClass::AllAir); Result.ClassifyVerdict = ClassifyVerdict; Result.bOutOfLayoutAirProof = true; TotalOutOfLayoutAirProofSkips.fetch_add(1, std::memory_order_relaxed); INC_DWORD_STAT(STAT_VoxelForgeTilesSkippedAllAir); bTrivialEmpty = true; } else if (bMeasureTile) { // Keep the measured proof cost visible on a tile that continues through the normal // path. The clean summary remains counter-only and pays no timer overhead. ClassifySeconds = FPlatformTime::Seconds() - ProofStartSeconds; } } if (ShouldAbortWork(ObsoleteFlag)) { AbortResult(); EmitTileProfile(); return; } const int32 OuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1); if (!IsParameterizedResearchFieldActive() && !bTrivialEmpty && OuterClassifierMode != 0 && !bWantCapture && SharedDensityOverride == nullptr && Generator && Mesher && Mesher->IsoLevel == 0.0f) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile); INC_DWORD_STAT(STAT_VoxelForgeTilesClassified); const double ClassifyStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0; const EVoxelTileClass Verdict = Generator->ClassifyTile( OriginVoxels, Step, Cells, bMeasureTile ? &ClassifierStats : nullptr); // ITEM 6 — the outer classifier is a proof candidate, not the final skip decision. The // mesher's block path validates candidates against the exact MC vertex lattice before it // skips anything; whole-tile verdicts need the same fence. Sample the core vertices // g=0..Cells (the halo is normal-only) with the mesher's LOD TLS, so a coarse tile is // checked against the field the mesher would actually consume. EVoxelTileClass ValidatedVerdict = Verdict; bool bValidationAborted = false; if (Verdict != EVoxelTileClass::Mixed) { const int32 ValidationStep = FMath::Max(1, Step); const int32 ValidationCells = FMath::Clamp(Cells, 2, CHUNK_SIZE); const int32 ValidationOctaveBias = (Mesher->LODOctaveDrop > 0 && ValidationStep > 1) ? Mesher->LODOctaveDrop * static_cast(FMath::FloorLog2(static_cast(ValidationStep))) : 0; TGuardValue ValidationOctaveBiasGuard( VoxelGenLOD::OctaveBias, ValidationOctaveBias); TGuardValue ValidationSampleStepGuard( VoxelGenLOD::SampleStep, ValidationStep); bool bExactUniform = true; for (int32 Z = 0; Z <= ValidationCells && bExactUniform && !bValidationAborted; ++Z) { if (ShouldAbortWork(ObsoleteFlag)) { bValidationAborted = true; break; } for (int32 Y = 0; Y <= ValidationCells && bExactUniform && !bValidationAborted; ++Y) { for (int32 X = 0; X <= ValidationCells; ++X) { if (ShouldAbortWork(ObsoleteFlag)) { bValidationAborted = true; break; } const float Density = Generator->GetDensityAt( OriginVoxels.X + X * ValidationStep, OriginVoxels.Y + Y * ValidationStep, OriginVoxels.Z + Z * ValidationStep); ++Result.ValidationDensityCalls; const bool bSampleAgrees = VoxelMath::IsFinite(Density) && Density != 0.0f && (Verdict != EVoxelTileClass::AllSolid || Density < 0.0f) && (Verdict != EVoxelTileClass::AllAir || Density > 0.0f); if (!bSampleAgrees) { bExactUniform = false; break; } } } } TotalValidationDensityCalls.fetch_add( static_cast(Result.ValidationDensityCalls), std::memory_order_relaxed); if (!bExactUniform) { // A false Mixed costs meshing; a false uniform verdict removes both render and // collision geometry. Fall back to the safe path whenever one exact vertex disagrees. ValidatedVerdict = EVoxelTileClass::Mixed; } } if (bValidationAborted) { AbortResult(); EmitTileProfile(); return; } ClassifySeconds += bMeasureTile ? FPlatformTime::Seconds() - ClassifyStartSeconds : 0.0; ClassifyVerdict = static_cast(ValidatedVerdict); Result.ClassifyVerdict = ClassifyVerdict; const int32 TrackedLOD = FMath::Clamp(Tile.Level, 0, TrackedClassifierLODCount - 1); OuterClassifierCallsByLOD[TrackedLOD].fetch_add(1, std::memory_order_relaxed); if (ClassifyVerdict >= 0 && ClassifyVerdict < 3) { OuterClassifierVerdictsByLOD[TrackedLOD][ClassifyVerdict].fetch_add( 1, std::memory_order_relaxed); } if (ShouldAbortWork(ObsoleteFlag)) { AbortResult(); EmitTileProfile(); return; } if (ValidatedVerdict == EVoxelTileClass::AllSolid) { INC_DWORD_STAT(STAT_VoxelForgeTilesSkippedAllSolid); } else if (ValidatedVerdict == EVoxelTileClass::AllAir) { INC_DWORD_STAT(STAT_VoxelForgeTilesSkippedAllAir); } bTrivialEmpty = (ValidatedVerdict != EVoxelTileClass::Mixed); } // `TilesMeshed` peut dépasser `TilesClassified` : les tuiles qui ratent cette porte sont // maillées sans classification. / `TilesMeshed` may exceed `TilesClassified`: tiles that // fail this gate are meshed without classification. FVoxelMeshData MeshData; if (!bTrivialEmpty) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GenerateMesh); const double MeshStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0; MeshData = Mesher->GenerateMesh(OriginVoxels, Step, Cells, bWantCapture ? &Result.CaptureGrid : nullptr, BandVoxLo, BandVoxHi, &Result.MesherDensityCalls, bWantPreEditDensityGrid ? &Result.PreEditDensityGrid : nullptr, SharedDensityOverride); TotalMesherDensityCalls.fetch_add( static_cast(Result.MesherDensityCalls), std::memory_order_relaxed); MeshSeconds = bMeasureTile ? FPlatformTime::Seconds() - MeshStartSeconds : 0.0; INC_DWORD_STAT(STAT_VoxelForgeTilesMeshed); if (ShouldAbortWork(ObsoleteFlag)) { AbortResult(); EmitTileProfile(); return; } } // T1.f — build the RMC geometry buffers HERE (worker), not on the game thread. Empty/all-air // tiles carry no streams (Result.bEmpty stays true) → no component on apply. if (!MeshData.IsEmpty()) { if (Result.bCaptureSurfaceWitnesses) { constexpr int32 MaxSurfaceWitnesses = 24; const int32 SurfaceVertexCount = FMath::Clamp( MeshData.NumSurfaceVertices, 0, MeshData.Vertices.Num()); const int32 WitnessCount = FMath::Min(MaxSurfaceWitnesses, SurfaceVertexCount); Result.SurfaceWitnesses.Reserve(WitnessCount); for (int32 WitnessIndex = 0; WitnessIndex < WitnessCount; ++WitnessIndex) { const int32 VertexIndex = static_cast( (static_cast(WitnessIndex) * SurfaceVertexCount) / WitnessCount); Result.SurfaceWitnesses.Add(MeshData.Vertices[VertexIndex]); } } TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_BuildStreams); const double StreamStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0; Result.Streams = MakeShared(); VoxelDensityProfile::FScopedTimer RuntimeStreamTimer( VoxelDensityProfile::EBucket::RuntimeStreamBuilding); BuildTileStreamSet(*Result.Streams, MeshData); Result.NumVertices = MeshData.Vertices.Num(); Result.GeometryBytes = 0; Result.Streams->ForEach([&](const RealtimeMesh::FRealtimeMeshStream& Stream) { Result.GeometryBytes += static_cast(Stream.GetAllocatedSize()); }); StreamSeconds = bMeasureTile ? FPlatformTime::Seconds() - StreamStartSeconds : 0.0; if (ShouldAbortWork(ObsoleteFlag)) { AbortResult(); EmitTileProfile(); return; } Result.bEmpty = false; // F17 — the mesher classified every triangle semantically (sky-cap vs ground, per // vertex against the column's TerrainZ/CeilSurf) and packed them as two contiguous // polygroup runs. Here we only record which sections exist for the apply path. // (Replaces the whole-tile normal VOTE, which painted mixed coarse tiles — terrain // AND cap in one tile — entirely with the winner's material.) const int32 NumTris = MeshData.Triangles.Num() / 3; Result.NumTriangles = NumTris; Result.NumCeilingTriangles = MeshData.NumCeilingTriangles; Result.NumSkirtTriangles = MeshData.NumSkirtTriangles; Result.bHasCeilingTris = MeshData.NumCeilingTriangles > 0; Result.bHasGroundTris = NumTris > MeshData.NumCeilingTriangles; if (GVoxelForgeStep1CoreCookBenchmark != 0 && Result.Tile.Level == 0 && (Result.WorkPriority == EVoxelTileWorkPriority::CollisionCritical || Result.bStep1SupportCohort)) { if (FQueuedThreadPool* CookPool = GVoxelForgeStep1CoreCookPool.GetOrCreate()) { CookPool->AddQueuedWork( new FVoxelStep1CoreCookWork( MakeShared(*Result.Streams), Result.Tile, Result.NumTriangles, Result.bStep1SupportCohort, false, FPlatformTime::Cycles64()), EQueuedWorkPriority::High); } else { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep1CoreCook] dedicated_pool_create_failed=1")); } } } // Parity instrumentation observes the same FVoxelMeshData that the runtime stream builder // consumes. It is intentionally after the abort fence, so a canceled worker cannot publish a // partial identity, and it records empty/proof tiles as well as visible meshes. RecordTileHash(Tile, OriginVoxels, Step, Cells, BandChunkLo, BandChunkHi, MeshData); Result.ClassifySeconds = ClassifySeconds; Result.MeshSeconds = MeshSeconds; Result.StreamSeconds = StreamSeconds; EmitTileProfile(); } // Section-group key shared by every tile component ("the" tile geometry group). static FRealtimeMeshSectionGroupKey VoxelTileGroupKey() { return FRealtimeMeshSectionGroupKey::Create(FRealtimeMeshLODKey(0), FName("Tile")); } //============================================================================= // TILE COMPONENT POOL (T2.c) //============================================================================= // Recycler les composants de tuile au lieu de les détruire/recréer. URealtimeMeshComponent* AVoxelWorld::AcquireTileComponent( double* OutCreationSeconds, double* OutRegistrationSeconds, bool* bOutCreated) { if (OutCreationSeconds != nullptr) { *OutCreationSeconds = 0.0; } if (OutRegistrationSeconds != nullptr) { *OutRegistrationSeconds = 0.0; } if (bOutCreated != nullptr) { *bOutCreated = false; } // Reuse a parked component when one is available — skips NewObject + RegisterComponent // (and the full proxy teardown/GC of a destroy) during fast travel & regen bursts. while (TileComponentPool.Num() > 0) { URealtimeMeshComponent* Pooled = TileComponentPool.Pop(); if (IsValid(Pooled)) { Pooled->SetVisibility(true); return Pooled; } } const bool bMeasureComponentPhases = OutCreationSeconds != nullptr || OutRegistrationSeconds != nullptr; const uint64 CreationStartCycles = bMeasureComponentPhases ? FPlatformTime::Cycles64() : 0; URealtimeMeshComponent* MeshComp = NewObject(this); // Generated once, never moves → Static so RMC's cached static draw path + VSM shadow // caching apply (see the root SetMobility note in BeginPlay). Must be set before register. // Re-mesh on carve recreates the section-group proxy (RMC's Static path already does this), // which is fine for an infrequent action. MeshComp->SetMobility(EComponentMobility::Static); MeshComp->SetGenerateOverlapEvents(false); // chunks use raycasts, not overlaps MeshComp->SetCanEverAffectNavigation(false); if (OutCreationSeconds != nullptr) { *OutCreationSeconds = FPlatformTime::ToSeconds64( FPlatformTime::Cycles64() - CreationStartCycles); } const uint64 RegistrationStartCycles = bMeasureComponentPhases ? FPlatformTime::Cycles64() : 0; MeshComp->RegisterComponent(); MeshComp->AttachToComponent(GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform); if (OutRegistrationSeconds != nullptr) { *OutRegistrationSeconds = FPlatformTime::ToSeconds64( FPlatformTime::Cycles64() - RegistrationStartCycles); } if (bOutCreated != nullptr) { *bOutCreated = true; } return MeshComp; } void AVoxelWorld::ReleaseTileComponent(URealtimeMeshComponent* Comp) { if (!IsValid(Comp)) { return; } // A pooled component may still have its previous BodySetup while RMC processes the asynchronous // RemoveSectionGroup collision update. Disable the component first so that stale geometry can // never act as a collision barrier for another tile while it is parked. Comp->SetCollisionEnabled(ECollisionEnabled::NoCollision); if (TileComponentPool.Num() >= MaxPooledTileComponents) { if (URealtimeMeshSimple* RTMesh = Comp->GetRealtimeMeshAs()) { UnbindRealtimeMeshCollisionEvent(RTMesh); } Comp->DestroyComponent(); return; } // Strip the tile's geometry NOW, not at reuse: removing the section group drops its // sections and their cooked collision, so a parked (hidden) component can't be collided // with and its render memory is released while it waits. The mesh OBJECT is kept — reuse // goes through the same RemoveSectionGroup/CreateSectionGroup path as a re-mesh. if (URealtimeMeshSimple* RTMesh = Comp->GetRealtimeMeshAs()) { RTMesh->RemoveSectionGroup(VoxelTileGroupKey()); } Comp->SetVisibility(false); TileComponentPool.Add(Comp); } void AVoxelWorld::UnloadTile(const FVoxelTileKey& Tile) { // RMC futures are not cancellable from the world side. Remove this tile's state first so a // late completion from the old component cannot resurrect readiness after pool reuse. // Generation work is different: mark it obsolete and let its normal aborted result drain so // PendingTiles cannot be removed on behalf of an older request after a replacement is queued. if (TSharedPtr* Cancellation = PendingTileCancellation.Find(Tile)) { (*Cancellation)->bObsolete.store(true, std::memory_order_release); } CollisionReadyTiles.Remove(Tile); CollisionSuppressedUntilCook.Remove(Tile); CollisionSuppressedAtCycles.Remove(Tile); CollisionNotRequiredTiles.Remove(Tile); CollisionSolidTiles.Remove(Tile); PendingCollisionCooks.Remove(Tile); // Water + decorations are not tile-bound (water is one player-following ocean plane via // UpdateWater; decorations stream by distance via UpdateDecorations) — nothing to clear per tile. if (URealtimeMeshComponent** Comp = TileComponents.Find(Tile)) { if (*Comp) { ReleaseTileComponent(*Comp); } // T2.c — park, don't destroy TileComponents.Remove(Tile); } LoadedTiles.Remove(Tile); AppliedModificationEpoch.Remove(Tile); LoadedTileGeometry.Remove(Tile); if (FVisualGapAuditState* GapAudit = GVoxelForgeVisualGapAudits.Find(this)) { GapAudit->WitnessesByTile.Remove(Tile); } TransitionHold.Remove(Tile); // couvre aussi le settled cull (qui ne tient pas la hold à jour) } bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ApplyMeshToChunk); const bool bProfileApply = GVoxelForgeProfileTileGeneration != 0; const uint64 ApplyStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0; // Streams are pre-built on the worker (T1.f) and guaranteed non-empty by the caller // (ProcessPendingChunks skips empty tiles). This path is game-thread-CHEAP: material lookup + // component get/create + the upload + per-section config. No per-vertex work here. const FVoxelTileKey& Tile = Result.Tile; RealtimeMesh::FRealtimeMeshStreamSet& Streams = *Result.Streams; const bool bHasGroundTris = Result.bHasGroundTris; const bool bHasCeilingTris = Result.bHasCeilingTris; const bool bLevel0 = (Tile.Level == 0); const bool bWantsCollision = bLevel0 && IsTileCollisionDesired(Tile) && GVoxelForgeStep1UploadDisableCollision == 0; const bool bCollisionSuppressed = CollisionSuppressedUntilCook.Contains(Tile); // RMC keeps its installed BodySetup active while the replacement collision cooks. Preserve // support readiness across that interval when this tile already has a live cooked body; the // cook-complete callback below replaces the body and decides readiness for the new geometry. // A first cook, an unavailable body, or a tile leaving collision range still withdraws ready. URealtimeMeshComponent* ExistingCollisionComponent = TileComponents.FindRef(Tile); const bool bRetainInstalledCollisionReady = bWantsCollision && !bCollisionSuppressed && CollisionReadyTiles.Contains(Tile) && IsValid(ExistingCollisionComponent) && ExistingCollisionComponent->GetCollisionEnabled() != ECollisionEnabled::NoCollision && ExistingCollisionComponent->GetBodySetup() != nullptr; if (!bRetainInstalledCollisionReady) { CollisionReadyTiles.Remove(Tile); } else if (GVoxelForgeDigMotionTests.Contains(this)) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionRetain] tile=(%d,%d,%d) old_body=1 ready_during_recook=1"), Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z); } CollisionNotRequiredTiles.Remove(Tile); CollisionSolidTiles.Remove(Tile); PendingCollisionCooks.Remove(Tile); // F17 — materials per POLYGROUP, not per tile. The mesher classified each triangle // semantically (sky-cap = down-facing near the column's CeilSurf; terrain overhangs and // future cave roofs stay ground) and packed two contiguous runs → RMC creates one section // per non-empty group. Ground (group 0): strate override else global default. Sky-cap // (group 1): the strate's CeilingMaterial when set (the rocky "night sky" overhead reads // flat/bright otherwise, since it casts no shadow) else same as ground. A coarse tile // spanning BOTH surfaces now renders both correctly (the old whole-tile vote painted the // loser with the winner's material — Jahni's "terrain and ceiling become one" artifact). UMaterialInterface* GroundMaterial = Settings ? Settings->VoxelMaterial : nullptr; UMaterialInterface* CeilingMaterial = nullptr; if (StrateManager) { // F17 — a coarse tile is 2^level CHUNKS TALL: its raw min/max corners can sit in a // NEIGHBOUR strate (or the inter-strate gap → null) that the mesh doesn't even contain // (strate content cut). Clamp the lookup Zs into the band the tile was MESHED with, then // resolve: ground at the (clamped) BOTTOM chunk, sky-cap at the (clamped) TOP chunk — // the cap is by definition the topmost surface in the tile (mid as a gap fallback). // This was the "far cap renders with the ground material" residue: the min-corner lookup // missed the surface strate entirely on tall far tiles. // Map the physical tile bounds into the existing owner stride layout. Fractional ratios // are valid (for example, 20 cm Core voxels span 1.6 legacy voxels each). // Convertir les bornes physiques de tuile vers les strates owner, y compris les rapports fractionnaires. const double OwnerChunksPerCoreTile = static_cast(GetActiveVoxelSizeCm()) / VOXEL_SIZE; const double TileExtentCoreVoxels = static_cast(CHUNK_SIZE << Tile.Level); const int32 MinOwnerChunkX = FMath::FloorToInt( Tile.Coord.X * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE); const int32 MinOwnerChunkY = FMath::FloorToInt( Tile.Coord.Y * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE); const int32 MinOwnerChunkZ = FMath::FloorToInt( Tile.Coord.Z * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE); const int32 MaxOwnerChunkX = FMath::CeilToInt( (Tile.Coord.X + 1.0) * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE) - 1; const int32 MaxOwnerChunkY = FMath::CeilToInt( (Tile.Coord.Y + 1.0) * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE) - 1; const int32 MaxOwnerChunkZ = FMath::CeilToInt( (Tile.Coord.Z + 1.0) * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE) - 1; const FIntVector MinChunk(MinOwnerChunkX, MinOwnerChunkY, MinOwnerChunkZ); const int32 ZLoC = FMath::Max(MinOwnerChunkZ, Result.BandChunkLo); const int32 ZHiC = FMath::Min(MaxOwnerChunkZ, Result.BandChunkHi); if (UVoxelStrateDefinition* StrateDef = StrateManager->GetStrateForChunk(FIntVector(MinChunk.X, MinChunk.Y, ZLoC))) { if (StrateDef->OverrideMaterial) { GroundMaterial = StrateDef->OverrideMaterial; } if (StrateDef->CeilingMaterial) { CeilingMaterial = StrateDef->CeilingMaterial; } } UVoxelStrateDefinition* CapDef = StrateManager->GetStrateForChunk(FIntVector(MinChunk.X, MinChunk.Y, ZHiC)); if (!CapDef || !CapDef->CeilingMaterial) { CapDef = StrateManager->GetStrateForChunk(FIntVector(MinChunk.X, MinChunk.Y, (ZLoC + ZHiC) / 2)); } if (CapDef && CapDef->CeilingMaterial) { CeilingMaterial = CapDef->CeilingMaterial; } } if (!CeilingMaterial) { CeilingMaterial = GroundMaterial; } // Route the resolved base material through one shared MID per base. With the volume disabled, // the MID carries neutral VolP* values and null VolTex* bindings so Custom nodes exit at P5.z=0. if (Settings) { if (UMaterialInstanceDynamic* MID = GetOrCreateTerrainMID(GroundMaterial)) { GroundMaterial = MID; } if (UMaterialInstanceDynamic* MID = GetOrCreateTerrainMID(CeilingMaterial)) { CeilingMaterial = MID; } } const double MaterialSeconds = bProfileApply ? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ApplyStartCycles) : 0.0; // The geometry stream set was built on the worker (BuildTileStreamSet, T1.f); we just upload it. // Vertices are world-space; the component sits at the actor origin. // One component per tile — the clipmap keeps the total tile count low (~1-2k), so this is // cheap on the game thread (no batching needed). Collision + content are level-0 only. // T2.c: the component comes from the pool when one is parked (see AcquireTileComponent). URealtimeMeshComponent* MeshComp = TileComponents.FindRef(Tile); const bool bComponentAlreadyPresent = MeshComp != nullptr; const uint64 ComponentStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0; double ComponentCreationSeconds = 0.0; double ComponentRegistrationSeconds = 0.0; bool bComponentCreated = false; if (!MeshComp) { MeshComp = AcquireTileComponent( bProfileApply ? &ComponentCreationSeconds : nullptr, bProfileApply ? &ComponentRegistrationSeconds : nullptr, &bComponentCreated); TileComponents.Add(Tile, MeshComp); } if (!IsValid(MeshComp)) { TileComponents.Remove(Tile); if (bProfileApply) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeApplyProfile] level=%d empty=0 failed_component_acquire=1 total=%.6f"), Tile.Level, FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ApplyStartCycles)); } return false; } const bool bWasPooledComponent = !bComponentAlreadyPresent && !bComponentCreated; const double ComponentSeconds = bProfileApply ? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ComponentStartCycles) : 0.0; // §9.4 RENDER-SKIP — a tile only a CollisionOnly anchor wants (not the player clipmap) cooks its // collision below but is hidden (no draw / VSM). Set every apply (overrides the pool's default- // visible state); ReconcileAnchorTileVisibility handles later flips on already-loaded tiles. // During an LOD handoff, keep a newly loaded replacement hidden until the complete covering set // is ready. ReconcileReadyTransitionVisibility then hides the retired component and reveals the // replacement set in the same game-thread turn, so old and new levels are never drawn together. bool bDeferTransitionVisibility = false; if (!CollisionOnlyTiles.Contains(Tile) && IsDesired(Tile)) { for (const TPair& Pair : TileComponents) { if (Pair.Key == Tile || !Pair.Value || IsDesired(Pair.Key) || !Pair.Value->IsVisible()) { continue; } if (VF_TileFootprintsOverlap(Tile, Pair.Key)) { bDeferTransitionVisibility = true; break; } } } MeshComp->SetVisibility( !CollisionOnlyTiles.Contains(Tile) && !bDeferTransitionVisibility); MeshComp->SetCollisionEnabled( bWantsCollision && !bCollisionSuppressed ? ECollisionEnabled::QueryAndPhysics : ECollisionEnabled::NoCollision); // Reuse the component's existing mesh object when it has one (pooled component or carve // re-mesh) — InitializeRealtimeMesh allocates a brand-new URealtimeMesh EVERY call, so // calling it unconditionally (as before) orphaned one mesh object per re-apply to the GC. // The RemoveSectionGroup below does the actual geometry clearing on reuse. URealtimeMeshSimple* RTMesh = MeshComp->GetRealtimeMeshAs(); const uint64 MeshInitStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0; if (!RTMesh) { RTMesh = MeshComp->InitializeRealtimeMesh(); } const double MeshInitSeconds = bProfileApply ? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - MeshInitStartCycles) : 0.0; if (!RTMesh) { if (bProfileApply) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeApplyProfile] level=%d empty=0 failed_mesh_init=1 total=%.6f"), Tile.Level, FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ApplyStartCycles)); } TileComponents.Remove(Tile); ReleaseTileComponent(MeshComp); return false; } BindRealtimeMeshCollisionEvent(RTMesh); // Shadow casting: far (level >= 2) tiles never cast; the sky-cap SECTION never casts either // — otherwise the high rock ceiling shadows the entire terrain below it. F17: shadow is // PER SECTION, so a mixed tile keeps its ground shadow while its cap stays shadowless. const bool bCastShadow = (Tile.Level <= 1); MeshComp->SetCastShadow(bCastShadow); const FRealtimeMeshSectionGroupKey GroupKey = VoxelTileGroupKey(); const uint64 RemoveGroupStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0; RTMesh->RemoveSectionGroup(GroupKey); // clear old geometry on re-mesh // (no-op on a fresh/pooled mesh) const double RemoveGroupSeconds = bProfileApply ? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - RemoveGroupStartCycles) : 0.0; const uint64 MaterialSlotStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0; RTMesh->SetupMaterialSlot(0, "Main", GroundMaterial); RTMesh->SetupMaterialSlot(1, "SkyCap", CeilingMaterial); const double MaterialSlotSeconds = bProfileApply ? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - MaterialSlotStartCycles) : 0.0; // RMC owns both the CPU stream-to-buffer conversion and the render-resource upload behind // CreateSectionGroup. Keep this as one phase: it is the exact plugin/RMC boundary we can // budget without modifying RealtimeMeshComponent. const uint64 StreamUploadStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0; RTMesh->CreateSectionGroup(GroupKey, MoveTemp(Streams)); const uint64 MeshSubmittedCycles = FPlatformTime::Cycles64(); const double StreamUploadSeconds = bProfileApply ? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - StreamUploadStartCycles) : 0.0; // RMC casts shadows PER SECTION (FRealtimeMeshSectionConfig::bCastsShadow, default true) — the // component-level UPrimitiveComponent::CastShadow is NOT honored by the RMC proxy, so the real // shadow lever is the section flag. RMC auto-created one section per non-empty polygroup above // (default config already maps material slot = polygroup index); only config sections that // exist — the bHas* flags come from the worker. Collision at level 0 only (T1.c), both groups. const uint64 CollisionConfigStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0; const int32 NumIndices = Result.NumTriangles * 3; const int32 FirstCapIndex = FMath::Clamp( NumIndices - Result.NumCeilingTriangles * 3, 0, NumIndices); if (bHasGroundTris) { FRealtimeMeshSectionConfig GroundConfig(0); GroundConfig.bCastsShadow = bCastShadow; const FRealtimeMeshStreamRange GroundRange = URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange( 0, Result.NumVertices, 0, FirstCapIndex); RTMesh->CreateSection( FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 0), GroundConfig, GroundRange, /*bShouldCreateCollision*/ bWantsCollision); } if (bHasCeilingTris) { FRealtimeMeshSectionConfig CapConfig(1); CapConfig.bCastsShadow = false; // the cap never casts (see above) const FRealtimeMeshStreamRange CapRange = URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange( 0, Result.NumVertices, FirstCapIndex, NumIndices); RTMesh->CreateSection( FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 1), CapConfig, CapRange, /*bShouldCreateCollision*/ bWantsCollision); } const double CollisionConfigSeconds = bProfileApply ? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - CollisionConfigStartCycles) : 0.0; if (bWantsCollision) { if (GVoxelForgeProfileTileGeneration != 0) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCollisionCookSubmitted] tile=(%d,%d,%d) level=0"), Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z); } // RMC's collision future is fulfilled only after its async cook has been applied to the // mesh BodySetup. This is the per-apply completion boundary; no submission-time guess is // promoted to CollisionReadyTiles. const uint64 SubmissionId = ++NextCollisionSubmissionId; const FRealtimeMeshCollisionConfiguration CollisionConfig = RTMesh->GetCollisionConfig(); const uint64 CollisionSubmittedCycles = FPlatformTime::Cycles64(); FPendingCollisionCook Pending; Pending.Mesh = RTMesh; Pending.SubmissionId = SubmissionId; Pending.MeshSubmittedCycles = MeshSubmittedCycles; Pending.CollisionSubmittedCycles = CollisionSubmittedCycles; Pending.CollisionSubmittedFrame = GFrameCounter; Pending.RequestStartCycles = Result.RequestStartCycles; Pending.GenerationStartCycles = Result.GenerationStartCycles; Pending.GenerationEndCycles = Result.GenerationEndCycles; Pending.ApplyStartCycles = Result.ApplyStartCycles; Pending.WorkPriority = Result.WorkPriority; Pending.bStep1SupportCohort = Result.bStep1SupportCohort; Pending.NumVertices = Result.NumVertices; Pending.NumTriangles = Result.NumTriangles; Pending.NumSkirtTriangles = Result.NumSkirtTriangles; Pending.PendingCooksAtSubmit = PendingCollisionCooks.Num(); Pending.bPooledComponent = bWasPooledComponent; Pending.bUseAsyncCook = CollisionConfig.bUseAsyncCook; Pending.bUseComplexAsSimpleCollision = CollisionConfig.bUseComplexAsSimpleCollision; Pending.bShouldFastCookMeshes = CollisionConfig.bShouldFastCookMeshes; Pending.bMergeAllMeshes = CollisionConfig.bMergeAllMeshes; Pending.SuppressedAtCycles = CollisionSuppressedAtCycles.FindRef(Tile); Pending.bRecordStreamingLatency = Result.bRecordStreamingLatency; PendingCollisionCooks.Add(Tile, Pending); // Register the serial before asking RMC to dirty collision. The current implementation // defers the future to end-of-frame, but this ordering also keeps an already-fulfilled // future or an inline body-update event from racing the per-tile record. TFuture CollisionFuture = RTMesh->SetCollisionConfig(CollisionConfig); TWeakObjectPtr WeakWorld(this); TWeakObjectPtr WeakMesh(RTMesh); CollisionFuture.Next( [WeakWorld, WeakMesh, Tile, SubmissionId](ERealtimeMeshCollisionUpdateResult CollisionResult) { if (AVoxelWorld* World = WeakWorld.Get()) { World->HandleTileCollisionCookComplete( Tile, SubmissionId, WeakMesh.Get(), static_cast(CollisionResult), FPlatformTime::Cycles64()); } }); } if (bProfileApply) { const double TotalSeconds = FPlatformTime::ToSeconds64( FPlatformTime::Cycles64() - ApplyStartCycles); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeApplyProfile] level=%d empty=0 created=%d pooled=%d " "material=%.6f component=%.6f component_create=%.6f " "component_register=%.6f mesh_init=%.6f remove_group=%.6f " "material_slots=%.6f stream_upload=%.6f create_section_group=%.6f " "collision_config=%.6f total=%.6f"), Tile.Level, bComponentCreated ? 1 : 0, (!bComponentAlreadyPresent && !bComponentCreated) ? 1 : 0, MaterialSeconds, ComponentSeconds, ComponentCreationSeconds, ComponentRegistrationSeconds, MeshInitSeconds, RemoveGroupSeconds, MaterialSlotSeconds, StreamUploadSeconds, StreamUploadSeconds, CollisionConfigSeconds, TotalSeconds); } // Water is not spawned per tile — it's a single player-following ocean plane (UpdateWater, // driven from Tick), so it renders at every LOD and to the horizon with no per-tile gaps. return true; } //============================================================================= // STRATE QUERIES //============================================================================= int32 AVoxelWorld::GetStrateAtPosition(FVector WorldPosition) const { if (!StrateManager) return -1; // GetStrateIndex veut des cm ACTEUR-LOCAUX (il convertit cm -> voxels lui-meme). // GetStrateIndex wants actor-LOCAL cm (it does the cm -> voxel conversion itself). const float LocalZCm = WorldToLocalCm(WorldPosition).Z; return StrateManager->GetStrateIndex(ToStrateManagerLocalZ(LocalZCm)); } float AVoxelWorld::ToStrateManagerLocalZ(float LocalZCm) const { return bUseVoxelForgeCore ? (LocalZCm / GetActiveVoxelSizeCm()) * VOXEL_SIZE : LocalZCm; } float AVoxelWorld::FromStrateManagerLocalZ(float ManagerLocalZCm) const { return bUseVoxelForgeCore ? (ManagerLocalZCm / VOXEL_SIZE) * GetActiveVoxelSizeCm() : ManagerLocalZCm; } FVoxelBiomeQuery AVoxelWorld::GetBiomeAtWorldLocation(FVector WorldLocation) const { FVoxelBiomeQuery Out; if (!Generator) return Out; // Bring the world point into actor-LOCAL voxel space — the SAME transform the decoration scatter // applies (UpdateDecorations), so the probe agrees with where props actually land. // Convert using the legacy generator's authored voxel size; Core still queries this biome field. // Convertir avec l'échelle historique du générateur; Core interroge toujours ce champ de biomes. const FVector Local = GetActorTransform().InverseTransformPosition(WorldLocation); const float VX = Local.X / VOXEL_SIZE; const float VY = Local.Y / VOXEL_SIZE; const int32 ChunkZ = FMath::FloorToInt((Local.Z / VOXEL_SIZE) / (float)CHUNK_SIZE); Generator->QueryBiomeAt(VX, VY, ChunkZ, Out); // Decoration streaming state of the region under this point — discriminates a render drop / empty // march / stuck build / never-requested region for a visibly-bare patch (see FVoxelBiomeQuery). if (ContentManager) { ContentManager->QueryDecoDebugAt(Local, Out.bDecoRegionApplied, Out.DecoAppliedInstances, Out.bDecoRegionBuilding, Out.DecoCellsAccounted, Out.DecoCellsTotal, Out.DecoLiveMarchSpawns, Out.DecoInstancesInCell); } return Out; } bool AVoxelWorld::GetVoxelSurfaceHeightAt(FVector WorldLocation, float& OutSurfaceWorldZ, float& OutCeilingWorldZ) const { OutSurfaceWorldZ = WorldLocation.Z; // sensible fallback: unchanged Z OutCeilingWorldZ = WorldLocation.Z; if (bUseVoxelForgeCore) { if (!IsVoxelForgeCoreReady()) return false; const TSharedPtr State = VoxelForgeCoreState; if (!State.IsValid()) return false; const FVector LocalCm = WorldToLocalCm(WorldLocation); const double CoreVoxelSize = GetActiveVoxelSizeCm(); const double XVoxel = LocalCm.X / CoreVoxelSize; const double YVoxel = LocalCm.Y / CoreVoxelSize; const double ZVoxel = LocalCm.Z / CoreVoxelSize; FReadScopeLock Lock(State->WorldLock); if (!State->World) return false; float PreviousDensity = State->World->densityAt({XVoxel, YVoxel, ZVoxel}); if (PreviousDensity <= 0.0f) return false; // Requête de hauteur définie depuis un point d'air. // This height query is defined from an air point. double GroundVoxelZ = ZVoxel; double CeilingVoxelZ = ZVoxel; bool bFoundGround = false; bool bFoundCeiling = false; float GroundPreviousDensity = PreviousDensity; float CeilingPreviousDensity = PreviousDensity; double GroundPreviousZ = ZVoxel; double CeilingPreviousZ = ZVoxel; constexpr int32 MaxSearchVoxels = 512; for (int32 Step = 1; Step <= MaxSearchVoxels && (!bFoundGround || !bFoundCeiling); ++Step) { if (!bFoundGround) { const double SampleZ = ZVoxel - Step; const float Density = State->World->densityAt({XVoxel, YVoxel, SampleZ}); if (Density <= 0.0f && GroundPreviousDensity > 0.0f) { const double Alpha = GroundPreviousDensity / static_cast(GroundPreviousDensity - Density); GroundVoxelZ = GroundPreviousZ + (SampleZ - GroundPreviousZ) * Alpha; bFoundGround = true; } GroundPreviousDensity = Density; GroundPreviousZ = SampleZ; } if (!bFoundCeiling) { const double SampleZ = ZVoxel + Step; const float Density = State->World->densityAt({XVoxel, YVoxel, SampleZ}); if (Density <= 0.0f && CeilingPreviousDensity > 0.0f) { const double Alpha = CeilingPreviousDensity / static_cast(CeilingPreviousDensity - Density); CeilingVoxelZ = CeilingPreviousZ + (SampleZ - CeilingPreviousZ) * Alpha; bFoundCeiling = true; } CeilingPreviousDensity = Density; CeilingPreviousZ = SampleZ; } } if (!bFoundGround && !bFoundCeiling) return false; if (bFoundGround) { OutSurfaceWorldZ = GetActorTransform().TransformPosition( FVector(LocalCm.X, LocalCm.Y, GroundVoxelZ * CoreVoxelSize)).Z; } if (bFoundCeiling) { OutCeilingWorldZ = GetActorTransform().TransformPosition( FVector(LocalCm.X, LocalCm.Y, CeilingVoxelZ * CoreVoxelSize)).Z; } return true; } if (!Generator) return false; // Undo the actor transform → voxel space (same convention as GetBiomeAtWorldLocation / the deco scatter). const FVector Local = GetActorTransform().InverseTransformPosition(WorldLocation); const float VX = Local.X / VOXEL_SIZE; const float VY = Local.Y / VOXEL_SIZE; const int32 ChunkZ = FMath::FloorToInt((Local.Z / VOXEL_SIZE) / (float)CHUNK_SIZE); float TerrainVZ, CeilVZ; if (!Generator->GetSurfaceHeightAt(VX, VY, ChunkZ, TerrainVZ, CeilVZ)) return false; // not a heightfield // Voxel Z → actor-local cm → world, keeping the query's XY so a tilted/scaled actor stays consistent. OutSurfaceWorldZ = GetActorTransform().TransformPosition(FVector(Local.X, Local.Y, TerrainVZ * VOXEL_SIZE)).Z; OutCeilingWorldZ = GetActorTransform().TransformPosition(FVector(Local.X, Local.Y, CeilVZ * VOXEL_SIZE)).Z; return true; } //============================================================================= // TERRAIN MODIFICATION — player carving & filling //============================================================================= // All brush entry points below build an FVoxelModification and funnel through ApplyModification // (diff layer + re-mesh). Strength sign convention: NEGATIVE = carve (air), POSITIVE = fill (solid). void AVoxelWorld::CarveAtPosition(FVector Position, float Radius, float Strength) { FVoxelModification Mod; Mod.Center = WorldToLocalVoxel(Position); // world cm → voxel space const float VoxelScale = GetLegacyVoxelToActiveVoxelScale(); Mod.Radius = Radius * VoxelScale; Mod.Strength = -FMath::Abs(Strength) * VoxelScale; // force negative for carving / Force le signe negatif pour creuser. ApplyModification(Mod); } void AVoxelWorld::FillAtPosition(FVector Position, float Radius, float Strength) { FVoxelModification Mod; Mod.Center = WorldToLocalVoxel(Position); const float VoxelScale = GetLegacyVoxelToActiveVoxelScale(); Mod.Radius = Radius * VoxelScale; Mod.Strength = FMath::Abs(Strength) * VoxelScale; // force positive for filling / Force le signe positif pour remplir. ApplyModification(Mod); } void AVoxelWorld::ApplyModification(const FVoxelModification& Modification) { if (bUseVoxelForgeCore) { ApplyCoreModification(Modification); return; } if (!DiffLayer) return; const bool bProfileModification = GVoxelForgeProfileTileGeneration != 0; const uint64 ModificationStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0; const uint64 DiffStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0; TArray AffectedChunks = DiffLayer->ApplyModification(Modification); const double DiffSeconds = bProfileModification ? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - DiffStartCycles) : 0.0; // If a carve removes the surface directly beneath this pawn, an installed old BodySetup would // otherwise keep it standing on geometry that the diff has already removed. Suppress only the // current support tile, and only when the brush bounds intersect the pawn bounds; nearby digs // keep their existing same-tile recook handoff. if (Modification.Strength < 0.0f && AffectedChunks.Num() > 0) { FVector PlayerPosition = FVector::ZeroVector; APawn* Pawn = nullptr; FVoxelTileKey SupportTile; if (TryGetPlayerPosition(PlayerPosition, &Pawn) && GetPlayerSupportTile(Pawn, PlayerPosition, SupportTile) && AffectedChunks.Contains(SupportTile.Coord) && IsTileCollisionReady(SupportTile)) { FVector EditMinVoxel, EditMaxVoxel; Modification.GetWorldBounds(EditMinVoxel, EditMaxVoxel); FBox EditWorldBounds(ForceInit); for (int32 Corner = 0; Corner < 8; ++Corner) { const FVector LocalVoxel( (Corner & 1) != 0 ? EditMaxVoxel.X : EditMinVoxel.X, (Corner & 2) != 0 ? EditMaxVoxel.Y : EditMinVoxel.Y, (Corner & 4) != 0 ? EditMaxVoxel.Z : EditMinVoxel.Z); EditWorldBounds += LocalVoxelToWorld(LocalVoxel); } FBox PawnWorldBounds(ForceInit); if (const ACharacter* Character = Cast(Pawn)) { if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent()) { PawnWorldBounds = Capsule->Bounds.GetBox(); } } if (!PawnWorldBounds.IsValid) { FVector PawnBoundsOrigin = FVector::ZeroVector; FVector PawnBoundsExtent = FVector::ZeroVector; Pawn->GetActorBounds(true, PawnBoundsOrigin, PawnBoundsExtent); PawnWorldBounds = FBox( PawnBoundsOrigin - PawnBoundsExtent, PawnBoundsOrigin + PawnBoundsExtent); } if (EditWorldBounds.Intersect(PawnWorldBounds)) { if (URealtimeMeshComponent* SupportComponent = TileComponents.FindRef(SupportTile)) { CollisionSuppressedUntilCook.Add(SupportTile); CollisionSuppressedAtCycles.Add(SupportTile, FPlatformTime::Cycles64()); CollisionReadyTiles.Remove(SupportTile); // A prior cook can still complete after this edit. Remove its serial so that // its completion cannot restore readiness over the replacement geometry. PendingCollisionCooks.Remove(SupportTile); SupportComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeUnderfootCollisionSuppressed] tile=(%d,%d,%d) " "frame=%llu brush_capsule_bounds_intersect=1"), SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z, static_cast(GFrameCounter)); UpdatePawnCollisionGate(Pawn, PlayerPosition, 0.0f); } } } } if (AffectedChunks.Num() > 0) { ++ModificationEpoch; Modification.GetWorldBounds(LastModificationMinVoxel, LastModificationMaxVoxel); if (FVisualGapAuditState* GapAudit = GVoxelForgeVisualGapAudits.Find(this)) { GapAudit->EditedBounds.Emplace(LastModificationMinVoxel, LastModificationMaxVoxel); if (GapAudit->EditedBounds.Num() > 256) { GapAudit->EditedBounds.RemoveAt(0, GapAudit->EditedBounds.Num() - 256, EAllowShrinking::No); } } FModificationRevision Revision; Revision.Epoch = ModificationEpoch; Revision.MinVoxel = LastModificationMinVoxel; Revision.MaxVoxel = LastModificationMaxVoxel; ModificationRevisions.Add(Revision); if (bVisualStalenessAudit && VisualStalenessAuditMode == 1) { VisualAuditEditEpoch = ModificationEpoch; VisualAuditEditMinVoxel = LastModificationMinVoxel; VisualAuditEditMaxVoxel = LastModificationMaxVoxel; VisualAuditEditBeginSeconds = FPlatformTime::Seconds(); VisualAuditEditStaleDurationSeconds = -1.0; VisualAuditEditMaxStaleTiles = 0; bVisualAuditEditSawStale = false; bVisualAuditEditResolved = false; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeVisualStaleness] edit_begin=1 epoch=%u bounds_min=(%.2f,%.2f,%.2f) " "bounds_max=(%.2f,%.2f,%.2f)"), VisualAuditEditEpoch, VisualAuditEditMinVoxel.X, VisualAuditEditMinVoxel.Y, VisualAuditEditMinVoxel.Z, VisualAuditEditMaxVoxel.X, VisualAuditEditMaxVoxel.Y, VisualAuditEditMaxVoxel.Z); } } // INSTANT DIG FEEL — synchronously re-mesh the level-0 tile the brush CENTRE sits in, so the hole // appears THIS frame right where the player is looking. Neighbour tiles (brush edge) re-mesh async // and prioritised (RemeshDirtyChunks → DirtyRemeshQueue @ BackgroundHigh), a frame or two behind — // imperceptible. One full-res tile gen on the game thread; only for the common case. // SKIP the sync when the centre tile is already mid-gen (an async task owns it): syncing would race // the in-flight stale result (which lands with no hole and would clobber ours). Instead let it flow // through the async queue, which now KEEPS in-flight tiles queued and re-gens them once the stale // result lands. The centre tile must be loaded to remesh in place (else it streams in with the diff). const FVoxelTileKey CenterTile(WorldToChunkCoord(Modification.Center * VOXEL_SIZE), 0); bool bSyncedCenter = false; if (AffectedChunks.Contains(CenterTile.Coord) && LoadedTiles.Contains(CenterTile) && !PendingTiles.Contains(CenterTile)) { const uint64 SyncStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0; SyncRemeshTile(CenterTile); if (bProfileModification) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeModificationProfile] center_sync=%.6f"), FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - SyncStartCycles)); } bSyncedCenter = true; } const uint64 QueueStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0; RemeshDirtyChunks(AffectedChunks, bSyncedCenter ? &CenterTile : nullptr); if (bProfileModification) { UE_LOG(LogTemp, Display, TEXT("[VoxelForgeModificationProfile] affected=%d diff=%.6f queue=%.6f total=%.6f " "center_synced=%d"), AffectedChunks.Num(), DiffSeconds, FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - QueueStartCycles), FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ModificationStartCycles), bSyncedCenter ? 1 : 0); } // Invalidate the complete decoration build state after a terrain edit. This removes live instances, // abandons in-flight pre-edit marches, and forces the next stationary update to resample the edited // field. Clearing all regions is deliberately conservative: FVoxelModification is actor-local voxel // geometry and may be a box/capsule, while decoration transforms are world-space; a partial sphere // conversion here would risk leaving floaters or allowing an old result to resurrect one. if (ContentManager && AffectedChunks.Num() > 0) { ContentManager->InvalidateDecorationBuilds(); } } void AVoxelWorld::ApplyCoreModification(const FVoxelModification& Modification) { const TSharedPtr State = VoxelForgeCoreState; if (!State.IsValid() || !State->bReady.load(std::memory_order_acquire) || State->bShuttingDown.load(std::memory_order_acquire)) { UE_LOG(LogTemp, Warning, TEXT("[VoxelForgeCoreEdit] rejected: Core world is not ready.")); return; } VoxelForgeCore::EditTransaction Transaction; Transaction.sequence = State->NextEditSequence.fetch_add(1, std::memory_order_relaxed); VoxelForgeCore::EditBrush Brush; Brush.operation = VoxelForgeCore::EditBrush::Operation::densityOffset; Brush.a = {Modification.Center.X, Modification.Center.Y, Modification.Center.Z}; Brush.b = {Modification.CapsuleEnd.X, Modification.CapsuleEnd.Y, Modification.CapsuleEnd.Z}; Brush.halfExtent = {Modification.BoxExtent.X, Modification.BoxExtent.Y, Modification.BoxExtent.Z}; Brush.radius = FMath::Max(0.0f, Modification.Radius); Brush.falloff = FMath::Max(0.0f, Modification.Falloff); // Legacy subtracts the modification offset; Core's densityOffset operation adds it. // Le legacy soustrait le decalage; l'operation densityOffset de Core l'ajoute. Brush.strength = -Modification.Strength; switch (Modification.Shape) { case EVoxelBrushShape::Box: Brush.shape = VoxelForgeCore::EditBrush::Shape::roundedBox; Brush.rounding = 0.0; break; case EVoxelBrushShape::Capsule: Brush.shape = VoxelForgeCore::EditBrush::Shape::capsule; break; case EVoxelBrushShape::Sphere: default: Brush.shape = VoxelForgeCore::EditBrush::Shape::sphere; break; } Transaction.brushes.push_back(Brush); // La transaction est la frontière édition/réplication de Core : une future autorité pourra // soumettre la même séquence et les mêmes brosses sans passer par l'entrée locale. // The transaction is Core's edit/replication boundary: a future authority can submit the same // sequence and brushes without going through local brush input. // Attendre les lecteurs et compilateurs de régions avant de muter le monde Core partagé. // Drain Core readers and region compilers before mutating the shared Core world. while (ActiveTaskCount.load(std::memory_order_acquire) > 0) { if (bShuttingDown.load(std::memory_order_acquire) || State->bShuttingDown.load(std::memory_order_acquire)) { return; } FPlatformProcess::Sleep(0.001f); } if (bCoreRegionCompilePending) { State->bRegionCompileFinished.exchange(false, std::memory_order_acq_rel); bCoreRegionCompilePending = false; PendingCoreRegionTiles.Reset(); PendingCoreRegionTileSet.Reset(); CoreRegionCompileTilesInFlight.Reset(); PendingCoreRetainedTiles.Reset(); CoreRegionPendingSinceSeconds = 0.0; } std::vector Desired; Desired.reserve(static_cast(DesiredSorted.Num())); for (const FVoxelTileKey& Key : DesiredSorted) { Desired.push_back({{Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}); } std::optional EditResult; std::string Error; { FWriteScopeLock Lock(State->WorldLock); if (State->World) { EditResult = State->World->applyEditTransaction(Transaction, Desired, Error); } } if (!EditResult) { UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreEdit] transaction=%llu failed: %s"), static_cast(Transaction.sequence), UTF8_TO_TCHAR(Error.c_str())); return; } ++GenerationEpoch; ++ModificationEpoch; LastVoxelForgeCoreEditDigest = EditResult->digest; LastVoxelForgeCoreEditSequence = Transaction.sequence; LastVoxelForgeCoreAffectedTiles.Reset(); DirtyRemeshQueue.Reset(); for (const std::vector& ByLod : EditResult->affectedByLod) { for (const VoxelForgeCore::TileKey& Key : ByLod) { const FVoxelTileKey RuntimeKey( FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod); LastVoxelForgeCoreAffectedTiles.Add(RuntimeKey); if (IsDesired(RuntimeKey)) DirtyRemeshQueue.Add(RuntimeKey); } } LastVoxelForgeCoreAffectedTiles.Sort([](const FVoxelTileKey& A, const FVoxelTileKey& B) { if (A.Level != B.Level) return A.Level < B.Level; if (A.Coord.X != B.Coord.X) return A.Coord.X < B.Coord.X; if (A.Coord.Y != B.Coord.Y) return A.Coord.Y < B.Coord.Y; return A.Coord.Z < B.Coord.Z; }); bAllChunksLoaded = DirtyRemeshQueue.IsEmpty(); // Conserver le transfert de collision sous les pieds : retirer l'ancien corps pendant la // génération et la cuisson de la nouvelle géométrie Core si la brosse touche la capsule. // Preserve the underfoot collision handoff while the new Core mesh/collision cook is pending // when the brush intersects the pawn capsule. if (Modification.Strength < 0.0f) { FVector PlayerPosition = FVector::ZeroVector; APawn* Pawn = nullptr; FVoxelTileKey SupportTile; if (TryGetPlayerPosition(PlayerPosition, &Pawn) && GetPlayerSupportTile(Pawn, PlayerPosition, SupportTile) && LastVoxelForgeCoreAffectedTiles.Contains(SupportTile) && IsTileCollisionReady(SupportTile)) { FVector MinVoxel, MaxVoxel; Modification.GetWorldBounds(MinVoxel, MaxVoxel); FBox EditWorldBounds(ForceInit); for (int32 Corner = 0; Corner < 8; ++Corner) { EditWorldBounds += LocalVoxelToWorld(FVector( (Corner & 1) ? MaxVoxel.X : MinVoxel.X, (Corner & 2) ? MaxVoxel.Y : MinVoxel.Y, (Corner & 4) ? MaxVoxel.Z : MinVoxel.Z)); } FBox PawnBounds(ForceInit); if (const ACharacter* Character = Cast(Pawn)) { if (const UCapsuleComponent* Capsule = Character->GetCapsuleComponent()) { PawnBounds = Capsule->Bounds.GetBox(); } } if (!PawnBounds.IsValid) { FVector Origin, Extent; Pawn->GetActorBounds(true, Origin, Extent); PawnBounds = FBox(Origin - Extent, Origin + Extent); } if (EditWorldBounds.Intersect(PawnBounds)) { if (URealtimeMeshComponent* Component = TileComponents.FindRef(SupportTile)) { CollisionSuppressedUntilCook.Add(SupportTile); CollisionSuppressedAtCycles.Add(SupportTile, FPlatformTime::Cycles64()); CollisionReadyTiles.Remove(SupportTile); PendingCollisionCooks.Remove(SupportTile); Component->SetCollisionEnabled(ECollisionEnabled::NoCollision); UpdatePawnCollisionGate(Pawn, PlayerPosition, 0.0f); } } } } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCoreEdit] sequence=%llu digest=%llu affected_tiles=%d epoch=%u"), static_cast(LastVoxelForgeCoreEditSequence), static_cast(LastVoxelForgeCoreEditDigest), LastVoxelForgeCoreAffectedTiles.Num(), GenerationEpoch); if (ContentManager && LastVoxelForgeCoreAffectedTiles.Num() > 0) { ContentManager->InvalidateDecorationBuilds(); } } void AVoxelWorld::CarveBox(FVector Position, FVector ExtentVoxels, float Strength) { FVoxelModification Mod; Mod.Shape = EVoxelBrushShape::Box; Mod.Center = WorldToLocalVoxel(Position); const float VoxelScale = GetLegacyVoxelToActiveVoxelScale(); Mod.BoxExtent = ExtentVoxels * VoxelScale; Mod.Falloff *= VoxelScale; Mod.Radius = ExtentVoxels.GetMax() * VoxelScale; // budget proxy Mod.Strength = -FMath::Abs(Strength) * VoxelScale; // carve / creuser ApplyModification(Mod); } void AVoxelWorld::FillBox(FVector Position, FVector ExtentVoxels, float Strength) { FVoxelModification Mod; Mod.Shape = EVoxelBrushShape::Box; Mod.Center = WorldToLocalVoxel(Position); const float VoxelScale = GetLegacyVoxelToActiveVoxelScale(); Mod.BoxExtent = ExtentVoxels * VoxelScale; Mod.Falloff *= VoxelScale; Mod.Radius = ExtentVoxels.GetMax() * VoxelScale; Mod.Strength = FMath::Abs(Strength) * VoxelScale; // fill / remplir ApplyModification(Mod); } void AVoxelWorld::CarveCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels, float Strength) { FVoxelModification Mod; Mod.Shape = EVoxelBrushShape::Capsule; Mod.Center = WorldToLocalVoxel(WorldA); Mod.CapsuleEnd = WorldToLocalVoxel(WorldB); const float VoxelScale = GetLegacyVoxelToActiveVoxelScale(); Mod.Radius = RadiusVoxels * VoxelScale; Mod.Falloff *= VoxelScale; Mod.Strength = -FMath::Abs(Strength) * VoxelScale; ApplyModification(Mod); } void AVoxelWorld::FillCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels, float Strength) { FVoxelModification Mod; Mod.Shape = EVoxelBrushShape::Capsule; Mod.Center = WorldToLocalVoxel(WorldA); Mod.CapsuleEnd = WorldToLocalVoxel(WorldB); const float VoxelScale = GetLegacyVoxelToActiveVoxelScale(); Mod.Radius = RadiusVoxels * VoxelScale; Mod.Falloff *= VoxelScale; Mod.Strength = FMath::Abs(Strength) * VoxelScale; ApplyModification(Mod); } void AVoxelWorld::EditorCarveSphere() { if (!DiffLayer) { UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] EditorCarveSphere: no DiffLayer (start PIE first).")); return; } CarveAtPosition(EditorBrushCenter, EditorBrushRadius, EditorBrushStrength); } void AVoxelWorld::EditorFillSphere() { if (!DiffLayer) { UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] EditorFillSphere: no DiffLayer (start PIE first).")); return; } FillAtPosition(EditorBrushCenter, EditorBrushRadius, EditorBrushStrength); } //============================================================================= // BIOME MAP PREVIEW — bake the XY biome field to Saved/BiomePreview.png //============================================================================= void AVoxelWorld::BakeBiomePreview() { if (!BiomePreviewStrate) { UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] BakeBiomePreview: assign BiomePreviewStrate first.")); return; } if (BiomePreviewChannel == EBiomePreviewChannel::Biome && BiomePreviewStrate->Biomes.Num() == 0) { UE_LOG(LogTemp, Warning, TEXT("[VoxelWorld] BakeBiomePreview: '%s' has no Biomes — bake Relief/Moisture instead, or add biomes."), *BiomePreviewStrate->GetName()); return; } // Transient generator so this works in the editor without PIE. UVoxelGenerator* Gen = NewObject(this); Gen->Seed = Settings ? Settings->Seed : 0; // Flatten the strate's biomes into a context (mirrors StrateManager::GetBiomeContextForChunk). FBiomeContext Ctx; Ctx.Map = BiomePreviewStrate->BiomeMapParams; for (int32 i = 0; i < BiomePreviewStrate->Biomes.Num(); ++i) { const UVoxelBiomeDefinition* B = BiomePreviewStrate->Biomes[i]; if (!B) continue; FBiomeResolved R; R.Index = i; R.ReliefMin = B->ReliefMin; R.ReliefMax = B->ReliefMax; R.MoistureMin = B->MoistureMin; R.MoistureMax = B->MoistureMax; R.DebugColor = B->DebugColor.ToFColor(true); Ctx.Biomes.Add(R); } const int32 Res = FMath::Clamp(BiomePreviewResolution, 64, 2048); const float Size = FMath::Max(BiomePreviewWorldSize, 1.0f); const float Step = Size / (float)Res; const float OriginX = BiomePreviewCenter.X - Size * 0.5f; const float OriginY = BiomePreviewCenter.Y - Size * 0.5f; TArray Pixels; Pixels.SetNumUninitialized(Res * Res); for (int32 py = 0; py < Res; ++py) for (int32 px = 0; px < Res; ++px) { const float wx = OriginX + (px + 0.5f) * Step; const float wy = OriginY + (py + 0.5f) * Step; FColor C = FColor::Black; switch (BiomePreviewChannel) { case EBiomePreviewChannel::Relief: { const float r = Gen->SampleRelief(wx, wy, Ctx.Map.ReliefFrequency, Ctx.Map.ReliefContrast); const uint8 v = (uint8)FMath::Clamp(r * 255.0f, 0.0f, 255.0f); C = FColor(v, v, v); break; } case EBiomePreviewChannel::Moisture: { const float m = Gen->SampleMoisture(wx, wy, Ctx.Map.MoistureFrequency); const uint8 v = (uint8)FMath::Clamp(m * 255.0f, 0.0f, 255.0f); C = FColor(0, v, (uint8)(255 - v)); // dry=blue → wet=green break; } default: // Biome { const FBiomeSample S = Gen->SampleBiomeAt(wx, wy, Ctx); if (Ctx.Biomes.IsValidIndex(S.DominantIndex)) { C = Ctx.Biomes[S.DominantIndex].DebugColor; if (S.NeighborWeight > 0.0f && Ctx.Biomes.IsValidIndex(S.NeighborIndex)) { const FLinearColor A(C); const FLinearColor Bn(Ctx.Biomes[S.NeighborIndex].DebugColor); C = FLinearColor::LerpUsingHSV(A, Bn, S.NeighborWeight).ToFColor(true); } } break; } } C.A = 255; Pixels[py * Res + px] = C; } // Encode PNG and write to Saved/. IImageWrapperModule& Module = FModuleManager::LoadModuleChecked(FName("ImageWrapper")); const TSharedPtr Wrapper = Module.CreateImageWrapper(EImageFormat::PNG); if (!Wrapper.IsValid() || !Wrapper->SetRaw(Pixels.GetData(), (int64)Pixels.Num() * sizeof(FColor), Res, Res, ERGBFormat::BGRA, 8)) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] BakeBiomePreview: failed to encode image.")); return; } const TArray64& Png = Wrapper->GetCompressed(100); const FString Path = FPaths::ProjectSavedDir() / TEXT("BiomePreview.png"); if (FFileHelper::SaveArrayToFile(Png, *Path)) { UE_LOG(LogTemp, Display, TEXT("[VoxelWorld] Biome preview (%dx%d, %s) saved to %s"), Res, Res, *UEnum::GetValueAsString(BiomePreviewChannel), *FPaths::ConvertRelativePathToFull(Path)); } else { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] BakeBiomePreview: failed to write %s"), *Path); } } void AVoxelWorld::ClearAllModifications() { if (!DiffLayer) return; DiffLayer->Clear(); // Regenerate all loaded chunks to restore procedural terrain RegenerateAllChunks(); } //============================================================================= // SEED / SEASON MANAGEMENT //============================================================================= void AVoxelWorld::ChangeSeed(int32 NewSeed) { if (!Settings) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] ChangeSeed failed — no Settings assigned")); return; } if (!Settings->Season.IsNull()) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] ChangeSeed rejected: a cooked Season owns the seed. Assign another season asset instead.")); return; } const int32 OldSeed = Settings->Seed; const int32 OldSeason = Settings->CurrentSeason; { FScopedGenerationPause Guard(this); if (!Guard.Acquired()) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] ChangeSeed: generation pause timed out; no mutation applied.")); return; } // 1. Update seed in Settings (the authoritative source) Settings->Seed = NewSeed; // 2. Increment season counter Settings->CurrentSeason++; // 3. Push new seed to Generator if (Generator) { Generator->InitializeSettings(Settings); } // 4. Rebuild strate layout with the new seed. // Strate assignments and passages all change. if (StrateManager) { StrateManager->Initialize(Settings, NewSeed); } // 5. Clear all player modifications — carvings from the old world are meaningless if (DiffLayer) { DiffLayer->Clear(); } // 5b. Update content placement seed so the new world scatters differently. if (ContentManager) { ContentManager->SetSeed(NewSeed); ContentManager->ClearAll(); } // 5c. Reset atmosphere — strate layout changed, re-apply on next Tick. if (AtmosphereManager) { AtmosphereManager->Reset(); } } // 6. Unload all existing chunks and let Tick reload them with new generation RegenerateAllChunks(); UE_LOG(LogTemp, Log, TEXT("[VoxelWorld] Seed changed: %d -> %d (Season %d -> %d). All chunks regenerated, mods cleared."), OldSeed, NewSeed, OldSeason, Settings->CurrentSeason); } int32 AVoxelWorld::GetCurrentSeed() const { return Settings ? Settings->GetEffectiveWorldSeed() : 0; } int32 AVoxelWorld::GetCurrentSeason() const { return Settings ? Settings->GetEffectiveSeasonNumber() : 0; } FString AVoxelWorld::GetCurrentSeasonContentHash() const { const FString SeasonHash = StrateManager ? StrateManager->GetSeasonContentHash() : FString(); // Only active Core generation includes the optional native world assembly in world identity. // Seul le Core actif inclut l'assemblage natif facultatif dans l'identite du monde. if (!Settings || !bUseVoxelForgeCore) { return SeasonHash; } FString AssemblyHash = VoxelForgeCoreWorldAssemblyHash; if (AssemblyHash.IsEmpty()) { FString AssemblyJson; FString AssemblyError; if (!Settings->GetWorldAssemblyManifestJson(AssemblyJson, AssemblyHash, AssemblyError)) { return FString(); } if (AssemblyHash.IsEmpty() && StrateManager != nullptr) { // Multiplayer/content-hash callers can ask before BeginPlay has cached the startup // artifact. Resolve the same deterministic plan from the already-resolved settings // and layout instead of temporarily returning an identity without that plan. FString SnapshotJson; if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(Settings, StrateManager, FMath::Clamp(CoreClipRadius, 1, 64), FMath::Clamp(CoreMaxClipLevel, 0, 8), GetActiveVoxelSizeCm(), SnapshotJson, AssemblyError)) { return FString(); } FTCHARToUTF8 Utf8Snapshot(*SnapshotJson); std::string CoreError; const std::optional Snapshot = VoxelForgeCore::ResolvedWorldSnapshot::FromJson( std::string_view(Utf8Snapshot.Get(), static_cast(Utf8Snapshot.Length())), CoreError); if (!Snapshot) { return FString(); } if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly()) { AssemblyHash = FString::Printf(TEXT("%016llx"), static_cast(Assembly->contentHash)); } } } const bool bHybridMesher = CoreMesherRecipe != EVoxelForgeCoreMesherRecipe::MarchingCubesV1; const uint32 CoreVoxelSizeMicrons = static_cast( FMath::RoundToInt(GetActiveVoxelSizeCm() * 10000.0f)); if (AssemblyHash.IsEmpty() && !bHybridMesher && CoreVoxelSizeMicrons == 250000) { return SeasonHash; } FString Payload = SeasonHash; if (Payload.IsEmpty()) { Payload = FString::Printf(TEXT("seed=%d"), StrateManager ? StrateManager->GetWorldSeed() : Settings->GetEffectiveWorldSeed()); if (StrateManager) { for (const FStrateSlot& Slot : StrateManager->GetLayout()) { Payload += FString::Printf(TEXT("|strate=%d,%d,%d,%s"), Slot.StrateIndex, Slot.TopChunkZ, Slot.BottomChunkZ, Slot.Definition ? *Slot.Definition->GetPathName() : TEXT("missing")); } } } if (!AssemblyHash.IsEmpty()) { Payload += TEXT("|world_assembly="); Payload += AssemblyHash; } if (CoreVoxelSizeMicrons != 250000) { Payload += FString::Printf(TEXT("|core_voxel_size_microns=%u"), CoreVoxelSizeMicrons); } if (CoreMesherRecipe == EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1) { Payload += TEXT("|core_mesher=WorldAssemblerHybridV1"); } else if (CoreMesherRecipe == EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV2) { Payload += TEXT("|core_mesher=WorldAssemblerHybridV2"); } FTCHARToUTF8 Utf8Payload(*Payload); uint8 Digest[FSHA1::DigestSize]; FSHA1::HashBuffer(Utf8Payload.Get(), Utf8Payload.Length(), Digest); return BytesToHex(Digest, FSHA1::DigestSize); } //============================================================================= // REMESH DIRTY CHUNKS — re-queue affected chunks after terrain modification //============================================================================= void AVoxelWorld::RemeshDirtyChunks(const TArray& DirtyCoords, const FVoxelTileKey* ExcludeTile) { // An edit changes every tile whose footprint intersects its affected level-0 chunks, including // coarse render tiles. Re-meshing only level 0 leaves a pre-edit coarse component covering the // same volume until a later LOD transition. The queue is drained FIRST and launched at // BackgroundHigh; the old component remains visible until its post-edit replacement is applied. auto TileCoversDirtyCoord = [&DirtyCoords](const FVoxelTileKey& Tile) -> bool { const int32 Span = 1 << Tile.Level; const FIntVector MinChunk = Tile.Coord * Span; const FIntVector MaxChunk = MinChunk + FIntVector(Span, Span, Span); for (const FIntVector& Coord : DirtyCoords) { if (Coord.X >= MinChunk.X && Coord.X < MaxChunk.X && Coord.Y >= MinChunk.Y && Coord.Y < MaxChunk.Y && Coord.Z >= MinChunk.Z && Coord.Z < MaxChunk.Z) { return true; } } return false; }; for (const FVoxelTileKey& Tile : LoadedTiles) { if (ExcludeTile && Tile == *ExcludeTile) continue; // handled synchronously this frame if (IsDesired(Tile) && TileCoversDirtyCoord(Tile)) { DirtyRemeshQueue.Add(Tile); } } // A pending worker captured the pre-edit field. Mark its token obsolete so its result drains // without publishing; keep the key in DirtyRemeshQueue so the submit budget requests it again. for (const FVoxelTileKey& Tile : PendingTiles) { if (!IsDesired(Tile) || !TileCoversDirtyCoord(Tile)) continue; DirtyRemeshQueue.Add(Tile); if (TSharedPtr* Cancellation = PendingTileCancellation.Find(Tile)) { if (*Cancellation) { (*Cancellation)->bObsolete.store(true, std::memory_order_release); } } } if (DirtyRemeshQueue.Num() > 0) { // Wake the submit loop even if the streaming set had settled (idle player digging). bAllChunksLoaded = false; } // Density volume: refill the clipmap cells overlapping each carved chunk so the shadow march // sees the edit (GetDensityAt includes the diff layer). Cheap + local; covers all carve shapes. if (DensityVolume && Settings && Settings->bEnableDensityVolume) { for (const FIntVector& Coord : DirtyCoords) { const FIntVector MinV = Coord * CHUNK_SIZE; const FIntVector MaxV = MinV + FIntVector(CHUNK_SIZE, CHUNK_SIZE, CHUNK_SIZE); DensityVolume->MarkDirtyVoxelBox(MinV, MaxV); } } UE_LOG(LogTemp, Verbose, TEXT("[VoxelWorld] RemeshDirtyChunks: %d coords, %d pending"), DirtyCoords.Num(), PendingTiles.Num()); } UVolumeTexture* AVoxelWorld::GetDensityVolumeTexture(int32 Level) const { return DensityVolume ? DensityVolume->GetLevelTexture(Level) : nullptr; } //============================================================================= // TERRAIN MATERIAL — density-volume / orb shadow params (MID-driven, see ApplyMeshToTile) //============================================================================= UMaterialInstanceDynamic* AVoxelWorld::GetOrCreateTerrainMID(UMaterialInterface* Base) { if (!Base) return nullptr; if (TObjectPtr* Found = TerrainMIDs.Find(Base)) { return Found->Get(); } UMaterialInstanceDynamic* MID = UMaterialInstanceDynamic::Create(Base, this); if (MID) { TerrainMIDs.Add(Base, MID); SetVolumeParamsOnMID(MID); // seed with the current frame's params } return MID; } void AVoxelWorld::SetDensityVolumeMaterialParamsDisabled() { TVP0 = FLinearColor::Black; TVP1 = FLinearColor::Black; TVP2 = FLinearColor::Black; TVP3 = FLinearColor::Black; TVP4 = FLinearColor::Black; TVP5 = FLinearColor(0.0f, VOXEL_SIZE, 0.0f, 0.0f); TVP6 = FLinearColor::Black; TVP7 = FLinearColor::Black; TVP8 = FLinearColor::Black; TVP9 = FLinearColor::Black; LastBoundVolTex0.Reset(); for (TPair, TObjectPtr>& Pair : TerrainMIDs) { SetVolumeParamsOnMID(Pair.Value.Get()); } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeDensityVolume] disabled=1 fill_thread=0 uploads=0 tick_update=0 " "VolP0..VolP4=black VolP5=(0,%.1f,0) VolP6..VolP9=black VolTex0..VolTex2=null"), VOXEL_SIZE); } void AVoxelWorld::SetVolumeParamsOnMID(UMaterialInstanceDynamic* MID) const { if (!MID) return; // Static FNames — this runs per MID on every param change; no per-call FName construction. static const FName VolPNames[10] = { FName("VolP0"), FName("VolP1"), FName("VolP2"), FName("VolP3"), FName("VolP4"), FName("VolP5"), FName("VolP6"), FName("VolP7"), FName("VolP8"), FName("VolP9") }; static const FName VolTexNames[3] = { FName("VolTex0"), FName("VolTex1"), FName("VolTex2") }; const FLinearColor* TVPs[10] = { &TVP0, &TVP1, &TVP2, &TVP3, &TVP4, &TVP5, &TVP6, &TVP7, &TVP8, &TVP9 }; for (int32 i = 0; i < 10; ++i) { MID->SetVectorParameterValue(VolPNames[i], *TVPs[i]); } const bool bVolumeActive = DensityVolume && Settings && Settings->bEnableDensityVolume; for (int32 L = 0; L < 3; ++L) { UVolumeTexture* Texture = bVolumeActive ? DensityVolume->GetLevelTexture(L) : nullptr; MID->SetTextureParameterValue(VolTexNames[L], Texture); } } void AVoxelWorld::UpdateTerrainMaterialParams() { if (!DensityVolume || !Settings || !Settings->bEnableDensityVolume) return; // --- Per-level clipmap transforms (L0 = finest/near; L1-2 = coarser for shadow REACH) --- // For each level the material maps WorldPos → RelPos = WorldPos - WindowOrigin → cellF = RelPos/Cell → // toroidal UVW = frac((OriginMod + cellF + 0.5)/Res). OriginMod = OriginCells mod Res precomputed here // so the shader never touches the large absolute cell coord (no float precision loss). The shader // derives each level's cell size from L0's (cell_L = L0Cell * 2^L); Res is shared across levels. const FTransform Xf = GetActorTransform(); int32 Res = 0; bool bHave = false; float CellWorldSize = VOXEL_SIZE; // L0 cm per cell FLinearColor OriginC[3] = { FLinearColor::Black, FLinearColor::Black, FLinearColor::Black }; FLinearColor ModC[3] = { FLinearColor::Black, FLinearColor::Black, FLinearColor::Black }; for (int32 L = 0; L < 3; ++L) { FIntVector OriginCells(0, 0, 0); float StepF = 1.0f; int32 LRes = 0; if (DensityVolume->GetLevelShaderParams(L, OriginCells, StepF, LRes) && LRes > 0) { const FVector OriginLocalCm = FVector(OriginCells.X, OriginCells.Y, OriginCells.Z) * (StepF * VOXEL_SIZE); const FVector OW = Xf.TransformPosition(OriginLocalCm); auto Mod = [LRes](int32 v) { const int32 m = v % LRes; return (float)((m < 0) ? m + LRes : m); }; OriginC[L] = FLinearColor(OW.X, OW.Y, OW.Z, 0.0f); ModC[L] = FLinearColor(Mod(OriginCells.X), Mod(OriginCells.Y), Mod(OriginCells.Z), 0.0f); if (L == 0) { bHave = true; Res = LRes; CellWorldSize = VOXEL_SIZE * StepF; } } } // Track whether anything actually changed — the push below enqueues render-thread updates per MID, // so on the (common) idle frames where the window didn't scroll and the orb didn't change, skip it. bool bDirty = false; auto SetTVP = [&bDirty](FLinearColor& Dst, const FLinearColor& V) { if (Dst != V) { Dst = V; bDirty = true; } }; SetTVP(TVP0, OriginC[0]); SetTVP(TVP1, ModC[0]); SetTVP(TVP6, OriginC[1]); SetTVP(TVP7, ModC[1]); SetTVP(TVP8, OriginC[2]); SetTVP(TVP9, ModC[2]); // --- Nearest active orb --- FVoxelActiveOrb Best; bool bHaveOrb = false; if (ContentManager) { TArray Orbs; ContentManager->GetActiveOrbs(Orbs); if (Orbs.Num() > 0) { const FVector P = GetPlayerPosition(); float BestD = FLT_MAX; for (const FVoxelActiveOrb& O : Orbs) { const float D = FVector::DistSquared(O.WorldPos, P); if (D < BestD) { BestD = D; Best = O; bHaveOrb = true; } } } } // All data lives in .xyz (a Vector Parameter only delivers float3 into a Custom node). Intensity is // premultiplied into the colour; Res / CellWorldSize / Enable go in TVP5. const float Enable = (bHave && bHaveOrb && Res > 0) ? 1.0f : 0.0f; const float Steps = (float)FMath::Clamp(Settings->DensityVolumeMarchSteps, 4, 256); SetTVP(TVP2, FLinearColor(Best.WorldPos.X, Best.WorldPos.Y, Best.WorldPos.Z, 0.0f)); SetTVP(TVP3, FLinearColor(Best.Color.R * Best.Intensity, Best.Color.G * Best.Intensity, Best.Color.B * Best.Intensity, 0.0f)); SetTVP(TVP4, FLinearColor(Best.MaxShadowDistWorld, Best.FalloffWorld, Steps, 0.0f)); SetTVP(TVP5, FLinearColor((float)FMath::Max(Res, 0), CellWorldSize, Enable, 0.0f)); // Re-push if the L0 texture object itself was recreated (resolution change) even when the packed // params happen to be identical — otherwise the MIDs would keep sampling the dropped texture. UVolumeTexture* Tex0 = DensityVolume->GetLevelTexture(0); if (LastBoundVolTex0.Get() != Tex0) { LastBoundVolTex0 = Tex0; bDirty = true; } if (!bDirty) return; // Push to every terrain MID (new MIDs are seeded on creation in GetOrCreateTerrainMID). for (TPair, TObjectPtr>& Pair : TerrainMIDs) { SetVolumeParamsOnMID(Pair.Value.Get()); } } void AVoxelWorld::UpdateOrbLightMPC() { if (!OrbLightMPC || !ContentManager) return; TArray Orbs; ContentManager->GetActiveOrbs(Orbs); // Nearest-first so Orb0..3 are the 4 closest orbs (the Light Function unions their pools; 4 is // plenty since only nearby pools are visible and the player sits inside one or two at a time). const FVector P = GetPlayerPosition(); Orbs.Sort([&P](const FVoxelActiveOrb& A, const FVoxelActiveOrb& B) { return FVector::DistSquared(A.WorldPos, P) < FVector::DistSquared(B.WorldPos, P); }); static const FName OrbNames[4] = { FName("Orb0"), FName("Orb1"), FName("Orb2"), FName("Orb3") }; for (int32 i = 0; i < 4; ++i) { // (x,y,z) = orb WORLD position, .w = reach radius in cm (FalloffWorld = how far the pool // extends). Unused slots = all-zero → radius 0 → the mask yields no pool for them. FLinearColor V(0.f, 0.f, 0.f, 0.f); if (i < Orbs.Num()) { const FVoxelActiveOrb& O = Orbs[i]; V = FLinearColor((float)O.WorldPos.X, (float)O.WorldPos.Y, (float)O.WorldPos.Z, O.FalloffWorld); } // ALWAYS write, even when unchanged. A skip-if-identical cache BREAKS the lighting: the // MPC's world INSTANCE can be reset/recreated behind our back (PIE init order, asset // recompile), and a cached skip then leaves it holding defaults forever. The per-frame // rewrite is what makes the collection self-healing — and 4 vector writes cost nothing. UKismetMaterialLibrary::SetVectorParameterValue(this, OrbLightMPC, OrbNames[i], V); } }