Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6357b573f | ||
|
|
9c70053543 | ||
|
|
aa458d3607 | ||
|
|
323fbe3736 |
No files matched your search
@@ -322,6 +322,108 @@ bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
|
|||||||
FirstFailure.IsEmpty() ? TEXT("none") : *FirstFailure));
|
FirstFailure.IsEmpty() ? TEXT("none") : *FirstFailure));
|
||||||
TestEqual(TEXT("all swept desired sets have zero overlapping pairs"), OverlapCases, 0);
|
TestEqual(TEXT("all swept desired sets have zero overlapping pairs"), OverlapCases, 0);
|
||||||
TestEqual(TEXT("all swept desired sets cover the legacy footprint"), CoverageFailures, 0);
|
TestEqual(TEXT("all swept desired sets cover the legacy footprint"), CoverageFailures, 0);
|
||||||
|
|
||||||
|
static const FIntVector DeltaCenters[] = {
|
||||||
|
FIntVector(-5, -2, -1), FIntVector(-3, 1, 2), FIntVector(-1, -3, 0),
|
||||||
|
FIntVector(0, 0, 0), FIntVector(1, 3, -2), FIntVector(3, -1, 1),
|
||||||
|
FIntVector(5, 2, -3),
|
||||||
|
};
|
||||||
|
static const FIntVector DeltaSteps[] = {
|
||||||
|
FIntVector(-1, 0, 0), FIntVector(1, 0, 0),
|
||||||
|
FIntVector(0, -1, 0), FIntVector(0, 1, 0),
|
||||||
|
FIntVector(0, 0, -1), FIntVector(0, 0, 1),
|
||||||
|
};
|
||||||
|
int32 DeltaCases = 0;
|
||||||
|
int32 DeltaFailures = 0;
|
||||||
|
FString FirstDeltaFailure;
|
||||||
|
for (const FIntVector& Center : DeltaCenters)
|
||||||
|
for (const FSweepSettings& Sweep : SweepSettings)
|
||||||
|
for (int32 ClampCase = 0; ClampCase < 3; ++ClampCase)
|
||||||
|
{
|
||||||
|
FParameters P;
|
||||||
|
P.ClipRadius = Sweep.Radius;
|
||||||
|
P.MaxClipLevel = Sweep.MaxLevel;
|
||||||
|
P.RenderDistanceChunks = Sweep.RenderDistance;
|
||||||
|
|
||||||
|
const auto GetVerticalBounds = [&](int32 CenterZ, int32& OutMinZ, int32& OutMaxZ)
|
||||||
|
{
|
||||||
|
OutMinZ = MIN_int32;
|
||||||
|
OutMaxZ = MAX_int32;
|
||||||
|
if (ClampCase == 1)
|
||||||
|
{
|
||||||
|
OutMinZ = CenterZ - 2;
|
||||||
|
OutMaxZ = CenterZ + 3;
|
||||||
|
}
|
||||||
|
else if (ClampCase == 2)
|
||||||
|
{
|
||||||
|
const int32 Band = VoxelClipmapDesiredTiles::FloorDiv(
|
||||||
|
CenterZ, 1 << Sweep.MaxLevel);
|
||||||
|
const int64 BandStart = static_cast<int64>(Band)
|
||||||
|
* (int64{1} << static_cast<uint32>(Sweep.MaxLevel));
|
||||||
|
OutMinZ = static_cast<int32>(BandStart - 1);
|
||||||
|
OutMaxZ = OutMinZ + 6;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
int32 PreviousMinZ = 0;
|
||||||
|
int32 PreviousMaxZ = 0;
|
||||||
|
GetVerticalBounds(Center.Z, PreviousMinZ, PreviousMaxZ);
|
||||||
|
TArray<FVoxelTileKey> PreviousTiles;
|
||||||
|
VoxelClipmapDesiredTiles::Build(Center, PreviousMinZ, PreviousMaxZ, P, PreviousTiles);
|
||||||
|
|
||||||
|
for (const FIntVector& Step : DeltaSteps)
|
||||||
|
{
|
||||||
|
const FIntVector NextCenter = Center + Step;
|
||||||
|
int32 NextMinZ = 0;
|
||||||
|
int32 NextMaxZ = 0;
|
||||||
|
GetVerticalBounds(NextCenter.Z, NextMinZ, NextMaxZ);
|
||||||
|
TArray<FVoxelTileKey> Entering;
|
||||||
|
TArray<FVoxelTileKey> Leaving;
|
||||||
|
VoxelClipmapDesiredTiles::BuildDelta(
|
||||||
|
Center, PreviousMinZ, PreviousMaxZ,
|
||||||
|
NextCenter, NextMinZ, NextMaxZ, P, Entering, Leaving);
|
||||||
|
|
||||||
|
TSet<FVoxelTileKey> Updated;
|
||||||
|
for (const FVoxelTileKey& Tile : PreviousTiles) Updated.Add(Tile);
|
||||||
|
for (const FVoxelTileKey& Tile : Leaving) Updated.Remove(Tile);
|
||||||
|
for (const FVoxelTileKey& Tile : Entering) Updated.Add(Tile);
|
||||||
|
|
||||||
|
TArray<FVoxelTileKey> FullNextTiles;
|
||||||
|
VoxelClipmapDesiredTiles::Build(
|
||||||
|
NextCenter, NextMinZ, NextMaxZ, P, FullNextTiles);
|
||||||
|
TSet<FVoxelTileKey> FullNextSet;
|
||||||
|
for (const FVoxelTileKey& Tile : FullNextTiles) FullNextSet.Add(Tile);
|
||||||
|
bool bMatches = Updated.Num() == FullNextSet.Num();
|
||||||
|
if (bMatches)
|
||||||
|
{
|
||||||
|
for (const FVoxelTileKey& Tile : Updated)
|
||||||
|
{
|
||||||
|
if (!FullNextSet.Contains(Tile))
|
||||||
|
{
|
||||||
|
bMatches = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!bMatches)
|
||||||
|
{
|
||||||
|
++DeltaFailures;
|
||||||
|
if (FirstDeltaFailure.IsEmpty())
|
||||||
|
{
|
||||||
|
FirstDeltaFailure = FString::Printf(
|
||||||
|
TEXT("center=(%d,%d,%d) step=(%d,%d,%d) radius=%d level=%d clamp=%d entering=%d leaving=%d full=%d updated=%d"),
|
||||||
|
Center.X, Center.Y, Center.Z, Step.X, Step.Y, Step.Z,
|
||||||
|
Sweep.Radius, Sweep.MaxLevel, ClampCase,
|
||||||
|
Entering.Num(), Leaving.Num(), FullNextSet.Num(), Updated.Num());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++DeltaCases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AddInfo(FString::Printf(
|
||||||
|
TEXT("clipmap delta cases=%d failures=%d first=%s"), DeltaCases, DeltaFailures,
|
||||||
|
FirstDeltaFailure.IsEmpty() ? TEXT("none") : *FirstDeltaFailure));
|
||||||
|
TestEqual(TEXT("clipmap delta update equals a full rebuild on every tested crossing"),
|
||||||
|
DeltaFailures, 0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -182,4 +182,218 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
PreviousRingMax = RingMax;
|
PreviousRingMax = RingMax;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute the exact key difference between two adjacent clipmap centers without visiting
|
||||||
|
// unchanged ring cells. A coarser ring can change membership when its finer child ring moves,
|
||||||
|
// even when the coarser ring bounds themselves stay fixed; the cover-box delta below includes
|
||||||
|
// those boundary keys as well.
|
||||||
|
FORCEINLINE void BuildDelta(
|
||||||
|
const FIntVector& PreviousCenter, int32 PreviousMinChunkZ, int32 PreviousMaxChunkZ,
|
||||||
|
const FIntVector& Center, int32 MinChunkZ, int32 MaxChunkZ,
|
||||||
|
const FParameters& Parameters,
|
||||||
|
TArray<FVoxelTileKey>& OutEntering, TArray<FVoxelTileKey>& OutLeaving)
|
||||||
|
{
|
||||||
|
struct FBox
|
||||||
|
{
|
||||||
|
FIntVector Min = FIntVector::ZeroValue;
|
||||||
|
FIntVector Max = FIntVector::ZeroValue;
|
||||||
|
bool bValid = false;
|
||||||
|
};
|
||||||
|
OutEntering.Reset();
|
||||||
|
OutLeaving.Reset();
|
||||||
|
|
||||||
|
const int32 Radius = FMath::Max(1, Parameters.ClipRadius);
|
||||||
|
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
|
||||||
|
const int32 OuterRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
|
||||||
|
FBox PreviousValid[9];
|
||||||
|
FBox CurrentValid[9];
|
||||||
|
FBox PreviousCovered[9];
|
||||||
|
FBox CurrentCovered[9];
|
||||||
|
const auto BuildRings = [&](const FIntVector& RingCenter, int32 RingMinChunkZ,
|
||||||
|
int32 RingMaxChunkZ, FBox* OutValid, FBox* OutCovered)
|
||||||
|
{
|
||||||
|
FIntVector ChildRingMin = FIntVector::ZeroValue;
|
||||||
|
FIntVector ChildRingMax = FIntVector::ZeroValue;
|
||||||
|
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
||||||
|
{
|
||||||
|
const int32 Pow = static_cast<int32>(uint32{1} << static_cast<uint32>(Level));
|
||||||
|
const FIntVector CenterLevel = FloorDiv(RingCenter, Pow);
|
||||||
|
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
||||||
|
const int32 RingAlignment = Level < MaxLevel ? 2 : 1;
|
||||||
|
FIntVector RingMin, RingMax;
|
||||||
|
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
||||||
|
FBox Valid;
|
||||||
|
Valid.Min = RingMin;
|
||||||
|
Valid.Max = RingMax;
|
||||||
|
Valid.Min.Z = FMath::Max(Valid.Min.Z, FloorDiv(RingMinChunkZ, Pow));
|
||||||
|
Valid.Max.Z = FMath::Min(Valid.Max.Z, FloorDiv(RingMaxChunkZ, Pow));
|
||||||
|
Valid.bValid = Valid.Min.X <= Valid.Max.X
|
||||||
|
&& Valid.Min.Y <= Valid.Max.Y && Valid.Min.Z <= Valid.Max.Z;
|
||||||
|
OutValid[Level] = Valid;
|
||||||
|
|
||||||
|
FBox Covered;
|
||||||
|
if (Level > 0)
|
||||||
|
{
|
||||||
|
Covered.Min = FIntVector(
|
||||||
|
CeilDiv(ChildRingMin.X, 2), CeilDiv(ChildRingMin.Y, 2),
|
||||||
|
CeilDiv(ChildRingMin.Z, 2));
|
||||||
|
Covered.Max = FIntVector(
|
||||||
|
ClampInt32(FloorDivWide(static_cast<int64>(ChildRingMax.X) - 1, 2)),
|
||||||
|
ClampInt32(FloorDivWide(static_cast<int64>(ChildRingMax.Y) - 1, 2)),
|
||||||
|
ClampInt32(FloorDivWide(static_cast<int64>(ChildRingMax.Z) - 1, 2)));
|
||||||
|
Covered.bValid = Covered.Min.X <= Covered.Max.X
|
||||||
|
&& Covered.Min.Y <= Covered.Max.Y && Covered.Min.Z <= Covered.Max.Z;
|
||||||
|
}
|
||||||
|
OutCovered[Level] = Covered;
|
||||||
|
ChildRingMin = RingMin;
|
||||||
|
ChildRingMax = RingMax;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
BuildRings(PreviousCenter, PreviousMinChunkZ, PreviousMaxChunkZ,
|
||||||
|
PreviousValid, PreviousCovered);
|
||||||
|
BuildRings(Center, MinChunkZ, MaxChunkZ, CurrentValid, CurrentCovered);
|
||||||
|
|
||||||
|
const auto Intersect = [](const FBox& A, const FBox& B)
|
||||||
|
{
|
||||||
|
FBox Result;
|
||||||
|
if (!A.bValid || !B.bValid) return Result;
|
||||||
|
Result.Min = FIntVector(
|
||||||
|
FMath::Max(A.Min.X, B.Min.X), FMath::Max(A.Min.Y, B.Min.Y),
|
||||||
|
FMath::Max(A.Min.Z, B.Min.Z));
|
||||||
|
Result.Max = FIntVector(
|
||||||
|
FMath::Min(A.Max.X, B.Max.X), FMath::Min(A.Max.Y, B.Max.Y),
|
||||||
|
FMath::Min(A.Max.Z, B.Max.Z));
|
||||||
|
Result.bValid = Result.Min.X <= Result.Max.X
|
||||||
|
&& Result.Min.Y <= Result.Max.Y && Result.Min.Z <= Result.Max.Z;
|
||||||
|
return Result;
|
||||||
|
};
|
||||||
|
const auto AppendDifference = [&Intersect](const FBox& A, const FBox& B, auto& OutBoxes)
|
||||||
|
{
|
||||||
|
if (!A.bValid) return;
|
||||||
|
const FBox Common = Intersect(A, B);
|
||||||
|
if (!Common.bValid)
|
||||||
|
{
|
||||||
|
OutBoxes.Add(A);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Six disjoint slabs cover A minus Common. The later slabs are narrowed to the
|
||||||
|
// common extent on each already-partitioned axis, so no key is emitted twice.
|
||||||
|
FBox Slab = A;
|
||||||
|
if (A.Min.X < Common.Min.X)
|
||||||
|
{
|
||||||
|
Slab.Max.X = Common.Min.X - 1;
|
||||||
|
Slab.bValid = Slab.Min.X <= Slab.Max.X;
|
||||||
|
if (Slab.bValid) OutBoxes.Add(Slab);
|
||||||
|
}
|
||||||
|
Slab = A;
|
||||||
|
if (Common.Max.X < A.Max.X)
|
||||||
|
{
|
||||||
|
Slab.Min.X = Common.Max.X + 1;
|
||||||
|
Slab.bValid = Slab.Min.X <= Slab.Max.X;
|
||||||
|
if (Slab.bValid) OutBoxes.Add(Slab);
|
||||||
|
}
|
||||||
|
|
||||||
|
Slab = A;
|
||||||
|
Slab.Min.X = Common.Min.X;
|
||||||
|
Slab.Max.X = Common.Max.X;
|
||||||
|
if (A.Min.Y < Common.Min.Y)
|
||||||
|
{
|
||||||
|
Slab.Max.Y = Common.Min.Y - 1;
|
||||||
|
Slab.bValid = Slab.Min.Y <= Slab.Max.Y;
|
||||||
|
if (Slab.bValid) OutBoxes.Add(Slab);
|
||||||
|
}
|
||||||
|
Slab = A;
|
||||||
|
Slab.Min.X = Common.Min.X;
|
||||||
|
Slab.Max.X = Common.Max.X;
|
||||||
|
if (Common.Max.Y < A.Max.Y)
|
||||||
|
{
|
||||||
|
Slab.Min.Y = Common.Max.Y + 1;
|
||||||
|
Slab.bValid = Slab.Min.Y <= Slab.Max.Y;
|
||||||
|
if (Slab.bValid) OutBoxes.Add(Slab);
|
||||||
|
}
|
||||||
|
|
||||||
|
Slab = A;
|
||||||
|
Slab.Min.X = Common.Min.X;
|
||||||
|
Slab.Max.X = Common.Max.X;
|
||||||
|
Slab.Min.Y = Common.Min.Y;
|
||||||
|
Slab.Max.Y = Common.Max.Y;
|
||||||
|
if (A.Min.Z < Common.Min.Z)
|
||||||
|
{
|
||||||
|
Slab.Max.Z = Common.Min.Z - 1;
|
||||||
|
Slab.bValid = Slab.Min.Z <= Slab.Max.Z;
|
||||||
|
if (Slab.bValid) OutBoxes.Add(Slab);
|
||||||
|
}
|
||||||
|
Slab = A;
|
||||||
|
Slab.Min.X = Common.Min.X;
|
||||||
|
Slab.Max.X = Common.Max.X;
|
||||||
|
Slab.Min.Y = Common.Min.Y;
|
||||||
|
Slab.Max.Y = Common.Max.Y;
|
||||||
|
if (Common.Max.Z < A.Max.Z)
|
||||||
|
{
|
||||||
|
Slab.Min.Z = Common.Max.Z + 1;
|
||||||
|
Slab.bValid = Slab.Min.Z <= Slab.Max.Z;
|
||||||
|
if (Slab.bValid) OutBoxes.Add(Slab);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const auto Contains = [](const FBox& Box, const FIntVector& Point)
|
||||||
|
{
|
||||||
|
return Box.bValid && Point.X >= Box.Min.X && Point.X <= Box.Max.X
|
||||||
|
&& Point.Y >= Box.Min.Y && Point.Y <= Box.Max.Y
|
||||||
|
&& Point.Z >= Box.Min.Z && Point.Z <= Box.Max.Z;
|
||||||
|
};
|
||||||
|
const auto ForEachBox = [](const FBox& Box, const auto& Callback)
|
||||||
|
{
|
||||||
|
if (!Box.bValid) return;
|
||||||
|
for (int64 Z = Box.Min.Z; Z <= Box.Max.Z; ++Z)
|
||||||
|
for (int64 Y = Box.Min.Y; Y <= Box.Max.Y; ++Y)
|
||||||
|
for (int64 X = Box.Min.X; X <= Box.Max.X; ++X)
|
||||||
|
{
|
||||||
|
Callback(FIntVector(static_cast<int32>(X), static_cast<int32>(Y),
|
||||||
|
static_cast<int32>(Z)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
||||||
|
{
|
||||||
|
TArray<FBox, TInlineAllocator<16>> ChangedBoxes;
|
||||||
|
AppendDifference(CurrentValid[Level], PreviousValid[Level], ChangedBoxes);
|
||||||
|
AppendDifference(PreviousValid[Level], CurrentValid[Level], ChangedBoxes);
|
||||||
|
|
||||||
|
if (Level > 0)
|
||||||
|
{
|
||||||
|
const FBox CommonValid = Intersect(CurrentValid[Level], PreviousValid[Level]);
|
||||||
|
if (CommonValid.bValid)
|
||||||
|
{
|
||||||
|
TArray<FBox, TInlineAllocator<16>> CoverDifferences;
|
||||||
|
AppendDifference(CurrentCovered[Level], PreviousCovered[Level], CoverDifferences);
|
||||||
|
AppendDifference(PreviousCovered[Level], CurrentCovered[Level], CoverDifferences);
|
||||||
|
for (const FBox& CoverDifference : CoverDifferences)
|
||||||
|
{
|
||||||
|
const FBox ChangedCover = Intersect(CoverDifference, CommonValid);
|
||||||
|
if (ChangedCover.bValid) ChangedBoxes.Add(ChangedCover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const FBox& ChangedBox : ChangedBoxes)
|
||||||
|
{
|
||||||
|
ForEachBox(ChangedBox, [&](const FIntVector& Coord)
|
||||||
|
{
|
||||||
|
const bool bCurrentMember = Contains(CurrentValid[Level], Coord)
|
||||||
|
&& !Contains(CurrentCovered[Level], Coord);
|
||||||
|
const bool bPreviousMember = Contains(PreviousValid[Level], Coord)
|
||||||
|
&& !Contains(PreviousCovered[Level], Coord);
|
||||||
|
if (bCurrentMember && !bPreviousMember)
|
||||||
|
{
|
||||||
|
OutEntering.Emplace(Coord, Level);
|
||||||
|
}
|
||||||
|
else if (bPreviousMember && !bCurrentMember)
|
||||||
|
{
|
||||||
|
OutLeaving.Emplace(Coord, Level);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1254,6 +1254,21 @@ public:
|
|||||||
float StartupTraceSpeedCmPerSecond = 0.0f;
|
float StartupTraceSpeedCmPerSecond = 0.0f;
|
||||||
bool bSpeedAwareTravelMode = false;
|
bool bSpeedAwareTravelMode = false;
|
||||||
bool bSpeedAwareFastMode = false;
|
bool bSpeedAwareFastMode = false;
|
||||||
|
TArray<FVoxelTileKey> CachedCoreDesiredTiles;
|
||||||
|
TSet<FVoxelTileKey> CachedCoreDesiredTileSet;
|
||||||
|
FIntVector CachedCoreDesiredCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
||||||
|
bool bHasCachedCoreDesiredTiles = false;
|
||||||
|
TArray<FVoxelTileKey> CachedDesiredCandidateKeys;
|
||||||
|
TSet<FVoxelTileKey> CachedDesiredCandidateSet;
|
||||||
|
TSet<FVoxelTileKey> CachedDesiredBaseVisualTiles;
|
||||||
|
TSet<FVoxelTileKey> CachedDesiredOverlayTiles;
|
||||||
|
FIntVector CachedDesiredMaterializationCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
||||||
|
bool bHasCachedDesiredMaterialization = false;
|
||||||
|
TArray<FVoxelTileKey> CachedDesiredPriorityOrder;
|
||||||
|
FIntVector CachedDesiredPriorityCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
||||||
|
bool bHasCachedDesiredPriorityOrder = false;
|
||||||
|
int32 IncrementalDesiredCenterCrossings = 0;
|
||||||
|
int32 IncrementalDesiredMaterializationUpdates = 0;
|
||||||
TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
|
TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
|
||||||
TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile
|
TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile
|
||||||
// Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this
|
// Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# L157 — near terrain at 30 m/s
|
||||||
|
|
||||||
|
**Result:** Early admission raised moving near coverage, but the W8 experiment did not meet the full latency and worst-tick bar. Keep both experiment controls disabled by default. The UE host was built from `/home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l157-tree/Source`; the VFHost source symlink has since been restored.
|
||||||
|
|
||||||
|
## Measurements
|
||||||
|
|
||||||
|
Five interleaved pairs used the same 30 m/s, 12 s movement plus 5 s dwell, 8 effective tile slots, and four-strate owner plan. Each run had 45–46 moving samples. The uncounted candidate smoke run warmed the same UE binary. The machine was quiet before the matrix (uptime load averages 1.09/1.40/1.22).
|
||||||
|
|
||||||
|
| Moving metric | `assembler/world` median | Early admission + 1 ms cap median | Paired median delta |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| LOD0–2 ready | 36.06% | 48.64% | +10.76 pp |
|
||||||
|
| 55° forward-cone ready | 12.12% | 16.86% | +5.95 pp |
|
||||||
|
| Game-thread tick p99 | 12.51 ms | 10.93 ms | +0.07 ms |
|
||||||
|
| Worst tick per run, median | 14.67 ms | 14.52 ms | +0.18 ms |
|
||||||
|
| Far holes | 4.00% | 5.82% | −0.13 pp |
|
||||||
|
| Initial near-set arrival | 2.80 s | 3.21 s | +0.01 s |
|
||||||
|
|
||||||
|
The p99 median is under 11 ms, but only three candidate runs were under that limit; the other two were 15.75 and 18.17 ms. The worst observed candidate tick was 19.88 ms versus 17.75 ms for baseline. Far-hole results were mixed: the paired median improved slightly, while two candidate pairs regressed and the worst candidate run had 12.48% holes versus a 7.08% baseline worst. Arrival was effectively tied by paired median, with one candidate pair 0.88 s slower. The combined bar therefore fails on repeatability and worst tick; far holes are not robustly no-worse.
|
||||||
|
|
||||||
|
The apply cap lowered median apply p99 from 2.11 to 1.40 ms, but it is not a hard per-frame ceiling: candidate apply p99 reached 6.61 ms and the maximum was 7.58 ms. The loop checks elapsed time after finishing a result. Candidate stream-update p99 reached 15.07 and 13.52 ms in the same two outlier runs. Historical L122 frame joins likewise show that applying extra results adds work on candidate tail frames (about 1.0–1.5 ms), while `UpdateChunks`/desired-set work forms the larger tail; `level_admit` and `submit_tiles` p99 were not consistently higher. The cap cannot make the whole tick fit when update work already exceeds the frame budget.
|
||||||
|
|
||||||
|
## Direction and next lever
|
||||||
|
|
||||||
|
The old metric counts every desired LOD0–2 tile equally, including tiles behind the player. Desired tiles split about 50.44% ahead and 49.56% behind; only 23.20% fell inside the 55° forward cone. Behind readiness was 50.93% baseline / 68.49% candidate, versus 21.71% / 30.60% ahead. The all-around count therefore overstates what is ready in the direction of travel.
|
||||||
|
|
||||||
|
For VR, readiness against the HMD/camera frustum is the useful visibility metric. The velocity-forward cone measured here is a movement proxy, not a head-pose measurement. Report it beside the old all-around residency count. No additional speed fix is validated; a narrow forward-cone admission probe is the next cheap experiment, while desired-set rebuild/update work remains the measured latency target.
|
||||||
|
|
||||||
|
## Checks and limits
|
||||||
|
|
||||||
|
- UBT built `VFHostEditor` successfully, 98/98 actions, using `-NoUBA -MaxParallelActions=4`. It compiled `Source/VoxelForge/Private/VoxelWorld.cpp` from the L157 worktree. Three existing RealtimeMesh deprecation warnings remained.
|
||||||
|
- Fast tier: 268 PASS, 0 FAIL, 5 older-baseline diagnostics. Current canonical owner hashes match L130; W1/W12 hashes match for both pinned owners. Random-camera tile hashes also match across W1/W8/W12.
|
||||||
|
- UE `TileHashDump` comparison was inconclusive: both runs logged `records=0`, despite streamed meshes. No direct UE tile-hash claim is made. The L157 changes are limited to admission/apply controls and trace accounting; no generator or mesher source changed.
|
||||||
|
- W12 movement timing and HMD-pose visibility were not run. W12 was skipped after W8 failed the worst-tick bar; direct UE hash capture emitted no records.
|
||||||
|
- `RunL157Movement.py` passed `py_compile`; `git diff --check` passed.
|
||||||
|
|
||||||
|
Logs and paired data are in `Saved/Linux/l157/`; movement traces and summary are in `Saved/Linux/l157/ue/movement/`.
|
||||||
|
|
||||||
|
Changed Unreal source: `Source/VoxelForge/Private/VoxelWorld.cpp`.
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Interleaved L157 UE movement runs for critical-gate and apply-budget ablations."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
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")
|
||||||
|
OUT = ROOT / "Saved/Linux/l157/ue/movement"
|
||||||
|
HOST_SAVED = HOST / "Saved/Linux/l157"
|
||||||
|
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l157"
|
||||||
|
SPAWN_M = (19.166015625, -14.2001953125, -753.3740234375)
|
||||||
|
MOVE_START_S = 6.0
|
||||||
|
|
||||||
|
CONDITIONS = {
|
||||||
|
"assembler_world": {"wait_for_critical": 1, "apply_budget_ms": 0.0},
|
||||||
|
"early_admission_apply_1ms": {"wait_for_critical": 0, "apply_budget_ms": 1.0},
|
||||||
|
}
|
||||||
|
ORDER = {
|
||||||
|
1: ("assembler_world", "early_admission_apply_1ms"),
|
||||||
|
2: ("early_admission_apply_1ms", "assembler_world"),
|
||||||
|
3: ("assembler_world", "early_admission_apply_1ms"),
|
||||||
|
4: ("early_admission_apply_1ms", "assembler_world"),
|
||||||
|
5: ("assembler_world", "early_admission_apply_1ms"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def percentile(values: list[float], fraction: float = 0.99) -> float | None:
|
||||||
|
if not values:
|
||||||
|
return None
|
||||||
|
ordered = sorted(values)
|
||||||
|
return ordered[max(0, min(len(ordered) - 1, math.ceil(len(ordered) * fraction) - 1))]
|
||||||
|
|
||||||
|
|
||||||
|
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 mean(values: list[float]) -> float | None:
|
||||||
|
return statistics.mean(values) if values else None
|
||||||
|
|
||||||
|
|
||||||
|
def percent(numerator: int | float, denominator: int | float) -> float | None:
|
||||||
|
return 100.0 * numerator / denominator if denominator > 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def run(condition: str, repetition: int, tile_hash_dump: Path | None = None) -> dict:
|
||||||
|
config = CONDITIONS[condition]
|
||||||
|
run_name = f"fast-r{repetition}"
|
||||||
|
out = OUT / condition
|
||||||
|
host_out = HOST_PLUGIN_SAVED / condition / "output"
|
||||||
|
userdir = HOST_SAVED / "user" / condition / run_name
|
||||||
|
zendata = HOST_SAVED / "zen" / condition / run_name
|
||||||
|
ddc = HOST_SAVED / "DDC"
|
||||||
|
trace = out / f"{run_name}.trace.jsonl"
|
||||||
|
abslog = out / f"{run_name}.log"
|
||||||
|
stdout = out / f"{run_name}.stdout.log"
|
||||||
|
export = host_out / run_name
|
||||||
|
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.StreamingCoarseFirst=1",
|
||||||
|
"-voxel.StreamingDynamicNearReserve=1",
|
||||||
|
"-voxel.StreamingLegacyPendingResultCap=0",
|
||||||
|
"-voxel.CoreRegionConcurrentAdmission=1",
|
||||||
|
"-voxel.StreamingResultBacklogCap=256", "-voxel.StreamingReadyQueueDepth=8",
|
||||||
|
"-voxel.StreamingBackgroundWorkerCap=1", "-voxel.CoreColumnCacheDirectMapped=0",
|
||||||
|
"-voxel.StreamingFarApplySupplement=1",
|
||||||
|
f"-voxel.StreamingFastTravelWaitForAllCriticalTiles={config['wait_for_critical']}",
|
||||||
|
f"-voxel.StreamingFastTravelApplyBudgetMilliseconds={config['apply_budget_ms']:.1f}",
|
||||||
|
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
|
||||||
|
"-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=3000.0",
|
||||||
|
f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}",
|
||||||
|
"-voxel.TestMoveDurationSeconds=12.0", "-voxel.TestExitSeconds=23.0",
|
||||||
|
"-voxel.TestMoveDirection=1,0,0", "-voxel.StreamingSchedulerPolicy=3",
|
||||||
|
"-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={abslog}",
|
||||||
|
]
|
||||||
|
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()}")
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command]
|
||||||
|
started = time.monotonic()
|
||||||
|
with stdout.open("w", encoding="utf-8") as stream:
|
||||||
|
process = subprocess.run(
|
||||||
|
wrapped, cwd=ROOT, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
check=False, timeout=1740)
|
||||||
|
|
||||||
|
events = read_events(trace)
|
||||||
|
samples = [event for event in events if event.get("type") == "streaming_sample"]
|
||||||
|
moving = [event for event in samples if float(event.get("speed_cm_s", 0.0)) >= 50.0]
|
||||||
|
stop_event = next((event for event in events
|
||||||
|
if event.get("type") == "streaming_test_move_stopped"), None)
|
||||||
|
backend = next((event for event in events if event.get("type") == "worker_backend_config"), {})
|
||||||
|
worker_cap = next((event for event in events
|
||||||
|
if event.get("type") == "streaming_worker_cap_readback"), {})
|
||||||
|
if stop_event is None:
|
||||||
|
raise RuntimeError(f"missing move-stop event: {trace}")
|
||||||
|
stop_s = float(stop_event.get("t_s", 0.0))
|
||||||
|
move_start_s = min((float(event.get("t_s", 0.0)) for event in moving), default=float("inf"))
|
||||||
|
dwell = [event for event in samples if float(event.get("t_s", 0.0)) >= stop_s
|
||||||
|
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||||
|
arrival = [event for event in samples if float(event.get("t_s", 0.0)) < move_start_s
|
||||||
|
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||||
|
if process.returncode or len(moving) < 40 or len(dwell) < 16:
|
||||||
|
raise RuntimeError(f"incomplete {condition} run: {trace}; exit={process.returncode}; "
|
||||||
|
f"move={len(moving)} dwell={len(dwell)}")
|
||||||
|
if (int(backend.get("streaming_fast_travel_wait_for_all_critical_tiles", -1))
|
||||||
|
!= config["wait_for_critical"]
|
||||||
|
or abs(float(backend.get("streaming_fast_travel_apply_budget_ms", -1.0))
|
||||||
|
- config["apply_budget_ms"]) > 0.01
|
||||||
|
or int(worker_cap.get("streaming_background_worker_cap", -1)) != 1
|
||||||
|
or int(backend.get("effective_tile_slots", -1)) != 8
|
||||||
|
or int(backend.get("streaming_far_apply_supplement", -1)) != 1):
|
||||||
|
raise RuntimeError(f"CVar/worker readback mismatch: {backend}; {worker_cap}")
|
||||||
|
|
||||||
|
moving_metrics = []
|
||||||
|
for event in moving:
|
||||||
|
desired = event.get("desired_by_lod", [0] * 9)
|
||||||
|
resident = event.get("resident_by_lod", [0] * 9)
|
||||||
|
near_desired = sum(desired[:3])
|
||||||
|
cone_desired = int(event.get("near_forward_cone_desired", 0))
|
||||||
|
weighted_desired = float(event.get("near_forward_weighted_desired", 0.0))
|
||||||
|
moving_metrics.append({
|
||||||
|
"near_ready_pct": percent(sum(resident[:3]), near_desired) or 0.0,
|
||||||
|
"lod0_ready_pct": percent(resident[0], desired[0]) or 0.0,
|
||||||
|
"lod1_ready_pct": percent(resident[1], desired[1]) or 0.0,
|
||||||
|
"lod2_ready_pct": percent(resident[2], desired[2]) or 0.0,
|
||||||
|
"far_hole_pct": percent(
|
||||||
|
sum(desired[3:]) - sum(event.get("covered_by_lod", resident)[3:]),
|
||||||
|
sum(desired[3:])) or 0.0,
|
||||||
|
"ahead_share_pct": percent(int(event.get("near_ahead_desired", 0)), near_desired),
|
||||||
|
"behind_share_pct": percent(int(event.get("near_behind_desired", 0)), near_desired),
|
||||||
|
"side_share_pct": 100.0 - (
|
||||||
|
percent(int(event.get("near_ahead_desired", 0)), near_desired) or 0.0
|
||||||
|
) - (
|
||||||
|
percent(int(event.get("near_behind_desired", 0)), near_desired) or 0.0
|
||||||
|
),
|
||||||
|
"ahead_ready_pct": percent(
|
||||||
|
int(event.get("near_ahead_resident", 0)),
|
||||||
|
int(event.get("near_ahead_desired", 0))),
|
||||||
|
"behind_ready_pct": percent(
|
||||||
|
int(event.get("near_behind_resident", 0)),
|
||||||
|
int(event.get("near_behind_desired", 0))),
|
||||||
|
"forward_cone_ready_pct": percent(
|
||||||
|
int(event.get("near_forward_cone_resident", 0)), cone_desired),
|
||||||
|
"forward_cone_share_pct": percent(cone_desired, near_desired),
|
||||||
|
"forward_weighted_ready_pct": percent(
|
||||||
|
float(event.get("near_forward_weighted_resident", 0.0)), weighted_desired),
|
||||||
|
})
|
||||||
|
|
||||||
|
arrival_near_settled = next((event for event in arrival
|
||||||
|
if sum(event.get("desired_by_lod", [0] * 9)[:3]) > 0
|
||||||
|
and sum(event.get("settled_by_lod", [0] * 9)[:3])
|
||||||
|
>= sum(event.get("desired_by_lod", [0] * 9)[:3])), None)
|
||||||
|
dwell_complete = next((event for event in dwell
|
||||||
|
if sum(event.get("resident_by_lod", [0] * 9)[:3])
|
||||||
|
>= sum(event.get("desired_by_lod", [0] * 9)[:3])
|
||||||
|
and sum(event.get("desired_by_lod", [0] * 9)[3:])
|
||||||
|
<= sum(event.get("covered_by_lod", [0] * 9)[3:])), None)
|
||||||
|
|
||||||
|
work = [event for event in events if event.get("type") == "game_thread_work_frame"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
applies = {event.get("frame"): event for event in events
|
||||||
|
if event.get("type") == "apply_frame"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||||
|
scheduler = {event.get("frame"): event for event in events
|
||||||
|
if event.get("type") == "scheduler_admission"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||||
|
frames = []
|
||||||
|
for event in work:
|
||||||
|
apply = applies.get(event.get("frame"), {})
|
||||||
|
admit = scheduler.get(event.get("frame"), {})
|
||||||
|
frames.append({
|
||||||
|
"frame": event.get("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)),
|
||||||
|
"submit_tiles_ms": float(admit.get("submit_tiles_ms", 0.0)),
|
||||||
|
"level_admit_ms": float(admit.get("level_admit_ms", 0.0)),
|
||||||
|
})
|
||||||
|
p99_field = lambda key: percentile([float(row[key]) for row in frames])
|
||||||
|
max_field = lambda key: max((float(row[key]) for row in frames), default=None)
|
||||||
|
dwell_complete_s = (float(dwell_complete.get("t_s", 0.0)) - stop_s
|
||||||
|
if dwell_complete else None)
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"condition": condition,
|
||||||
|
"repetition": repetition,
|
||||||
|
"speed_mps": 30.0,
|
||||||
|
"wait_for_all_critical_tiles": config["wait_for_critical"],
|
||||||
|
"fast_travel_apply_budget_ms": config["apply_budget_ms"],
|
||||||
|
"effective_tile_slots": backend.get("effective_tile_slots"),
|
||||||
|
"exit_code": process.returncode,
|
||||||
|
"wall_s": round(time.monotonic() - started, 3),
|
||||||
|
"moving_samples": len(moving),
|
||||||
|
"dwell_samples": len(dwell),
|
||||||
|
"arrival_samples": len(arrival),
|
||||||
|
"near_ready_moving_mean_pct": mean([m["near_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod0_ready_moving_mean_pct": mean([m["lod0_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod1_ready_moving_mean_pct": mean([m["lod1_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod2_ready_moving_mean_pct": mean([m["lod2_ready_pct"] for m in moving_metrics]),
|
||||||
|
"far_holes_moving_mean_pct": mean([m["far_hole_pct"] for m in moving_metrics]),
|
||||||
|
"near_ahead_desired_share_pct": mean([m["ahead_share_pct"] for m in moving_metrics
|
||||||
|
if m["ahead_share_pct"] is not None]),
|
||||||
|
"near_behind_desired_share_pct": mean([m["behind_share_pct"] for m in moving_metrics
|
||||||
|
if m["behind_share_pct"] is not None]),
|
||||||
|
"near_side_desired_share_pct": mean([m["side_share_pct"] for m in moving_metrics]),
|
||||||
|
"near_ahead_ready_pct": mean([m["ahead_ready_pct"] for m in moving_metrics
|
||||||
|
if m["ahead_ready_pct"] is not None]),
|
||||||
|
"near_behind_ready_pct": mean([m["behind_ready_pct"] for m in moving_metrics
|
||||||
|
if m["behind_ready_pct"] is not None]),
|
||||||
|
"near_forward_cone_half_angle_deg": 55,
|
||||||
|
"near_forward_cone_desired_share_pct": mean([m["forward_cone_share_pct"]
|
||||||
|
for m in moving_metrics
|
||||||
|
if m["forward_cone_share_pct"] is not None]),
|
||||||
|
"near_forward_cone_ready_pct": mean([m["forward_cone_ready_pct"]
|
||||||
|
for m in moving_metrics
|
||||||
|
if m["forward_cone_ready_pct"] is not None]),
|
||||||
|
"near_forward_cosine_weighted_ready_pct": mean(
|
||||||
|
[m["forward_weighted_ready_pct"] for m in moving_metrics
|
||||||
|
if m["forward_weighted_ready_pct"] is not None]),
|
||||||
|
"arrival_lod02_settled_t_s": (
|
||||||
|
float(arrival_near_settled.get("t_s", 0.0)) if arrival_near_settled else None),
|
||||||
|
"near_ready_dwell_end_pct": (100.0 * sum(dwell[-1].get("resident_by_lod", [0] * 9)[:3])
|
||||||
|
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[:3]))),
|
||||||
|
"far_holes_dwell_end_pct": (100.0 * (
|
||||||
|
sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:])
|
||||||
|
- sum(dwell[-1].get("covered_by_lod", [0] * 9)[3:]))
|
||||||
|
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:]))),
|
||||||
|
"dwell_complete_after_stop_s": dwell_complete_s,
|
||||||
|
"game_thread_tick_p99_ms": p99_field("tick_ms"),
|
||||||
|
"game_thread_tick_max_ms": max_field("tick_ms"),
|
||||||
|
"stream_update_p99_ms": p99_field("update_chunks_ms"),
|
||||||
|
"pending_drain_p99_ms": p99_field("pending_drain_ms"),
|
||||||
|
"apply_p99_ms": p99_field("apply_ms"),
|
||||||
|
"apply_max_ms": max_field("apply_ms"),
|
||||||
|
"apply_budget_min_ms": min((f["apply_budget_ms"] for f in frames), default=None),
|
||||||
|
"apply_budget_max_ms": max((f["apply_budget_ms"] for f in frames), default=None),
|
||||||
|
"apply_time_budget_hit_frames": sum(f["time_budget_hit"] for f in frames),
|
||||||
|
"apply_frames": sum(f["meshes_applied"] > 0 for f in frames),
|
||||||
|
"apply_meshes_total": sum(f["meshes_applied"] for f in frames),
|
||||||
|
"desired_rebuild_p99_ms": p99_field("desired_rebuild_ms"),
|
||||||
|
"submit_tiles_p99_ms": p99_field("submit_tiles_ms"),
|
||||||
|
"level_admit_p99_ms": p99_field("level_admit_ms"),
|
||||||
|
"top_tick_frames": sorted(frames, key=lambda f: f["tick_ms"], reverse=True)[:5],
|
||||||
|
"trace": str(trace.relative_to(ROOT)),
|
||||||
|
"log": str(abslog.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
print("[L157Movement] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--smoke", action="store_true",
|
||||||
|
help="run one candidate trace to validate CVar readback and event fields")
|
||||||
|
parser.add_argument("--geometry-hash-check", action="store_true",
|
||||||
|
help="compare UE per-tile geometry hashes for baseline and early admission")
|
||||||
|
args = parser.parse_args()
|
||||||
|
missing = [path for path in (EDITOR, HOST / "VFHost.uproject") if not path.exists()]
|
||||||
|
if missing:
|
||||||
|
raise SystemExit(f"missing UE runner input: {missing}")
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
if args.geometry_hash_check:
|
||||||
|
hash_dir = OUT / "geometry-hash"
|
||||||
|
baseline_path = hash_dir / "assembler-world.json"
|
||||||
|
candidate_path = hash_dir / "early-admission-apply-1ms.json"
|
||||||
|
run("assembler_world", 99, baseline_path)
|
||||||
|
run("early_admission_apply_1ms", 99, candidate_path)
|
||||||
|
baseline_data = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||||
|
candidate_data = json.loads(candidate_path.read_text(encoding="utf-8"))
|
||||||
|
key_fields = ("level", "tile_x", "tile_y", "tile_z")
|
||||||
|
|
||||||
|
def indexed(data: dict) -> dict[tuple[int, ...], tuple]:
|
||||||
|
result = {}
|
||||||
|
for record in data.get("records", []):
|
||||||
|
key = tuple(int(record[field]) for field in key_fields)
|
||||||
|
result[key] = (str(record.get("geometry_hash", "")),
|
||||||
|
int(record.get("triangle_count", -1)),
|
||||||
|
bool(record.get("empty", False)))
|
||||||
|
return result
|
||||||
|
|
||||||
|
baseline_hashes = indexed(baseline_data)
|
||||||
|
candidate_hashes = indexed(candidate_data)
|
||||||
|
common = baseline_hashes.keys() & candidate_hashes.keys()
|
||||||
|
mismatches = [key for key in common
|
||||||
|
if baseline_hashes[key] != candidate_hashes[key]]
|
||||||
|
result = {
|
||||||
|
"baseline_records": len(baseline_hashes),
|
||||||
|
"candidate_records": len(candidate_hashes),
|
||||||
|
"common_records": len(common),
|
||||||
|
"hash_mismatches": len(mismatches),
|
||||||
|
"baseline_only_records": len(baseline_hashes.keys() - candidate_hashes.keys()),
|
||||||
|
"candidate_only_records": len(candidate_hashes.keys() - baseline_hashes.keys()),
|
||||||
|
"baseline_dump": str(baseline_path.relative_to(ROOT)),
|
||||||
|
"candidate_dump": str(candidate_path.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
(hash_dir / "comparison.json").write_text(
|
||||||
|
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
print("[L157GeometryHash] " + json.dumps(result, sort_keys=True), flush=True)
|
||||||
|
return 0 if len(common) >= 100 and not mismatches else 1
|
||||||
|
if args.smoke:
|
||||||
|
run("early_admission_apply_1ms", 0)
|
||||||
|
return 0
|
||||||
|
rows = []
|
||||||
|
partial = OUT / "runs-partial.json"
|
||||||
|
for repetition in range(1, 6):
|
||||||
|
for condition in ORDER[repetition]:
|
||||||
|
row = run(condition, repetition)
|
||||||
|
rows.append(row)
|
||||||
|
partial.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
|
||||||
|
metrics = (
|
||||||
|
"near_ready_moving_mean_pct", "far_holes_moving_mean_pct",
|
||||||
|
"game_thread_tick_p99_ms", "game_thread_tick_max_ms",
|
||||||
|
"arrival_lod02_settled_t_s", "dwell_complete_after_stop_s",
|
||||||
|
"near_ahead_desired_share_pct", "near_behind_desired_share_pct",
|
||||||
|
"near_side_desired_share_pct", "near_forward_cone_desired_share_pct",
|
||||||
|
"near_ahead_ready_pct", "near_behind_ready_pct",
|
||||||
|
"near_forward_cone_ready_pct", "near_forward_cosine_weighted_ready_pct",
|
||||||
|
"apply_p99_ms", "desired_rebuild_p99_ms", "level_admit_p99_ms",
|
||||||
|
)
|
||||||
|
medians = {
|
||||||
|
condition: {metric: statistics.median(
|
||||||
|
row[metric] for row in rows if row["condition"] == condition)
|
||||||
|
for metric in metrics}
|
||||||
|
for condition in CONDITIONS
|
||||||
|
}
|
||||||
|
pairs = []
|
||||||
|
for repetition in range(1, 6):
|
||||||
|
by_condition = {row["condition"]: row for row in rows
|
||||||
|
if row["repetition"] == repetition}
|
||||||
|
for candidate in ("early_admission_apply_1ms",):
|
||||||
|
pairs.append({
|
||||||
|
"repetition": repetition,
|
||||||
|
"candidate": candidate,
|
||||||
|
**{f"{metric}_delta": by_condition[candidate][metric]
|
||||||
|
- by_condition["assembler_world"][metric]
|
||||||
|
for metric in metrics if by_condition[candidate][metric] is not None
|
||||||
|
and by_condition["assembler_world"][metric] is not None},
|
||||||
|
})
|
||||||
|
summary = {
|
||||||
|
"source_commit": subprocess.run(
|
||||||
|
["git", "-c", f"safe.directory={ROOT}", "rev-parse", "HEAD"],
|
||||||
|
cwd=ROOT, check=True, capture_output=True, text=True).stdout.strip(),
|
||||||
|
"protocol": "Five interleaved 30 m/s UE runs per condition; 12 s move plus 5 s dwell; 8 effective tile slots.",
|
||||||
|
"conditions": CONDITIONS,
|
||||||
|
"movement_medians": medians,
|
||||||
|
"paired_deltas_vs_assembler_world": pairs,
|
||||||
|
"runs": rows,
|
||||||
|
}
|
||||||
|
(OUT / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
print("[L157Summary] " + json.dumps({"movement_medians": medians,
|
||||||
|
"runs": len(rows)}, sort_keys=True), flush=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# L160 — ahead-first admission and desired rebuild cost
|
||||||
|
|
||||||
|
**Result:** Forward-first admission plus same-center rebuild suppression and Core-key caching raised median 55° travel-cone readiness by 3.75 percentage points. All five candidate game-thread p99s were at or below 11 ms, but the full bar failed: worst ticks regressed in 3/5 pairs, far holes in 3/5, and arrival in 2/5. Median all-around LOD0–2 readiness also fell. Keep the experiment controls off by default.
|
||||||
|
|
||||||
|
## Measurements
|
||||||
|
|
||||||
|
Five interleaved W8 UE pairs ran at 30 m/s for 12 s plus a 5 s dwell, with eight effective tile slots. The pre-run load averages were 0.15 / 1.12 / 1.49. Each run recorded 45 center crossings. The forward-cone value uses travel direction: null-RHI supplied no camera-forward samples. These are desired-residency counts, not measured HMD visibility.
|
||||||
|
|
||||||
|
| Moving readiness | `assembler/world` median | Candidate median | Paired median change |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| All-around LOD0–2 | 39.46% | 36.59% | −3.07 pp |
|
||||||
|
| 55° forward cone | 14.19% | 18.24% | +3.75 pp |
|
||||||
|
|
||||||
|
The cone covered about 23% of desired LOD0–2 tiles in both conditions. The cone improvement is clear, while the all-around average fell; behind readiness was 55.52% baseline versus 50.03% candidate.
|
||||||
|
|
||||||
|
| Gate metric | `assembler/world` median | Candidate median | Candidate result |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| Game-thread tick p99 | 10.571 ms | 10.509 ms | All five candidate runs: 10.106–10.560 ms; pass ≤11 ms |
|
||||||
|
| Worst tick per run | 13.615 ms | 14.057 ms | No-worse failed in pairs 1, 2, 4; observed maxima 15.092 / 15.141 ms |
|
||||||
|
| Far holes while moving | 3.238% | 3.260% | No-worse failed in pairs 2, 4, 5 |
|
||||||
|
| First settled LOD0–2 arrival | 2.458 s | 2.460 s | No-worse failed in pairs 1 and 3 |
|
||||||
|
|
||||||
|
The median `UpdateChunks` p99 changed from 9.30 to 9.18 ms. This small difference did not remove the worst-tick regressions.
|
||||||
|
|
||||||
|
### Admission order
|
||||||
|
|
||||||
|
The baseline’s first twelve submitted near tiles were four travel-ahead critical tiles (direction class 1), then three forward-cone critical tiles (class 0), then five forward-cone regular tiles. Candidate order put the three cone critical tiles first, followed by the four travel-ahead critical tiles, then the same cone regular lane. Within cone and ahead classes, entries were ordered by distance with coordinate tie-breaks; side and behind entries retained stable order. Here class 0 is the 55° camera cone when available, or the travel cone in these null-RHI runs; class 1 is travel-ahead outside the cone.
|
||||||
|
|
||||||
|
### Desired-set rebuilds
|
||||||
|
|
||||||
|
The set was not rebuilt only at center crossings. With exactly 45 crossings per run, baseline rebuilt on 313–334 frames (median 324, or 7.20 rebuilds per crossing). The candidate rebuilt on 193–211 frames (median 200, or 4.44 per crossing). Baseline recorded 171–198 same-center Core-region requests per run; candidate recorded 148–166.
|
||||||
|
|
||||||
|
The nonzero `BuildDesiredTiles` p95 was 7.08 ms baseline and 7.52 ms candidate (median across runs); its per-rebuild cost did not fall. The median Core `desiredTiles` query p95 fell from 1.96 to 1.25 ms, with a 77.5% median same-center cache-hit share. Median rebuild p50 was 3.91 / 3.98 ms. The candidate suppresses a forced rebuild after Core region compilation completes and reuses the Core base key list at an unchanged center. `BuildDesiredTiles` still rebuilds the UE desired and overlay sets on each trigger; this is neither an incremental full-set delta nor a time-sliced rebuild, so the remaining rebuild tail persists.
|
||||||
|
|
||||||
|
## Checks and limits
|
||||||
|
|
||||||
|
- Built `VFHostEditor` from the worktree with `taskset -c 0-11`, `-NoUBA`, and `-MaxParallelActions=4`; final build succeeded (3/3 actions). The VFHost `PluginDeps/VoxelForge/Source` symlink was restored to `/home/jahni/VoxelM/Plugins/VoxelForge/Source` afterward.
|
||||||
|
- Fast tier passed: 268 PASS, 0 FAIL, 5 existing diagnostics. Canonical owner hashes match the L130 baseline; W1/W12 owner hashes and randomized W1/W8/W12 worker comparisons match. No generator or mesher source changed.
|
||||||
|
- `RunL160Movement.py` passed `py_compile`; `git diff --check` passed. Final `git diff --check` was also run after writing this report.
|
||||||
|
- HMD/camera-pose readiness and W12 movement timing were not measured. The directional proxy falls back to travel direction because the null-RHI run has no local camera sample.
|
||||||
|
- The candidate combines admission ordering, region-completion rebuild suppression, and same-center Core-key caching, so the five-pair matrix does not isolate each effect. Geometry remained unchanged; no `WorldAssemblyEvaluatorVersion` bump was needed.
|
||||||
|
|
||||||
|
Logs and paired traces are in `Saved/Linux/l160/`; the matrix summary is `Saved/Linux/l160/ue/movement/summary.json`.
|
||||||
|
|
||||||
|
Changed Unreal source: `Source/VoxelForge/Private/VoxelWorld.cpp`, `Source/VoxelForge/Public/VoxelWorld.h`.
|
||||||
@@ -0,0 +1,521 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Interleaved L160 UE movement runs for forward admission and Core rebuild ablations."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
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")
|
||||||
|
OUT = ROOT / "Saved/Linux/l160/ue/movement"
|
||||||
|
HOST_SAVED = HOST / "Saved/Linux/l160"
|
||||||
|
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l160"
|
||||||
|
SPAWN_M = (19.166015625, -14.2001953125, -753.3740234375)
|
||||||
|
MOVE_START_S = 6.0
|
||||||
|
|
||||||
|
CONDITIONS = {
|
||||||
|
"assembler_world": {
|
||||||
|
"wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||||
|
"forward_first": 0, "rebuild_on_region_completion": 1,
|
||||||
|
"core_desired_cache": 0,
|
||||||
|
},
|
||||||
|
"forward_first": {
|
||||||
|
"wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||||
|
"forward_first": 1, "rebuild_on_region_completion": 0,
|
||||||
|
"core_desired_cache": 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ORDER = {
|
||||||
|
1: ("assembler_world", "forward_first"),
|
||||||
|
2: ("forward_first", "assembler_world"),
|
||||||
|
3: ("assembler_world", "forward_first"),
|
||||||
|
4: ("forward_first", "assembler_world"),
|
||||||
|
5: ("assembler_world", "forward_first"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def percentile(values: list[float], fraction: float = 0.99) -> float | None:
|
||||||
|
if not values:
|
||||||
|
return None
|
||||||
|
ordered = sorted(values)
|
||||||
|
return ordered[max(0, min(len(ordered) - 1, math.ceil(len(ordered) * fraction) - 1))]
|
||||||
|
|
||||||
|
|
||||||
|
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 mean(values: list[float]) -> float | None:
|
||||||
|
return statistics.mean(values) if values else None
|
||||||
|
|
||||||
|
|
||||||
|
def percent(numerator: int | float, denominator: int | float) -> float | None:
|
||||||
|
return 100.0 * numerator / denominator if denominator > 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def run(condition: str, repetition: int, tile_hash_dump: Path | None = None) -> dict:
|
||||||
|
config = CONDITIONS[condition]
|
||||||
|
run_name = f"fast-r{repetition}"
|
||||||
|
out = OUT / condition
|
||||||
|
host_out = HOST_PLUGIN_SAVED / condition / "output"
|
||||||
|
userdir = HOST_SAVED / "user" / condition / run_name
|
||||||
|
zendata = HOST_SAVED / "zen" / condition / run_name
|
||||||
|
ddc = HOST_SAVED / "DDC"
|
||||||
|
trace = out / f"{run_name}.trace.jsonl"
|
||||||
|
abslog = out / f"{run_name}.log"
|
||||||
|
stdout = out / f"{run_name}.stdout.log"
|
||||||
|
export = host_out / run_name
|
||||||
|
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.StreamingCoarseFirst=1",
|
||||||
|
"-voxel.StreamingDynamicNearReserve=1",
|
||||||
|
"-voxel.StreamingLegacyPendingResultCap=0",
|
||||||
|
"-voxel.CoreRegionConcurrentAdmission=1",
|
||||||
|
"-voxel.StreamingResultBacklogCap=256", "-voxel.StreamingReadyQueueDepth=8",
|
||||||
|
"-voxel.StreamingBackgroundWorkerCap=1", "-voxel.CoreColumnCacheDirectMapped=0",
|
||||||
|
"-voxel.StreamingFarApplySupplement=1",
|
||||||
|
f"-voxel.StreamingFastTravelWaitForAllCriticalTiles={config['wait_for_critical']}",
|
||||||
|
f"-voxel.StreamingFastTravelApplyBudgetMilliseconds={config['apply_budget_ms']:.1f}",
|
||||||
|
f"-voxel.StreamingFastTravelForwardFirst={config['forward_first']}",
|
||||||
|
f"-voxel.StreamingRebuildOnCoreRegionCompletion={config['rebuild_on_region_completion']}",
|
||||||
|
f"-voxel.StreamingCoreDesiredCacheByCenter={config['core_desired_cache']}",
|
||||||
|
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
|
||||||
|
"-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=3000.0",
|
||||||
|
f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}",
|
||||||
|
"-voxel.TestMoveDurationSeconds=12.0", "-voxel.TestExitSeconds=23.0",
|
||||||
|
"-voxel.TestMoveDirection=1,0,0", "-voxel.StreamingSchedulerPolicy=3",
|
||||||
|
"-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={abslog}",
|
||||||
|
]
|
||||||
|
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()}")
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command]
|
||||||
|
started = time.monotonic()
|
||||||
|
with stdout.open("w", encoding="utf-8") as stream:
|
||||||
|
process = subprocess.run(
|
||||||
|
wrapped, cwd=ROOT, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
check=False, timeout=1740)
|
||||||
|
|
||||||
|
events = read_events(trace)
|
||||||
|
samples = [event for event in events if event.get("type") == "streaming_sample"]
|
||||||
|
moving = [event for event in samples if float(event.get("speed_cm_s", 0.0)) >= 50.0]
|
||||||
|
stop_event = next((event for event in events
|
||||||
|
if event.get("type") == "streaming_test_move_stopped"), None)
|
||||||
|
backend = next((event for event in events if event.get("type") == "worker_backend_config"), {})
|
||||||
|
scheduler_config = next((event for event in events
|
||||||
|
if event.get("type") == "scheduler_admission"), {})
|
||||||
|
worker_cap = next((event for event in events
|
||||||
|
if event.get("type") == "streaming_worker_cap_readback"), {})
|
||||||
|
if stop_event is None:
|
||||||
|
raise RuntimeError(f"missing move-stop event: {trace}")
|
||||||
|
stop_s = float(stop_event.get("t_s", 0.0))
|
||||||
|
move_start_s = min((float(event.get("t_s", 0.0)) for event in moving), default=float("inf"))
|
||||||
|
dwell = [event for event in samples if float(event.get("t_s", 0.0)) >= stop_s
|
||||||
|
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||||
|
arrival = [event for event in samples if float(event.get("t_s", 0.0)) < move_start_s
|
||||||
|
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||||
|
if process.returncode or len(moving) < 40 or len(dwell) < 16:
|
||||||
|
raise RuntimeError(f"incomplete {condition} run: {trace}; exit={process.returncode}; "
|
||||||
|
f"move={len(moving)} dwell={len(dwell)}")
|
||||||
|
if (int(backend.get("streaming_fast_travel_wait_for_all_critical_tiles", -1))
|
||||||
|
!= config["wait_for_critical"]
|
||||||
|
or abs(float(backend.get("streaming_fast_travel_apply_budget_ms", -1.0))
|
||||||
|
- config["apply_budget_ms"]) > 0.01
|
||||||
|
or int(worker_cap.get("streaming_background_worker_cap", -1)) != 1
|
||||||
|
or int(backend.get("effective_tile_slots", -1)) != 8
|
||||||
|
or int(backend.get("streaming_far_apply_supplement", -1)) != 1
|
||||||
|
or int(scheduler_config.get("forward_first", -1)) != config["forward_first"]
|
||||||
|
or int(scheduler_config.get("rebuild_on_core_region_completion", -1))
|
||||||
|
!= config["rebuild_on_region_completion"]
|
||||||
|
or int(scheduler_config.get("core_desired_cache_by_center", -1))
|
||||||
|
!= config["core_desired_cache"]):
|
||||||
|
raise RuntimeError(f"CVar/worker readback mismatch: {backend}; {worker_cap}; {scheduler_config}")
|
||||||
|
|
||||||
|
moving_metrics = []
|
||||||
|
for event in moving:
|
||||||
|
desired = event.get("desired_by_lod", [0] * 9)
|
||||||
|
resident = event.get("resident_by_lod", [0] * 9)
|
||||||
|
near_desired = sum(desired[:3])
|
||||||
|
cone_desired = int(event.get("near_forward_cone_desired", 0))
|
||||||
|
view_cone_desired = int(event.get("near_view_forward_cone_desired", 0))
|
||||||
|
view_valid = bool(event.get("near_view_forward_cone_valid", 0))
|
||||||
|
weighted_desired = float(event.get("near_forward_weighted_desired", 0.0))
|
||||||
|
travel_cone_ready = percent(
|
||||||
|
int(event.get("near_forward_cone_resident", 0)), cone_desired)
|
||||||
|
view_cone_ready = percent(
|
||||||
|
int(event.get("near_view_forward_cone_resident", 0)), view_cone_desired)
|
||||||
|
moving_metrics.append({
|
||||||
|
"near_ready_pct": percent(sum(resident[:3]), near_desired) or 0.0,
|
||||||
|
"lod0_ready_pct": percent(resident[0], desired[0]) or 0.0,
|
||||||
|
"lod1_ready_pct": percent(resident[1], desired[1]) or 0.0,
|
||||||
|
"lod2_ready_pct": percent(resident[2], desired[2]) or 0.0,
|
||||||
|
"far_hole_pct": percent(
|
||||||
|
sum(desired[3:]) - sum(event.get("covered_by_lod", resident)[3:]),
|
||||||
|
sum(desired[3:])) or 0.0,
|
||||||
|
"ahead_share_pct": percent(int(event.get("near_ahead_desired", 0)), near_desired),
|
||||||
|
"behind_share_pct": percent(int(event.get("near_behind_desired", 0)), near_desired),
|
||||||
|
"side_share_pct": 100.0 - (
|
||||||
|
percent(int(event.get("near_ahead_desired", 0)), near_desired) or 0.0
|
||||||
|
) - (
|
||||||
|
percent(int(event.get("near_behind_desired", 0)), near_desired) or 0.0
|
||||||
|
),
|
||||||
|
"ahead_ready_pct": percent(
|
||||||
|
int(event.get("near_ahead_resident", 0)),
|
||||||
|
int(event.get("near_ahead_desired", 0))),
|
||||||
|
"behind_ready_pct": percent(
|
||||||
|
int(event.get("near_behind_resident", 0)),
|
||||||
|
int(event.get("near_behind_desired", 0))),
|
||||||
|
"travel_forward_cone_ready_pct": travel_cone_ready,
|
||||||
|
"view_forward_cone_ready_pct": view_cone_ready,
|
||||||
|
"forward_cone_ready_pct": view_cone_ready if view_valid else travel_cone_ready,
|
||||||
|
"forward_cone_source": "camera" if view_valid else "travel_fallback",
|
||||||
|
"forward_cone_share_pct": percent(cone_desired, near_desired),
|
||||||
|
"forward_weighted_ready_pct": percent(
|
||||||
|
float(event.get("near_forward_weighted_resident", 0.0)), weighted_desired),
|
||||||
|
})
|
||||||
|
|
||||||
|
arrival_near_settled = next((event for event in arrival
|
||||||
|
if sum(event.get("desired_by_lod", [0] * 9)[:3]) > 0
|
||||||
|
and sum(event.get("settled_by_lod", [0] * 9)[:3])
|
||||||
|
>= sum(event.get("desired_by_lod", [0] * 9)[:3])), None)
|
||||||
|
dwell_complete = next((event for event in dwell
|
||||||
|
if sum(event.get("resident_by_lod", [0] * 9)[:3])
|
||||||
|
>= sum(event.get("desired_by_lod", [0] * 9)[:3])
|
||||||
|
and sum(event.get("desired_by_lod", [0] * 9)[3:])
|
||||||
|
<= sum(event.get("covered_by_lod", [0] * 9)[3:])), None)
|
||||||
|
|
||||||
|
work = [event for event in events if event.get("type") == "game_thread_work_frame"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
applies = {event.get("frame"): event for event in events
|
||||||
|
if event.get("type") == "apply_frame"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||||
|
scheduler = {event.get("frame"): event for event in events
|
||||||
|
if event.get("type") == "scheduler_admission"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||||
|
frames = []
|
||||||
|
for event in work:
|
||||||
|
apply = applies.get(event.get("frame"), {})
|
||||||
|
admit = scheduler.get(event.get("frame"), {})
|
||||||
|
frames.append({
|
||||||
|
"frame": event.get("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_rebuilt": int(admit.get("desired_rebuilt", 0)),
|
||||||
|
"submit_tiles_ms": float(admit.get("submit_tiles_ms", 0.0)),
|
||||||
|
"level_admit_ms": float(admit.get("level_admit_ms", 0.0)),
|
||||||
|
})
|
||||||
|
p99_field = lambda key: percentile([float(row[key]) for row in frames])
|
||||||
|
max_field = lambda key: max((float(row[key]) for row in frames), default=None)
|
||||||
|
rebuild_frames = [row for row in frames if row["desired_rebuilt"]]
|
||||||
|
rebuild_costs = [float(row["desired_rebuild_ms"]) for row in rebuild_frames]
|
||||||
|
moving_scheduler = [event for event in scheduler.values()]
|
||||||
|
center_crossings = 0
|
||||||
|
previous_center = None
|
||||||
|
for event in sorted(moving_scheduler, key=lambda item: int(item.get("frame", 0))):
|
||||||
|
center = tuple(event.get("center", ()))
|
||||||
|
if previous_center is not None and center != previous_center:
|
||||||
|
center_crossings += 1
|
||||||
|
previous_center = center
|
||||||
|
core_requests = [event for event in events
|
||||||
|
if event.get("type") == "core_region_compile_request"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
same_center_core_requests = sum(
|
||||||
|
event.get("reason") == "desired_set_changed_same_center" for event in core_requests)
|
||||||
|
core_desired_queries = [event for event in events
|
||||||
|
if event.get("type") == "core_desired_cache"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
core_desired_query_ms = [float(event.get("query_ms", 0.0))
|
||||||
|
for event in core_desired_queries if float(event.get("query_ms", 0.0)) > 0.0]
|
||||||
|
core_desired_cache_hits = sum(int(event.get("hit", 0)) for event in core_desired_queries)
|
||||||
|
direction_submissions = [event.get("near_submitted_by_direction", [0, 0, 0, 0])
|
||||||
|
for event in moving_scheduler]
|
||||||
|
direction_submissions = [counts for counts in direction_submissions
|
||||||
|
if isinstance(counts, list) and len(counts) == 4]
|
||||||
|
direction_submission_totals = [
|
||||||
|
sum(int(counts[index]) for counts in direction_submissions) for index in range(4)
|
||||||
|
]
|
||||||
|
first_admission_order = next((event.get("near_admission_order_first", [])
|
||||||
|
for event in moving_scheduler
|
||||||
|
if event.get("near_admission_order_first")), [])
|
||||||
|
camera_samples = sum(bool(event.get("near_view_forward_cone_valid", 0)) for event in moving)
|
||||||
|
dwell_complete_s = (float(dwell_complete.get("t_s", 0.0)) - stop_s
|
||||||
|
if dwell_complete else None)
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"condition": condition,
|
||||||
|
"repetition": repetition,
|
||||||
|
"speed_mps": 30.0,
|
||||||
|
"wait_for_all_critical_tiles": config["wait_for_critical"],
|
||||||
|
"fast_travel_apply_budget_ms": config["apply_budget_ms"],
|
||||||
|
"forward_first": config["forward_first"],
|
||||||
|
"rebuild_on_core_region_completion": config["rebuild_on_region_completion"],
|
||||||
|
"core_desired_cache_by_center": config["core_desired_cache"],
|
||||||
|
"effective_tile_slots": backend.get("effective_tile_slots"),
|
||||||
|
"exit_code": process.returncode,
|
||||||
|
"wall_s": round(time.monotonic() - started, 3),
|
||||||
|
"moving_samples": len(moving),
|
||||||
|
"dwell_samples": len(dwell),
|
||||||
|
"arrival_samples": len(arrival),
|
||||||
|
"near_ready_moving_mean_pct": mean([m["near_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod0_ready_moving_mean_pct": mean([m["lod0_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod1_ready_moving_mean_pct": mean([m["lod1_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod2_ready_moving_mean_pct": mean([m["lod2_ready_pct"] for m in moving_metrics]),
|
||||||
|
"far_holes_moving_mean_pct": mean([m["far_hole_pct"] for m in moving_metrics]),
|
||||||
|
"near_ahead_desired_share_pct": mean([m["ahead_share_pct"] for m in moving_metrics
|
||||||
|
if m["ahead_share_pct"] is not None]),
|
||||||
|
"near_behind_desired_share_pct": mean([m["behind_share_pct"] for m in moving_metrics
|
||||||
|
if m["behind_share_pct"] is not None]),
|
||||||
|
"near_side_desired_share_pct": mean([m["side_share_pct"] for m in moving_metrics]),
|
||||||
|
"near_ahead_ready_pct": mean([m["ahead_ready_pct"] for m in moving_metrics
|
||||||
|
if m["ahead_ready_pct"] is not None]),
|
||||||
|
"near_behind_ready_pct": mean([m["behind_ready_pct"] for m in moving_metrics
|
||||||
|
if m["behind_ready_pct"] is not None]),
|
||||||
|
"near_forward_cone_half_angle_deg": 55,
|
||||||
|
"near_forward_cone_desired_share_pct": mean([m["forward_cone_share_pct"]
|
||||||
|
for m in moving_metrics
|
||||||
|
if m["forward_cone_share_pct"] is not None]),
|
||||||
|
"near_forward_cone_ready_pct": mean([m["forward_cone_ready_pct"]
|
||||||
|
for m in moving_metrics
|
||||||
|
if m["forward_cone_ready_pct"] is not None]),
|
||||||
|
"near_travel_forward_cone_ready_pct": mean(
|
||||||
|
[m["travel_forward_cone_ready_pct"] for m in moving_metrics
|
||||||
|
if m["travel_forward_cone_ready_pct"] is not None]),
|
||||||
|
"near_view_forward_cone_ready_pct": mean(
|
||||||
|
[m["view_forward_cone_ready_pct"] for m in moving_metrics
|
||||||
|
if m["view_forward_cone_ready_pct"] is not None]),
|
||||||
|
"camera_forward_samples": camera_samples,
|
||||||
|
"camera_forward_sample_share_pct": percent(camera_samples, len(moving)),
|
||||||
|
"near_forward_cosine_weighted_ready_pct": mean(
|
||||||
|
[m["forward_weighted_ready_pct"] for m in moving_metrics
|
||||||
|
if m["forward_weighted_ready_pct"] is not None]),
|
||||||
|
"arrival_lod02_settled_t_s": (
|
||||||
|
float(arrival_near_settled.get("t_s", 0.0)) if arrival_near_settled else None),
|
||||||
|
"near_ready_dwell_end_pct": (100.0 * sum(dwell[-1].get("resident_by_lod", [0] * 9)[:3])
|
||||||
|
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[:3]))),
|
||||||
|
"far_holes_dwell_end_pct": (100.0 * (
|
||||||
|
sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:])
|
||||||
|
- sum(dwell[-1].get("covered_by_lod", [0] * 9)[3:]))
|
||||||
|
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:]))),
|
||||||
|
"dwell_complete_after_stop_s": dwell_complete_s,
|
||||||
|
"game_thread_tick_p99_ms": p99_field("tick_ms"),
|
||||||
|
"game_thread_tick_max_ms": max_field("tick_ms"),
|
||||||
|
"stream_update_p99_ms": p99_field("update_chunks_ms"),
|
||||||
|
"pending_drain_p99_ms": p99_field("pending_drain_ms"),
|
||||||
|
"apply_p99_ms": p99_field("apply_ms"),
|
||||||
|
"apply_max_ms": max_field("apply_ms"),
|
||||||
|
"apply_budget_min_ms": min((f["apply_budget_ms"] for f in frames), default=None),
|
||||||
|
"apply_budget_max_ms": max((f["apply_budget_ms"] for f in frames), default=None),
|
||||||
|
"apply_time_budget_hit_frames": sum(f["time_budget_hit"] for f in frames),
|
||||||
|
"apply_frames": sum(f["meshes_applied"] > 0 for f in frames),
|
||||||
|
"apply_meshes_total": sum(f["meshes_applied"] for f in frames),
|
||||||
|
"desired_rebuild_p99_ms": p99_field("desired_rebuild_ms"),
|
||||||
|
"desired_rebuild_count": len(rebuild_frames),
|
||||||
|
"desired_rebuilds_per_second": len(rebuild_frames) / 12.0,
|
||||||
|
"center_crossings": center_crossings,
|
||||||
|
"same_center_core_region_requests": same_center_core_requests,
|
||||||
|
"core_desired_cache_hit_count": core_desired_cache_hits,
|
||||||
|
"core_desired_cache_hit_share_pct": percent(
|
||||||
|
core_desired_cache_hits, len(core_desired_queries)),
|
||||||
|
"core_desired_query_nonzero_p50_ms": statistics.median(core_desired_query_ms)
|
||||||
|
if core_desired_query_ms else None,
|
||||||
|
"core_desired_query_nonzero_p95_ms": percentile(core_desired_query_ms, 0.95),
|
||||||
|
"core_desired_query_nonzero_max_ms": max(core_desired_query_ms)
|
||||||
|
if core_desired_query_ms else None,
|
||||||
|
"desired_rebuild_nonzero_p50_ms": statistics.median(rebuild_costs)
|
||||||
|
if rebuild_costs else None,
|
||||||
|
"desired_rebuild_nonzero_p95_ms": percentile(rebuild_costs, 0.95),
|
||||||
|
"desired_rebuild_nonzero_max_ms": max(rebuild_costs) if rebuild_costs else None,
|
||||||
|
"near_submitted_by_direction": direction_submission_totals,
|
||||||
|
"near_admission_first12": first_admission_order,
|
||||||
|
"submit_tiles_p99_ms": p99_field("submit_tiles_ms"),
|
||||||
|
"level_admit_p99_ms": p99_field("level_admit_ms"),
|
||||||
|
"top_tick_frames": sorted(frames, key=lambda f: f["tick_ms"], reverse=True)[:5],
|
||||||
|
"trace": str(trace.relative_to(ROOT)),
|
||||||
|
"log": str(abslog.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
print("[L160Movement] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--smoke", action="store_true",
|
||||||
|
help="run one forward-first candidate trace to validate CVar readback and event fields")
|
||||||
|
parser.add_argument("--geometry-hash-check", action="store_true",
|
||||||
|
help="compare UE per-tile geometry hashes for baseline and forward-first admission")
|
||||||
|
args = parser.parse_args()
|
||||||
|
missing = [path for path in (EDITOR, HOST / "VFHost.uproject") if not path.exists()]
|
||||||
|
if missing:
|
||||||
|
raise SystemExit(f"missing UE runner input: {missing}")
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
if args.geometry_hash_check:
|
||||||
|
hash_dir = OUT / "geometry-hash"
|
||||||
|
baseline_path = hash_dir / "assembler-world.json"
|
||||||
|
candidate_path = hash_dir / "forward-first.json"
|
||||||
|
run("assembler_world", 99, baseline_path)
|
||||||
|
run("forward_first", 99, candidate_path)
|
||||||
|
baseline_data = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||||
|
candidate_data = json.loads(candidate_path.read_text(encoding="utf-8"))
|
||||||
|
key_fields = ("level", "tile_x", "tile_y", "tile_z")
|
||||||
|
|
||||||
|
def indexed(data: dict) -> dict[tuple[int, ...], tuple]:
|
||||||
|
result = {}
|
||||||
|
for record in data.get("records", []):
|
||||||
|
key = tuple(int(record[field]) for field in key_fields)
|
||||||
|
result[key] = (str(record.get("geometry_hash", "")),
|
||||||
|
int(record.get("triangle_count", -1)),
|
||||||
|
bool(record.get("empty", False)))
|
||||||
|
return result
|
||||||
|
|
||||||
|
baseline_hashes = indexed(baseline_data)
|
||||||
|
candidate_hashes = indexed(candidate_data)
|
||||||
|
common = baseline_hashes.keys() & candidate_hashes.keys()
|
||||||
|
mismatches = [key for key in common
|
||||||
|
if baseline_hashes[key] != candidate_hashes[key]]
|
||||||
|
result = {
|
||||||
|
"baseline_records": len(baseline_hashes),
|
||||||
|
"candidate_records": len(candidate_hashes),
|
||||||
|
"common_records": len(common),
|
||||||
|
"hash_mismatches": len(mismatches),
|
||||||
|
"baseline_only_records": len(baseline_hashes.keys() - candidate_hashes.keys()),
|
||||||
|
"candidate_only_records": len(candidate_hashes.keys() - baseline_hashes.keys()),
|
||||||
|
"baseline_dump": str(baseline_path.relative_to(ROOT)),
|
||||||
|
"candidate_dump": str(candidate_path.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
(hash_dir / "comparison.json").write_text(
|
||||||
|
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
print("[L160GeometryHash] " + json.dumps(result, sort_keys=True), flush=True)
|
||||||
|
return 0 if len(common) >= 100 and not mismatches else 1
|
||||||
|
if args.smoke:
|
||||||
|
run("forward_first", 0)
|
||||||
|
return 0
|
||||||
|
rows = []
|
||||||
|
partial = OUT / "runs-partial.json"
|
||||||
|
for repetition in range(1, 6):
|
||||||
|
for condition in ORDER[repetition]:
|
||||||
|
row = run(condition, repetition)
|
||||||
|
rows.append(row)
|
||||||
|
partial.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
|
||||||
|
metrics = (
|
||||||
|
"near_ready_moving_mean_pct", "far_holes_moving_mean_pct",
|
||||||
|
"game_thread_tick_p99_ms", "game_thread_tick_max_ms",
|
||||||
|
"arrival_lod02_settled_t_s", "dwell_complete_after_stop_s",
|
||||||
|
"near_ahead_desired_share_pct", "near_behind_desired_share_pct",
|
||||||
|
"near_side_desired_share_pct", "near_forward_cone_desired_share_pct",
|
||||||
|
"near_ahead_ready_pct", "near_behind_ready_pct",
|
||||||
|
"near_forward_cone_ready_pct", "near_forward_cosine_weighted_ready_pct",
|
||||||
|
"near_travel_forward_cone_ready_pct",
|
||||||
|
"apply_p99_ms", "desired_rebuild_p99_ms", "desired_rebuild_count",
|
||||||
|
"desired_rebuild_nonzero_p95_ms", "center_crossings",
|
||||||
|
"same_center_core_region_requests", "level_admit_p99_ms",
|
||||||
|
"core_desired_cache_hit_count", "core_desired_query_nonzero_p95_ms",
|
||||||
|
)
|
||||||
|
medians = {
|
||||||
|
condition: {metric: statistics.median(
|
||||||
|
row[metric] for row in rows if row["condition"] == condition)
|
||||||
|
for metric in metrics}
|
||||||
|
for condition in CONDITIONS
|
||||||
|
}
|
||||||
|
pairs = []
|
||||||
|
for repetition in range(1, 6):
|
||||||
|
by_condition = {row["condition"]: row for row in rows
|
||||||
|
if row["repetition"] == repetition}
|
||||||
|
candidate = "forward_first"
|
||||||
|
pairs.append({
|
||||||
|
"repetition": repetition,
|
||||||
|
"candidate": candidate,
|
||||||
|
**{f"{metric}_delta": by_condition[candidate][metric]
|
||||||
|
- by_condition["assembler_world"][metric]
|
||||||
|
for metric in metrics if by_condition[candidate][metric] is not None
|
||||||
|
and by_condition["assembler_world"][metric] is not None},
|
||||||
|
})
|
||||||
|
by_pair = {
|
||||||
|
repetition: {row["condition"]: row for row in rows if row["repetition"] == repetition}
|
||||||
|
for repetition in range(1, 6)
|
||||||
|
}
|
||||||
|
candidate_rows = [row for row in rows if row["condition"] == "forward_first"]
|
||||||
|
candidate_gates = {
|
||||||
|
"every_candidate_run_p99_le_11_ms": all(
|
||||||
|
row["game_thread_tick_p99_ms"] <= 11.0 for row in candidate_rows),
|
||||||
|
"paired_worst_tick_no_worse": all(
|
||||||
|
by_pair[rep]["forward_first"]["game_thread_tick_max_ms"]
|
||||||
|
<= by_pair[rep]["assembler_world"]["game_thread_tick_max_ms"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"paired_far_holes_no_worse": all(
|
||||||
|
by_pair[rep]["forward_first"]["far_holes_moving_mean_pct"]
|
||||||
|
<= by_pair[rep]["assembler_world"]["far_holes_moving_mean_pct"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"paired_arrival_no_worse": all(
|
||||||
|
by_pair[rep]["forward_first"]["arrival_lod02_settled_t_s"] is not None
|
||||||
|
and by_pair[rep]["assembler_world"]["arrival_lod02_settled_t_s"] is not None
|
||||||
|
and by_pair[rep]["forward_first"]["arrival_lod02_settled_t_s"]
|
||||||
|
<= by_pair[rep]["assembler_world"]["arrival_lod02_settled_t_s"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"forward_cone_median_delta_pp": statistics.median(
|
||||||
|
by_pair[rep]["forward_first"]["near_forward_cone_ready_pct"]
|
||||||
|
- by_pair[rep]["assembler_world"]["near_forward_cone_ready_pct"]
|
||||||
|
for rep in by_pair),
|
||||||
|
}
|
||||||
|
summary = {
|
||||||
|
"source_commit": subprocess.run(
|
||||||
|
["git", "-c", f"safe.directory={ROOT}", "rev-parse", "HEAD"],
|
||||||
|
cwd=ROOT, check=True, capture_output=True, text=True).stdout.strip(),
|
||||||
|
"protocol": "Five interleaved 30 m/s UE pairs; 12 s move plus 5 s dwell; 8 effective tile slots. L157 critical wait remains on and apply cap remains 0 ms.",
|
||||||
|
"conditions": CONDITIONS,
|
||||||
|
"movement_medians": medians,
|
||||||
|
"paired_deltas_vs_assembler_world": pairs,
|
||||||
|
"candidate_gates": candidate_gates,
|
||||||
|
"runs": rows,
|
||||||
|
}
|
||||||
|
(OUT / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
print("[L160Summary] " + json.dumps({"movement_medians": medians,
|
||||||
|
"candidate_gates": candidate_gates,
|
||||||
|
"runs": len(rows)}, sort_keys=True), flush=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# L165 — incremental desired-set rebuild
|
||||||
|
|
||||||
|
**Result:** The Core clipmap key update is incremental for ordinary adjacent crossings and cuts median nonzero `BuildDesiredTiles` p95 by 47.7% (7.80 to 4.08 ms). Equality checks pass. Worst tick, p99, near readiness, arrival, and five-pair median far-hole coverage all improved. Far holes regressed by 0.291 percentage points in pair 5, so the strict paired no-regression gate is not clean across all five pairs.
|
||||||
|
|
||||||
|
## Count first
|
||||||
|
|
||||||
|
A count-only trace of the baseline run covered movement from 6.174 to 17.981 s: 316 desired-set updates, including 44 center crossings. At a crossing, 93.813% of prior desired keys were unchanged in aggregate (median crossing 93.405%; range 93.384–97.318%). Same-center updates had 99.955% median overlap.
|
||||||
|
|
||||||
|
| LOD | Prior keys unchanged at crossings |
|
||||||
|
| --- | ---: |
|
||||||
|
| 0 | 86.929% |
|
||||||
|
| 1 | 91.071% |
|
||||||
|
| 2 | 94.981% |
|
||||||
|
| 3 | 97.017% |
|
||||||
|
| 4 | 98.674% |
|
||||||
|
| 5 | 99.053% |
|
||||||
|
| 6 | 99.811% |
|
||||||
|
| 7 | 100.000% |
|
||||||
|
| 8 | 100.000% |
|
||||||
|
|
||||||
|
This trace was used only for key counts. Its commandlet ended before its separate readiness check, so its timings are excluded.
|
||||||
|
|
||||||
|
## Change and movement results
|
||||||
|
|
||||||
|
`BuildDelta` computes entering and leaving box slabs per LOD, including coarser-ring coverage changes. The runtime caches the Core key array and set, applies the delta at one-tile crossings, and updates the transition-descendant index from entering/leaving keys. It falls back to `World::desiredTiles` for larger jumps or when a changed key touches LOD4 or coarser. The UE overlay sets and priority/admission order are still rebuilt on each trigger; this round does not make those materialization steps incremental.
|
||||||
|
|
||||||
|
Five interleaved W8 UE pairs ran at 30 m/s, 45 crossings each, with the L160 `assembler/world` baseline settings and forward-first disabled. The machine was quiet before timing (load averages 0.35 / 1.46 / 1.25). Metrics below are medians of the five runs; rebuild p50/p95 are over nonzero `BuildDesiredTiles` samples in the 12 s movement window.
|
||||||
|
|
||||||
|
| Metric | `assembler/world` | Incremental | Paired result |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| `BuildDesiredTiles` p50 | 3.96 ms | 1.28 ms | lower in all five pairs |
|
||||||
|
| `BuildDesiredTiles` p95 | 7.80 ms | 4.08 ms | −3.72 ms median; lower in all five pairs |
|
||||||
|
| Game-thread p99 | 11.46 ms | 7.97 ms | every candidate run was 7.90–8.26 ms |
|
||||||
|
| Worst tick | 13.73 ms | 12.11 ms | lower in all five pairs by 0.86–3.94 ms |
|
||||||
|
| Near LOD0–2 readiness | 41.41% | 42.90% | better in all five pairs |
|
||||||
|
| 55° forward-cone readiness | 14.69% | 15.37% | better in all five pairs |
|
||||||
|
| Far holes while moving | 3.09% | 2.61% | better in four pairs; pair 5 was +0.291 pp |
|
||||||
|
| First settled LOD0–2 arrival | 2.453 s | 2.437 s | earlier in all five pairs |
|
||||||
|
|
||||||
|
The cone uses the travel-direction fallback because null-RHI produced no camera-forward samples. Far-hole coverage measures loaded desired coverage; the desired key set itself matched in every-crossing control checks.
|
||||||
|
|
||||||
|
## Checks and limits
|
||||||
|
|
||||||
|
- Built `VFHostEditor` from this worktree with `taskset -c 0-11`, `-NoUBA`, and `-MaxParallelActions=4`; final build succeeded. Restored `VFHost/PluginDeps/VoxelForge/Source` to `/home/jahni/VoxelM/Plugins/VoxelForge/Source`.
|
||||||
|
- Fast tier passed: 268 PASS, 0 FAIL, 5 diagnostics. Both canonical owner hash sets match the L130 baseline exactly (12/12 rows each); W1/W8/W12 determinism checks passed.
|
||||||
|
- Clipmap automation passed 630 delta cases across six unit steps and three vertical-clamp settings. Its log reports `Result=Success`, failures=0; UE returned exit status 1 when `TestExit=Automation Test Queue Empty` fired.
|
||||||
|
- The runtime equality audit passed all 45 crossings with interval 1, and all 20 sampled audits across the five measured candidate runs (four per run). Injecting an impossible reference key failed at crossing 1 with `missing=1` and triggered the assertion/nonzero UE exit.
|
||||||
|
- `git diff --check` and runner `py_compile` passed. The movement summarizer reports the strict pairwise far-hole gate as failed in pair 5; the five-pair median still improved. The other movement gates passed. HMD/camera-forward readiness and W12 movement were not measured.
|
||||||
|
- The final transition-index range correction only changes configurations whose desired max LOD is below 8; this W8 scene includes LOD8, so the measured code path is unchanged. The forward-first default remains off.
|
||||||
|
|
||||||
|
Changed Unreal `Source/` files:
|
||||||
|
|
||||||
|
- `Source/VoxelForge/Private/VoxelClipmapDesiredTiles.h`
|
||||||
|
- `Source/VoxelForge/Private/VoxelWorld.cpp`
|
||||||
|
- `Source/VoxelForge/Private/Tests/VoxelForgeClipmapNoOverlapTest.cpp`
|
||||||
|
- `Source/VoxelForge/Public/VoxelWorld.h`
|
||||||
|
|
||||||
|
Logs and summaries are in `Saved/Linux/l165/`.
|
||||||
@@ -0,0 +1,606 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Interleaved L165 UE movement runs for incremental desired-set rebuilds."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
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")
|
||||||
|
OUT = ROOT / "Saved/Linux/l165/ue/movement"
|
||||||
|
HOST_SAVED = HOST / "Saved/Linux/l165"
|
||||||
|
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l165"
|
||||||
|
SPAWN_M = (19.166015625, -14.2001953125, -753.3740234375)
|
||||||
|
MOVE_START_S = 6.0
|
||||||
|
|
||||||
|
CONDITIONS = {
|
||||||
|
"assembler_world": {
|
||||||
|
"wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||||
|
"forward_first": 0, "rebuild_on_region_completion": 1,
|
||||||
|
"core_desired_cache": 0, "incremental_desired": 0,
|
||||||
|
"audit_every": 0, "inject_mismatch": 0,
|
||||||
|
},
|
||||||
|
"incremental": {
|
||||||
|
"wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||||
|
"forward_first": 0, "rebuild_on_region_completion": 1,
|
||||||
|
"core_desired_cache": 0, "incremental_desired": 1,
|
||||||
|
"audit_every": 10, "inject_mismatch": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ORDER = {
|
||||||
|
1: ("assembler_world", "incremental"),
|
||||||
|
2: ("incremental", "assembler_world"),
|
||||||
|
3: ("assembler_world", "incremental"),
|
||||||
|
4: ("incremental", "assembler_world"),
|
||||||
|
5: ("assembler_world", "incremental"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def percentile(values: list[float], fraction: float = 0.99) -> float | None:
|
||||||
|
if not values:
|
||||||
|
return None
|
||||||
|
ordered = sorted(values)
|
||||||
|
return ordered[max(0, min(len(ordered) - 1, math.ceil(len(ordered) * fraction) - 1))]
|
||||||
|
|
||||||
|
|
||||||
|
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 mean(values: list[float]) -> float | None:
|
||||||
|
return statistics.mean(values) if values else None
|
||||||
|
|
||||||
|
|
||||||
|
def percent(numerator: int | float, denominator: int | float) -> float | None:
|
||||||
|
return 100.0 * numerator / denominator if denominator > 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def run(condition: str, repetition: int, tile_hash_dump: Path | None = None,
|
||||||
|
expected_mismatch: bool = False) -> dict:
|
||||||
|
config = CONDITIONS[condition]
|
||||||
|
run_name = f"fast-r{repetition}"
|
||||||
|
out = OUT / condition
|
||||||
|
host_out = HOST_PLUGIN_SAVED / condition / "output"
|
||||||
|
userdir = HOST_SAVED / "user" / condition / run_name
|
||||||
|
zendata = HOST_SAVED / "zen" / condition / run_name
|
||||||
|
ddc = HOST_SAVED / "DDC"
|
||||||
|
trace = out / f"{run_name}.trace.jsonl"
|
||||||
|
abslog = out / f"{run_name}.log"
|
||||||
|
stdout = out / f"{run_name}.stdout.log"
|
||||||
|
export = host_out / run_name
|
||||||
|
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.StreamingCoarseFirst=1",
|
||||||
|
"-voxel.StreamingDynamicNearReserve=1",
|
||||||
|
"-voxel.StreamingLegacyPendingResultCap=0",
|
||||||
|
"-voxel.CoreRegionConcurrentAdmission=1",
|
||||||
|
"-voxel.StreamingResultBacklogCap=256", "-voxel.StreamingReadyQueueDepth=8",
|
||||||
|
"-voxel.StreamingBackgroundWorkerCap=1", "-voxel.CoreColumnCacheDirectMapped=0",
|
||||||
|
"-voxel.StreamingFarApplySupplement=1",
|
||||||
|
f"-voxel.StreamingFastTravelWaitForAllCriticalTiles={config['wait_for_critical']}",
|
||||||
|
f"-voxel.StreamingFastTravelApplyBudgetMilliseconds={config['apply_budget_ms']:.1f}",
|
||||||
|
f"-voxel.StreamingFastTravelForwardFirst={config['forward_first']}",
|
||||||
|
f"-voxel.StreamingRebuildOnCoreRegionCompletion={config['rebuild_on_region_completion']}",
|
||||||
|
f"-voxel.StreamingCoreDesiredCacheByCenter={config['core_desired_cache']}",
|
||||||
|
f"-voxel.StreamingIncrementalDesiredSet={config['incremental_desired']}",
|
||||||
|
f"-voxel.StreamingIncrementalDesiredAuditEvery={config['audit_every']}",
|
||||||
|
f"-voxel.StreamingIncrementalDesiredAuditInjectMismatch={config['inject_mismatch']}",
|
||||||
|
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
|
||||||
|
"-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=3000.0",
|
||||||
|
f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}",
|
||||||
|
"-voxel.TestMoveDurationSeconds=12.0", "-voxel.TestExitSeconds=0.0",
|
||||||
|
"-voxel.TestMoveDirection=1,0,0", "-voxel.StreamingSchedulerPolicy=3",
|
||||||
|
"-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={abslog}",
|
||||||
|
]
|
||||||
|
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()}")
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command]
|
||||||
|
started = time.monotonic()
|
||||||
|
with stdout.open("w", encoding="utf-8") as stream:
|
||||||
|
process = subprocess.run(
|
||||||
|
wrapped, cwd=ROOT, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
check=False, timeout=1740)
|
||||||
|
|
||||||
|
events = read_events(trace)
|
||||||
|
if expected_mismatch:
|
||||||
|
diagnostic = "VoxelForgeIncrementalDesiredAudit"
|
||||||
|
logs = "\n".join(path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
for path in (abslog, stdout) if path.is_file())
|
||||||
|
audit_events = [event for event in events
|
||||||
|
if event.get("type") == "incremental_desired_audit"]
|
||||||
|
failed_audit = any(int(event.get("pass", 1)) == 0 for event in audit_events)
|
||||||
|
if process.returncode == 0 or diagnostic not in logs or "mismatch" not in logs.lower():
|
||||||
|
raise RuntimeError(
|
||||||
|
f"injected mismatch did not fail as expected: exit={process.returncode}; "
|
||||||
|
f"audit_events={len(audit_events)}; log={abslog}")
|
||||||
|
print("[L165MismatchControl] " + json.dumps({
|
||||||
|
"exit_code": process.returncode,
|
||||||
|
"diagnostic_seen": True,
|
||||||
|
"failed_audit_event_seen": failed_audit,
|
||||||
|
"trace": str(trace.relative_to(ROOT)),
|
||||||
|
"log": str(abslog.relative_to(ROOT)),
|
||||||
|
}, sort_keys=True), flush=True)
|
||||||
|
return {"condition": condition, "exit_code": process.returncode,
|
||||||
|
"failed_audit_event_seen": failed_audit}
|
||||||
|
samples = [event for event in events if event.get("type") == "streaming_sample"]
|
||||||
|
moving = [event for event in samples if float(event.get("speed_cm_s", 0.0)) >= 50.0]
|
||||||
|
stop_event = next((event for event in events
|
||||||
|
if event.get("type") == "streaming_test_move_stopped"), None)
|
||||||
|
backend = next((event for event in events if event.get("type") == "worker_backend_config"), {})
|
||||||
|
scheduler_config = next((event for event in events
|
||||||
|
if event.get("type") == "scheduler_admission"), {})
|
||||||
|
worker_cap = next((event for event in events
|
||||||
|
if event.get("type") == "streaming_worker_cap_readback"), {})
|
||||||
|
if stop_event is None:
|
||||||
|
raise RuntimeError(f"missing move-stop event: {trace}")
|
||||||
|
stop_s = float(stop_event.get("t_s", 0.0))
|
||||||
|
move_start_s = min((float(event.get("t_s", 0.0)) for event in moving), default=float("inf"))
|
||||||
|
dwell = [event for event in samples if float(event.get("t_s", 0.0)) >= stop_s
|
||||||
|
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||||
|
arrival = [event for event in samples if float(event.get("t_s", 0.0)) < move_start_s
|
||||||
|
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||||
|
if process.returncode or len(moving) < 40 or len(dwell) < 16:
|
||||||
|
raise RuntimeError(f"incomplete {condition} run: {trace}; exit={process.returncode}; "
|
||||||
|
f"move={len(moving)} dwell={len(dwell)}")
|
||||||
|
if (int(backend.get("streaming_fast_travel_wait_for_all_critical_tiles", -1))
|
||||||
|
!= config["wait_for_critical"]
|
||||||
|
or abs(float(backend.get("streaming_fast_travel_apply_budget_ms", -1.0))
|
||||||
|
- config["apply_budget_ms"]) > 0.01
|
||||||
|
or int(worker_cap.get("streaming_background_worker_cap", -1)) != 1
|
||||||
|
or int(backend.get("effective_tile_slots", -1)) != 8
|
||||||
|
or int(backend.get("streaming_far_apply_supplement", -1)) != 1
|
||||||
|
or int(scheduler_config.get("forward_first", -1)) != config["forward_first"]
|
||||||
|
or int(scheduler_config.get("rebuild_on_core_region_completion", -1))
|
||||||
|
!= config["rebuild_on_region_completion"]
|
||||||
|
or int(scheduler_config.get("core_desired_cache_by_center", -1))
|
||||||
|
!= config["core_desired_cache"]
|
||||||
|
or int(scheduler_config.get("incremental_desired_set", -1))
|
||||||
|
!= config["incremental_desired"]
|
||||||
|
or int(scheduler_config.get("incremental_desired_audit_every", -1))
|
||||||
|
!= config["audit_every"]
|
||||||
|
or int(scheduler_config.get("incremental_desired_audit_inject_mismatch", -1))
|
||||||
|
!= config["inject_mismatch"]):
|
||||||
|
raise RuntimeError(f"CVar/worker readback mismatch: {backend}; {worker_cap}; {scheduler_config}")
|
||||||
|
|
||||||
|
moving_metrics = []
|
||||||
|
for event in moving:
|
||||||
|
desired = event.get("desired_by_lod", [0] * 9)
|
||||||
|
resident = event.get("resident_by_lod", [0] * 9)
|
||||||
|
near_desired = sum(desired[:3])
|
||||||
|
cone_desired = int(event.get("near_forward_cone_desired", 0))
|
||||||
|
view_cone_desired = int(event.get("near_view_forward_cone_desired", 0))
|
||||||
|
view_valid = bool(event.get("near_view_forward_cone_valid", 0))
|
||||||
|
weighted_desired = float(event.get("near_forward_weighted_desired", 0.0))
|
||||||
|
travel_cone_ready = percent(
|
||||||
|
int(event.get("near_forward_cone_resident", 0)), cone_desired)
|
||||||
|
view_cone_ready = percent(
|
||||||
|
int(event.get("near_view_forward_cone_resident", 0)), view_cone_desired)
|
||||||
|
moving_metrics.append({
|
||||||
|
"near_ready_pct": percent(sum(resident[:3]), near_desired) or 0.0,
|
||||||
|
"lod0_ready_pct": percent(resident[0], desired[0]) or 0.0,
|
||||||
|
"lod1_ready_pct": percent(resident[1], desired[1]) or 0.0,
|
||||||
|
"lod2_ready_pct": percent(resident[2], desired[2]) or 0.0,
|
||||||
|
"far_hole_pct": percent(
|
||||||
|
sum(desired[3:]) - sum(event.get("covered_by_lod", resident)[3:]),
|
||||||
|
sum(desired[3:])) or 0.0,
|
||||||
|
"ahead_share_pct": percent(int(event.get("near_ahead_desired", 0)), near_desired),
|
||||||
|
"behind_share_pct": percent(int(event.get("near_behind_desired", 0)), near_desired),
|
||||||
|
"side_share_pct": 100.0 - (
|
||||||
|
percent(int(event.get("near_ahead_desired", 0)), near_desired) or 0.0
|
||||||
|
) - (
|
||||||
|
percent(int(event.get("near_behind_desired", 0)), near_desired) or 0.0
|
||||||
|
),
|
||||||
|
"ahead_ready_pct": percent(
|
||||||
|
int(event.get("near_ahead_resident", 0)),
|
||||||
|
int(event.get("near_ahead_desired", 0))),
|
||||||
|
"behind_ready_pct": percent(
|
||||||
|
int(event.get("near_behind_resident", 0)),
|
||||||
|
int(event.get("near_behind_desired", 0))),
|
||||||
|
"travel_forward_cone_ready_pct": travel_cone_ready,
|
||||||
|
"view_forward_cone_ready_pct": view_cone_ready,
|
||||||
|
"forward_cone_ready_pct": view_cone_ready if view_valid else travel_cone_ready,
|
||||||
|
"forward_cone_source": "camera" if view_valid else "travel_fallback",
|
||||||
|
"forward_cone_share_pct": percent(cone_desired, near_desired),
|
||||||
|
"forward_weighted_ready_pct": percent(
|
||||||
|
float(event.get("near_forward_weighted_resident", 0.0)), weighted_desired),
|
||||||
|
})
|
||||||
|
|
||||||
|
arrival_near_settled = next((event for event in arrival
|
||||||
|
if sum(event.get("desired_by_lod", [0] * 9)[:3]) > 0
|
||||||
|
and sum(event.get("settled_by_lod", [0] * 9)[:3])
|
||||||
|
>= sum(event.get("desired_by_lod", [0] * 9)[:3])), None)
|
||||||
|
dwell_complete = next((event for event in dwell
|
||||||
|
if sum(event.get("resident_by_lod", [0] * 9)[:3])
|
||||||
|
>= sum(event.get("desired_by_lod", [0] * 9)[:3])
|
||||||
|
and sum(event.get("desired_by_lod", [0] * 9)[3:])
|
||||||
|
<= sum(event.get("covered_by_lod", [0] * 9)[3:])), None)
|
||||||
|
|
||||||
|
work = [event for event in events if event.get("type") == "game_thread_work_frame"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
applies = {event.get("frame"): event for event in events
|
||||||
|
if event.get("type") == "apply_frame"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||||
|
scheduler = {event.get("frame"): event for event in events
|
||||||
|
if event.get("type") == "scheduler_admission"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||||
|
frames = []
|
||||||
|
for event in work:
|
||||||
|
apply = applies.get(event.get("frame"), {})
|
||||||
|
admit = scheduler.get(event.get("frame"), {})
|
||||||
|
frames.append({
|
||||||
|
"frame": event.get("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_rebuilt": int(admit.get("desired_rebuilt", 0)),
|
||||||
|
"submit_tiles_ms": float(admit.get("submit_tiles_ms", 0.0)),
|
||||||
|
"level_admit_ms": float(admit.get("level_admit_ms", 0.0)),
|
||||||
|
})
|
||||||
|
p99_field = lambda key: percentile([float(row[key]) for row in frames])
|
||||||
|
max_field = lambda key: max((float(row[key]) for row in frames), default=None)
|
||||||
|
rebuild_frames = [row for row in frames if row["desired_rebuilt"]]
|
||||||
|
rebuild_costs = [float(row["desired_rebuild_ms"]) for row in rebuild_frames]
|
||||||
|
moving_scheduler = [event for event in scheduler.values()]
|
||||||
|
center_crossings = 0
|
||||||
|
previous_center = None
|
||||||
|
for event in sorted(moving_scheduler, key=lambda item: int(item.get("frame", 0))):
|
||||||
|
center = tuple(event.get("center", ()))
|
||||||
|
if previous_center is not None and center != previous_center:
|
||||||
|
center_crossings += 1
|
||||||
|
previous_center = center
|
||||||
|
core_requests = [event for event in events
|
||||||
|
if event.get("type") == "core_region_compile_request"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
same_center_core_requests = sum(
|
||||||
|
event.get("reason") == "desired_set_changed_same_center" for event in core_requests)
|
||||||
|
core_desired_queries = [event for event in events
|
||||||
|
if event.get("type") == "core_desired_cache"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
core_desired_query_ms = [float(event.get("query_ms", 0.0))
|
||||||
|
for event in core_desired_queries if float(event.get("query_ms", 0.0)) > 0.0]
|
||||||
|
core_desired_cache_hits = sum(int(event.get("hit", 0)) for event in core_desired_queries)
|
||||||
|
incremental_audits = [event for event in events
|
||||||
|
if event.get("type") == "incremental_desired_audit"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
incremental_audit_failures = sum(int(event.get("pass", 1)) == 0
|
||||||
|
for event in incremental_audits)
|
||||||
|
if config["incremental_desired"] and (
|
||||||
|
not incremental_audits or incremental_audit_failures):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"incremental desired equality audit missing/failed: {trace}; "
|
||||||
|
f"audits={len(incremental_audits)} failures={incremental_audit_failures}")
|
||||||
|
direction_submissions = [event.get("near_submitted_by_direction", [0, 0, 0, 0])
|
||||||
|
for event in moving_scheduler]
|
||||||
|
direction_submissions = [counts for counts in direction_submissions
|
||||||
|
if isinstance(counts, list) and len(counts) == 4]
|
||||||
|
direction_submission_totals = [
|
||||||
|
sum(int(counts[index]) for counts in direction_submissions) for index in range(4)
|
||||||
|
]
|
||||||
|
first_admission_order = next((event.get("near_admission_order_first", [])
|
||||||
|
for event in moving_scheduler
|
||||||
|
if event.get("near_admission_order_first")), [])
|
||||||
|
camera_samples = sum(bool(event.get("near_view_forward_cone_valid", 0)) for event in moving)
|
||||||
|
dwell_complete_s = (float(dwell_complete.get("t_s", 0.0)) - stop_s
|
||||||
|
if dwell_complete else None)
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"condition": condition,
|
||||||
|
"repetition": repetition,
|
||||||
|
"speed_mps": 30.0,
|
||||||
|
"wait_for_all_critical_tiles": config["wait_for_critical"],
|
||||||
|
"fast_travel_apply_budget_ms": config["apply_budget_ms"],
|
||||||
|
"forward_first": config["forward_first"],
|
||||||
|
"rebuild_on_core_region_completion": config["rebuild_on_region_completion"],
|
||||||
|
"core_desired_cache_by_center": config["core_desired_cache"],
|
||||||
|
"incremental_desired_set": config["incremental_desired"],
|
||||||
|
"incremental_desired_audit_every": config["audit_every"],
|
||||||
|
"incremental_desired_audit_count": len(incremental_audits),
|
||||||
|
"incremental_desired_audit_failures": incremental_audit_failures,
|
||||||
|
"effective_tile_slots": backend.get("effective_tile_slots"),
|
||||||
|
"exit_code": process.returncode,
|
||||||
|
"wall_s": round(time.monotonic() - started, 3),
|
||||||
|
"moving_samples": len(moving),
|
||||||
|
"dwell_samples": len(dwell),
|
||||||
|
"arrival_samples": len(arrival),
|
||||||
|
"near_ready_moving_mean_pct": mean([m["near_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod0_ready_moving_mean_pct": mean([m["lod0_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod1_ready_moving_mean_pct": mean([m["lod1_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod2_ready_moving_mean_pct": mean([m["lod2_ready_pct"] for m in moving_metrics]),
|
||||||
|
"far_holes_moving_mean_pct": mean([m["far_hole_pct"] for m in moving_metrics]),
|
||||||
|
"near_ahead_desired_share_pct": mean([m["ahead_share_pct"] for m in moving_metrics
|
||||||
|
if m["ahead_share_pct"] is not None]),
|
||||||
|
"near_behind_desired_share_pct": mean([m["behind_share_pct"] for m in moving_metrics
|
||||||
|
if m["behind_share_pct"] is not None]),
|
||||||
|
"near_side_desired_share_pct": mean([m["side_share_pct"] for m in moving_metrics]),
|
||||||
|
"near_ahead_ready_pct": mean([m["ahead_ready_pct"] for m in moving_metrics
|
||||||
|
if m["ahead_ready_pct"] is not None]),
|
||||||
|
"near_behind_ready_pct": mean([m["behind_ready_pct"] for m in moving_metrics
|
||||||
|
if m["behind_ready_pct"] is not None]),
|
||||||
|
"near_forward_cone_half_angle_deg": 55,
|
||||||
|
"near_forward_cone_desired_share_pct": mean([m["forward_cone_share_pct"]
|
||||||
|
for m in moving_metrics
|
||||||
|
if m["forward_cone_share_pct"] is not None]),
|
||||||
|
"near_forward_cone_ready_pct": mean([m["forward_cone_ready_pct"]
|
||||||
|
for m in moving_metrics
|
||||||
|
if m["forward_cone_ready_pct"] is not None]),
|
||||||
|
"near_travel_forward_cone_ready_pct": mean(
|
||||||
|
[m["travel_forward_cone_ready_pct"] for m in moving_metrics
|
||||||
|
if m["travel_forward_cone_ready_pct"] is not None]),
|
||||||
|
"near_view_forward_cone_ready_pct": mean(
|
||||||
|
[m["view_forward_cone_ready_pct"] for m in moving_metrics
|
||||||
|
if m["view_forward_cone_ready_pct"] is not None]),
|
||||||
|
"camera_forward_samples": camera_samples,
|
||||||
|
"camera_forward_sample_share_pct": percent(camera_samples, len(moving)),
|
||||||
|
"near_forward_cosine_weighted_ready_pct": mean(
|
||||||
|
[m["forward_weighted_ready_pct"] for m in moving_metrics
|
||||||
|
if m["forward_weighted_ready_pct"] is not None]),
|
||||||
|
"arrival_lod02_settled_t_s": (
|
||||||
|
float(arrival_near_settled.get("t_s", 0.0)) if arrival_near_settled else None),
|
||||||
|
"near_ready_dwell_end_pct": (100.0 * sum(dwell[-1].get("resident_by_lod", [0] * 9)[:3])
|
||||||
|
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[:3]))),
|
||||||
|
"far_holes_dwell_end_pct": (100.0 * (
|
||||||
|
sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:])
|
||||||
|
- sum(dwell[-1].get("covered_by_lod", [0] * 9)[3:]))
|
||||||
|
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:]))),
|
||||||
|
"dwell_complete_after_stop_s": dwell_complete_s,
|
||||||
|
"game_thread_tick_p99_ms": p99_field("tick_ms"),
|
||||||
|
"game_thread_tick_max_ms": max_field("tick_ms"),
|
||||||
|
"stream_update_p99_ms": p99_field("update_chunks_ms"),
|
||||||
|
"pending_drain_p99_ms": p99_field("pending_drain_ms"),
|
||||||
|
"apply_p99_ms": p99_field("apply_ms"),
|
||||||
|
"apply_max_ms": max_field("apply_ms"),
|
||||||
|
"apply_budget_min_ms": min((f["apply_budget_ms"] for f in frames), default=None),
|
||||||
|
"apply_budget_max_ms": max((f["apply_budget_ms"] for f in frames), default=None),
|
||||||
|
"apply_time_budget_hit_frames": sum(f["time_budget_hit"] for f in frames),
|
||||||
|
"apply_frames": sum(f["meshes_applied"] > 0 for f in frames),
|
||||||
|
"apply_meshes_total": sum(f["meshes_applied"] for f in frames),
|
||||||
|
"desired_rebuild_p99_ms": p99_field("desired_rebuild_ms"),
|
||||||
|
"desired_rebuild_count": len(rebuild_frames),
|
||||||
|
"desired_rebuilds_per_second": len(rebuild_frames) / 12.0,
|
||||||
|
"center_crossings": center_crossings,
|
||||||
|
"same_center_core_region_requests": same_center_core_requests,
|
||||||
|
"core_desired_cache_hit_count": core_desired_cache_hits,
|
||||||
|
"core_desired_cache_hit_share_pct": percent(
|
||||||
|
core_desired_cache_hits, len(core_desired_queries)),
|
||||||
|
"core_desired_query_nonzero_p50_ms": statistics.median(core_desired_query_ms)
|
||||||
|
if core_desired_query_ms else None,
|
||||||
|
"core_desired_query_nonzero_p95_ms": percentile(core_desired_query_ms, 0.95),
|
||||||
|
"core_desired_query_nonzero_max_ms": max(core_desired_query_ms)
|
||||||
|
if core_desired_query_ms else None,
|
||||||
|
"desired_rebuild_nonzero_p50_ms": statistics.median(rebuild_costs)
|
||||||
|
if rebuild_costs else None,
|
||||||
|
"desired_rebuild_nonzero_p95_ms": percentile(rebuild_costs, 0.95),
|
||||||
|
"desired_rebuild_nonzero_max_ms": max(rebuild_costs) if rebuild_costs else None,
|
||||||
|
"near_submitted_by_direction": direction_submission_totals,
|
||||||
|
"near_admission_first12": first_admission_order,
|
||||||
|
"submit_tiles_p99_ms": p99_field("submit_tiles_ms"),
|
||||||
|
"level_admit_p99_ms": p99_field("level_admit_ms"),
|
||||||
|
"top_tick_frames": sorted(frames, key=lambda f: f["tick_ms"], reverse=True)[:5],
|
||||||
|
"trace": str(trace.relative_to(ROOT)),
|
||||||
|
"log": str(abslog.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
print("[L165Movement] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--smoke", action="store_true",
|
||||||
|
help="run one incremental trace to validate readback and equality audits")
|
||||||
|
parser.add_argument("--geometry-hash-check", action="store_true",
|
||||||
|
help="compare UE per-tile geometry hashes for baseline and incremental desired sets")
|
||||||
|
parser.add_argument("--controls-only", action="store_true",
|
||||||
|
help="run a per-crossing equality audit and injected mismatch negative control")
|
||||||
|
parser.add_argument("--summarize-existing", action="store_true",
|
||||||
|
help="summarize the ten completed rows in runs-partial.json without rerunning UE")
|
||||||
|
args = parser.parse_args()
|
||||||
|
missing = [path for path in (EDITOR, HOST / "VFHost.uproject") if not path.exists()]
|
||||||
|
if missing:
|
||||||
|
raise SystemExit(f"missing UE runner input: {missing}")
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
if args.geometry_hash_check:
|
||||||
|
hash_dir = OUT / "geometry-hash"
|
||||||
|
baseline_path = hash_dir / "assembler-world.json"
|
||||||
|
candidate_path = hash_dir / "incremental.json"
|
||||||
|
run("assembler_world", 99, baseline_path)
|
||||||
|
run("incremental", 99, candidate_path)
|
||||||
|
baseline_data = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||||
|
candidate_data = json.loads(candidate_path.read_text(encoding="utf-8"))
|
||||||
|
key_fields = ("level", "tile_x", "tile_y", "tile_z")
|
||||||
|
|
||||||
|
def indexed(data: dict) -> dict[tuple[int, ...], tuple]:
|
||||||
|
result = {}
|
||||||
|
for record in data.get("records", []):
|
||||||
|
key = tuple(int(record[field]) for field in key_fields)
|
||||||
|
result[key] = (str(record.get("geometry_hash", "")),
|
||||||
|
int(record.get("triangle_count", -1)),
|
||||||
|
bool(record.get("empty", False)))
|
||||||
|
return result
|
||||||
|
|
||||||
|
baseline_hashes = indexed(baseline_data)
|
||||||
|
candidate_hashes = indexed(candidate_data)
|
||||||
|
common = baseline_hashes.keys() & candidate_hashes.keys()
|
||||||
|
mismatches = [key for key in common
|
||||||
|
if baseline_hashes[key] != candidate_hashes[key]]
|
||||||
|
result = {
|
||||||
|
"baseline_records": len(baseline_hashes),
|
||||||
|
"candidate_records": len(candidate_hashes),
|
||||||
|
"common_records": len(common),
|
||||||
|
"hash_mismatches": len(mismatches),
|
||||||
|
"baseline_only_records": len(baseline_hashes.keys() - candidate_hashes.keys()),
|
||||||
|
"candidate_only_records": len(candidate_hashes.keys() - baseline_hashes.keys()),
|
||||||
|
"baseline_dump": str(baseline_path.relative_to(ROOT)),
|
||||||
|
"candidate_dump": str(candidate_path.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
(hash_dir / "comparison.json").write_text(
|
||||||
|
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
print("[L165GeometryHash] " + json.dumps(result, sort_keys=True), flush=True)
|
||||||
|
return 0 if len(common) >= 100 and not mismatches else 1
|
||||||
|
if args.smoke:
|
||||||
|
run("incremental", 0)
|
||||||
|
return 0
|
||||||
|
if args.controls_only:
|
||||||
|
control = dict(CONDITIONS["incremental"])
|
||||||
|
control["audit_every"] = 1
|
||||||
|
control["inject_mismatch"] = 0
|
||||||
|
CONDITIONS["audit_every_1_control"] = control
|
||||||
|
run("audit_every_1_control", 0)
|
||||||
|
mismatch = dict(control)
|
||||||
|
mismatch["inject_mismatch"] = 1
|
||||||
|
CONDITIONS["injected_mismatch_control"] = mismatch
|
||||||
|
run("injected_mismatch_control", 0, expected_mismatch=True)
|
||||||
|
return 0
|
||||||
|
partial = OUT / "runs-partial.json"
|
||||||
|
if args.summarize_existing:
|
||||||
|
if not partial.is_file():
|
||||||
|
raise RuntimeError(f"no saved movement rows: {partial}")
|
||||||
|
rows = json.loads(partial.read_text(encoding="utf-8"))
|
||||||
|
if len(rows) != 10:
|
||||||
|
raise RuntimeError(f"expected ten completed movement rows, found {len(rows)}")
|
||||||
|
else:
|
||||||
|
rows = []
|
||||||
|
# Both conditions use this editor binary. Warm each path before measured pairs.
|
||||||
|
run("assembler_world", 0)
|
||||||
|
run("incremental", 0)
|
||||||
|
for repetition in range(1, 6):
|
||||||
|
for condition in ORDER[repetition]:
|
||||||
|
row = run(condition, repetition)
|
||||||
|
rows.append(row)
|
||||||
|
partial.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
|
||||||
|
metrics = (
|
||||||
|
"near_ready_moving_mean_pct", "far_holes_moving_mean_pct",
|
||||||
|
"game_thread_tick_p99_ms", "game_thread_tick_max_ms",
|
||||||
|
"arrival_lod02_settled_t_s", "dwell_complete_after_stop_s",
|
||||||
|
"near_ahead_desired_share_pct", "near_behind_desired_share_pct",
|
||||||
|
"near_side_desired_share_pct", "near_forward_cone_desired_share_pct",
|
||||||
|
"near_ahead_ready_pct", "near_behind_ready_pct",
|
||||||
|
"near_forward_cone_ready_pct", "near_forward_cosine_weighted_ready_pct",
|
||||||
|
"near_travel_forward_cone_ready_pct",
|
||||||
|
"apply_p99_ms", "desired_rebuild_p99_ms", "desired_rebuild_count",
|
||||||
|
"desired_rebuild_nonzero_p95_ms", "center_crossings",
|
||||||
|
"same_center_core_region_requests", "level_admit_p99_ms",
|
||||||
|
"core_desired_cache_hit_count", "core_desired_query_nonzero_p95_ms",
|
||||||
|
)
|
||||||
|
medians = {}
|
||||||
|
for condition in CONDITIONS:
|
||||||
|
medians[condition] = {}
|
||||||
|
for metric in metrics:
|
||||||
|
values = [row[metric] for row in rows
|
||||||
|
if row["condition"] == condition and row[metric] is not None]
|
||||||
|
medians[condition][metric] = statistics.median(values) if values else None
|
||||||
|
pairs = []
|
||||||
|
for repetition in range(1, 6):
|
||||||
|
by_condition = {row["condition"]: row for row in rows
|
||||||
|
if row["repetition"] == repetition}
|
||||||
|
candidate = "incremental"
|
||||||
|
pairs.append({
|
||||||
|
"repetition": repetition,
|
||||||
|
"candidate": candidate,
|
||||||
|
**{f"{metric}_delta": by_condition[candidate][metric]
|
||||||
|
- by_condition["assembler_world"][metric]
|
||||||
|
for metric in metrics if by_condition[candidate][metric] is not None
|
||||||
|
and by_condition["assembler_world"][metric] is not None},
|
||||||
|
})
|
||||||
|
by_pair = {
|
||||||
|
repetition: {row["condition"]: row for row in rows if row["repetition"] == repetition}
|
||||||
|
for repetition in range(1, 6)
|
||||||
|
}
|
||||||
|
candidate_rows = [row for row in rows if row["condition"] == "incremental"]
|
||||||
|
baseline_rows = [row for row in rows if row["condition"] == "assembler_world"]
|
||||||
|
candidate_gates = {
|
||||||
|
"every_candidate_run_p99_le_11_ms": all(
|
||||||
|
row["game_thread_tick_p99_ms"] <= 11.0 for row in candidate_rows),
|
||||||
|
"paired_worst_tick_no_worse": all(
|
||||||
|
by_pair[rep]["incremental"]["game_thread_tick_max_ms"]
|
||||||
|
<= by_pair[rep]["assembler_world"]["game_thread_tick_max_ms"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"paired_near_all_around_no_worse": all(
|
||||||
|
by_pair[rep]["incremental"]["near_ready_moving_mean_pct"]
|
||||||
|
>= by_pair[rep]["assembler_world"]["near_ready_moving_mean_pct"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"paired_forward_cone_no_worse": all(
|
||||||
|
by_pair[rep]["incremental"]["near_forward_cone_ready_pct"]
|
||||||
|
>= by_pair[rep]["assembler_world"]["near_forward_cone_ready_pct"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"paired_far_holes_no_worse": all(
|
||||||
|
by_pair[rep]["incremental"]["far_holes_moving_mean_pct"]
|
||||||
|
<= by_pair[rep]["assembler_world"]["far_holes_moving_mean_pct"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"paired_arrival_no_worse": all(
|
||||||
|
by_pair[rep]["incremental"]["arrival_lod02_settled_t_s"] is not None
|
||||||
|
and by_pair[rep]["assembler_world"]["arrival_lod02_settled_t_s"] is not None
|
||||||
|
and by_pair[rep]["incremental"]["arrival_lod02_settled_t_s"]
|
||||||
|
<= by_pair[rep]["assembler_world"]["arrival_lod02_settled_t_s"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"all_candidate_equality_audits_pass": all(
|
||||||
|
row["incremental_desired_audit_count"] > 0
|
||||||
|
and row["incremental_desired_audit_failures"] == 0
|
||||||
|
for row in candidate_rows),
|
||||||
|
"median_build_desired_p95_down": statistics.median(
|
||||||
|
row["desired_rebuild_nonzero_p95_ms"] for row in candidate_rows)
|
||||||
|
< statistics.median(row["desired_rebuild_nonzero_p95_ms"]
|
||||||
|
for row in baseline_rows),
|
||||||
|
}
|
||||||
|
summary = {
|
||||||
|
"source_commit": subprocess.run(
|
||||||
|
["git", "-c", f"safe.directory={ROOT}", "rev-parse", "HEAD"],
|
||||||
|
cwd=ROOT, check=True, capture_output=True, text=True).stdout.strip(),
|
||||||
|
"protocol": "Five interleaved 30 m/s UE pairs; 12 s move plus 5 s dwell; 8 effective tile slots. L157 critical wait remains on and apply cap remains 0 ms.",
|
||||||
|
"conditions": CONDITIONS,
|
||||||
|
"movement_medians": medians,
|
||||||
|
"paired_deltas_vs_assembler_world": pairs,
|
||||||
|
"candidate_gates": candidate_gates,
|
||||||
|
"runs": rows,
|
||||||
|
}
|
||||||
|
(OUT / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
print("[L165Summary] " + json.dumps({"movement_medians": medians,
|
||||||
|
"candidate_gates": candidate_gates,
|
||||||
|
"runs": len(rows)}, sort_keys=True), flush=True)
|
||||||
|
return 0 if all(candidate_gates.values()) else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# L166 — Incremental streaming update
|
||||||
|
|
||||||
|
**Result:** Core and overlay candidate materialization now apply entering/leaving deltas, and same-center priority order updates merge only changed keys. Seven 30 m/s W8 pairs show lower median rebuild p95, tick p99, worst tick, near readiness, forward-cone readiness, and far holes. The strict pairwise bar is not fully clean: one worst-tick pair, three far-hole pairs, and four arrival pairs regressed slightly. Set/order audits and the fast tier pass.
|
||||||
|
|
||||||
|
## Count first
|
||||||
|
|
||||||
|
Stage costs below are milliseconds per desired rebuild, reported as the median of the seven runs' p50 and p95 values. Each run's window is its 12-second move at 30 m/s; there were 44–45 center crossings per run.
|
||||||
|
|
||||||
|
| Stage | `assembler/world` p50 / p95 | Incremental p50 / p95 |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| Core query/preparation | 0.832 / 2.229 | 0.0002 / 0.142 |
|
||||||
|
| Core crossing delta | — | 0.000 / 0.138 |
|
||||||
|
| UE overlay materialization | 0.350 / 0.723 | 0.103 / 0.356 |
|
||||||
|
| Priority sort/order | 0.306 / 0.656 | 0.038 / 0.437 |
|
||||||
|
| Critical admission order | 0.063 / 0.134 | 0.065 / 0.222 |
|
||||||
|
|
||||||
|
The incremental Core path updated on 98.4% of desired rebuilds. Priority order merged key deltas on 52.2%, reused the cached order unchanged on 33.5%, and fully sorted on the remaining 14.3% (the center-crossing updates). LOD4+ crossings caused five full Core-query fallbacks per 12-second move; their median per-run maximum was 1.19 ms and their median total was 5.25 ms spread across those five events.
|
||||||
|
|
||||||
|
The median worst tick was 11.652 ms versus 12.980 ms for the baseline. The largest candidate worst-tick sample was 12.764 ms at a center crossing: its priority sort took 1.311 ms, overlay materialization 0.915 ms, and Core delta 0.601 ms; no LOD4+ fallback occurred on that tick. Priority sorting remained the largest measured substage in that worst tick. Fallbacks were not the source of the remaining worst ticks.
|
||||||
|
|
||||||
|
## Change and equality checks
|
||||||
|
|
||||||
|
`BuildCoreDesiredTiles` now retains the Core visual set, UE overlay set, candidate set, and priority order. It applies Core and overlay membership deltas to the persistent sets. Live anchor, gate, and support inputs are still walked into a temporary overlay union on each trigger. At an unchanged center, it removes leaving keys and merges sorted entering keys into the cached priority order. Center changes still require a full priority sort because the distance comparator changes. The critical-key promotion list is still rebuilt on each trigger.
|
||||||
|
|
||||||
|
The equality audit independently obtains a full Core desired set, rebuilds the overlay reference, and compares candidate and collision-only sets. A second audit builds the full prioritized and critical order and compares it with the incremental result. Across the seven measured candidate runs, all 28 sampled set audits and all 28 order audits passed. A positive materialization-control run passed 13 additional set and order audits. The injected mismatch control logged `missing=1` and failed with a nonzero UE exit (`-11`), as expected.
|
||||||
|
|
||||||
|
## Seven interleaved pairs
|
||||||
|
|
||||||
|
Each pair compared `assembler/world` at `9c70053` against this worktree, with a 12-second 30 m/s move, 5-second dwell, and eight effective tile slots. Values are medians across seven runs; paired gates are stated separately.
|
||||||
|
|
||||||
|
| Metric | `assembler/world` | Incremental | Paired result |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| `BuildDesiredTiles` p95 | 7.281 ms | 3.738 ms | 48.7% lower median |
|
||||||
|
| Game-thread tick p99 | 10.521 ms | 7.420 ms | Lower in all 7 pairs; candidate range 7.031–7.526 ms |
|
||||||
|
| Worst tick | 12.980 ms | 11.652 ms | Lower in 6/7 pairs; pair 5 was 0.347 ms higher |
|
||||||
|
| Near LOD0–2 readiness | 41.995% | 43.668% | Better in all 7 pairs |
|
||||||
|
| 55° forward-cone readiness | 14.779% | 16.116% | Better in all 7 pairs |
|
||||||
|
| Far holes while moving | 3.197% | 2.739% | Lower median; better in 4/7 pairs |
|
||||||
|
| First settled LOD0–2 arrival | 2.432 s | 2.439 s | Paired median delta +0.002 s; later in 4/7 pairs |
|
||||||
|
|
||||||
|
The paired far-hole deltas were −0.072, +0.144, −0.059, +0.301, −1.052, −0.773, and +0.069 percentage points. The L165 +0.291 pp result is within this observed spread; L166 also had one +0.301 pp pair, but the seven-pair median delta was −0.059 pp and four pairs improved. This is consistent with substantial run-to-run variation, not a consistent far-hole regression; it does not establish a strict no-regression guarantee.
|
||||||
|
|
||||||
|
Null-RHI produced no camera-forward samples, so the 55° cone uses the travel-direction fallback. The arrival measure also varied sharply between pairs, including two candidate runs that settled 0.22–0.26 s earlier. Its paired median was effectively flat, but the strict per-pair arrival gate did not pass.
|
||||||
|
|
||||||
|
## Checks and limits
|
||||||
|
|
||||||
|
- VFHost Linux Development build succeeded with `-NoUBA -MaxParallelActions=4`; `VFHost/PluginDeps/VoxelForge/Source` was restored to `/home/jahni/VoxelM/Plugins/VoxelForge/Source`.
|
||||||
|
- `verify.py fast` passed: 268 checks, 0 failures, 5 diagnostics, 281.5 s. Clang/GCC W1/W8/W12 determinism and canonical geometry checks passed.
|
||||||
|
- The live movement `TileHashDump` hook was enabled but wrote zero records for both conditions, so that direct runtime geometry comparison is inconclusive. No meshing or geometry-generation code changed; the fast-tier geometry hashes and sampled full-rebuild order audit passed.
|
||||||
|
- `git diff --check` and runner `py_compile` passed. W12 movement timing, camera-forward cone capture, and separate movement runs on each of the four owner layers were not run.
|
||||||
|
- The strict movement summary fails `paired_worst_tick_no_worse`, `paired_far_holes_no_worse`, and `paired_arrival_no_worse`; p99, near readiness, forward-cone readiness, audits, and median rebuild-p95 gates pass.
|
||||||
|
|
||||||
|
Changed Unreal `Source/` files:
|
||||||
|
|
||||||
|
- `Source/VoxelForge/Private/VoxelWorld.cpp`
|
||||||
|
- `Source/VoxelForge/Public/VoxelWorld.h`
|
||||||
|
|
||||||
|
Logs, traces, summary, and verifier artifacts are under `Saved/Linux/l166/`.
|
||||||
@@ -0,0 +1,685 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Interleaved L166 UE movement runs for incremental desired-set rebuilds."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
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")
|
||||||
|
OUT = ROOT / "Saved/Linux/l166/ue/movement"
|
||||||
|
HOST_SAVED = HOST / "Saved/Linux/l166"
|
||||||
|
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l166"
|
||||||
|
SPAWN_M = (19.166015625, -14.2001953125, -753.3740234375)
|
||||||
|
MOVE_START_S = 6.0
|
||||||
|
|
||||||
|
CONDITIONS = {
|
||||||
|
"assembler_world": {
|
||||||
|
"wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||||
|
"forward_first": 0, "rebuild_on_region_completion": 1,
|
||||||
|
"core_desired_cache": 0, "incremental_desired": 0,
|
||||||
|
"audit_every": 0, "materialization_audit_every": 0, "inject_mismatch": 0,
|
||||||
|
},
|
||||||
|
"incremental": {
|
||||||
|
"wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||||
|
"forward_first": 0, "rebuild_on_region_completion": 1,
|
||||||
|
"core_desired_cache": 0, "incremental_desired": 1,
|
||||||
|
"audit_every": 10, "materialization_audit_every": 0, "inject_mismatch": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ORDER = {
|
||||||
|
1: ("assembler_world", "incremental"),
|
||||||
|
2: ("incremental", "assembler_world"),
|
||||||
|
3: ("assembler_world", "incremental"),
|
||||||
|
4: ("incremental", "assembler_world"),
|
||||||
|
5: ("assembler_world", "incremental"),
|
||||||
|
6: ("incremental", "assembler_world"),
|
||||||
|
7: ("assembler_world", "incremental"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def percentile(values: list[float], fraction: float = 0.99) -> float | None:
|
||||||
|
if not values:
|
||||||
|
return None
|
||||||
|
ordered = sorted(values)
|
||||||
|
return ordered[max(0, min(len(ordered) - 1, math.ceil(len(ordered) * fraction) - 1))]
|
||||||
|
|
||||||
|
|
||||||
|
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 mean(values: list[float]) -> float | None:
|
||||||
|
return statistics.mean(values) if values else None
|
||||||
|
|
||||||
|
|
||||||
|
def desired_breakdown(events: list[dict], move_start_s: float, stop_s: float) -> dict:
|
||||||
|
rows = [event for event in events
|
||||||
|
if event.get("type") == "desired_rebuild_breakdown"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
keys = ("total_ms", "core_prepare_ms", "core_delta_ms", "fallback_ms",
|
||||||
|
"overlay_materialize_ms", "priority_sort_ms", "critical_order_ms")
|
||||||
|
result = {"rebuild_count": len(rows)}
|
||||||
|
result["incremental_materialization_update_count"] = sum(
|
||||||
|
int(event.get("materialization_update", 0)) != 0 for event in rows)
|
||||||
|
result["priority_order_reuse_count"] = sum(
|
||||||
|
int(event.get("priority_order_reused", 0)) != 0 for event in rows)
|
||||||
|
result["priority_order_update_count"] = sum(
|
||||||
|
int(event.get("priority_order_updated", 0)) != 0 for event in rows)
|
||||||
|
result["incremental_materialization_update_share_pct"] = percent(
|
||||||
|
result["incremental_materialization_update_count"], len(rows))
|
||||||
|
result["priority_order_reuse_share_pct"] = percent(
|
||||||
|
result["priority_order_reuse_count"], len(rows))
|
||||||
|
result["priority_order_update_share_pct"] = percent(
|
||||||
|
result["priority_order_update_count"], len(rows))
|
||||||
|
for key in keys:
|
||||||
|
values = [float(event.get(key, 0.0)) for event in rows]
|
||||||
|
result[f"{key}_p50"] = statistics.median(values) if values else None
|
||||||
|
result[f"{key}_p95"] = percentile(values, 0.95)
|
||||||
|
result[f"{key}_max"] = max(values) if values else None
|
||||||
|
fallback_rows = [event for event in events
|
||||||
|
if event.get("type") == "core_desired_cache"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s
|
||||||
|
and int(event.get("full_fallback", 0)) != 0]
|
||||||
|
result["fallback_count"] = len(fallback_rows)
|
||||||
|
result["lod4plus_fallback_count"] = sum(
|
||||||
|
int(event.get("fallback_reason", 0)) == 1 for event in fallback_rows)
|
||||||
|
result["other_fallback_count"] = sum(
|
||||||
|
int(event.get("fallback_reason", 0)) == 2 for event in fallback_rows)
|
||||||
|
result["fallback_cost_sum_ms"] = sum(
|
||||||
|
float(event.get("fallback_ms", 0.0)) for event in fallback_rows)
|
||||||
|
result["fallback_cost_max_ms"] = max(
|
||||||
|
(float(event.get("fallback_ms", 0.0)) for event in fallback_rows), default=None)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def percent(numerator: int | float, denominator: int | float) -> float | None:
|
||||||
|
return 100.0 * numerator / denominator if denominator > 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def run(condition: str, repetition: int, tile_hash_dump: Path | None = None,
|
||||||
|
expected_mismatch: bool = False) -> dict:
|
||||||
|
config = CONDITIONS[condition]
|
||||||
|
run_name = f"fast-r{repetition}"
|
||||||
|
out = OUT / condition
|
||||||
|
host_out = HOST_PLUGIN_SAVED / condition / "output"
|
||||||
|
userdir = HOST_SAVED / "user" / condition / run_name
|
||||||
|
zendata = HOST_SAVED / "zen" / condition / run_name
|
||||||
|
ddc = HOST_SAVED / "DDC"
|
||||||
|
trace = out / f"{run_name}.trace.jsonl"
|
||||||
|
abslog = out / f"{run_name}.log"
|
||||||
|
stdout = out / f"{run_name}.stdout.log"
|
||||||
|
export = host_out / run_name
|
||||||
|
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.StreamingCoarseFirst=1",
|
||||||
|
"-voxel.StreamingDynamicNearReserve=1",
|
||||||
|
"-voxel.StreamingLegacyPendingResultCap=0",
|
||||||
|
"-voxel.CoreRegionConcurrentAdmission=1",
|
||||||
|
"-voxel.StreamingResultBacklogCap=256", "-voxel.StreamingReadyQueueDepth=8",
|
||||||
|
"-voxel.StreamingBackgroundWorkerCap=1", "-voxel.CoreColumnCacheDirectMapped=0",
|
||||||
|
"-voxel.StreamingFarApplySupplement=1",
|
||||||
|
f"-voxel.StreamingFastTravelWaitForAllCriticalTiles={config['wait_for_critical']}",
|
||||||
|
f"-voxel.StreamingFastTravelApplyBudgetMilliseconds={config['apply_budget_ms']:.1f}",
|
||||||
|
f"-voxel.StreamingFastTravelForwardFirst={config['forward_first']}",
|
||||||
|
f"-voxel.StreamingRebuildOnCoreRegionCompletion={config['rebuild_on_region_completion']}",
|
||||||
|
f"-voxel.StreamingCoreDesiredCacheByCenter={config['core_desired_cache']}",
|
||||||
|
f"-voxel.StreamingIncrementalDesiredSet={config['incremental_desired']}",
|
||||||
|
f"-voxel.StreamingIncrementalDesiredAuditEvery={config['audit_every']}",
|
||||||
|
f"-voxel.StreamingIncrementalDesiredMaterializationAuditEvery={config['materialization_audit_every']}",
|
||||||
|
f"-voxel.StreamingIncrementalDesiredAuditInjectMismatch={config['inject_mismatch']}",
|
||||||
|
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
|
||||||
|
"-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=3000.0",
|
||||||
|
f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}",
|
||||||
|
"-voxel.TestMoveDurationSeconds=12.0", "-voxel.TestExitSeconds=0.0",
|
||||||
|
"-voxel.TestMoveDirection=1,0,0", "-voxel.StreamingSchedulerPolicy=3",
|
||||||
|
"-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={abslog}",
|
||||||
|
]
|
||||||
|
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()}")
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command]
|
||||||
|
started = time.monotonic()
|
||||||
|
with stdout.open("w", encoding="utf-8") as stream:
|
||||||
|
process = subprocess.run(
|
||||||
|
wrapped, cwd=ROOT, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
check=False, timeout=1740)
|
||||||
|
|
||||||
|
events = read_events(trace)
|
||||||
|
if expected_mismatch:
|
||||||
|
diagnostic = "VoxelForgeIncrementalDesiredAudit"
|
||||||
|
logs = "\n".join(path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
for path in (abslog, stdout) if path.is_file())
|
||||||
|
audit_events = [event for event in events
|
||||||
|
if event.get("type") in {"incremental_desired_audit",
|
||||||
|
"incremental_desired_order_audit"}]
|
||||||
|
failed_audit = any(int(event.get("pass", 1)) == 0 for event in audit_events)
|
||||||
|
failed_audit = failed_audit or (
|
||||||
|
diagnostic in logs and "mismatch" in logs.lower())
|
||||||
|
if process.returncode == 0 or diagnostic not in logs or "mismatch" not in logs.lower():
|
||||||
|
raise RuntimeError(
|
||||||
|
f"injected mismatch did not fail as expected: exit={process.returncode}; "
|
||||||
|
f"audit_events={len(audit_events)}; log={abslog}")
|
||||||
|
print("[L166MismatchControl] " + json.dumps({
|
||||||
|
"exit_code": process.returncode,
|
||||||
|
"diagnostic_seen": True,
|
||||||
|
"failed_audit_event_seen": failed_audit,
|
||||||
|
"trace": str(trace.relative_to(ROOT)),
|
||||||
|
"log": str(abslog.relative_to(ROOT)),
|
||||||
|
}, sort_keys=True), flush=True)
|
||||||
|
return {"condition": condition, "exit_code": process.returncode,
|
||||||
|
"failed_audit_event_seen": failed_audit}
|
||||||
|
samples = [event for event in events if event.get("type") == "streaming_sample"]
|
||||||
|
moving = [event for event in samples if float(event.get("speed_cm_s", 0.0)) >= 50.0]
|
||||||
|
stop_event = next((event for event in events
|
||||||
|
if event.get("type") == "streaming_test_move_stopped"), None)
|
||||||
|
backend = next((event for event in events if event.get("type") == "worker_backend_config"), {})
|
||||||
|
scheduler_config = next((event for event in events
|
||||||
|
if event.get("type") == "scheduler_admission"), {})
|
||||||
|
worker_cap = next((event for event in events
|
||||||
|
if event.get("type") == "streaming_worker_cap_readback"), {})
|
||||||
|
if stop_event is None:
|
||||||
|
raise RuntimeError(f"missing move-stop event: {trace}")
|
||||||
|
stop_s = float(stop_event.get("t_s", 0.0))
|
||||||
|
move_start_s = min((float(event.get("t_s", 0.0)) for event in moving), default=float("inf"))
|
||||||
|
dwell = [event for event in samples if float(event.get("t_s", 0.0)) >= stop_s
|
||||||
|
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||||
|
arrival = [event for event in samples if float(event.get("t_s", 0.0)) < move_start_s
|
||||||
|
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||||
|
desired_stage_breakdown = desired_breakdown(events, move_start_s, stop_s)
|
||||||
|
if process.returncode or len(moving) < 40 or len(dwell) < 16:
|
||||||
|
raise RuntimeError(f"incomplete {condition} run: {trace}; exit={process.returncode}; "
|
||||||
|
f"move={len(moving)} dwell={len(dwell)}")
|
||||||
|
if (int(backend.get("streaming_fast_travel_wait_for_all_critical_tiles", -1))
|
||||||
|
!= config["wait_for_critical"]
|
||||||
|
or abs(float(backend.get("streaming_fast_travel_apply_budget_ms", -1.0))
|
||||||
|
- config["apply_budget_ms"]) > 0.01
|
||||||
|
or int(worker_cap.get("streaming_background_worker_cap", -1)) != 1
|
||||||
|
or int(backend.get("effective_tile_slots", -1)) != 8
|
||||||
|
or int(backend.get("streaming_far_apply_supplement", -1)) != 1
|
||||||
|
or int(scheduler_config.get("forward_first", -1)) != config["forward_first"]
|
||||||
|
or int(scheduler_config.get("rebuild_on_core_region_completion", -1))
|
||||||
|
!= config["rebuild_on_region_completion"]
|
||||||
|
or int(scheduler_config.get("core_desired_cache_by_center", -1))
|
||||||
|
!= config["core_desired_cache"]
|
||||||
|
or int(scheduler_config.get("incremental_desired_set", -1))
|
||||||
|
!= config["incremental_desired"]
|
||||||
|
or int(scheduler_config.get("incremental_desired_audit_every", -1))
|
||||||
|
!= config["audit_every"]
|
||||||
|
or int(scheduler_config.get("incremental_desired_materialization_audit_every", -1))
|
||||||
|
!= config["materialization_audit_every"]
|
||||||
|
or int(scheduler_config.get("incremental_desired_audit_inject_mismatch", -1))
|
||||||
|
!= config["inject_mismatch"]):
|
||||||
|
raise RuntimeError(f"CVar/worker readback mismatch: {backend}; {worker_cap}; {scheduler_config}")
|
||||||
|
|
||||||
|
moving_metrics = []
|
||||||
|
for event in moving:
|
||||||
|
desired = event.get("desired_by_lod", [0] * 9)
|
||||||
|
resident = event.get("resident_by_lod", [0] * 9)
|
||||||
|
near_desired = sum(desired[:3])
|
||||||
|
cone_desired = int(event.get("near_forward_cone_desired", 0))
|
||||||
|
view_cone_desired = int(event.get("near_view_forward_cone_desired", 0))
|
||||||
|
view_valid = bool(event.get("near_view_forward_cone_valid", 0))
|
||||||
|
weighted_desired = float(event.get("near_forward_weighted_desired", 0.0))
|
||||||
|
travel_cone_ready = percent(
|
||||||
|
int(event.get("near_forward_cone_resident", 0)), cone_desired)
|
||||||
|
view_cone_ready = percent(
|
||||||
|
int(event.get("near_view_forward_cone_resident", 0)), view_cone_desired)
|
||||||
|
moving_metrics.append({
|
||||||
|
"near_ready_pct": percent(sum(resident[:3]), near_desired) or 0.0,
|
||||||
|
"lod0_ready_pct": percent(resident[0], desired[0]) or 0.0,
|
||||||
|
"lod1_ready_pct": percent(resident[1], desired[1]) or 0.0,
|
||||||
|
"lod2_ready_pct": percent(resident[2], desired[2]) or 0.0,
|
||||||
|
"far_hole_pct": percent(
|
||||||
|
sum(desired[3:]) - sum(event.get("covered_by_lod", resident)[3:]),
|
||||||
|
sum(desired[3:])) or 0.0,
|
||||||
|
"ahead_share_pct": percent(int(event.get("near_ahead_desired", 0)), near_desired),
|
||||||
|
"behind_share_pct": percent(int(event.get("near_behind_desired", 0)), near_desired),
|
||||||
|
"side_share_pct": 100.0 - (
|
||||||
|
percent(int(event.get("near_ahead_desired", 0)), near_desired) or 0.0
|
||||||
|
) - (
|
||||||
|
percent(int(event.get("near_behind_desired", 0)), near_desired) or 0.0
|
||||||
|
),
|
||||||
|
"ahead_ready_pct": percent(
|
||||||
|
int(event.get("near_ahead_resident", 0)),
|
||||||
|
int(event.get("near_ahead_desired", 0))),
|
||||||
|
"behind_ready_pct": percent(
|
||||||
|
int(event.get("near_behind_resident", 0)),
|
||||||
|
int(event.get("near_behind_desired", 0))),
|
||||||
|
"travel_forward_cone_ready_pct": travel_cone_ready,
|
||||||
|
"view_forward_cone_ready_pct": view_cone_ready,
|
||||||
|
"forward_cone_ready_pct": view_cone_ready if view_valid else travel_cone_ready,
|
||||||
|
"forward_cone_source": "camera" if view_valid else "travel_fallback",
|
||||||
|
"forward_cone_share_pct": percent(cone_desired, near_desired),
|
||||||
|
"forward_weighted_ready_pct": percent(
|
||||||
|
float(event.get("near_forward_weighted_resident", 0.0)), weighted_desired),
|
||||||
|
})
|
||||||
|
|
||||||
|
arrival_near_settled = next((event for event in arrival
|
||||||
|
if sum(event.get("desired_by_lod", [0] * 9)[:3]) > 0
|
||||||
|
and sum(event.get("settled_by_lod", [0] * 9)[:3])
|
||||||
|
>= sum(event.get("desired_by_lod", [0] * 9)[:3])), None)
|
||||||
|
dwell_complete = next((event for event in dwell
|
||||||
|
if sum(event.get("resident_by_lod", [0] * 9)[:3])
|
||||||
|
>= sum(event.get("desired_by_lod", [0] * 9)[:3])
|
||||||
|
and sum(event.get("desired_by_lod", [0] * 9)[3:])
|
||||||
|
<= sum(event.get("covered_by_lod", [0] * 9)[3:])), None)
|
||||||
|
|
||||||
|
work = [event for event in events if event.get("type") == "game_thread_work_frame"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
applies = {event.get("frame"): event for event in events
|
||||||
|
if event.get("type") == "apply_frame"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||||
|
scheduler = {event.get("frame"): event for event in events
|
||||||
|
if event.get("type") == "scheduler_admission"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||||
|
frames = []
|
||||||
|
for event in work:
|
||||||
|
apply = applies.get(event.get("frame"), {})
|
||||||
|
admit = scheduler.get(event.get("frame"), {})
|
||||||
|
frames.append({
|
||||||
|
"frame": event.get("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_rebuilt": int(admit.get("desired_rebuilt", 0)),
|
||||||
|
"submit_tiles_ms": float(admit.get("submit_tiles_ms", 0.0)),
|
||||||
|
"level_admit_ms": float(admit.get("level_admit_ms", 0.0)),
|
||||||
|
})
|
||||||
|
p99_field = lambda key: percentile([float(row[key]) for row in frames])
|
||||||
|
max_field = lambda key: max((float(row[key]) for row in frames), default=None)
|
||||||
|
rebuild_frames = [row for row in frames if row["desired_rebuilt"]]
|
||||||
|
rebuild_costs = [float(row["desired_rebuild_ms"]) for row in rebuild_frames]
|
||||||
|
moving_scheduler = [event for event in scheduler.values()]
|
||||||
|
center_crossings = 0
|
||||||
|
previous_center = None
|
||||||
|
for event in sorted(moving_scheduler, key=lambda item: int(item.get("frame", 0))):
|
||||||
|
center = tuple(event.get("center", ()))
|
||||||
|
if previous_center is not None and center != previous_center:
|
||||||
|
center_crossings += 1
|
||||||
|
previous_center = center
|
||||||
|
core_requests = [event for event in events
|
||||||
|
if event.get("type") == "core_region_compile_request"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
same_center_core_requests = sum(
|
||||||
|
event.get("reason") == "desired_set_changed_same_center" for event in core_requests)
|
||||||
|
core_desired_queries = [event for event in events
|
||||||
|
if event.get("type") == "core_desired_cache"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
core_desired_query_ms = [float(event.get("query_ms", 0.0))
|
||||||
|
for event in core_desired_queries if float(event.get("query_ms", 0.0)) > 0.0]
|
||||||
|
core_desired_cache_hits = sum(int(event.get("hit", 0)) for event in core_desired_queries)
|
||||||
|
incremental_audits = [event for event in events
|
||||||
|
if event.get("type") == "incremental_desired_audit"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
incremental_audit_failures = sum(int(event.get("pass", 1)) == 0
|
||||||
|
for event in incremental_audits)
|
||||||
|
order_audits = [event for event in events
|
||||||
|
if event.get("type") == "incremental_desired_order_audit"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
order_audit_failures = sum(int(event.get("pass", 1)) == 0 for event in order_audits)
|
||||||
|
materialization_audits = [event for event in incremental_audits
|
||||||
|
if int(event.get("materialization_audit", 0)) != 0]
|
||||||
|
if config["incremental_desired"] and (
|
||||||
|
not incremental_audits or incremental_audit_failures
|
||||||
|
or not order_audits or order_audit_failures
|
||||||
|
or (config["materialization_audit_every"] > 0
|
||||||
|
and not materialization_audits)):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"incremental desired equality audit missing/failed: {trace}; "
|
||||||
|
f"set={len(incremental_audits)}/{incremental_audit_failures} "
|
||||||
|
f"order={len(order_audits)}/{order_audit_failures} "
|
||||||
|
f"materialization={len(materialization_audits)}")
|
||||||
|
direction_submissions = [event.get("near_submitted_by_direction", [0, 0, 0, 0])
|
||||||
|
for event in moving_scheduler]
|
||||||
|
direction_submissions = [counts for counts in direction_submissions
|
||||||
|
if isinstance(counts, list) and len(counts) == 4]
|
||||||
|
direction_submission_totals = [
|
||||||
|
sum(int(counts[index]) for counts in direction_submissions) for index in range(4)
|
||||||
|
]
|
||||||
|
first_admission_order = next((event.get("near_admission_order_first", [])
|
||||||
|
for event in moving_scheduler
|
||||||
|
if event.get("near_admission_order_first")), [])
|
||||||
|
camera_samples = sum(bool(event.get("near_view_forward_cone_valid", 0)) for event in moving)
|
||||||
|
dwell_complete_s = (float(dwell_complete.get("t_s", 0.0)) - stop_s
|
||||||
|
if dwell_complete else None)
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"condition": condition,
|
||||||
|
"repetition": repetition,
|
||||||
|
"speed_mps": 30.0,
|
||||||
|
"wait_for_all_critical_tiles": config["wait_for_critical"],
|
||||||
|
"fast_travel_apply_budget_ms": config["apply_budget_ms"],
|
||||||
|
"forward_first": config["forward_first"],
|
||||||
|
"rebuild_on_core_region_completion": config["rebuild_on_region_completion"],
|
||||||
|
"core_desired_cache_by_center": config["core_desired_cache"],
|
||||||
|
"incremental_desired_set": config["incremental_desired"],
|
||||||
|
"incremental_desired_audit_every": config["audit_every"],
|
||||||
|
"incremental_desired_materialization_audit_every": config["materialization_audit_every"],
|
||||||
|
"incremental_desired_audit_count": len(incremental_audits),
|
||||||
|
"incremental_desired_audit_failures": incremental_audit_failures,
|
||||||
|
"incremental_desired_order_audit_count": len(order_audits),
|
||||||
|
"incremental_desired_order_audit_failures": order_audit_failures,
|
||||||
|
"incremental_desired_materialization_audit_count": len(materialization_audits),
|
||||||
|
"effective_tile_slots": backend.get("effective_tile_slots"),
|
||||||
|
"exit_code": process.returncode,
|
||||||
|
"wall_s": round(time.monotonic() - started, 3),
|
||||||
|
"moving_samples": len(moving),
|
||||||
|
"dwell_samples": len(dwell),
|
||||||
|
"arrival_samples": len(arrival),
|
||||||
|
"near_ready_moving_mean_pct": mean([m["near_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod0_ready_moving_mean_pct": mean([m["lod0_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod1_ready_moving_mean_pct": mean([m["lod1_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod2_ready_moving_mean_pct": mean([m["lod2_ready_pct"] for m in moving_metrics]),
|
||||||
|
"far_holes_moving_mean_pct": mean([m["far_hole_pct"] for m in moving_metrics]),
|
||||||
|
"near_ahead_desired_share_pct": mean([m["ahead_share_pct"] for m in moving_metrics
|
||||||
|
if m["ahead_share_pct"] is not None]),
|
||||||
|
"near_behind_desired_share_pct": mean([m["behind_share_pct"] for m in moving_metrics
|
||||||
|
if m["behind_share_pct"] is not None]),
|
||||||
|
"near_side_desired_share_pct": mean([m["side_share_pct"] for m in moving_metrics]),
|
||||||
|
"near_ahead_ready_pct": mean([m["ahead_ready_pct"] for m in moving_metrics
|
||||||
|
if m["ahead_ready_pct"] is not None]),
|
||||||
|
"near_behind_ready_pct": mean([m["behind_ready_pct"] for m in moving_metrics
|
||||||
|
if m["behind_ready_pct"] is not None]),
|
||||||
|
"near_forward_cone_half_angle_deg": 55,
|
||||||
|
"near_forward_cone_desired_share_pct": mean([m["forward_cone_share_pct"]
|
||||||
|
for m in moving_metrics
|
||||||
|
if m["forward_cone_share_pct"] is not None]),
|
||||||
|
"near_forward_cone_ready_pct": mean([m["forward_cone_ready_pct"]
|
||||||
|
for m in moving_metrics
|
||||||
|
if m["forward_cone_ready_pct"] is not None]),
|
||||||
|
"near_travel_forward_cone_ready_pct": mean(
|
||||||
|
[m["travel_forward_cone_ready_pct"] for m in moving_metrics
|
||||||
|
if m["travel_forward_cone_ready_pct"] is not None]),
|
||||||
|
"near_view_forward_cone_ready_pct": mean(
|
||||||
|
[m["view_forward_cone_ready_pct"] for m in moving_metrics
|
||||||
|
if m["view_forward_cone_ready_pct"] is not None]),
|
||||||
|
"camera_forward_samples": camera_samples,
|
||||||
|
"camera_forward_sample_share_pct": percent(camera_samples, len(moving)),
|
||||||
|
"near_forward_cosine_weighted_ready_pct": mean(
|
||||||
|
[m["forward_weighted_ready_pct"] for m in moving_metrics
|
||||||
|
if m["forward_weighted_ready_pct"] is not None]),
|
||||||
|
"arrival_lod02_settled_t_s": (
|
||||||
|
float(arrival_near_settled.get("t_s", 0.0)) if arrival_near_settled else None),
|
||||||
|
"near_ready_dwell_end_pct": (100.0 * sum(dwell[-1].get("resident_by_lod", [0] * 9)[:3])
|
||||||
|
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[:3]))),
|
||||||
|
"far_holes_dwell_end_pct": (100.0 * (
|
||||||
|
sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:])
|
||||||
|
- sum(dwell[-1].get("covered_by_lod", [0] * 9)[3:]))
|
||||||
|
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:]))),
|
||||||
|
"dwell_complete_after_stop_s": dwell_complete_s,
|
||||||
|
"game_thread_tick_p99_ms": p99_field("tick_ms"),
|
||||||
|
"game_thread_tick_max_ms": max_field("tick_ms"),
|
||||||
|
"stream_update_p99_ms": p99_field("update_chunks_ms"),
|
||||||
|
"pending_drain_p99_ms": p99_field("pending_drain_ms"),
|
||||||
|
"apply_p99_ms": p99_field("apply_ms"),
|
||||||
|
"apply_max_ms": max_field("apply_ms"),
|
||||||
|
"apply_budget_min_ms": min((f["apply_budget_ms"] for f in frames), default=None),
|
||||||
|
"apply_budget_max_ms": max((f["apply_budget_ms"] for f in frames), default=None),
|
||||||
|
"apply_time_budget_hit_frames": sum(f["time_budget_hit"] for f in frames),
|
||||||
|
"apply_frames": sum(f["meshes_applied"] > 0 for f in frames),
|
||||||
|
"apply_meshes_total": sum(f["meshes_applied"] for f in frames),
|
||||||
|
"desired_rebuild_p99_ms": p99_field("desired_rebuild_ms"),
|
||||||
|
"desired_rebuild_count": len(rebuild_frames),
|
||||||
|
"desired_rebuilds_per_second": len(rebuild_frames) / 12.0,
|
||||||
|
"center_crossings": center_crossings,
|
||||||
|
"same_center_core_region_requests": same_center_core_requests,
|
||||||
|
"core_desired_cache_hit_count": core_desired_cache_hits,
|
||||||
|
"core_desired_cache_hit_share_pct": percent(
|
||||||
|
core_desired_cache_hits, len(core_desired_queries)),
|
||||||
|
"core_desired_query_nonzero_p50_ms": statistics.median(core_desired_query_ms)
|
||||||
|
if core_desired_query_ms else None,
|
||||||
|
"core_desired_query_nonzero_p95_ms": percentile(core_desired_query_ms, 0.95),
|
||||||
|
"core_desired_query_nonzero_max_ms": max(core_desired_query_ms)
|
||||||
|
if core_desired_query_ms else None,
|
||||||
|
"desired_rebuild_nonzero_p50_ms": statistics.median(rebuild_costs)
|
||||||
|
if rebuild_costs else None,
|
||||||
|
"desired_rebuild_nonzero_p95_ms": percentile(rebuild_costs, 0.95),
|
||||||
|
"desired_rebuild_nonzero_max_ms": max(rebuild_costs) if rebuild_costs else None,
|
||||||
|
"desired_breakdown": desired_stage_breakdown,
|
||||||
|
"near_submitted_by_direction": direction_submission_totals,
|
||||||
|
"near_admission_first12": first_admission_order,
|
||||||
|
"submit_tiles_p99_ms": p99_field("submit_tiles_ms"),
|
||||||
|
"level_admit_p99_ms": p99_field("level_admit_ms"),
|
||||||
|
"top_tick_frames": sorted(frames, key=lambda f: f["tick_ms"], reverse=True)[:5],
|
||||||
|
"trace": str(trace.relative_to(ROOT)),
|
||||||
|
"log": str(abslog.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
print("[L166Movement] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--smoke", action="store_true",
|
||||||
|
help="run one incremental trace to validate readback and equality audits")
|
||||||
|
parser.add_argument("--count-only", action="store_true",
|
||||||
|
help="run one baseline/candidate pair for desired-stage cost attribution")
|
||||||
|
parser.add_argument("--geometry-hash-check", action="store_true",
|
||||||
|
help="compare UE per-tile geometry hashes for baseline and incremental desired sets")
|
||||||
|
parser.add_argument("--controls-only", action="store_true",
|
||||||
|
help="run a per-crossing equality audit and injected mismatch negative control")
|
||||||
|
parser.add_argument("--summarize-existing", action="store_true",
|
||||||
|
help="summarize the fourteen completed rows in runs-partial.json without rerunning UE")
|
||||||
|
args = parser.parse_args()
|
||||||
|
missing = [path for path in (EDITOR, HOST / "VFHost.uproject") if not path.exists()]
|
||||||
|
if missing:
|
||||||
|
raise SystemExit(f"missing UE runner input: {missing}")
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
if args.geometry_hash_check:
|
||||||
|
hash_dir = OUT / "geometry-hash"
|
||||||
|
baseline_path = hash_dir / "assembler-world.json"
|
||||||
|
candidate_path = hash_dir / "incremental.json"
|
||||||
|
run("assembler_world", 99, baseline_path)
|
||||||
|
run("incremental", 99, candidate_path)
|
||||||
|
baseline_data = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||||
|
candidate_data = json.loads(candidate_path.read_text(encoding="utf-8"))
|
||||||
|
key_fields = ("level", "tile_x", "tile_y", "tile_z")
|
||||||
|
|
||||||
|
def indexed(data: dict) -> dict[tuple[int, ...], tuple]:
|
||||||
|
result = {}
|
||||||
|
for record in data.get("records", []):
|
||||||
|
key = tuple(int(record[field]) for field in key_fields)
|
||||||
|
result[key] = (str(record.get("geometry_hash", "")),
|
||||||
|
int(record.get("triangle_count", -1)),
|
||||||
|
bool(record.get("empty", False)))
|
||||||
|
return result
|
||||||
|
|
||||||
|
baseline_hashes = indexed(baseline_data)
|
||||||
|
candidate_hashes = indexed(candidate_data)
|
||||||
|
common = baseline_hashes.keys() & candidate_hashes.keys()
|
||||||
|
mismatches = [key for key in common
|
||||||
|
if baseline_hashes[key] != candidate_hashes[key]]
|
||||||
|
result = {
|
||||||
|
"baseline_records": len(baseline_hashes),
|
||||||
|
"candidate_records": len(candidate_hashes),
|
||||||
|
"common_records": len(common),
|
||||||
|
"hash_mismatches": len(mismatches),
|
||||||
|
"baseline_only_records": len(baseline_hashes.keys() - candidate_hashes.keys()),
|
||||||
|
"candidate_only_records": len(candidate_hashes.keys() - baseline_hashes.keys()),
|
||||||
|
"baseline_dump": str(baseline_path.relative_to(ROOT)),
|
||||||
|
"candidate_dump": str(candidate_path.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
(hash_dir / "comparison.json").write_text(
|
||||||
|
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
print("[L166GeometryHash] " + json.dumps(result, sort_keys=True), flush=True)
|
||||||
|
return 0 if len(common) >= 100 and not mismatches else 1
|
||||||
|
if args.count_only:
|
||||||
|
run("assembler_world", 99)
|
||||||
|
run("incremental", 99)
|
||||||
|
return 0
|
||||||
|
if args.smoke:
|
||||||
|
run("incremental", 0)
|
||||||
|
return 0
|
||||||
|
if args.controls_only:
|
||||||
|
control = dict(CONDITIONS["incremental"])
|
||||||
|
control["audit_every"] = 0
|
||||||
|
control["materialization_audit_every"] = 25
|
||||||
|
control["inject_mismatch"] = 0
|
||||||
|
CONDITIONS["materialization_audit_control"] = control
|
||||||
|
run("materialization_audit_control", 0)
|
||||||
|
mismatch = dict(control)
|
||||||
|
mismatch["materialization_audit_every"] = 1
|
||||||
|
mismatch["inject_mismatch"] = 1
|
||||||
|
CONDITIONS["injected_materialization_mismatch_control"] = mismatch
|
||||||
|
run("injected_materialization_mismatch_control", 0, expected_mismatch=True)
|
||||||
|
return 0
|
||||||
|
partial = OUT / "runs-partial.json"
|
||||||
|
if args.summarize_existing:
|
||||||
|
if not partial.is_file():
|
||||||
|
raise RuntimeError(f"no saved movement rows: {partial}")
|
||||||
|
rows = json.loads(partial.read_text(encoding="utf-8"))
|
||||||
|
if len(rows) != 14:
|
||||||
|
raise RuntimeError(f"expected fourteen completed movement rows, found {len(rows)}")
|
||||||
|
else:
|
||||||
|
rows = []
|
||||||
|
# Both conditions use this editor binary. Warm each path before measured pairs.
|
||||||
|
run("assembler_world", 0)
|
||||||
|
run("incremental", 0)
|
||||||
|
for repetition in range(1, 8):
|
||||||
|
for condition in ORDER[repetition]:
|
||||||
|
row = run(condition, repetition)
|
||||||
|
rows.append(row)
|
||||||
|
partial.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
|
||||||
|
metrics = (
|
||||||
|
"near_ready_moving_mean_pct", "far_holes_moving_mean_pct",
|
||||||
|
"game_thread_tick_p99_ms", "game_thread_tick_max_ms",
|
||||||
|
"arrival_lod02_settled_t_s", "dwell_complete_after_stop_s",
|
||||||
|
"near_ahead_desired_share_pct", "near_behind_desired_share_pct",
|
||||||
|
"near_side_desired_share_pct", "near_forward_cone_desired_share_pct",
|
||||||
|
"near_ahead_ready_pct", "near_behind_ready_pct",
|
||||||
|
"near_forward_cone_ready_pct", "near_forward_cosine_weighted_ready_pct",
|
||||||
|
"near_travel_forward_cone_ready_pct",
|
||||||
|
"apply_p99_ms", "desired_rebuild_p99_ms", "desired_rebuild_count",
|
||||||
|
"desired_rebuild_nonzero_p95_ms", "center_crossings",
|
||||||
|
"same_center_core_region_requests", "level_admit_p99_ms",
|
||||||
|
"core_desired_cache_hit_count", "core_desired_query_nonzero_p95_ms",
|
||||||
|
)
|
||||||
|
medians = {}
|
||||||
|
for condition in CONDITIONS:
|
||||||
|
medians[condition] = {}
|
||||||
|
for metric in metrics:
|
||||||
|
values = [row[metric] for row in rows
|
||||||
|
if row["condition"] == condition and row[metric] is not None]
|
||||||
|
medians[condition][metric] = statistics.median(values) if values else None
|
||||||
|
pairs = []
|
||||||
|
for repetition in range(1, 8):
|
||||||
|
by_condition = {row["condition"]: row for row in rows
|
||||||
|
if row["repetition"] == repetition}
|
||||||
|
candidate = "incremental"
|
||||||
|
pairs.append({
|
||||||
|
"repetition": repetition,
|
||||||
|
"candidate": candidate,
|
||||||
|
**{f"{metric}_delta": by_condition[candidate][metric]
|
||||||
|
- by_condition["assembler_world"][metric]
|
||||||
|
for metric in metrics if by_condition[candidate][metric] is not None
|
||||||
|
and by_condition["assembler_world"][metric] is not None},
|
||||||
|
})
|
||||||
|
by_pair = {
|
||||||
|
repetition: {row["condition"]: row for row in rows if row["repetition"] == repetition}
|
||||||
|
for repetition in range(1, 8)
|
||||||
|
}
|
||||||
|
candidate_rows = [row for row in rows if row["condition"] == "incremental"]
|
||||||
|
baseline_rows = [row for row in rows if row["condition"] == "assembler_world"]
|
||||||
|
candidate_gates = {
|
||||||
|
"every_candidate_run_p99_le_11_ms": all(
|
||||||
|
row["game_thread_tick_p99_ms"] <= 11.0 for row in candidate_rows),
|
||||||
|
"paired_p99_no_worse": all(
|
||||||
|
by_pair[rep]["incremental"]["game_thread_tick_p99_ms"]
|
||||||
|
<= by_pair[rep]["assembler_world"]["game_thread_tick_p99_ms"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"paired_worst_tick_no_worse": all(
|
||||||
|
by_pair[rep]["incremental"]["game_thread_tick_max_ms"]
|
||||||
|
<= by_pair[rep]["assembler_world"]["game_thread_tick_max_ms"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"paired_near_all_around_no_worse": all(
|
||||||
|
by_pair[rep]["incremental"]["near_ready_moving_mean_pct"]
|
||||||
|
>= by_pair[rep]["assembler_world"]["near_ready_moving_mean_pct"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"paired_forward_cone_no_worse": all(
|
||||||
|
by_pair[rep]["incremental"]["near_forward_cone_ready_pct"]
|
||||||
|
>= by_pair[rep]["assembler_world"]["near_forward_cone_ready_pct"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"paired_far_holes_no_worse": all(
|
||||||
|
by_pair[rep]["incremental"]["far_holes_moving_mean_pct"]
|
||||||
|
<= by_pair[rep]["assembler_world"]["far_holes_moving_mean_pct"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"paired_arrival_no_worse": all(
|
||||||
|
by_pair[rep]["incremental"]["arrival_lod02_settled_t_s"] is not None
|
||||||
|
and by_pair[rep]["assembler_world"]["arrival_lod02_settled_t_s"] is not None
|
||||||
|
and by_pair[rep]["incremental"]["arrival_lod02_settled_t_s"]
|
||||||
|
<= by_pair[rep]["assembler_world"]["arrival_lod02_settled_t_s"]
|
||||||
|
for rep in by_pair),
|
||||||
|
"all_candidate_equality_audits_pass": all(
|
||||||
|
row["incremental_desired_audit_count"] > 0
|
||||||
|
and row["incremental_desired_audit_failures"] == 0
|
||||||
|
and row["incremental_desired_order_audit_count"] > 0
|
||||||
|
and row["incremental_desired_order_audit_failures"] == 0
|
||||||
|
for row in candidate_rows),
|
||||||
|
"median_build_desired_p95_down": statistics.median(
|
||||||
|
row["desired_rebuild_nonzero_p95_ms"] for row in candidate_rows)
|
||||||
|
< statistics.median(row["desired_rebuild_nonzero_p95_ms"]
|
||||||
|
for row in baseline_rows),
|
||||||
|
}
|
||||||
|
summary = {
|
||||||
|
"source_commit": subprocess.run(
|
||||||
|
["git", "-c", f"safe.directory={ROOT}", "rev-parse", "HEAD"],
|
||||||
|
cwd=ROOT, check=True, capture_output=True, text=True).stdout.strip(),
|
||||||
|
"protocol": "Seven interleaved 30 m/s UE pairs; 12 s move plus 5 s dwell; 8 effective tile slots. L157 critical wait remains on and apply cap remains 0 ms.",
|
||||||
|
"conditions": CONDITIONS,
|
||||||
|
"movement_medians": medians,
|
||||||
|
"paired_deltas_vs_assembler_world": pairs,
|
||||||
|
"candidate_gates": candidate_gates,
|
||||||
|
"runs": rows,
|
||||||
|
}
|
||||||
|
(OUT / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
print("[L166Summary] " + json.dumps({"movement_medians": medians,
|
||||||
|
"candidate_gates": candidate_gates,
|
||||||
|
"runs": len(rows)}, sort_keys=True), flush=True)
|
||||||
|
return 0 if all(candidate_gates.values()) else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in new issue
Block a user