diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 42ac6f5..2c7046b 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -195,10 +195,13 @@ public: std::atomic bShuttingDown{false}; std::atomic NextEditSequence{1}; std::atomic RegionCompileStartCycles{0}; + std::atomic RegionCompileWorkerStartCycles{0}; std::atomic RegionCompileEndCycles{0}; std::atomic RegionCompileTileCount{0}; std::atomic RegionCompileSerial{0}; + std::atomic RegionCompileWorkerThreadId{0}; std::atomic RegionCompileWorkerSeconds{0.0}; + std::atomic RegionCompileWallSeconds{0.0}; int32 FullResolutionLodCount = 0; FString InitialFailureReason; }; @@ -262,6 +265,12 @@ namespace GVoxelForgeCoreRegionConcurrentAdmission, TEXT("Allow tiles independent of a pending Core region compile to proceed; 0 restores the global streaming stop.")); + int32 GVoxelForgeCoreRegionLookahead = 0; + FAutoConsoleVariableRef CVarVoxelForgeCoreRegionLookahead( + TEXT("voxel.CoreRegionLookahead"), + GVoxelForgeCoreRegionLookahead, + TEXT("Retain and compile the next near region set along velocity; 0 disables region lookahead.")); + int32 GVoxelForgeStreamingResultBacklogCap = 256; FAutoConsoleVariableRef CVarVoxelForgeStreamingResultBacklogCap( TEXT("voxel.StreamingResultBacklogCap"), @@ -1745,6 +1754,10 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason) { if (Discard.bObsolete) { ++ObsoleteTileAbortCount; } } + FCoreRegionCompileResult DiscardedRegionCompile; + while (CoreRegionCompileResults.Dequeue(DiscardedRegionCompile)) + { + } UE_LOG(LogTemp, Display, TEXT("[VoxelWorld] EndPlay: workers drained in %.6fs; obsolete_tile_aborts=%d"), FPlatformTime::Seconds() - EndPlayStartSeconds, ObsoleteTileAbortCount); VoxelForgeCoreState.Reset(); @@ -2093,6 +2106,11 @@ void AVoxelWorld::BeginPlay() PendingCoreRegionTiles.Reset(); PendingCoreRegionTileSet.Reset(); PendingCoreRetainedTiles.Reset(); + CoreRegionRetainedDesiredTiles.Reset(); + CoreRegionLookaheadTiles.Reset(); + LastCoreRegionLookaheadCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32); + LastCoreRegionLookaheadStep = FIntVector::ZeroValue; + LastCoreRegionLookaheadGenerationEpoch = GenerationEpoch; CoreRegionCompileTilesInFlight.Reset(); LastCoreRegionRequestCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32); PendingCoreRegionRequestCenter = FIntVector::ZeroValue; @@ -2149,6 +2167,7 @@ void AVoxelWorld::BeginPlay() int32 CommandLineSampleStacks = GVoxelForgeSampleStacks; int32 CommandLineCollisionGate = GVoxelForgeCollisionGate; int32 CommandLineCoreRegionConcurrentAdmission = GVoxelForgeCoreRegionConcurrentAdmission; + int32 CommandLineCoreRegionLookahead = GVoxelForgeCoreRegionLookahead; int32 CommandLineStreamingCollisionLookahead = GVoxelForgeStreamingCollisionLookahead; int32 CommandLineStreamingResultBacklogCap = GVoxelForgeStreamingResultBacklogCap; int32 CommandLineStreamingReadyQueueDepth = GVoxelForgeStreamingReadyQueueDepth; @@ -2257,6 +2276,11 @@ void AVoxelWorld::BeginPlay() GVoxelForgeCoreRegionConcurrentAdmission = CommandLineCoreRegionConcurrentAdmission != 0 ? 1 : 0; } + if (FParse::Value(FCommandLine::Get(), TEXT("voxel.CoreRegionLookahead="), + CommandLineCoreRegionLookahead)) + { + GVoxelForgeCoreRegionLookahead = CommandLineCoreRegionLookahead != 0 ? 1 : 0; + } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingResultBacklogCap="), CommandLineStreamingResultBacklogCap)) { @@ -2747,6 +2771,7 @@ void AVoxelWorld::BeginPlay() "\"streaming_coarse_first\":%d,\"streaming_dynamic_near_reserve\":%d," "\"streaming_legacy_pending_result_cap\":%d," "\"streaming_collision_lookahead\":%d,\"core_region_concurrent_admission\":%d," + "\"core_region_lookahead\":%d," "\"collision_gate\":%d," "\"streaming_result_backlog_cap\":%d,\"streaming_startup_apply_boost\":%d," "\"streaming_far_apply_supplement\":%d," @@ -2769,6 +2794,7 @@ void AVoxelWorld::BeginPlay() GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve, GVoxelForgeStreamingLegacyPendingResultCap, GVoxelForgeStreamingCollisionLookahead, GVoxelForgeCoreRegionConcurrentAdmission, + GVoxelForgeCoreRegionLookahead, GVoxelForgeCollisionGate, GVoxelForgeStreamingResultBacklogCap, GVoxelForgeStreamingStartupApplyBoost, GVoxelForgeStreamingFarApplySupplement, @@ -3246,11 +3272,13 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest() float Speed = 800.0f; float StartSeconds = 20.0f; float MoveDurationSeconds = 0.0f; + float MoveTurnAfterSeconds = 0.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.TestMoveDurationSeconds="), MoveDurationSeconds); + FParse::Value(CommandLine, TEXT("voxel.TestMoveTurnAfterSeconds="), MoveTurnAfterSeconds); FParse::Value(CommandLine, TEXT("voxel.TestExitSeconds="), ExitSeconds); FParse::Value(CommandLine, TEXT("voxel.TestTeleportAfterSeconds="), HeadlessStreamingTestTeleportAtSeconds); @@ -3418,9 +3446,44 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest() { HeadlessStreamingTestDirection = FVector::XAxisVector; } + FString TurnDirectionText; + const FString TurnDirectionKey(TEXT("voxel.TestMoveTurnDirection=")); + const int32 TurnDirectionKeyIndex = CommandLineText.Find( + TurnDirectionKey, ESearchCase::IgnoreCase, ESearchDir::FromStart); + if (TurnDirectionKeyIndex != INDEX_NONE) + { + TurnDirectionText = CommandLineText.Mid( + TurnDirectionKeyIndex + TurnDirectionKey.Len()); + const int32 DirectionEnd = TurnDirectionText.Find( + TEXT(" "), ESearchCase::CaseSensitive, ESearchDir::FromStart); + if (DirectionEnd != INDEX_NONE) + { + TurnDirectionText.LeftInline(DirectionEnd, EAllowShrinking::No); + } + TurnDirectionText.TrimStartAndEndInline(); + TurnDirectionText.RemoveFromStart(TEXT("\"")); + TurnDirectionText.RemoveFromEnd(TEXT("\"")); + TArray Components; + TurnDirectionText.ParseIntoArray(Components, TEXT(","), true); + if (Components.Num() == 3) + { + HeadlessStreamingTestTurnDirection = FVector( + FCString::Atof(*Components[0]), + FCString::Atof(*Components[1]), + FCString::Atof(*Components[2])); + } + } + HeadlessStreamingTestTurnDirection = + HeadlessStreamingTestTurnDirection.GetSafeNormal(); + if (HeadlessStreamingTestTurnDirection.IsNearlyZero()) + { + HeadlessStreamingTestTurnDirection = FVector::YAxisVector; + } HeadlessStreamingTestSpeedCmPerSecond = FMath::Max(Speed, 0.0f); HeadlessStreamingTestStartSeconds = FMath::Max(StartSeconds, 0.0f); HeadlessStreamingTestMoveDurationSeconds = FMath::Max(MoveDurationSeconds, 0.0f); + HeadlessStreamingTestTurnAfterSeconds = FMath::Max(MoveTurnAfterSeconds, 0.0f); + bHeadlessStreamingTestTurnDone = false; bHeadlessStreamingTestMovement = HeadlessStreamingTestSpeedCmPerSecond > 0.0f; } @@ -3431,15 +3494,20 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest() HeadlessStreamingTestBeginSeconds = FPlatformTime::Seconds(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStreamingTest] clean=1 movement=%d speed_cm_s=%.3f start_s=%.3f " - "move_duration_s=%.3f exit_s=%.3f direction=(%.3f,%.3f,%.3f)"), + "move_duration_s=%.3f turn_after_s=%.3f exit_s=%.3f " + "direction=(%.3f,%.3f,%.3f) turn_direction=(%.3f,%.3f,%.3f)"), bHeadlessStreamingTestMovement ? 1 : 0, HeadlessStreamingTestSpeedCmPerSecond, HeadlessStreamingTestStartSeconds, HeadlessStreamingTestMoveDurationSeconds, + HeadlessStreamingTestTurnAfterSeconds, HeadlessStreamingTestExitSeconds, HeadlessStreamingTestDirection.X, HeadlessStreamingTestDirection.Y, - HeadlessStreamingTestDirection.Z); + HeadlessStreamingTestDirection.Z, + HeadlessStreamingTestTurnDirection.X, + HeadlessStreamingTestTurnDirection.Y, + HeadlessStreamingTestTurnDirection.Z); } int32 CollisionGateStressValue = 0; @@ -4503,6 +4571,32 @@ void AVoxelWorld::AdvanceHeadlessStreamingTest( return; } + if (!bHeadlessStreamingTestTurnDone && HeadlessStreamingTestTurnAfterSeconds > 0.0f + && ElapsedSeconds - static_cast(HeadlessStreamingTestStartSeconds) + >= static_cast(HeadlessStreamingTestTurnAfterSeconds)) + { + bHeadlessStreamingTestTurnDone = true; + const FVector PreviousDirection = HeadlessStreamingTestDirection; + HeadlessStreamingTestDirection = HeadlessStreamingTestTurnDirection; + if (VoxelForgeStartupTrace::IsActive()) + { + VoxelForgeStartupTrace::RecordEvent(TEXT("streaming_test_move_turned"), + FString::Printf( + TEXT("\"elapsed_s\":%s,\"from\":[%.0f,%.0f,%.0f]," + "\"to\":[%.0f,%.0f,%.0f]"), + *FString::SanitizeFloat(ElapsedSeconds), + PreviousDirection.X, PreviousDirection.Y, PreviousDirection.Z, + HeadlessStreamingTestDirection.X, HeadlessStreamingTestDirection.Y, + HeadlessStreamingTestDirection.Z)); + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeStreamingTest] turn=1 elapsed_s=%.3f from=(%.0f,%.0f,%.0f) " + "to=(%.0f,%.0f,%.0f)"), + ElapsedSeconds, PreviousDirection.X, PreviousDirection.Y, PreviousDirection.Z, + HeadlessStreamingTestDirection.X, HeadlessStreamingTestDirection.Y, + HeadlessStreamingTestDirection.Z); + } + if (HeadlessStreamingTestMoveDurationSeconds > 0.0f && ElapsedSeconds - static_cast(HeadlessStreamingTestStartSeconds) >= static_cast(HeadlessStreamingTestMoveDurationSeconds)) @@ -11258,16 +11352,113 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& } BuildDesiredTransitionIndex(); - bool bRetainedSetChanged = false; - int32 RetainedTileCount = 0; + FIntVector RegionLookaheadStep = FIntVector::ZeroValue; + if (GVoxelForgeCoreRegionLookahead != 0 && PlayerHeading.Size() >= 50.0f) + { + RegionLookaheadStep = GetStreamingHeadingStep(PlayerHeading, PlayerPawn); + } + const bool bRegionLookaheadTurn = RegionLookaheadStep != FIntVector::ZeroValue + && LastCoreRegionLookaheadStep != FIntVector::ZeroValue + && LastCoreRegionLookaheadStep != RegionLookaheadStep + && LastCoreRegionLookaheadGenerationEpoch == GenerationEpoch; + const FIntVector PreviousRegionLookaheadStep = LastCoreRegionLookaheadStep; + const TArray PreviousRegionLookaheadTiles = bRegionLookaheadTurn + ? CoreRegionLookaheadTiles : TArray(); + if (RegionLookaheadStep != FIntVector::ZeroValue + && (LastCoreRegionLookaheadCenter != Center + || LastCoreRegionLookaheadStep != RegionLookaheadStep + || LastCoreRegionLookaheadGenerationEpoch != GenerationEpoch)) + { + const FVector AheadCameraVoxels = CameraVoxels + FVector( + static_cast(RegionLookaheadStep.X), + static_cast(RegionLookaheadStep.Y), + static_cast(RegionLookaheadStep.Z)) * static_cast(CHUNK_SIZE); + std::vector AheadDesired; + { + FReadScopeLock Lock(State->WorldLock); + if (State->World) + { + AheadDesired = State->World->desiredTiles( + {AheadCameraVoxels.X, AheadCameraVoxels.Y, AheadCameraVoxels.Z}); + } + } + CoreRegionLookaheadTiles.Reset(); + TSet AheadSet; + for (const VoxelForgeCore::TileKey& Key : AheadDesired) + { + if (Key.lod >= State->FullResolutionLodCount) continue; + const FVoxelTileKey RuntimeKey( + FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod); + if (!AheadSet.Contains(RuntimeKey)) + { + AheadSet.Add(RuntimeKey); + CoreRegionLookaheadTiles.Add(RuntimeKey); + } + } + if (bRegionLookaheadTurn) + { + TSet ActualSet; + for (const FVoxelTileKey& Key : DesiredSorted) ActualSet.Add(Key); + int32 StaleRetainedLookaheadTiles = 0; + { + FScopeLock RetainedLock(&State->RetainedTilesLock); + for (const FVoxelTileKey& Key : PreviousRegionLookaheadTiles) + { + if (ActualSet.Contains(Key) || AheadSet.Contains(Key)) continue; + const VoxelForgeCore::TileKey CoreKey{ + {Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}; + if (State->RetainedTiles.contains(CoreKey)) + { + ++StaleRetainedLookaheadTiles; + } + } + } + if (VoxelForgeStartupTrace::IsActive()) + { + VoxelForgeStartupTrace::RecordEvent(TEXT("core_region_lookahead_turn"), + FString::Printf( + TEXT("\"previous_step\":[%d,%d,%d],\"new_step\":[%d,%d,%d]," + "\"previous_prefetch_tiles\":%d," + "\"stale_retained_lookahead_tiles\":%d"), + PreviousRegionLookaheadStep.X, PreviousRegionLookaheadStep.Y, + PreviousRegionLookaheadStep.Z, RegionLookaheadStep.X, + RegionLookaheadStep.Y, RegionLookaheadStep.Z, + PreviousRegionLookaheadTiles.Num(), StaleRetainedLookaheadTiles)); + } + } + LastCoreRegionLookaheadCenter = Center; + LastCoreRegionLookaheadStep = RegionLookaheadStep; + LastCoreRegionLookaheadGenerationEpoch = GenerationEpoch; + } + else if (GVoxelForgeCoreRegionLookahead == 0) + { + CoreRegionLookaheadTiles.Reset(); + LastCoreRegionLookaheadCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32); + LastCoreRegionLookaheadStep = FIntVector::ZeroValue; + LastCoreRegionLookaheadGenerationEpoch = GenerationEpoch; + } + + CoreRegionRetainedDesiredTiles = DesiredSorted; TSet DesiredSet; + DesiredSet.Reserve(DesiredSorted.Num() + CoreRegionLookaheadTiles.Num()); for (const FVoxelTileKey& Key : DesiredSorted) DesiredSet.Add(Key); + for (const FVoxelTileKey& Key : CoreRegionLookaheadTiles) + { + if (!DesiredSet.Contains(Key)) + { + DesiredSet.Add(Key); + CoreRegionRetainedDesiredTiles.Add(Key); + } + } + + bool bRetainedSetChanged = false; + int32 RetainedTileCount = 0; TArray MissingRetainedFineTiles; { FScopeLock Lock(&State->RetainedTilesLock); RetainedTileCount = static_cast(State->RetainedTiles.size()); bRetainedSetChanged = DesiredSet.Num() != RetainedTileCount; - for (const FVoxelTileKey& Key : DesiredSorted) + for (const FVoxelTileKey& Key : CoreRegionRetainedDesiredTiles) { const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}; const bool bAlreadyRetained = State->RetainedTiles.contains(CoreKey); @@ -11313,7 +11504,7 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& { PendingCoreRegionTileSet.Add(Key); } - PendingCoreRetainedTiles = DesiredSorted; + PendingCoreRetainedTiles = CoreRegionRetainedDesiredTiles; bCoreRegionCompilePending = true; ++CoreRegionCompileRequestSerial; if (VoxelForgeStartupTrace::IsActive()) @@ -11322,6 +11513,7 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& 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," + "\"lookahead_region_tiles\":%d,\"lookahead_step\":[%d,%d,%d]," "\"desired_epoch\":%u,\"active_tile_tasks\":%d"), CoreRegionCompileRequestSerial, !bHadPreviousCenter ? TEXT("initial_desired_region_set") @@ -11332,7 +11524,9 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& bCenterMoved ? Center.X - PreviousCenter.X : 0, bCenterMoved ? Center.Y - PreviousCenter.Y : 0, bCenterMoved ? Center.Z - PreviousCenter.Z : 0, - DesiredSorted.Num(), RetainedTileCount, PendingCoreRegionTiles.Num(), DesiredEpoch, + DesiredSorted.Num(), RetainedTileCount, PendingCoreRegionTiles.Num(), + CoreRegionLookaheadTiles.Num(), RegionLookaheadStep.X, RegionLookaheadStep.Y, + RegionLookaheadStep.Z, DesiredEpoch, ActiveTileTaskCount.load(std::memory_order_relaxed))); } StartPendingCoreRegionCompile(); @@ -11363,13 +11557,27 @@ void AVoxelWorld::StartPendingCoreRegionCompile() State->bRegionCompileSucceeded.store(false, std::memory_order_release); const TArray MissingTiles = PendingCoreRegionTiles; const TArray RetainedTiles = PendingCoreRetainedTiles; + TSet ActualDesiredSet; + TSet PrefetchSet; + for (const FVoxelTileKey& Key : DesiredSorted) ActualDesiredSet.Add(Key); + for (const FVoxelTileKey& Key : CoreRegionLookaheadTiles) PrefetchSet.Add(Key); + TArray PrefetchTiles; + for (const FVoxelTileKey& Key : MissingTiles) + { + if (PrefetchSet.Contains(Key) && !ActualDesiredSet.Contains(Key)) + { + PrefetchTiles.Add(Key); + } + } const FIntVector CompileCenter = PendingCoreRegionRequestCenter; const bool bAllowConcurrentGeneration = GVoxelForgeCoreRegionConcurrentAdmission != 0; const uint32 TaskEpoch = GenerationEpoch; const uint32 TaskDesiredEpoch = DesiredEpoch; + const int32 RequestSerial = CoreRegionCompileRequestSerial; const uint32 WorkerLimit = static_cast(FMath::Clamp(GetMaxConcurrentTasks(), 1, 16)); ActiveCoreRegionCompileCenter = CompileCenter; ActiveCoreRegionCompileRequestSerial = CoreRegionCompileRequestSerial; + ActiveCoreRegionWastedPrefetchTiles = 0; ActiveCoreRegionCompileStartSeconds = FPlatformTime::Seconds(); ActiveCoreRegionCompileStartCycles = FPlatformTime::Cycles64(); CoreRegionCompileTilesInFlight.Reset(); @@ -11381,25 +11589,32 @@ void AVoxelWorld::StartPendingCoreRegionCompile() 1, std::memory_order_relaxed) + 1; State->RegionCompileStartCycles.store(ActiveCoreRegionCompileStartCycles, std::memory_order_release); + State->RegionCompileWorkerStartCycles.store(0, std::memory_order_release); State->RegionCompileEndCycles.store(0, std::memory_order_release); State->RegionCompileTileCount.store(0, std::memory_order_release); + State->RegionCompileWorkerThreadId.store(0, std::memory_order_release); State->RegionCompileWorkerSeconds.store(0.0, std::memory_order_release); + State->RegionCompileWallSeconds.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"), + "\"prefetch_compile_tiles\":%d," + "\"active_tile_tasks\":%d,\"active_generation_workers\":%d," + "\"active_task_guard\":%d,\"game_thread_id\":%u"), CompileSerial, CoreRegionCompileRequestSerial, CompileCenter.X, CompileCenter.Y, CompileCenter.Z, MissingTiles.Num(), RetainedTiles.Num(), bAllowConcurrentGeneration ? 1 : 0, + PrefetchTiles.Num(), ActiveTileTaskCount.load(std::memory_order_relaxed), - ActiveTaskCount.load(std::memory_order_relaxed))); + ActiveGenerationWorkerCount.load(std::memory_order_relaxed), + ActiveTaskCount.load(std::memory_order_relaxed), FPlatformTLS::GetCurrentThreadId())); } ActiveTaskCount.fetch_add(1, std::memory_order_relaxed); UE::Tasks::Launch(TEXT("VoxelForgeCoreTileRegion"), - [this, State, MissingTiles, RetainedTiles, TaskEpoch, TaskDesiredEpoch, WorkerLimit, - bAllowConcurrentGeneration, CompileSerial]() + [this, State, MissingTiles, RetainedTiles, PrefetchTiles, CompileCenter, TaskEpoch, + TaskDesiredEpoch, RequestSerial, WorkerLimit, bAllowConcurrentGeneration, CompileSerial]() { struct FTaskGuard { @@ -11407,12 +11622,23 @@ void AVoxelWorld::StartPendingCoreRegionCompile() ~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_release); } } Guard{ActiveTaskCount}; + FCoreRegionCompileResult Result; + Result.RuntimeState = State; + Result.GenerationEpoch = TaskEpoch; + Result.DesiredEpoch = TaskDesiredEpoch; + Result.Center = CompileCenter; + Result.RequestSerial = RequestSerial; + Result.CompileSerial = CompileSerial; + Result.PrefetchTiles = PrefetchTiles; + Result.WorkerStartCycles = FPlatformTime::Cycles64(); + Result.WorkerThreadId = FPlatformTLS::GetCurrentThreadId(); + 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); + Result.bAborted = true; + Result.WorkerEndCycles = FPlatformTime::Cycles64(); + CoreRegionCompileResults.Enqueue(MoveTemp(Result)); return; } std::vector Missing; @@ -11456,38 +11682,14 @@ void AVoxelWorld::StartPendingCoreRegionCompile() 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); + Result.bSucceeded = bCompiled; + Result.RetainedTiles = RetainedTiles; + Result.Error = UTF8_TO_TCHAR(Error.c_str()); + Result.CompiledTileCount = static_cast(CompiledTileCount); + Result.WorkerSeconds = CompileStats.workerSeconds; + Result.CoreWallSeconds = CompileStats.wallSeconds; + Result.WorkerEndCycles = FPlatformTime::Cycles64(); + CoreRegionCompileResults.Enqueue(MoveTemp(Result)); }, UE::Tasks::ETaskPriority::BackgroundNormal); } @@ -12331,6 +12533,88 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw RecordSchedulerSkip(TEXT("core_not_ready")); return; // Core mode never falls through to the legacy generator. } + FCoreRegionCompileResult RegionResult; + while (CoreRegionCompileResults.Dequeue(RegionResult)) + { + if (RegionResult.RuntimeState.IsValid()) + { + RegionResult.RuntimeState->bRegionCompileInFlight.store( + false, std::memory_order_release); + } + if (RegionResult.bAborted + || RegionResult.RuntimeState != State + || RegionResult.GenerationEpoch != GenerationEpoch) + { + if (VoxelForgeStartupTrace::IsActive()) + { + VoxelForgeStartupTrace::RecordEvent(TEXT("core_region_compile_discard"), + FString::Printf( + TEXT("\"request_serial\":%d,\"compile_serial\":%d," + "\"result_epoch\":%u,\"current_epoch\":%u,\"aborted\":%d"), + RegionResult.RequestSerial, RegionResult.CompileSerial, + RegionResult.GenerationEpoch, GenerationEpoch, + RegionResult.bAborted ? 1 : 0)); + } + continue; + } + + State->RegionCompileWorkerStartCycles.store( + RegionResult.WorkerStartCycles, std::memory_order_release); + State->RegionCompileEndCycles.store( + RegionResult.WorkerEndCycles, std::memory_order_release); + State->RegionCompileTileCount.store( + RegionResult.CompiledTileCount, std::memory_order_release); + State->RegionCompileWorkerThreadId.store( + RegionResult.WorkerThreadId, std::memory_order_release); + State->RegionCompileWorkerSeconds.store( + RegionResult.WorkerSeconds, std::memory_order_release); + State->RegionCompileWallSeconds.store( + RegionResult.CoreWallSeconds, std::memory_order_release); + State->bRegionCompileSucceeded.store( + RegionResult.bSucceeded, std::memory_order_release); + ActiveCoreRegionCompileCenter = RegionResult.Center; + ActiveCoreRegionCompileRequestSerial = RegionResult.RequestSerial; + TSet CurrentDesiredSet; + for (const FVoxelTileKey& Key : DesiredSorted) CurrentDesiredSet.Add(Key); + ActiveCoreRegionWastedPrefetchTiles = 0; + for (const FVoxelTileKey& Key : RegionResult.PrefetchTiles) + { + if (!CurrentDesiredSet.Contains(Key) && !CoreRegionLookaheadTiles.Contains(Key)) + { + ++ActiveCoreRegionWastedPrefetchTiles; + } + } + if (RegionResult.bSucceeded) + { + { + FScopeLock RetainedLock(&State->RetainedTilesLock); + State->RetainedTiles.clear(); + for (const FVoxelTileKey& Key : RegionResult.RetainedTiles) + { + State->RetainedTiles.insert({ + {Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}); + } + } + 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"), + RegionResult.CompiledTileCount, RegionResult.RetainedTiles.Num(), + RegionResult.GenerationEpoch, RegionResult.DesiredEpoch, + RegionResult.CoreWallSeconds * 1000.0, RegionResult.WorkerSeconds, + RegionResult.CompileSerial); + } + else + { + State->bRegionCompileFailed.store(true, std::memory_order_release); + UE_LOG(LogTemp, Error, + TEXT("[VoxelForgeCoreRuntime] tile-region compile failed epoch=%u " + "desired_epoch=%u: %s"), + RegionResult.GenerationEpoch, RegionResult.DesiredEpoch, + *RegionResult.Error); + } + State->bRegionCompileFinished.store(true, std::memory_order_release); + } if (State->bRebuildDesired.exchange(false, std::memory_order_acq_rel)) { bForceDesiredRebuild = true; @@ -12345,9 +12629,17 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw State->RegionCompileStartCycles.load(std::memory_order_acquire); const uint64 CompileEndCycles = State->RegionCompileEndCycles.load(std::memory_order_acquire); + const uint64 CompileWorkerStartCycles = + State->RegionCompileWorkerStartCycles.load(std::memory_order_acquire); const double CompileElapsedMilliseconds = CompileEndCycles >= CompileStartCycles ? FPlatformTime::ToSeconds64(CompileEndCycles - CompileStartCycles) * 1000.0 : 0.0; + const double CompileQueueMilliseconds = CompileWorkerStartCycles >= CompileStartCycles + ? FPlatformTime::ToSeconds64(CompileWorkerStartCycles - CompileStartCycles) * 1000.0 + : 0.0; + const double CompileTaskMilliseconds = CompileEndCycles >= CompileWorkerStartCycles + ? FPlatformTime::ToSeconds64(CompileEndCycles - CompileWorkerStartCycles) * 1000.0 + : 0.0; const double GateElapsedMilliseconds = CoreRegionPendingSinceSeconds > 0.0 ? FMath::Max(0.0, FPlatformTime::Seconds() - CoreRegionPendingSinceSeconds) * 1000.0 : 0.0; @@ -12355,6 +12647,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw State->RegionCompileTileCount.load(std::memory_order_acquire); const double CompileWorkerSeconds = State->RegionCompileWorkerSeconds.load(std::memory_order_acquire); + const double CompileCoreWallSeconds = + State->RegionCompileWallSeconds.load(std::memory_order_acquire); + const uint32 CompileWorkerThreadId = + State->RegionCompileWorkerThreadId.load(std::memory_order_acquire); const int32 CompileSerial = State->RegionCompileSerial.load(std::memory_order_acquire); const uint64 IndependentSubmissions = CoreRegionIndependentSubmissions; @@ -12364,16 +12660,23 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw 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," + "\"compile_queue_ms\":%s,\"compile_task_ms\":%s," + "\"compile_core_wall_ms\":%s,\"compile_worker_s\":%s," + "\"compile_worker_thread_id\":%u,\"pending_gate_elapsed_ms\":%s," + "\"independent_submissions\":%llu,\"wasted_prefetch_tiles\":%d," "\"dependent_declines\":%llu,\"concurrent_admission\":%d"), CompileSerial, ActiveCoreRegionCompileRequestSerial, ActiveCoreRegionCompileCenter.X, ActiveCoreRegionCompileCenter.Y, ActiveCoreRegionCompileCenter.Z, bRegionCompileSucceeded ? 1 : 0, CompiledTileCount, *FString::SanitizeFloat(CompileElapsedMilliseconds), + *FString::SanitizeFloat(CompileQueueMilliseconds), + *FString::SanitizeFloat(CompileTaskMilliseconds), + *FString::SanitizeFloat(CompileCoreWallSeconds * 1000.0), *FString::SanitizeFloat(CompileWorkerSeconds), + CompileWorkerThreadId, *FString::SanitizeFloat(GateElapsedMilliseconds), static_cast(IndependentSubmissions), + ActiveCoreRegionWastedPrefetchTiles, static_cast(DependentDeclines), GVoxelForgeCoreRegionConcurrentAdmission != 0 ? 1 : 0)); } @@ -12388,8 +12691,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw { FScopeLock RetainedLock(&State->RetainedTilesLock); bRetainedSetMatchesDesired = - DesiredSorted.Num() == static_cast(State->RetainedTiles.size()); - for (const FVoxelTileKey& Key : DesiredSorted) + CoreRegionRetainedDesiredTiles.Num() + == static_cast(State->RetainedTiles.size()); + for (const FVoxelTileKey& Key : CoreRegionRetainedDesiredTiles) { const VoxelForgeCore::TileKey CoreKey{ {Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}; @@ -12431,7 +12735,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw { PendingCoreRegionTileSet.Add(Key); } - PendingCoreRetainedTiles = DesiredSorted; + PendingCoreRetainedTiles = CoreRegionRetainedDesiredTiles; PendingCoreRegionRequestCenter = DesiredEpochCenterChunk; bCoreRegionCompilePending = true; StartPendingCoreRegionCompile(); @@ -12584,6 +12888,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw && bWalkableRingReadyForReserve; int32 SchedulerSubmittedByLevel[9] = {}; int32 SchedulerLoadAttemptsByLevel[9] = {}; + int32 SchedulerRegionCompileDeclinesByLevel[9] = {}; + int32 SchedulerFarParentDeclinesByLevel[9] = {}; int32 SchedulerLevelCapReturns[9] = {}; int32 SchedulerLevelCapBreaks[9] = {}; int32 SchedulerCriticalFullCapBreaks = 0; @@ -12951,7 +13257,21 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw if(CoreState.IsValid() &&CoreState->bFarWorldRequired.load(std::memory_order_acquire) &&!CoreState->bFarWorldReady.load(std::memory_order_acquire)) + { + if (bTraceStreamingScheduler && Tile.Level >= 0 && Tile.Level < 9) + { + ++SchedulerFarParentDeclinesByLevel[Tile.Level]; + } return; + } + } + if (bTraceStreamingScheduler && bUseVoxelForgeCore && bCoreRegionCompilePending + && GVoxelForgeCoreRegionConcurrentAdmission != 0 + && (PendingCoreRegionTileSet.Contains(Tile) + || CoreRegionCompileTilesInFlight.Contains(Tile)) + && Tile.Level >= 0 && Tile.Level < 9) + { + ++SchedulerRegionCompileDeclinesByLevel[Tile.Level]; } if (Tile.Level >= 0 && Tile.Level < 9) { @@ -13661,10 +13981,37 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw const double BusySeconds = FPlatformTime::ToSeconds64(WorkerCyclesDelta); const double SchedulerUpdateMilliseconds = (SchedulerTraceNow - StreamingUpdateStartSeconds) * 1000.0; + int32 CriticalMissingTiles = 0; + for (const FVoxelTileKey& Tile : CriticalDesiredTiles) + { + if (!LoadedTiles.Contains(Tile) && !CollisionNotRequiredTiles.Contains(Tile)) + { + ++CriticalMissingTiles; + } + } + int32 CollisionGateMissingTiles = 0; + for (const FVoxelTileKey& Tile : CollisionGateDesiredTiles) + { + if (!LoadedTiles.Contains(Tile) && !CollisionNotRequiredTiles.Contains(Tile)) + { + ++CollisionGateMissingTiles; + } + } + const FIntVector DesiredCenterLag = DesiredEpochCenterChunk - CenterChunk; + const TSharedPtr TraceCoreState = + VoxelForgeCoreState; + const bool bFarWorldRequiredForTrace = TraceCoreState.IsValid() + && TraceCoreState->bFarWorldRequired.load(std::memory_order_relaxed); + const bool bFarWorldReadyForTrace = TraceCoreState.IsValid() + && TraceCoreState->bFarWorldReady.load(std::memory_order_relaxed); 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," + "\"pawn_collision_gate_engaged\":%d,\"critical_tiles_ready\":%d," + "\"critical_missing\":%d,\"collision_gate_missing\":%d," + "\"desired_center_lag\":[%d,%d,%d],\"desired_center_current\":%d," + "\"far_world_required\":%d,\"far_world_ready\":%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," @@ -13685,6 +14032,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw "\"level_candidate_scan_ms\":%s,\"passage_score_ms\":%s," "\"level_sort_ms\":%s,\"level_admit_ms\":%s," "\"desired_missing_by_lod\":%s,\"loaded_by_lod\":%s,\"pending_by_lod\":%s," + "\"region_compile_declines_by_lod\":%s," + "\"far_parent_declines_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," @@ -13695,6 +14044,11 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw static_cast(GFrameCounter), CenterChunk.X, CenterChunk.Y, CenterChunk.Z, bDesiredRebuiltThisUpdate ? 1 : 0, bSubmissionLoopRan ? 1 : 0, bAllChunksLoaded ? 1 : 0, bWalkableRingReadyForReserve ? 1 : 0, bPawnMoving ? 1 : 0, + bPawnCollisionGateEngaged ? 1 : 0, CriticalMissingTiles == 0 ? 1 : 0, + CriticalMissingTiles, CollisionGateMissingTiles, + DesiredCenterLag.X, DesiredCenterLag.Y, DesiredCenterLag.Z, + DesiredCenterLag == FIntVector::ZeroValue ? 1 : 0, + bFarWorldRequiredForTrace ? 1 : 0, bFarWorldReadyForTrace ? 1 : 0, bCoreRegionCompilePending ? 1 : 0, IsVoxelForgeCoreReady() ? 1 : 0, GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve, GVoxelForgeStreamingLegacyPendingResultCap, @@ -13721,7 +14075,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw *FString::SanitizeFloat(LevelSortMilliseconds), *FString::SanitizeFloat(LevelAdmissionMilliseconds), *IntArrayJson(DesiredMissingByLevel), *IntArrayJson(LoadedByLevel), - *IntArrayJson(PendingByLevel), *IntArrayJson(SchedulerSubmittedByLevel), + *IntArrayJson(PendingByLevel), *IntArrayJson(SchedulerRegionCompileDeclinesByLevel), + *IntArrayJson(SchedulerFarParentDeclinesByLevel), *IntArrayJson(SchedulerSubmittedByLevel), *IntArrayJson(SchedulerLoadAttemptsByLevel), *IntArrayJson(AdmissionCapByLevel), *IntArrayJson(DeclinedRuleByLevel), *IntArrayJson(SchedulerLevelCapReturns), *IntArrayJson(SchedulerLevelCapBreaks), FullCapDeclinedTiles, ReserveDeclinedTiles, diff --git a/Source/VoxelForge/Public/VoxelWorld.h b/Source/VoxelForge/Public/VoxelWorld.h index b9fa34c..5979f29 100644 --- a/Source/VoxelForge/Public/VoxelWorld.h +++ b/Source/VoxelForge/Public/VoxelWorld.h @@ -207,6 +207,28 @@ struct FChunkResult TArray SurfaceWitnesses; }; +/** Worker-to-game-thread completion for immutable Core tile-region publication. */ +struct FCoreRegionCompileResult +{ + TSharedPtr RuntimeState; + TArray RetainedTiles; + TArray PrefetchTiles; + FString Error; + FIntVector Center = FIntVector::ZeroValue; + uint32 GenerationEpoch = 0; + uint32 DesiredEpoch = 0; + uint64 WorkerStartCycles = 0; + uint64 WorkerEndCycles = 0; + uint32 WorkerThreadId = 0; + int32 CompileSerial = 0; + int32 RequestSerial = 0; + int32 CompiledTileCount = 0; + double WorkerSeconds = 0.0; + double CoreWallSeconds = 0.0; + bool bSucceeded = false; + bool bAborted = false; +}; + /** One worker-produced mesh identity emitted only by the explicit parity harness. */ struct FVoxelTileHashDumpRecord { @@ -798,6 +820,11 @@ private: TArray PendingCoreRegionTiles; TSet PendingCoreRegionTileSet; TArray PendingCoreRetainedTiles; + TArray CoreRegionRetainedDesiredTiles; + TArray CoreRegionLookaheadTiles; + FIntVector LastCoreRegionLookaheadCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32); + FIntVector LastCoreRegionLookaheadStep = FIntVector::ZeroValue; + uint32 LastCoreRegionLookaheadGenerationEpoch = 0; TSet CoreRegionCompileTilesInFlight; FIntVector LastCoreRegionRequestCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32); FIntVector PendingCoreRegionRequestCenter = FIntVector::ZeroValue; @@ -810,6 +837,7 @@ private: uint64 CoreRegionDependentDeclines = 0; int32 CoreRegionCompileRequestSerial = 0; int32 ActiveCoreRegionCompileRequestSerial = 0; + int32 ActiveCoreRegionWastedPrefetchTiles = 0; public: @@ -1139,6 +1167,7 @@ public: TQueue EditedProcessQueue; TQueue NearProcessQueue; TQueue ProcessQueue; + TQueue CoreRegionCompileResults; // The cap counts completed MPSC results plus admitted tile tasks, so a slow game-thread apply // cannot accumulate an unbounded payload queue while completed work never consumes worker slots. // Le plafond compte les resultats MPSC et les taches admises; un apply lent ne peut donc pas @@ -1362,6 +1391,9 @@ public: float HeadlessStreamingTestSpeedCmPerSecond = 0.0f; float HeadlessStreamingTestStartSeconds = 0.0f; float HeadlessStreamingTestMoveDurationSeconds = 0.0f; + float HeadlessStreamingTestTurnAfterSeconds = 0.0f; + bool bHeadlessStreamingTestTurnDone = false; + FVector HeadlessStreamingTestTurnDirection = FVector::YAxisVector; float HeadlessStreamingTestExitSeconds = 0.0f; float HeadlessStreamingTestTeleportAtSeconds = 0.0f; float HeadlessStreamingTestTeleportDistanceCm = 0.0f;