From 786ecc4cdfaaa4a41dbe9ade422e28fc3fa0e08e Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 2 Nov 2026 16:45:20 +0100 Subject: [PATCH] streaming: bounded early admission - partial level sort, apply preflight, sorted transition index (L169, opt-in) With StreamingFastTravelWaitForAllCriticalTiles=0 and ApplyBudget=1 ms (test values; defaults stay 1 and 0), 7 pairs vs ca0bf76: near 43.3 -> 56.1%, forward 15.5 -> 17.9%, far holes 1.8 -> 0.0%, p99 7.06 -> 6.52 ms, worst 12.2 -> 11.8 ms, every run p99 <= 7.4 and worst <= 13.8 ms; arrival 1.671 -> 1.683 s (slower in 7/7) so not promoted. Source/: VoxelWorld.cpp, VoxelWorld.h. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- Source/VoxelForge/Private/VoxelWorld.cpp | 181 ++++++- Source/VoxelForge/Public/VoxelWorld.h | 2 +- .../WorldAssemblerM3/Linux169/REPORT.md | 54 +++ .../Linux169/RunL169Movement.py | 442 ++++++++++++++++++ 4 files changed, 664 insertions(+), 15 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux169/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux169/RunL169Movement.py diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 86138cc..8bf7021 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -7191,7 +7191,7 @@ void AVoxelWorld::LogReadyTransitionVisibilityMeasure() void AVoxelWorld::Tick(float DeltaTime) { const bool bTraceFrameTiming = VoxelForgeStartupTrace::IsActive(); - const uint64 TickStartCycles = bTraceFrameTiming ? FPlatformTime::Cycles64() : 0; + const uint64 TickStartCycles = FPlatformTime::Cycles64(); uint64 CollisionGateCycles = 0; uint64 UpdateChunksCycles = 0; uint64 PendingDrainCycles = 0; @@ -7418,7 +7418,7 @@ void AVoxelWorld::Tick(float DeltaTime) } const uint64 PendingDrainStartCycles = bTraceFrameTiming ? FPlatformTime::Cycles64() : 0; - ProcessPendingChunks(); + ProcessPendingChunks(TickStartCycles); if (bTraceFrameTiming) { PendingDrainCycles += FPlatformTime::Cycles64() - PendingDrainStartCycles; @@ -9899,7 +9899,7 @@ void AVoxelWorld::HandleTileCollisionCookComplete( } } -void AVoxelWorld::ProcessPendingChunks() +void AVoxelWorld::ProcessPendingChunks(uint64 TickStartCycles) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ProcessPending); // This runs on the game thread, called from Tick. @@ -10105,6 +10105,7 @@ void AVoxelWorld::ProcessPendingChunks() GetStreamingResultBacklogCount(), GVoxelForgeStreamingResultBacklogCap)); }; FChunkResult DequeuedChunk; + TArray DeferredApplyResults; const auto DequeueNextResult = [&]() -> bool { bDequeuedSupplementalFar = false; @@ -10234,8 +10235,6 @@ void AVoxelWorld::ProcessPendingChunks() 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. @@ -10244,6 +10243,8 @@ void AVoxelWorld::ProcessPendingChunks() // if the tile leaves and re-enters the desired set before the queue is drained. if (DequeuedChunk.bAborted || bRequestObsolete) { + PendingTileCancellation.Remove(DequeuedChunk.Tile); + PendingTiles.Remove(DequeuedChunk.Tile); if (VoxelForgeStartupTrace::IsActive()) { VoxelForgeStartupTrace::RecordEvent(TEXT("tile_discard"), FString::Printf( @@ -10264,6 +10265,55 @@ void AVoxelWorld::ProcessPendingChunks() continue; } + // Estimate visible mesh submission cost before entering RMC's atomic CreateSectionGroup + // call. If this result would consume the remaining 11 ms tick target plus a 0.5 ms tail + // reserve, hold it for a later frame. The prediction is calibrated from L169 tile sizes: + // 2.2 ms fixed headroom + per-triangle and per-vertex work. Keep PendingTiles and its + // cancellation token alive while queued so the scheduler cannot submit a duplicate. + const bool bPreflightVisibleMesh = bFastTravelApplyBudgetActive + && DequeuedChunk.Epoch == GenerationEpoch + && IsDesired(DequeuedChunk.Tile) + && !DequeuedChunk.bEmpty + && DequeuedChunk.Streams.IsValid() + && DequeuedChunk.NumTriangles > 0; + if (bPreflightVisibleMesh) + { + const uint64 PreflightCycles = FPlatformTime::Cycles64(); + const double TickElapsedMilliseconds = TickStartCycles != 0 + && PreflightCycles >= TickStartCycles + ? FPlatformTime::ToSeconds64(PreflightCycles - TickStartCycles) * 1000.0 + : 0.0; + const double EstimatedApplyMilliseconds = 2.2 + + static_cast(FMath::Max(0, DequeuedChunk.NumTriangles)) * 0.00012 + + static_cast(FMath::Max(0, DequeuedChunk.NumVertices)) * 0.00003; + constexpr double TickApplyTargetMilliseconds = 11.0; + constexpr double TickApplyTailReserveMilliseconds = 0.5; + const double RemainingApplyMilliseconds = FMath::Max( + 0.0, TickApplyTargetMilliseconds - TickApplyTailReserveMilliseconds + - TickElapsedMilliseconds); + if (EstimatedApplyMilliseconds > RemainingApplyMilliseconds) + { + if (VoxelForgeStartupTrace::IsActive()) + { + VoxelForgeStartupTrace::RecordEvent(TEXT("tile_apply_deferred"), FString::Printf( + TEXT("\"frame\":%llu,\"tile\":[%d,%d,%d],\"level\":%d," + "\"triangles\":%d,\"vertices\":%d,\"estimated_apply_ms\":%.3f," + "\"elapsed_tick_ms\":%.3f,\"remaining_apply_ms\":%.3f"), + static_cast(GFrameCounter), + DequeuedChunk.Tile.Coord.X, DequeuedChunk.Tile.Coord.Y, + DequeuedChunk.Tile.Coord.Z, DequeuedChunk.Tile.Level, + DequeuedChunk.NumTriangles, DequeuedChunk.NumVertices, + EstimatedApplyMilliseconds, TickElapsedMilliseconds, + RemainingApplyMilliseconds)); + } + DeferredApplyResults.Add(MoveTemp(DequeuedChunk)); + continue; + } + } + + PendingTileCancellation.Remove(DequeuedChunk.Tile); + PendingTiles.Remove(DequeuedChunk.Tile); + // 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. @@ -10323,6 +10373,11 @@ void AVoxelWorld::ProcessPendingChunks() } } + for (FChunkResult& DeferredResult : DeferredApplyResults) + { + EnqueueStreamingResult(MoveTemp(DeferredResult)); + } + if (bProfileApply && ResultsDrained > 0) { UE_LOG(LogTemp, Display, @@ -10827,7 +10882,11 @@ void AVoxelWorld::HideOverlappingDesiredVisuals(const FVoxelTileKey& RetiredTile TArray> OverlappingDesired; if (const TArray* Descendants = DesiredTransitionDescendants.Find(RetiredTile)) { - OverlappingDesired.Append(*Descendants); + OverlappingDesired.Reserve(OverlappingDesired.Num() + Descendants->Num()); + for (const FVoxelTileKey& Descendant : *Descendants) + { + OverlappingDesired.Add(Descendant); + } } for (int32 Level = RetiredTile.Level + 1; Level <= DesiredTransitionMaxLevel; ++Level) { @@ -11064,9 +11123,18 @@ static FORCEINLINE void VF_OuterShell(const UVoxelSettings* Settings, int32 R, i VoxelClipmapDesiredTiles::OuterShell(Parameters, R, MaxLevel, OutLevel, OutRadius); } +static bool VF_TransitionIndexKeyLess(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; +} + void AVoxelWorld::BuildDesiredTransitionIndex() { DesiredTransitionDescendants.Reset(); + DesiredTransitionDescendants.Reserve(DesiredSorted.Num()); DesiredTransitionMaxLevel = 0; for (const FVoxelTileKey& Key : DesiredSorted) { @@ -11079,7 +11147,9 @@ void AVoxelWorld::BuildDesiredTransitionIndex() constexpr int32 MaxSupportedTileLevel = 8; const int32 DescendantIndexMaxLevel = FMath::Max( DesiredTransitionMaxLevel, MaxSupportedTileLevel); - for (const FVoxelTileKey& Key : DesiredSorted) + TArray TransitionIndexBuildKeys = DesiredSorted; + TransitionIndexBuildKeys.Sort(VF_TransitionIndexKeyLess); + for (const FVoxelTileKey& Key : TransitionIndexBuildKeys) { for (int32 Level = Key.Level + 1; Level <= DescendantIndexMaxLevel; ++Level) { @@ -11087,7 +11157,12 @@ void AVoxelWorld::BuildDesiredTransitionIndex() VF_AppendTouchingAncestors(Key, Level, TouchingAncestors); for (const FVoxelTileKey& Ancestor : TouchingAncestors) { - DesiredTransitionDescendants.FindOrAdd(Ancestor).Add(Key); + TArray& Descendants = + DesiredTransitionDescendants.FindOrAdd(Ancestor); + if (Descendants.IsEmpty() || Descendants.Last() != Key) + { + Descendants.Add(Key); + } } } } @@ -11187,6 +11262,10 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& double OverlayMaterializationMilliseconds = 0.0; double PrioritySortMilliseconds = 0.0; double CriticalAdmissionOrderMilliseconds = 0.0; + double DesiredCollisionAndStampMilliseconds = 0.0; + double DesiredTransitionIndexMilliseconds = 0.0; + double DesiredRetainedRegionMilliseconds = 0.0; + double DesiredRegionReconcileMilliseconds = 0.0; int32 IncrementalChangedLod4PlusKeyCount = 0; int32 IncrementalFallbackReason = 0; bool bCoreDesiredCacheHit = false; @@ -11954,6 +12033,8 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& (FPlatformTime::Seconds() - PrioritySortStartSeconds) * 1000.0; } + const double DesiredCollisionAndStampStartSeconds = bTraceDesiredBreakdown + ? FPlatformTime::Seconds() : 0.0; RestoreKnownEmptyCollisionTiles(); ReconcileCollisionDesiredTiles(); for (auto It = DesiredStamped.CreateIterator(); It; ++It) @@ -11965,6 +12046,11 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& } } CancelObsoleteTileWork(); + if (DesiredCollisionAndStampStartSeconds != 0.0) + { + DesiredCollisionAndStampMilliseconds = + (FPlatformTime::Seconds() - DesiredCollisionAndStampStartSeconds) * 1000.0; + } const double CriticalAdmissionOrderStartSeconds = bTraceDesiredBreakdown ? FPlatformTime::Seconds() : 0.0; @@ -12135,6 +12221,8 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& checkf(false, TEXT("VoxelForge incremental desired priority-order equality failure")); } } + const double DesiredTransitionIndexStartSeconds = bTraceDesiredBreakdown + ? FPlatformTime::Seconds() : 0.0; if (bIncrementalDesiredSet && bHadPreviousDesiredSet) { constexpr int32 MaxSupportedTileLevel = 8; @@ -12143,6 +12231,27 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& const auto ApplyTransitionDelta = [this, TransitionIndexMaxLevel]( const FVoxelTileKey& Key, bool bAdd) { + const auto LowerBound = [](const TArray& Descendants, + const FVoxelTileKey& SearchKey) + { + int32 First = 0; + int32 Count = Descendants.Num(); + while (Count > 0) + { + const int32 Step = Count / 2; + const int32 Index = First + Step; + if (VF_TransitionIndexKeyLess(Descendants[Index], SearchKey)) + { + First = Index + 1; + Count -= Step + 1; + } + else + { + Count = Step; + } + } + return First; + }; for (int32 Level = Key.Level + 1; Level <= TransitionIndexMaxLevel; ++Level) { TArray> TouchingAncestors; @@ -12153,12 +12262,21 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& { TArray& Descendants = DesiredTransitionDescendants.FindOrAdd(Ancestor); - if (!Descendants.Contains(Key)) Descendants.Add(Key); + const int32 InsertIndex = LowerBound(Descendants, Key); + if (InsertIndex == Descendants.Num() || Descendants[InsertIndex] != Key) + { + Descendants.Insert(Key, InsertIndex); + } } else if (TArray* Descendants = DesiredTransitionDescendants.Find(Ancestor)) { - Descendants->RemoveSingleSwap(Key, EAllowShrinking::No); + const int32 RemoveIndex = LowerBound(*Descendants, Key); + if (RemoveIndex < Descendants->Num() + && (*Descendants)[RemoveIndex] == Key) + { + Descendants->RemoveAt(RemoveIndex, 1, EAllowShrinking::No); + } if (Descendants->IsEmpty()) DesiredTransitionDescendants.Remove(Ancestor); } } @@ -12171,7 +12289,14 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& { BuildDesiredTransitionIndex(); } + if (DesiredTransitionIndexStartSeconds != 0.0) + { + DesiredTransitionIndexMilliseconds = + (FPlatformTime::Seconds() - DesiredTransitionIndexStartSeconds) * 1000.0; + } + const double DesiredRetainedRegionStartSeconds = bTraceDesiredBreakdown + ? FPlatformTime::Seconds() : 0.0; bool bRetainedSetChanged = false; int32 RetainedTileCount = 0; TArray MissingRetainedFineTiles; @@ -12190,6 +12315,13 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& } } } + if (DesiredRetainedRegionStartSeconds != 0.0) + { + DesiredRetainedRegionMilliseconds = + (FPlatformTime::Seconds() - DesiredRetainedRegionStartSeconds) * 1000.0; + } + const double DesiredRegionReconcileStartSeconds = bTraceDesiredBreakdown + ? FPlatformTime::Seconds() : 0.0; TArray MissingRegionTiles; { FReadScopeLock Lock(State->WorldLock); @@ -12249,6 +12381,11 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& } StartPendingCoreRegionCompile(); } + if (DesiredRegionReconcileStartSeconds != 0.0) + { + DesiredRegionReconcileMilliseconds = + (FPlatformTime::Seconds() - DesiredRegionReconcileStartSeconds) * 1000.0; + } if (bTraceDesiredBreakdown) { @@ -12258,7 +12395,10 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& TEXT("\"frame\":%llu,\"center\":[%d,%d,%d],\"total_ms\":%s," "\"core_prepare_ms\":%s,\"core_delta_ms\":%s,\"fallback_ms\":%s," "\"overlay_materialize_ms\":%s,\"priority_sort_ms\":%s," - "\"critical_order_ms\":%s,\"core_keys\":%d,\"candidate_keys\":%d," + "\"critical_order_ms\":%s,\"collision_stamp_ms\":%s," + "\"transition_index_ms\":%s,\"retained_region_ms\":%s," + "\"region_reconcile_ms\":%s," + "\"core_keys\":%d,\"candidate_keys\":%d," "\"overlay_keys\":%d,\"critical_keys\":%d,\"fallback_reason\":%d," "\"changed_lod4plus\":%d,\"materialization_update\":%d," "\"priority_order_reused\":%d,\"priority_order_updated\":%d," @@ -12271,6 +12411,10 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& *FString::SanitizeFloat(static_cast(OverlayMaterializationMilliseconds)), *FString::SanitizeFloat(static_cast(PrioritySortMilliseconds)), *FString::SanitizeFloat(static_cast(CriticalAdmissionOrderMilliseconds)), + *FString::SanitizeFloat(static_cast(DesiredCollisionAndStampMilliseconds)), + *FString::SanitizeFloat(static_cast(DesiredTransitionIndexMilliseconds)), + *FString::SanitizeFloat(static_cast(DesiredRetainedRegionMilliseconds)), + *FString::SanitizeFloat(static_cast(DesiredRegionReconcileMilliseconds)), CoreDesiredTiles ? CoreDesiredTiles->Num() : static_cast(UncachedCoreDesiredTiles.size()), CandidateKeys.Num(), OverlayDesiredTiles.Num(), CriticalDesiredTiles.Num(), @@ -14118,14 +14262,17 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw { const uint64 TraceLevelSortStartCycles = bTraceStreamingScheduler ? FPlatformTime::Cycles64() : 0; + const int32 LevelSubmissionCount = FMath::Min( + LevelTiles.Num(), FMath::Max( + 0, AdmissionCap - GetGenerationAdmissionCount())); const bool bUseForwardFirstOrder = GVoxelForgeStreamingFastTravelForwardFirst != 0 && bPolicy3FastTravel && Level <= 2; - if (bUseForwardFirstOrder) + if (LevelSubmissionCount > 0 && bUseForwardFirstOrder) { SortNearTilesForwardFirst(LevelTiles); } - else + else if (LevelSubmissionCount > 0) { FVector Heading = GetActorTransform().InverseTransformVectorNoScale(PlayerHeading); if (Heading.IsNearlyZero() && IsValid(PlayerPawn)) @@ -14138,7 +14285,12 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw const double TileSizeCm = static_cast(VF_ClipLevelExtentVoxels(Level)) * GetActiveVoxelSizeCm(); const bool bUseFastNearLeadOrder = bPolicy3FastTravel && Level <= 2; - std::stable_sort(LevelTiles.GetData(), LevelTiles.GetData() + LevelTiles.Num(), + // The full order is deterministic because the comparator breaks every tie + // with the tile coordinates. Select only the prefix this admission pass can + // submit; sorting every outstanding key made a small worker cap pay O(N log N). + std::partial_sort(LevelTiles.GetData(), + LevelTiles.GetData() + LevelSubmissionCount, + LevelTiles.GetData() + LevelTiles.Num(), [&](const FVoxelTileKey& A, const FVoxelTileKey& B) { const FVector CenterA( @@ -14193,6 +14345,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw if (A.Coord.Y != B.Coord.Y) return A.Coord.Y < B.Coord.Y; return A.Coord.Z < B.Coord.Z; }); + LevelTiles.SetNum(LevelSubmissionCount, EAllowShrinking::No); } if (bTraceStreamingScheduler) { diff --git a/Source/VoxelForge/Public/VoxelWorld.h b/Source/VoxelForge/Public/VoxelWorld.h index 0a0078c..9b8cbb1 100644 --- a/Source/VoxelForge/Public/VoxelWorld.h +++ b/Source/VoxelForge/Public/VoxelWorld.h @@ -1617,7 +1617,7 @@ public: // ONE frame → a game-thread teardown spike. Drained by ProcessUnloadQueue (catch-up scaled). TSet PendingUnload; - void ProcessPendingChunks(); + void ProcessPendingChunks(uint64 TickStartCycles); void ProcessUnloadQueue(); // budgeted teardown drain (see PendingUnload) void QueuePendingUnload(const FVoxelTileKey& Tile); void HideOverlappingDesiredVisuals(const FVoxelTileKey& RetiredTile); diff --git a/Standalone/Research/WorldAssemblerM3/Linux169/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux169/REPORT.md new file mode 100644 index 0000000..2fdcf21 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux169/REPORT.md @@ -0,0 +1,54 @@ +# L169 — Early admission stays opt-in + +**Result:** Do not promote L157 early admission. Six of seven promotion gates passed, but candidate LOD0–2 arrival was slower in every pair: median **1.683 s** versus **1.671 s** on `assembler/world` `ca0bf76` (+12.4 ms). The source defaults are restored to wait-for-critical **1** and apply-budget **0 ms**. The candidate settings remain explicit overrides in the runner. + +## Measurement + +Seven interleaved W8 pairs used the four-strata owner plan, 25 cm core voxels, 30 m/s movement, a 12-second move from t=6 to t=18, and a 5-second dwell. Each run had about 46 moving samples; one warm-up per binary was excluded. No product CVar was changed except the two L157 switches under test. At measurement time, the candidate binary compiled those settings as defaults; the runner now supplies the same values explicitly so the experiment can be repeated with shipping defaults off. + +| Moving-window metric (median across seven runs) | `ca0bf76` | Candidate | Result | +|---|---:|---:|---:| +| Tick p99 | 7.060 ms | 6.519 ms | lower | +| Worst tick | 12.219 ms | 11.776 ms | lower | +| Near LOD0–2 readiness | 43.28% | 56.12% | +12.84 pp | +| Forward-cone readiness | 15.52% | 17.93% | +2.41 pp | +| Far holes | 1.80% | 0.00% | lower | +| First settled LOD0–2 | 1.671 s | 1.683 s | **12.4 ms slower** | + +Every run stayed below 11 ms p99 and 14 ms worst tick. The largest observed p99 was 7.400 ms; the largest worst tick was 13.776 ms. Candidate worst-tick median was not worse. Arrival remained slower after subtracting each run’s first streaming sample (1.305 s vs 1.299 s), so the raw arrival failure is not explained entirely by trace start time. + +## Spike count and changes + +All counts below use the 12-second moving window unless identified as a startup diagnostic. + +- **Level sorting:** With L157 on at the base defaults, the largest full-sort sample was 0.865 ms and no sample exceeded 1 ms; the L167 8.3 ms spike did not recur. Candidate partial sorting reduced the largest sample to 0.123 ms, with no sample above 1 ms. +- **Desired rebuild and overlay:** Base per-run desired-rebuild peaks had a 7.476 ms median and reached 8.110 ms. Candidate peaks had a 6.928 ms median and reached 7.260 ms; rebuilds over 5 ms still occurred (median four per run, maximum five). Overlay materialization never exceeded 2 ms; its candidate maximum was 1.094 ms. The transition descendant index now uses sorted arrays and binary-search deltas. In a separate pre-move startup trace with 2,233 Core keys, the initial index build took 1.605 ms; the maximum moving-window index phase was 3.626 ms. +- **Mesh apply:** The 2.8 ms historical apply spike did not recur. Maximum per-frame apply time fell from 2.548 ms at base to 1.590 ms candidate; largest individual candidate apply was 1.512 ms. The preflight deferred 21–145 results per run (median 109), with a largest estimate of 3.447 ms for 8,979 triangles. RealtimeMesh submission remains atomic, so the estimate is a heuristic and does not split a single upload. + +The cold-start arrival gate prevents default promotion even though movement readiness and tick behavior improved. No further adjustment was made after the seven-pair result. + +## Defaults and checks + +| CVar | Final default | Candidate test value | +|---|---:|---:| +| `voxel.StreamingFastTravelWaitForAllCriticalTiles` | 1 | 0 | +| `voxel.StreamingFastTravelApplyBudgetMilliseconds` | 0 ms | 1 ms | +| `voxel.StreamingIncrementalDesiredSet` | 1 | 1 | +| `voxel.StreamingFastTravelForwardFirst` | 0 | 0 | +| `voxel.StreamingRebuildOnCoreRegionCompletion` | 1 | 1 | +| `voxel.StreamingCoreDesiredCacheByCenter` | 0 | 0 | +| `voxel.StreamingIncrementalDesiredAuditEvery` | 0 | 0 | +| `voxel.StreamingIncrementalDesiredMaterializationAuditEvery` | 0 | 0 | +| `voxel.StreamingIncrementalDesiredAuditInjectMismatch` | 0 | 0 | + +- Final VFHost Linux Development build passed with `-NoUBA -MaxParallelActions=4`; three existing deprecated RealtimeMesh API warnings remain. +- Final-source `verify.py fast` passed in 289.8 s: 268 passes, 0 failures, 5 diagnostics. Clang and GCC geometry hashes and W1/W8/W12 determinism passed. +- `git diff --check` and the runner AST syntax check passed. The direct live `TileHashDump` hook returned zero records for the Core path on both binaries and is inconclusive; the fast-tier geometry and determinism checks passed. +- UE W12 movement was not run: this VFHost procedure exposed eight effective tile slots. Fast-tier W12 determinism was run. + +Remaining risk: desired rebuilds still reach 7.260 ms in the candidate, and the apply estimate cannot bound an atomic RMC upload. Most importantly, the arrival gate failed, so early admission remains opt-in. + +Changed Unreal `Source/` files: + +- `Source/VoxelForge/Private/VoxelWorld.cpp` +- `Source/VoxelForge/Public/VoxelWorld.h` diff --git a/Standalone/Research/WorldAssemblerM3/Linux169/RunL169Movement.py b/Standalone/Research/WorldAssemblerM3/Linux169/RunL169Movement.py new file mode 100644 index 0000000..2178c38 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux169/RunL169Movement.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +"""L169 default-based UE movement comparison and early-admission spike trace.""" +from __future__ import annotations + +import argparse +import json +import math +import shutil +import statistics +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +HOST = Path("/home/jahni/VFHost") +EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd") +HOST_PLUGIN = HOST / "Binaries/Linux/libUnrealEditor-VoxelForge.so" +OUT = ROOT / "Saved/Linux/l169/ue/movement" +BIN_DIR = ROOT / "Saved/Linux/l169/bin" +SPAWN_M = (19.166015625, -14.2001953125, -753.3740234375) +MOVE_START_S = 6.0 +MOVE_DURATION_S = 12.0 + +BASELINE_BINARY = BIN_DIR / "ca0bf76/libUnrealEditor-VoxelForge.so" +CANDIDATE_BINARY = BIN_DIR / "early-admission/libUnrealEditor-VoxelForge.so" +DIAGNOSTIC_BINARY = BIN_DIR / "apply-preflight/libUnrealEditor-VoxelForge.so" + +EXPECTED = { + "baseline": {"wait": 1, "apply_budget_ms": 0.0, "incremental": 1}, + "early_override": {"wait": 0, "apply_budget_ms": 1.0, "incremental": 1}, + "candidate": {"wait": 0, "apply_budget_ms": 1.0, "incremental": 1}, +} + +ORDER = { + 1: ("baseline", "candidate"), + 2: ("candidate", "baseline"), + 3: ("baseline", "candidate"), + 4: ("candidate", "baseline"), + 5: ("baseline", "candidate"), + 6: ("candidate", "baseline"), + 7: ("baseline", "candidate"), +} + + +def percentile(values: list[float], fraction: float = 0.99) -> float | None: + if not values: + return None + ordered = sorted(values) + index = max(0, min(len(ordered) - 1, math.ceil(len(ordered) * fraction) - 1)) + return ordered[index] + + +def percent(numerator: int | float, denominator: int | float) -> float | None: + return 100.0 * numerator / denominator if denominator > 0 else None + + +def read_events(path: Path) -> list[dict]: + rows = [] + if path.is_file(): + for line in path.read_text(encoding="utf-8", errors="replace").splitlines(): + if not line.startswith("{"): + continue + try: + rows.append(json.loads(line)) + except json.JSONDecodeError: + continue + return rows + + +def summarize(condition: str, run_name: str, trace: Path, log: Path, exit_code: int, + wall_s: float, audit_or_diag: bool = False) -> dict: + expected = EXPECTED[condition] + events = read_events(trace) + backend = next((e for e in events if e.get("type") == "worker_backend_config"), {}) + scheduler_all = [e for e in events if e.get("type") == "scheduler_admission"] + if not scheduler_all: + raise RuntimeError(f"missing scheduler readback: {trace}") + if any(int(e.get("incremental_desired_set", -1)) != expected["incremental"] + for e in scheduler_all): + raise RuntimeError(f"incremental desired-set readback mismatch: {trace}") + if any(int(e.get("forward_first", -1)) != 0 + or int(e.get("rebuild_on_core_region_completion", -1)) != 1 + or int(e.get("core_desired_cache_by_center", -1)) != 0 + or int(e.get("incremental_desired_audit_every", -1)) != 0 + or int(e.get("incremental_desired_materialization_audit_every", -1)) != 0 + or int(e.get("incremental_desired_audit_inject_mismatch", -1)) != 0 + for e in scheduler_all): + raise RuntimeError(f"non-default L160/audit CVar readback: {trace}") + if (int(backend.get("streaming_fast_travel_wait_for_all_critical_tiles", -1)) + != expected["wait"] + or abs(float(backend.get("streaming_fast_travel_apply_budget_ms", -999.0)) + - expected["apply_budget_ms"]) > 0.01 + or int(backend.get("effective_tile_slots", -1)) != 8): + raise RuntimeError(f"early-admission/worker readback mismatch: {trace}; {backend}") + + move_stop = next((e for e in events if e.get("type") == "streaming_test_move_stopped"), None) + if move_stop is None: + raise RuntimeError(f"missing move-stop event: {trace}") + stop_s = float(move_stop.get("t_s", 0.0)) + samples = [e for e in events if e.get("type") == "streaming_sample"] + moving = [e for e in samples if MOVE_START_S <= float(e.get("t_s", 0.0)) < stop_s + and float(e.get("speed_cm_s", 0.0)) >= 50.0] + dwell = [e for e in samples if float(e.get("t_s", 0.0)) >= stop_s + and float(e.get("speed_cm_s", 0.0)) < 50.0] + if exit_code != 0 or len(moving) < 40 or len(dwell) < 16: + raise RuntimeError(f"incomplete {run_name}: exit={exit_code}, move={len(moving)}, " + f"dwell={len(dwell)}") + move_start = min(float(e.get("t_s", 0.0)) for e in moving) + + work = [e for e in events if e.get("type") == "game_thread_work_frame" + and move_start <= float(e.get("t_s", 0.0)) < stop_s] + scheduler_moving = [e for e in scheduler_all + if move_start <= float(e.get("t_s", 0.0)) < stop_s] + rebuild_events = [e for e in events if e.get("type") == "desired_rebuild_breakdown" + and move_start <= float(e.get("t_s", 0.0)) < stop_s] + applies = {int(e.get("frame", -1)): e for e in events + if e.get("type") == "apply_frame" + and move_start <= float(e.get("t_s", 0.0)) < stop_s} + scheduler = {int(e.get("frame", -1)): e for e in scheduler_all + if move_start <= float(e.get("t_s", 0.0)) < stop_s} + desired = {int(e.get("frame", -1)): e for e in events + if e.get("type") == "desired_rebuild_breakdown" + and move_start <= float(e.get("t_s", 0.0)) < stop_s} + moving_tiles = [e for e in events if e.get("type") == "tile" + and move_start <= float(e.get("t_s", 0.0)) < stop_s] + apply_deferrals = [e for e in events if e.get("type") == "tile_apply_deferred" + and move_start <= float(e.get("t_s", 0.0)) < stop_s] + + frames = [] + for event in work: + frame = int(event.get("frame", -1)) + apply = applies.get(frame, {}) + admit = scheduler.get(frame, {}) + rebuild = desired.get(frame, {}) + frames.append({ + "frame": frame, + "tick_ms": float(event.get("tick_ms", 0.0)), + "update_chunks_ms": float(event.get("update_chunks_ms", 0.0)), + "pending_drain_ms": float(event.get("pending_drain_ms", 0.0)), + "apply_ms": float(apply.get("apply_ms", 0.0)), + "apply_budget_ms": float(apply.get("apply_budget_ms", 0.0)), + "meshes_applied": int(apply.get("meshes_applied", 0)), + "time_budget_hit": int(apply.get("time_limit_reached", 0)), + "desired_rebuild_ms": float(admit.get("desired_rebuild_ms", 0.0)), + "desired_rebuild_event_ms": float(rebuild.get("total_ms", 0.0)), + "overlay_materialize_ms": float(rebuild.get("overlay_materialize_ms", 0.0)), + "core_prepare_ms": float(rebuild.get("core_prepare_ms", 0.0)), + "core_delta_ms": float(rebuild.get("core_delta_ms", 0.0)), + "priority_sort_ms": float(rebuild.get("priority_sort_ms", 0.0)), + "critical_order_ms": float(rebuild.get("critical_order_ms", 0.0)), + "collision_stamp_ms": float(rebuild.get("collision_stamp_ms", 0.0)), + "transition_index_ms": float(rebuild.get("transition_index_ms", 0.0)), + "retained_region_ms": float(rebuild.get("retained_region_ms", 0.0)), + "region_reconcile_ms": float(rebuild.get("region_reconcile_ms", 0.0)), + "level_sort_ms": float(admit.get("level_sort_ms", 0.0)), + "level_candidate_scan_ms": float(admit.get("level_candidate_scan_ms", 0.0)), + "submit_tiles_ms": float(admit.get("submit_tiles_ms", 0.0)), + }) + + near = [] + cone = [] + far_holes = [] + for event in moving: + desired_by_lod = event.get("desired_by_lod", [0] * 9) + resident_by_lod = event.get("resident_by_lod", [0] * 9) + covered_by_lod = event.get("covered_by_lod", resident_by_lod) + near.append(percent(sum(resident_by_lod[:3]), sum(desired_by_lod[:3])) or 0.0) + cone.append(percent(int(event.get("near_forward_cone_resident", 0)), + int(event.get("near_forward_cone_desired", 0))) or 0.0) + far_holes.append(percent(sum(desired_by_lod[3:]) - sum(covered_by_lod[3:]), + sum(desired_by_lod[3:])) or 0.0) + settled = next((e for e in samples if float(e.get("t_s", 0.0)) < move_start + and float(e.get("speed_cm_s", 0.0)) < 50.0 + and sum(e.get("settled_by_lod", [0] * 9)[:3]) + >= sum(e.get("desired_by_lod", [0] * 9)[:3]) > 0), None) + p99 = percentile([r["tick_ms"] for r in frames]) + worst = max(frames, key=lambda r: r["tick_ms"], default={}) + row = { + "condition": condition, + "run": run_name, + "diagnostic_override": audit_or_diag, + "source_default_incremental": expected["incremental"], + "early_admission_wait_for_critical": expected["wait"], + "early_admission_apply_budget_ms": expected["apply_budget_ms"], + "speed_mps": 30, + "effective_tile_slots": backend.get("effective_tile_slots"), + "moving_samples": len(moving), + "dwell_samples": len(dwell), + "tick_p99_ms": p99, + "tick_worst_ms": worst.get("tick_ms"), + "worst_frame": worst, + "near_readiness_moving_mean_pct": statistics.mean(near) if near else None, + "forward_cone_readiness_moving_mean_pct": statistics.mean(cone) if cone else None, + "far_holes_moving_median_pct": statistics.median(far_holes) if far_holes else None, + "arrival_lod02_settled_s": float(settled.get("t_s", 0.0)) if settled else None, + "desired_rebuild_count": len(rebuild_events), + "incremental_materialization_updates": sum( + int(e.get("materialization_update", 0)) != 0 for e in events + if e.get("type") == "desired_rebuild_breakdown" + and move_start <= float(e.get("t_s", 0.0)) < stop_s), + "core_incremental_deltas": sum( + int(e.get("incremental_delta", 0)) != 0 for e in events + if e.get("type") == "core_desired_cache" + and move_start <= float(e.get("t_s", 0.0)) < stop_s), + "level_sort_samples_over_1ms": sum( + float(e.get("level_sort_ms", 0.0)) > 1.0 for e in scheduler_moving), + "level_sort_max_ms": max((float(e.get("level_sort_ms", 0.0)) + for e in scheduler_moving), default=0.0), + "desired_rebuild_events_over_5ms": sum( + float(e.get("total_ms", 0.0)) > 5.0 for e in rebuild_events), + "desired_rebuild_max_ms": max( + (float(e.get("total_ms", 0.0)) for e in rebuild_events), default=0.0), + "overlay_materialize_events_over_2ms": sum( + float(e.get("overlay_materialize_ms", 0.0)) > 2.0 for e in rebuild_events), + "overlay_materialize_max_ms": max( + (float(e.get("overlay_materialize_ms", 0.0)) for e in rebuild_events), default=0.0), + "desired_subphase_max_ms": { + key: max((float(e.get(key, 0.0)) for e in rebuild_events), default=0.0) + for key in ("core_prepare_ms", "overlay_materialize_ms", "priority_sort_ms", + "critical_order_ms", "collision_stamp_ms", "transition_index_ms", + "retained_region_ms", "region_reconcile_ms")}, + "apply_budget_hit_frames": sum(f["time_budget_hit"] for f in frames), + "apply_frame_count": sum(f["meshes_applied"] > 0 for f in frames), + "apply_ms_max": max((f["apply_ms"] for f in frames), default=0.0), + "tiles_over_1ms_apply": sum(float(e.get("apply_s", 0.0)) > 0.001 + for e in moving_tiles), + "apply_preflight_deferred_count": len(apply_deferrals), + "apply_preflight_deferred_max_estimate_ms": max( + (float(e.get("estimated_apply_ms", 0.0)) for e in apply_deferrals), default=0.0), + "apply_preflight_deferred_max_triangles": max( + (int(e.get("triangles", 0)) for e in apply_deferrals), default=0), + "largest_tile_applies": sorted(({ + "apply_ms": float(e.get("apply_s", 0.0)) * 1000.0, + "triangles": int(e.get("triangles", 0)), + "vertices": int(e.get("vertices", 0)), + "level": int(e.get("level", -1)), + } for e in moving_tiles), + key=lambda e: e["apply_ms"], reverse=True)[:8], + "top_ticks": sorted(frames, key=lambda r: r["tick_ms"], reverse=True)[:8], + "trace": str(trace.relative_to(ROOT)), + "log": str(log.relative_to(ROOT)), + "wall_s": round(wall_s, 3), + } + return row + + +def run(condition: str, run_name: str, diagnostic_override: bool = False, + tile_hash_dump: Path | None = None) -> dict: + if condition == "baseline": + binary = BASELINE_BINARY + elif condition == "early_override": + binary = DIAGNOSTIC_BINARY if DIAGNOSTIC_BINARY.is_file() else BASELINE_BINARY + elif condition == "candidate": + binary = CANDIDATE_BINARY + else: + raise ValueError(condition) + if not binary.is_file(): + raise FileNotFoundError(binary) + HOST_PLUGIN.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(binary, HOST_PLUGIN) + out = OUT / condition + export = HOST / "Plugins/VoxelForge/Saved/Linux/l169" / condition / run_name + userdir = HOST / "Saved/Linux/l169/user" / condition / run_name + zendata = HOST / "Saved/Linux/l169/zen" / condition / run_name + ddc = HOST / "Saved/Linux/l169/DDC" + trace = out / f"{run_name}.trace.jsonl" + log = out / f"{run_name}.log" + stdout = out / f"{run_name}.stdout.log" + for directory in (out, export, userdir, zendata, ddc): + directory.mkdir(parents=True, exist_ok=True) + trace.unlink(missing_ok=True) + spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWN_M) + command = [ + str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended", + "-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch", + "-notraceserver", "-nocrashreports", "-DisablePlugins=AndroidFileServer", + "-ddc=NoZenLocalFallback", f"-userdir={userdir}", f"-ZenDataPath={zendata}", + f"-LocalDataCachePath={ddc}", "-run=VoxelForgeExplore", "-seed=0", + "-archetype=TunnelNetwork", "-slot=0", "-modes=export", "-gameconfig=1", + "-coreworldtest=1", "-ownerfourstrateplan=1", "-corevoxelsizecm=25", + "-coreislandtest=0", "-corefallbacktest=0", "-coreofftest=0", + "-coreplantest=0", "-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings", + "-corehybridmesher=0", f"-out={export}", + "-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=3000.0", + f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}", + f"-voxel.TestMoveDurationSeconds={MOVE_DURATION_S:.1f}", + "-voxel.TestExitSeconds=0.0", "-voxel.TestMoveDirection=1,0,0", + "-voxel.TestExitOnSteady=0", "-coreworldstreamtrace=1", + f"-voxel.StartupTraceSpawnCm={spawn_cm}", + "-voxel.StartupTraceUntilEndPlay=1", "-voxel.StartupTraceThroughCrossing=1", + "-voxel.StartupTraceMaxTiles=16384", f"-voxel.StartupTraceFile={trace}", + f"-abslog={log}", + ] + if condition in ("early_override", "candidate"): + command += ["-voxel.StreamingFastTravelWaitForAllCriticalTiles=0", + "-voxel.StreamingFastTravelApplyBudgetMilliseconds=1.0"] + if tile_hash_dump is not None: + tile_hash_dump.parent.mkdir(parents=True, exist_ok=True) + tile_hash_dump.unlink(missing_ok=True) + command.append(f"-voxel.TileHashDump={tile_hash_dump.resolve()}") + started = time.monotonic() + with stdout.open("w", encoding="utf-8") as stream: + process = subprocess.run( + ["taskset", "-c", "0-11", "timeout", "1740s", *command], + cwd=ROOT, stdout=stream, stderr=subprocess.STDOUT, check=False, timeout=1740) + return summarize(condition, run_name, trace, log, process.returncode, + time.monotonic() - started, diagnostic_override) + + +def main() -> int: + parser = argparse.ArgumentParser() + modes = parser.add_mutually_exclusive_group(required=True) + modes.add_argument("--diagnostic", action="store_true", + help="one early-admission trace on the selected diagnostic binary") + modes.add_argument("--matrix", action="store_true", + help="warm both binaries, then run seven interleaved pairs with only the early-admission switches overridden") + modes.add_argument("--geometry-hash-check", action="store_true", + help="compare shared tile hashes with defaults vs the promoted switches") + args = parser.parse_args() + missing = [p for p in (EDITOR, HOST / "VFHost.uproject", ROOT / "VoxelForge.uplugin") + if not p.exists()] + if missing: + raise SystemExit(f"missing UE runner input: {missing}") + OUT.mkdir(parents=True, exist_ok=True) + + if args.diagnostic: + warmup = run("early_override", "diagnostic-warmup", diagnostic_override=True) + row = run("early_override", "l169-default-base-diagnostic", diagnostic_override=True) + row["warmup"] = warmup + path = OUT / "diagnostic.json" + path.write_text(json.dumps(row, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("[L169Diagnostic] " + json.dumps(row, sort_keys=True), flush=True) + return 0 + + if args.geometry_hash_check: + hash_dir = OUT / "geometry-hash" + baseline_dump = hash_dir / "baseline.json" + candidate_dump = hash_dir / "candidate.json" + run("baseline", "geometry-hash", tile_hash_dump=baseline_dump) + run("candidate", "geometry-hash", tile_hash_dump=candidate_dump) + baseline_data = json.loads(baseline_dump.read_text(encoding="utf-8")) + candidate_data = json.loads(candidate_dump.read_text(encoding="utf-8")) + key_fields = ("level", "tile_x", "tile_y", "tile_z") + + def indexed(data: dict) -> dict[tuple[int, ...], tuple]: + return {tuple(int(record[field]) for field in key_fields): ( + str(record.get("geometry_hash", "")), + int(record.get("triangle_count", -1)), + bool(record.get("empty", False))) + for record in data.get("records", [])} + + base = indexed(baseline_data) + candidate = indexed(candidate_data) + common = base.keys() & candidate.keys() + mismatches = [key for key in common if base[key] != candidate[key]] + result = {"baseline_records": len(base), "candidate_records": len(candidate), + "common_records": len(common), "hash_mismatches": len(mismatches), + "baseline_only_records": len(base.keys() - candidate.keys()), + "candidate_only_records": len(candidate.keys() - base.keys()), + "baseline_dump": str(baseline_dump.relative_to(ROOT)), + "candidate_dump": str(candidate_dump.relative_to(ROOT))} + (hash_dir / "comparison.json").write_text( + json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("[L169GeometryHash] " + json.dumps(result, sort_keys=True), flush=True) + return 0 if len(common) >= 100 and not mismatches else 1 + + warmups = [run("baseline", "warmup"), run("candidate", "warmup")] + rows = [] + partial = OUT / "runs-partial.json" + for pair, sequence in ORDER.items(): + for condition in sequence: + rows.append(run(condition, f"pair-{pair}")) + partial.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n", + encoding="utf-8") + metrics = ("tick_p99_ms", "tick_worst_ms", "near_readiness_moving_mean_pct", + "forward_cone_readiness_moving_mean_pct", "far_holes_moving_median_pct", + "arrival_lod02_settled_s") + medians = {condition: {metric: statistics.median( + row[metric] for row in rows if row["condition"] == condition + and row.get(metric) is not None) for metric in metrics} + for condition in ("baseline", "candidate")} + by_pair = {pair: {row["condition"]: row for row in rows if row["run"] == f"pair-{pair}"} + for pair in ORDER} + deltas = [ + {"pair": pair, **{f"{metric}_delta": + by_pair[pair]["candidate"][metric] - by_pair[pair]["baseline"][metric] + for metric in metrics}} + for pair in ORDER + ] + summary = {"protocol": "Seven interleaved 30 m/s UE pairs using base defaults except for " + "the two early-admission switches at candidate values; 12 s move plus 5 s " + "dwell; eight effective tile slots; one warm-up per binary excluded.", + "defaults": {"incremental_desired_set": 1, + "streaming_fast_travel_wait_for_all_critical_tiles": 1, + "streaming_fast_travel_apply_budget_ms": 0.0, + "forward_first": 0, "rebuild_on_core_region_completion": 1, + "core_desired_cache_by_center": 0, + "incremental_desired_audit_every": 0, + "incremental_desired_materialization_audit_every": 0, + "incremental_desired_audit_inject_mismatch": 0}, + "candidate_switch_overrides": { + "streaming_fast_travel_wait_for_all_critical_tiles": 0, + "streaming_fast_travel_apply_budget_ms": 1.0}, + "warmups": warmups, "movement_medians": medians, + "paired_deltas_candidate_minus_baseline": deltas, "runs": rows} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", + encoding="utf-8") + print("[L169Summary] " + json.dumps({"movement_medians": medians, + "paired_deltas": deltas}, sort_keys=True), flush=True) + all_runs_p99_pass = all(row["tick_p99_ms"] is not None and row["tick_p99_ms"] <= 11.0 + for row in rows) + all_runs_worst_pass = all(row["tick_worst_ms"] is not None + and row["tick_worst_ms"] <= 14.0 for row in rows) + candidate_worst = [row["tick_worst_ms"] for row in rows + if row["condition"] == "candidate"] + baseline_worst = [row["tick_worst_ms"] for row in rows + if row["condition"] == "baseline"] + gates = { + "every_run_p99_le_11_ms": all_runs_p99_pass, + "every_run_worst_le_14_ms": all_runs_worst_pass, + "candidate_worst_tick_median_not_worse": bool(candidate_worst and baseline_worst) + and statistics.median(candidate_worst) <= statistics.median(baseline_worst), + "near_readiness_up": medians["candidate"]["near_readiness_moving_mean_pct"] + > medians["baseline"]["near_readiness_moving_mean_pct"], + "forward_cone_readiness_up": medians["candidate"][ + "forward_cone_readiness_moving_mean_pct"] + > medians["baseline"]["forward_cone_readiness_moving_mean_pct"], + "far_holes_not_worse": medians["candidate"]["far_holes_moving_median_pct"] + <= medians["baseline"]["far_holes_moving_median_pct"], + "arrival_not_worse": medians["candidate"]["arrival_lod02_settled_s"] + <= medians["baseline"]["arrival_lod02_settled_s"], + } + summary["promotion_gates"] = gates + (OUT / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", + encoding="utf-8") + print("[L169Gates] " + json.dumps(gates, sort_keys=True), flush=True) + return 0 if all(gates.values()) else 1 + + +if __name__ == "__main__": + raise SystemExit(main())