From e7586a12566fc8a1f00b6380a464f4c2f554b1a7 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 19 Oct 2026 10:56:54 +0200 Subject: [PATCH] wip(assembler): round S9 -- collision look-ahead: ground ring ready on arrival up to 40 m/s and 50 m/s falls voxel.StreamingCollisionLookahead (default on) predicts the support tile 350 ms ahead along full 3D velocity and prioritises its 9-tile collision ring. Owner plan, BuildHost actor: 40 m/s arrival-ready 25/74 -> 74/74 (CVar-off control 15/74); 6/12/25 m/s stay 100%; -Z 50 m/s 93/93. Spawn unchanged (~7.7 s). Critical LOD0 path at 40 m/s: generation 67 ms p50, RMC body update 38 ms p50. LOD8 cost counted (native horizon 48 ms of 60), optimisation deferred. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- ARCHITECTURE.md | 39 +++ CODEMAP.md | 12 + Source/VoxelForge/Private/VoxelWorld.cpp | 251 +++++++++++++----- Source/VoxelForge/Public/VoxelWorld.h | 4 + .../Private/VoxelForgeExploreCommandlet.cpp | 22 +- .../WorldAssemblerM3/Round13/REPORT.md | 226 ++++++++++++++++ Tools/TestVoxelForgeCoreWorldHost.ps1 | 23 +- 7 files changed, 507 insertions(+), 70 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Round13/REPORT.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 21e51be..f453b62 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1609,6 +1609,45 @@ The cap kept the observed backlog finite without reinstating the S6/S7 finished- decline. Full timings, per-center records, quality checks and hashes are in [`Standalone/Research/WorldAssemblerM3/Round12/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round12/REPORT.md). +### 8.30 Velocity-scaled support-ring lookahead — World Assembler S9 / M3 Round 13 + +The ring gate uses the support tile, which can sit one tile below the clipmap center on a surface +world. Both scheduler branches must use the same `GetPlayerSupportTile` result for the active 3x3 +collision-critical ring and its predicted rings. Predicting from the stream center can spend work on +the wrong vertical slice, so the trace includes both `center` and `walkable_center`. + +`voxel.StreamingCollisionLookahead` defaults to `1`; `0` disables only the prediction and is the +negative control. `GetStreamingHeadingStep` transforms velocity into actor space and selects its +dominant axis, including Z for a fall; when motion is zero it falls back to pawn forward. The +lookahead targets 350 ms of travel, rounds up to whole active LOD0 tiles, and clamps the lead to one +or two rings. The scheduler adds the future 3x3 keys only when they already belong to the desired +clipmap, then moves those collision keys into `CriticalDesiredTiles`. It does not reserve more worker +slots, expand the clipmap, cancel in-flight jobs, or change tile arithmetic. `collision_lookahead` +records speed, direction, stream/support centers, promoted-key count and missing desired keys. + +On the staged SurfaceWorld owner plan at 25 cm, the S9 `+Y` trace found the ring ready at all 74/74 +support-center arrivals through 40 m/s; the corrected-source CVar-off control found 15/74. The +S8 no-lookahead baseline was 25/74. At 50 m/s scripted downward motion, 93/93 support-center +arrivals were ready. The 40 m/s matched on/off initial-ready times were 7.743/7.778 s, so the +prediction did not slow spawn in this pair. These figures support 40 m/s as the highest horizontal +speed tested; they do not measure a higher horizontal ceiling. The vertical trace is a scripted +actor move through the owner plan, not a free-fall physics replay. + +For 108 critical LOD0 tile jobs with complete stage joins at 40 m/s, request-to-worker start was +0.02/9.59 ms p50/p95, generation 66.6/84.0 ms, result wait 9.3/17.7 ms, game-thread apply to mesh +submit 0.17/0.24 ms, RMC collision submit 0.01/0.02 ms, and RMC submit-to-body-update 37.6/85.9 ms. +The latter is the observable body-ready interval, not a Chaos-only CPU timer. The current trace saw +no scheduler skips, a 113-result peak under the 256 cap and 33 ms maximum region-page compile with +concurrent admission. Generation and body update dominate the measured tile path; the lookahead +gives those stages time to finish before the support center changes. LOD0-missing clipmap frames +remain separately visible in the Round 13 table; they are not equivalent to a missing support ring. + +The scheduler change does not alter field, mesh or collision output, so `WorldAssemblerPlanV1` and +the far-horizon recipe versions stay unchanged. Core-off legacy, M2 fixtures, SurfaceWorld and +FloatingIslands checks passed; the S3/S4 ground, cap and coverage audit passed. The full sweep, +negative control, hashes, stage ledger and the deferred LOD8 optimization plan are in +[`Standalone/Research/WorldAssemblerM3/Round13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round13/REPORT.md). + ## 9. Multiplayer model (listen-server first, dedicated-friendly) > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a diff --git a/CODEMAP.md b/CODEMAP.md index 163418a..13c5e3b 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -1041,6 +1041,18 @@ separate from the region-gate control. Round12 found 11/11 ready support centers immediately ready at 60 m/s with a 400 m teleport; see the traces under `Saved/BuildHost` and [`Standalone/Research/WorldAssemblerM3/Round12/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round12/REPORT.md). +### 3.16 Velocity-scaled support-ring lookahead — World Assembler S9 / M3 Round 13 + +`voxel.StreamingCollisionLookahead` defaults on; `0` is the negative control. The shared +`GetStreamingHeadingStep` uses actor-local motion, including vertical speed. The support tile from +`GetPlayerSupportTile` is the reference for both the active 3x3 critical ring and up to two future +3x3 rings. `GetStreamingCollisionLookaheadTiles` aims 350 ms ahead, rounded up to whole LOD0 tiles +and capped at two. Prediction only promotes keys already inside the desired clipmap. The +`collision_lookahead` trace records stream/support centers, direction, speed, ring count and any +missing desired keys. `VoxelForgeExploreCommandlet.cpp` and `Tools/TestVoxelForgeCoreWorldHost.ps1` +accept a movement vector so the same trace covers horizontal and vertical travel. See ARCHITECTURE +§8.30 and [`Standalone/Research/WorldAssemblerM3/Round13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round13/REPORT.md). + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 1a62acb..45bbb0e 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -197,6 +197,12 @@ namespace 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"), @@ -2056,6 +2062,7 @@ void AVoxelWorld::BeginPlay() int32 CommandLineSampleStacks = GVoxelForgeSampleStacks; int32 CommandLineCollisionGate = GVoxelForgeCollisionGate; int32 CommandLineCoreRegionConcurrentAdmission = GVoxelForgeCoreRegionConcurrentAdmission; + int32 CommandLineStreamingCollisionLookahead = GVoxelForgeStreamingCollisionLookahead; int32 CommandLineStreamingResultBacklogCap = GVoxelForgeStreamingResultBacklogCap; int32 CommandLineStep1GenerationCap = GVoxelForgeStep1GenerationCap; int32 CommandLineStep1ExitAfterFrames = GVoxelForgeStep1ExitAfterFrames; @@ -2134,6 +2141,12 @@ void AVoxelWorld::BeginPlay() 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)) { @@ -6896,8 +6909,8 @@ void AVoxelWorld::Tick(float DeltaTime) AdvanceHeadlessCollisionGateStressTest(PlayerLastPos, PlayerHeading, PlayerPawn); AdvanceHeadlessDigMotionTest(PlayerLastPos, PlayerPawn); PeakObservedPawnSpeedCmPerSecond = FMath::Max( - PeakObservedPawnSpeedCmPerSecond, PlayerHeading.Size2D()); - StartupTraceSpeedCmPerSecond = PlayerHeading.Size2D(); + PeakObservedPawnSpeedCmPerSecond, PlayerHeading.Size()); + StartupTraceSpeedCmPerSecond = PlayerHeading.Size(); UpdatePawnCollisionGate(PlayerPawn, PlayerLastPos, DeltaTime); RecordHeadlessDigMotionFrame(PlayerPawn, PlayerLastPos, DeltaTime); ObserveHeadlessStrateCrossingTest(PlayerLastPos, PlayerPawn); @@ -10253,6 +10266,52 @@ void AVoxelWorld::BuildDesiredTransitionIndex() } } +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) @@ -10279,6 +10338,12 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& {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; @@ -10357,6 +10422,49 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& 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)) @@ -10391,13 +10499,6 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& } CancelObsoleteTileWork(); - const FVoxelTileKey PlayerTile(GetTileCoordAtLocalCm(WorldToLocalCm(PlayerPosition)), 0); - FVoxelTileKey OccupiedTile = PlayerTile; - FVoxelTileKey SupportTile; - if (GetPlayerSupportTile(PlayerPawn, PlayerPosition, SupportTile)) - { - OccupiedTile = SupportTile; - } auto AddCriticalTile = [this](const FVoxelTileKey& Key) { if (IsDesired(Key) && !CriticalDesiredTiles.Contains(Key)) @@ -10415,22 +10516,7 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& } AddCriticalTile(OccupiedTile); - FVector Heading = GetActorTransform().InverseTransformVectorNoScale(PlayerHeading); - if (Heading.IsNearlyZero() && IsValid(PlayerPawn)) - { - Heading = GetActorTransform().InverseTransformVectorNoScale( - PlayerPawn->GetActorForwardVector()); - } - FIntVector HeadingStep = FIntVector::ZeroValue; - 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) - { - if (AbsX >= AbsY && AbsX >= AbsZ) HeadingStep.X = Heading.X >= 0.0f ? 1 : -1; - else if (AbsY >= AbsZ) HeadingStep.Y = Heading.Y >= 0.0f ? 1 : -1; - else HeadingStep.Z = Heading.Z >= 0.0f ? 1 : -1; - } + const FIntVector HeadingStep = LookaheadHeadingStep; if (HeadingStep != FIntVector::ZeroValue) { FVoxelTileKey NextTile = OccupiedTile; @@ -10443,9 +10529,19 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& for (int32 Dy = -1; Dy <= 1; ++Dy) for (int32 Dx = -1; Dx <= 1; ++Dx) { - const FVoxelTileKey WalkableKey(PlayerTile.Coord + FIntVector(Dx, Dy, 0), 0); + 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]); @@ -10710,6 +10806,11 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla ++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; @@ -10791,6 +10892,49 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla 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; @@ -10860,15 +11004,6 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla // 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. - const FVoxelTileKey PlayerTile( - WorldToChunkCoord(WorldToLocalCm(PlayerPosition)), 0); - FVoxelTileKey OccupiedTile = PlayerTile; - FVoxelTileKey SupportTile; - if (GetPlayerSupportTile(PlayerPawn, PlayerPosition, SupportTile)) - { - OccupiedTile = SupportTile; - } - auto AddCriticalTile = [this](const FVoxelTileKey& Key) { if (IsDesired(Key) && !CriticalDesiredTiles.Contains(Key)) @@ -10889,31 +11024,7 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla } AddCriticalTile(OccupiedTile); - FVector Heading = PlayerHeading; - if (Heading.IsNearlyZero() && IsValid(PlayerPawn)) - { - Heading = PlayerPawn->GetActorForwardVector(); - } - Heading = GetActorTransform().InverseTransformVectorNoScale(Heading); - const float AbsX = FMath::Abs(Heading.X); - const float AbsY = FMath::Abs(Heading.Y); - const float AbsZ = FMath::Abs(Heading.Z); - FIntVector HeadingStep = FIntVector::ZeroValue; - if (AbsX > KINDA_SMALL_NUMBER || AbsY > KINDA_SMALL_NUMBER || AbsZ > KINDA_SMALL_NUMBER) - { - if (AbsX >= AbsY && AbsX >= AbsZ) - { - HeadingStep.X = Heading.X >= 0.0f ? 1 : -1; - } - else if (AbsY >= AbsZ) - { - HeadingStep.Y = Heading.Y >= 0.0f ? 1 : -1; - } - else - { - HeadingStep.Z = Heading.Z >= 0.0f ? 1 : -1; - } - } + const FIntVector HeadingStep = LookaheadHeadingStep; if (HeadingStep != FIntVector::ZeroValue) { FVoxelTileKey NextTile = OccupiedTile; @@ -10926,9 +11037,19 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla for (int32 Dy = -1; Dy <= 1; ++Dy) for (int32 Dx = -1; Dx <= 1; ++Dx) { - const FVoxelTileKey WalkableKey(PlayerTile.Coord + FIntVector(Dx, Dy, 0), 0); + 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. @@ -11378,7 +11499,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw ? LastStreamingSchedulerSampleSeconds : StreamingUpdateStartSeconds; const double IntervalSeconds = FMath::Max(0.0, Now - IntervalStart); const double BusySeconds = FPlatformTime::ToSeconds64(WorkerCyclesDelta); - const double SpeedCmPerSecond = PlayerHeading.Size2D(); + 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," @@ -11589,7 +11710,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw bGateCenterChanged ? WalkableCenter.Y - PreviousGateCenter.Y : 0, bGateCenterChanged ? WalkableCenter.Z - PreviousGateCenter.Z : 0, bGateCenterChanged ? 1 : 0, bWalkableRingReadyForReserve ? 1 : 0, - WalkableRingReadyCount, *FString::SanitizeFloat(PlayerHeading.Size2D()), DesiredEpoch)); + WalkableRingReadyCount, *FString::SanitizeFloat(PlayerHeading.Size()), DesiredEpoch)); bHasLastWalkableGateSample = true; LastWalkableGateCenter = WalkableCenter; bLastWalkableGateReady = bWalkableRingReadyForReserve; @@ -11600,7 +11721,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw RecordSchedulerSkip(TEXT("core_region_compile_pending")); return; } - const bool bPawnMoving = PlayerHeading.Size2D() >= 50.0f; + const bool bPawnMoving = PlayerHeading.Size() >= 50.0f; EffectiveNearTaskReserve = GVoxelForgeStreamingDynamicNearReserve == 0 ? AuthoredNearTaskReserve : ((bPawnMoving || !bWalkableRingReadyForReserve) ? AuthoredNearTaskReserve : 0); @@ -12219,7 +12340,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw CenterChunk.X, CenterChunk.Y, CenterChunk.Z, *FString::SanitizeFloat(LocalPawn.X), *FString::SanitizeFloat(LocalPawn.Y), *FString::SanitizeFloat(LocalPawn.Z), - *FString::SanitizeFloat(PlayerHeading.Size2D()), + *FString::SanitizeFloat(PlayerHeading.Size()), bWalkableRingReadyForReserve ? 1 : 0, *DesiredCounts, *ResidentCounts, *PendingCounts, GetActiveGenerationTaskCount(), GetGenerationAdmissionCount(), MaxTasks, NearTaskReserve, diff --git a/Source/VoxelForge/Public/VoxelWorld.h b/Source/VoxelForge/Public/VoxelWorld.h index 744c4d9..2b5cfe9 100644 --- a/Source/VoxelForge/Public/VoxelWorld.h +++ b/Source/VoxelForge/Public/VoxelWorld.h @@ -702,6 +702,10 @@ private: void BuildCoreDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition, APawn* PlayerPawn, const FVector& PlayerHeading, TArray& OutLeavers); + FIntVector GetStreamingHeadingStep(const FVector& PlayerHeading, + const APawn* PlayerPawn) const; + int32 GetStreamingCollisionLookaheadTiles(const FVector& PlayerHeading, + const FIntVector& HeadingStep) const; void StartPendingCoreRegionCompile(); void LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority WorkPriority); void EnqueueStreamingResult(FChunkResult&& Result); diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index 2fae682..63e0d5f 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -17490,6 +17490,24 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin float WalkSpeedCmPerSecond = 600.0f; FParse::Value(FCommandLine::Get(), TEXT("voxel.TestMoveSpeedCmPerSecond="), WalkSpeedCmPerSecond); + FString WalkDirectionText(TEXT("0,1,0")); + const FString DirectionKey(TEXT("voxel.TestMoveDirection=")); + const FString CommandLineText(FCommandLine::Get()); + const int32 DirectionKeyIndex = CommandLineText.Find( + DirectionKey, ESearchCase::IgnoreCase, ESearchDir::FromStart); + if (DirectionKeyIndex != INDEX_NONE) + { + WalkDirectionText = CommandLineText.Mid(DirectionKeyIndex + DirectionKey.Len()); + const int32 DirectionEnd = WalkDirectionText.Find( + TEXT(" "), ESearchCase::CaseSensitive, ESearchDir::FromStart); + if (DirectionEnd != INDEX_NONE) + { + WalkDirectionText.LeftInline(DirectionEnd, EAllowShrinking::No); + } + WalkDirectionText.TrimStartAndEndInline(); + WalkDirectionText.RemoveFromStart(TEXT("\"")); + WalkDirectionText.RemoveFromEnd(TEXT("\"")); + } TArray SpawnTickMilliseconds; TArray WalkTickMilliseconds; TArray> TickSamples; @@ -17569,11 +17587,11 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin return false; } UE_LOG(LogTemp, Display, - TEXT("[VoxelForgeCoreWorldStreamTrace] pass complete=1 recipe=%s voxel_cm=%.3f walk_speed_cm_s=%.0f walk_direction=+Y"), + TEXT("[VoxelForgeCoreWorldStreamTrace] pass complete=1 recipe=%s voxel_cm=%.3f walk_speed_cm_s=%.0f walk_direction=%s"), Arguments.CoreHybridMesherVersion == 2 ? TEXT("WorldAssemblerHybridV2") : (Arguments.CoreHybridMesherVersion == 1 ? TEXT("WorldAssemblerHybridV1") : TEXT("MarchingCubesV1")), - Arguments.CoreVoxelSizeCm, WalkSpeedCmPerSecond); + Arguments.CoreVoxelSizeCm, WalkSpeedCmPerSecond, *WalkDirectionText); return true; } diff --git a/Standalone/Research/WorldAssemblerM3/Round13/REPORT.md b/Standalone/Research/WorldAssemblerM3/Round13/REPORT.md new file mode 100644 index 0000000..79573b9 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round13/REPORT.md @@ -0,0 +1,226 @@ +# World assembler S9 / M3 Round 13 — support-ring lookahead + +**Result:** the support ring is ready at all tested center arrivals through 40 m/s horizontally, +and through a scripted 50 m/s vertical descent. At 40 m/s the matched CVar-off control reached +the ring at only 15/74 arrivals; lookahead reached it at 74/74. Initial readiness was 7.743 s with +lookahead and 7.778 s without it. I did not reserve more workers, and I left the LOD8 geometry +unchanged; its counted optimization plan is in §5. + +The lookahead is default-on through `voxel.StreamingCollisionLookahead=1`; `0` disables prediction. +It follows the support tile, predicts 350 ms ahead (rounded up to one or two 8 m LOD0 tiles at the +25 cm owner scale), and promotes only rings already inside the desired clipmap. I first predicted +from the stream center. The trace showed that this was one tile above the walkable center on this +SurfaceWorld plan. I corrected both scheduler branches to predict and prioritize from +`GetPlayerSupportTile`. No field or mesh arithmetic changed, so the recipe versions remain as-is. + +## 1. Speed sweep + +Fresh BuildHost commandlets used the staged owner SurfaceWorld plan, seed 0, 25 cm Core scale, +Legacy streaming order, Dynamic near reserve, apply boost 0, region concurrent admission 1, result +backlog cap 256, Normal process priority, a 12-action host limit, and movement from 30 to 45 s. +The S8 traces are the pre-lookahead baseline. Arrival readiness is the `walkable_gate.ready` value +on the support-center-change event. Lag is the maximum time to a later 9/9 sample before the next +support-center change; unresolved centers are counted separately. LOD0 missing is the number of +`scheduler_admission` samples with any desired LOD0 key missing during 30–45 s. + +| Motion | S8 arrival-ready | S8 worst resolved lag / unresolved | S8 LOD0-missing frames | S9 arrival-ready | S9 worst lag / unresolved | S9 LOD0-missing frames | +|---|---:|---:|---:|---:|---:|---:| +| +Y 6 m/s | 11/11 (100%) | 0 ms / 0 | 168/838 (20.0%) | 11/11 (100%) | 0 ms / 0 | 165/839 (19.7%) | +| +Y 12 m/s | 22/22 (100%) | 0 ms / 0 | 431/776 (55.5%) | 22/22 (100%) | 0 ms / 0 | 436/775 (56.3%) | +| +Y 25 m/s | 46/46 (100%) | 0 ms / 0 | 625/677 (92.3%) | 46/46 (100%) | 0 ms / 0 | 627/679 (92.3%) | +| +Y 40 m/s | 25/74 (33.8%) | 191.1 ms / 8 | 579/595 (97.3%) | 74/74 (100%) | 0 ms / 0 | 576/593 (97.1%) | +| −Z 50 m/s | — | — | — | 93/93 (100%) | 0 ms / 0 | 605/626 (96.6%) | + +The matched 40 m/s CVar-negative control on the corrected source reached 15/74 centers (20.3%), +with 196.3 ms worst resolved lag, 7 unresolved centers, and 574/590 LOD0-missing samples (97.3%). +This isolates prediction from the support-center correction. The old S8 60 m/s + 400 m teleport +remains a separate discontinuity case: 8/113 centers were ready at arrival, 3 more became ready +before the next center, and 102/113 never reached 9/9. + +At 40 m/s the current run therefore supports **40 m/s as the highest tested horizontal speed**; +it does not establish a higher horizontal ceiling. The 50 m/s trace used the harness's scripted +downward actor movement through the staged owner plan, not a Chaos free-fall simulation in a shaft. +The ring remained ready through all 93 center changes. + +Spawn did not regress in the matched 40 m/s pair: `initial_ready_s` was 7.743349 s on and 7.777605 s +off, with spawn-tick p95 2.288 ms and 2.292 ms respectively. The S9 on-run initial-ready values +at 6/12/25/40 m/s were 7.813133/7.815265/7.821660/7.743349 s. Movement begins at 30 s, and the +lookahead adds no stationary-spawn requests. + +## 2. Critical path at 40 m/s + +For 108 traced critical LOD0 jobs with complete tile-event joins, p50/p95 were: + +| Stage | p50 | p95 | +|---|---:|---:| +| Request → worker start | 0.02 ms | 9.59 ms | +| Worker generation | 66.6 ms | 84.0 ms | +| Result queue → game-thread apply | 9.3 ms | 17.7 ms | +| Game-thread apply → mesh submit | 0.17 ms | 0.24 ms | +| Mesh → collision submit | 0.01 ms | 0.02 ms | +| RMC submit → body-update event | 37.6 ms | 85.9 ms | + +The body-update interval is the observable collision-ready delay; it is not an isolated Chaos CPU +cook timer. Generation and body update dominate. Request-to-worker delay is small at the median, +and game-thread apply/submission is below a quarter millisecond at p95. The lookahead gives the +generation and body-update stages time to finish before support-center arrival. + +At 40 m/s the trace reported two lookahead tiles and three newly collision-promoted keys per +scheduler rebuild, with zero desired keys outside the clipmap. Worker occupancy hit the 10-task +limit, while the result queue peaked at 113 entries / 16.8 MB under the 256 cap and recorded no cap +stops. Region compile time was 1.74 ms p50, 22.16 ms p95, 33.32 ms max; concurrent admission +recorded no global scheduler skips. Those systems still consume time, but the matched CVar test +shows prediction is sufficient at this plan's 40 m/s horizontal limit without adding reserved +slots. + +## 3. Output quality and determinism + +- BuildHost Core-off legacy passed at 12.5 cm. M2 HybridV1 and HybridV2 each passed five analytic + fixtures plus border checks at 25 cm. +- SurfaceWorld passed at 25 cm with collision, anchor movement, carve and travel checks. Its + W1/W8/W16 actor hash matrix was stable: density `0cd2a1a4152ee4aa`, mesh `80a5b054b904b1a2`, + payload `bf2f1b40dff38acc`, topology `56074198682d5e18`. Peak commandlet private memory was + 3.90 GB. +- FloatingIslands passed at 25 cm with fixed water, edits, collision and LOD0/1 coverage; the + BuildHost actor matrix reported W1/W8/W16. Peak private memory was 3.25 GB. +- The current Standalone W1/W8/W12 full-clipmap and per-LOD 0–8 density, mesh, payload and topology + hashes match. Full-clipmap hash tuple: density `c10697a8f48380c7`, mesh `430d2be2bdd8b988`, + payload `b7b33e14092225c9`, topology `c5c46d66606a819e`. Full-detail hashes also match. +- The Standalone S4 audit passed: 385/385 far-cap bands, 128 agreement points (maximum 0.0504 cm + under the 2 cm gate), the 100 cm offset negative control, and 2,520/2,520 cap-coverage rays. + The current BuildHost SurfaceWorld S3 check passed 128 points with 0.0427 cm maximum far/near + error and detected its 100 cm offset control. +- A sampled Standalone W12 benchmark process peaked at 0.68 GB private memory. + +I did **not** run Standalone `--workers=16`: that implementation constructs 16 worker threads, +which conflicts with the explicit 12-thread launch cap. Current Standalone W1/W8/W12 match, and +the BuildHost SurfaceWorld and FloatingIslands W1/W8/W16 matrices passed. I requested whether the +owner wants a one-run W16 exception; until then the 12-thread cap stands. No new `UECC-*` folder +was present; the only folder was the existing 2026-09-17 crash. + +No geometry, field, density or recipe output changed. The far-horizon recipe stays at v2 and the +SurfaceWorld plan identity stays unchanged. Water remains fixed for the season; player carve/fill +remains unrestricted. + +## 4. Goal 2 — counted LOD8 plan, optimization deferred + +The S8 cold owner trace measured 306 LOD8 jobs at 18.798 worker-seconds total (61.4 ms average, +79.8 ms p95, 94.7 ms max), 45.6% of worker time. Its full set reached readiness in about 7.7 s. +The current 25 cm BuildHost feature-cost probe warmed eight LOD8 keys and reported p95 per-tile +diagnostics: full/candidate feature path 59.508/59.585 ms, column computation 30.771 ms, marching +cubes 6.018 ms, native ground+cap horizon refinement 48.205 ms, classification 0.0046 ms, feature +list 0.0007 ms, and shell work 0.0001 ms (repair skipped). The profile counters are overlapping +inclusive measurements, so they must not be summed. The same probe reported 15,529 field calls, +9,679 column-cache misses, 8,192 refined ground triangles, 531 ground triangles removed, and +6,732 cap triangles at the sampled high-cost LOD8 tile. + +`HeightfieldColumnCache` is a 65,536-entry thread-local cache keyed by global XY. It reuses columns +within a worker, but does not share cache entries across workers. The current probe does not count +cross-tile duplicate samples by worker, and does not split ground-refinement time from cap-refinement +time. The next measurement should add exclusive counters for column evaluation, MC, ground refine, +cap refine, classification, and cache hits/misses per tile and worker, then trace duplicate global +columns across neighboring LOD8 jobs. + +No LOD8 optimization or far-mesh change was made this round, so no before/after far-horizon/cap +captures were produced and the below-7.7 s Goal 2 target remains open. The counted next step is to +prototype shared immutable heightfield-column pages for planar families, compare against sampling +at displayed resolution and tolerance-based refinement skips, then rerun S3 ground/S4 cap gates +(2 cm agreement, 100% coverage) and matched viewer captures at VSync/60 fps, closing the viewer +right after each capture. Do not relax tolerances without a metre-scale error bound. + +## 5. Builds, commands, binaries and run record + +BuildHost and Standalone builds passed on their first attempt. Standalone build printed the existing +`vswhere.exe` lookup warning and C4838 conversions in `VoxelStrateManager.cpp:3974–3975`, then +built both targets successfully. All commandlets were fresh, serial, Normal priority and below +8 GB; the largest sampled BuildHost private memory was 3.90 GB. No process exceeded the 15-minute +step limit. + +Build commands: + +```powershell +.\Tools\BuildVoxelForgeCoreHost.ps1 -MaxParallelActions 12 -TimeoutSeconds 900 +.\Standalone\Build.ps1 -Target All -CompileWorkers 12 -BuildPriority Normal +``` + +The final horizontal trace command (repeat with speeds `600`, `1200`, `2500`, and `4000` cm/s): + +```powershell +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StartupApplyBoost 0 -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -StreamingCollisionLookahead 1 -TestMoveDirection '0,1,0' -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 600 -TimeoutSeconds 900 +``` + +The pre-S9 baseline used the same staged plan and settings with the S8 binary, before the lookahead +CVar existed; repeat with the same four speed values: + +```powershell +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StartupApplyBoost 0 -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 600 -TimeoutSeconds 900 +``` + +Vertical descent and the matched 40 m/s CVar-negative control: + +```powershell +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StartupApplyBoost 0 -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -StreamingCollisionLookahead 1 -TestMoveDirection '0,0,-1' -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 5000 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StartupApplyBoost 0 -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -StreamingCollisionLookahead 0 -TestMoveDirection '0,1,0' -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 4000 -TimeoutSeconds 900 +``` + +Quality commands: + +```powershell +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -CoreOffTest -CoreVoxelSizeCm 12.5 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -HybridFixturesOnly -CoreMesherRecipe WorldAssemblerHybridV1 -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -HybridFixturesOnly -CoreMesherRecipe WorldAssemblerHybridV2 -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -FloatingIslandsOnly -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +.\Standalone\bin\VoxelForgeCoreStandalone.exe --world-assembler-audit --workers=12 --clip-radius=3 --max-clip-level=8 --camera=0,-35,-487.94,0,-22 --process-priority=Normal --process-affinity=4095 --frame-limit=60 +``` + +Standalone determinism command template (fresh process, `--workers=1`, `8`, and `12`; `16` was +not launched under the thread cap): + +```powershell +.\Standalone\bin\VoxelForgeCoreStandalone.exe --workers=1 --world-assembler-prototype --stream-benchmark --stream-hash --force-generate --clip-radius=3 --max-clip-level=8 --camera=0,-35,-487.94,0,-22 --process-priority=Normal --process-affinity=4095 --frame-limit=60 +``` + +One initial CVar-off command was rejected by PowerShell before launch because the movement vector +was not quoted (`Cannot convert value to type System.String`). I retried with +`-TestMoveDirection '0,1,0'`; the BuildHost run passed. No Unreal process launched for the rejected +attempt. No other test/build attempt failed or retried. + +Binary SHA-256: + +| Binary | SHA-256 | +|---|---| +| UE 5.7 `UnrealEditor-Cmd.exe` | `ECCE9996191F7F7E69B3D104EECDD0E678AE69176FDCCD79828C3E75A0654D6D` | +| S8 baseline BuildHost `UnrealEditor-VoxelForge.dll` | `8B82EE6C6FBB511AE766EB3D8646A7E9491952F41445CB62D49D71D0B9783418` | +| S8 baseline BuildHost `UnrealEditor-VoxelForgeEditor.dll` | `B3BC5E61172C5B21D928A5ACB5F22F8D9E00179B481B675804838DE9A620010D` | +| BuildHost `UnrealEditor-VoxelForge.dll` | `F542FC1F014190798485CC088470FD498D368D2E369639EAD32A0EEE8FDB1193` | +| BuildHost `UnrealEditor-VoxelForgeCore.dll` | `0A1EB7BB27C3C74A4AA0996E0A24E74224D7B8FFFFF113B1071FA42E18E8375A` | +| BuildHost `UnrealEditor-VoxelForgeEditor.dll` | `15B6460FD4E48D14CBE751C99447F06B62B44CF35FFD440AED106585AECEDC79` | +| `Standalone/bin/VoxelForgeCoreStandalone.exe` | `FD4CD39CD49BB0E8429250295BD16FACA0A6FBE412240522AB79C086245978C0` | + +Trace JSONL and actor stdout remain under +`Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/`. The initial +lookahead results are retained in `Saved/BuildHost/Round13/BeforeSupportTileCorrection/`; Standalone +hash logs and the S4 audit log are under `Saved/BuildHost/Round13/`. No file over 300 KB was added +outside `Saved/BuildHost/`. No `.uproject`, editor, `.uasset`, RealtimeMeshComponent source, commit, +push or stash was touched. + +Modified source files: + +- `Source/VoxelForge/Private/VoxelWorld.cpp` — CVar, support-centered lookahead and critical ring ordering; BuildHost `VoxelForge` runtime compile. +- `Source/VoxelForge/Public/VoxelWorld.h` — private helper declarations; same runtime compile. +- `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp` — movement-vector harness; BuildHost `VoxelForgeEditor` compile. +- `Tools/TestVoxelForgeCoreWorldHost.ps1` — movement-vector and CVar arguments. + +New source files: none. Standalone source files were not changed. Documentation updated: +`CODEMAP.md` §3.16 and `ARCHITECTURE.md` §8.30. + +Async-safety diff review: `Epoch` propagation/checks are untouched; all result queues remain +`EQueueMode::Mpsc`; `EndPlay` still drains after active tasks finish; workers still check +`bShuttingDown`; `ModsLock` and immutable region-page lifetime are unchanged; UObject and RMC +mutation remain on the game thread. + +### Dialogue + +**User:** Continue S9 from the 6 and 12 m/s lookahead traces after the service interruption. +**Luna:** Resumed at the 25 m/s run, found and fixed the support/stream-center mismatch, then measured the corrected speed sweep and matched 40 m/s CVar control. diff --git a/Tools/TestVoxelForgeCoreWorldHost.ps1 b/Tools/TestVoxelForgeCoreWorldHost.ps1 index 9f44665..8da0934 100644 --- a/Tools/TestVoxelForgeCoreWorldHost.ps1 +++ b/Tools/TestVoxelForgeCoreWorldHost.ps1 @@ -9,6 +9,8 @@ param( [ValidateRange(0, 1)][int]$StartupApplyBoost = 0, [switch]$StreamingTraceOnly, [ValidateRange(1, 6000)][int]$TestMoveSpeedCmPerSecond = 600, + [ValidateRange(0, 1)][int]$StreamingCollisionLookahead = 1, + [string]$TestMoveDirection = '0,1,0', [ValidateRange(0, 1)][int]$CoreRegionConcurrentAdmission = 1, [ValidateRange(0, 4096)][int]$StreamingResultBacklogCap = 256, [ValidateRange(0, 900)][int]$Step1MaxMeshApplies = 0, @@ -79,7 +81,18 @@ if ($StreamingTraceOnly -and ($SharedFeatureAuditOnly -or $EnableHybridFixturePl $HybridCliffOnly -or $HybridFixturesOnly -or $WorldAssemblyManifestPath)) { throw '-StreamingTraceOnly uses the staged owner plan without additional fixture or manifest overrides.' } +if ($TestMoveDirection -notmatch '^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+),[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+),[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$') { + throw '-TestMoveDirection must be three comma-separated invariant-culture numbers without spaces.' +} +$MoveDirectionComponents = @($TestMoveDirection.Split(',') | ForEach-Object { + [double]::Parse($_, [Globalization.CultureInfo]::InvariantCulture) +}) +if ($MoveDirectionComponents[0] -eq 0 -and $MoveDirectionComponents[1] -eq 0 -and + $MoveDirectionComponents[2] -eq 0) { + throw '-TestMoveDirection must not be the zero vector.' +} if (-not $StreamingTraceOnly -and ($TestMoveSpeedCmPerSecond -ne 600 -or + $StreamingCollisionLookahead -ne 1 -or $TestMoveDirection -ne '0,1,0' -or $CoreRegionConcurrentAdmission -ne 1 -or $StreamingResultBacklogCap -ne 256 -or $Step1MaxMeshApplies -ne 0 -or $TestTeleportAfterSeconds -ne 0 -or $TestTeleportDistanceCm -ne 0)) { @@ -169,7 +182,8 @@ $RunName += switch ($NearReserveMode) { if ($EnforceCollisionGate) { $RunName += '_collision_gate' } if ($StartupTrace -or $StreamingTraceOnly) { $RunName += '_trace' } if ($StreamingTraceOnly) { - $RunName += "_move_${TestMoveSpeedCmPerSecond}_region_${CoreRegionConcurrentAdmission}_backlog_${StreamingResultBacklogCap}" + $DirectionTag = $TestMoveDirection.Replace(',', '_').Replace('-', 'm').Replace('+', 'p').Replace('.', 'p') + $RunName += "_move_${TestMoveSpeedCmPerSecond}_lookahead_${StreamingCollisionLookahead}_dir_${DirectionTag}_region_${CoreRegionConcurrentAdmission}_backlog_${StreamingResultBacklogCap}" if ($Step1MaxMeshApplies -gt 0) { $RunName += "_applymax_$Step1MaxMeshApplies" } if ($TestTeleportAfterSeconds -gt 0) { $RunName += "_teleport_${TestTeleportAfterSeconds}s_${TestTeleportDistanceCm}cm" @@ -310,7 +324,8 @@ if ($StreamingTraceOnly) { $BaseArguments += '-voxel.TestMove=1' $BaseArguments += "-voxel.TestMoveSpeedCmPerSecond=$TestMoveSpeedCmPerSecond" $BaseArguments += '-voxel.TestMoveStartSeconds=30' - $BaseArguments += '-voxel.TestMoveDirection=0,1,0' + $BaseArguments += "-voxel.TestMoveDirection=$TestMoveDirection" + $BaseArguments += "-voxel.StreamingCollisionLookahead=$StreamingCollisionLookahead" if ($TestTeleportAfterSeconds -gt 0) { $BaseArguments += "-voxel.TestTeleportAfterSeconds=$TestTeleportAfterSeconds" $BaseArguments += "-voxel.TestTeleportDistanceCm=$TestTeleportDistanceCm" @@ -447,8 +462,10 @@ if ($SharedFeatureAuditOnly) { return } if ($StreamingTraceOnly) { + $ExpectedWalkDirection = "walk_direction=$TestMoveDirection" if ($Text -notmatch '\[VoxelForgeCoreWorldStreamTrace\] initial_ready=1 .*walk_tick_ms_p95=.*walk_tick_ms_max=' -or - $Text -notmatch ("\[VoxelForgeCoreWorldStreamTrace\] pass complete=1 .*walk_speed_cm_s=$TestMoveSpeedCmPerSecond walk_direction=\+Y") -or + $Text -notmatch ("\[VoxelForgeCoreWorldStreamTrace\] pass complete=1 .*walk_speed_cm_s=$TestMoveSpeedCmPerSecond") -or + -not $Text.Contains($ExpectedWalkDirection) -or $Text -notmatch '\[VoxelForgeCoreWorldTest\] pass complete=1') { throw "BuildHost actor-only stream trace did not prove initial readiness and a completed walk sample. See $FinalLog." }