streaming: incremental clipmap desired-set delta for adjacent crossings (L165)
BuildDelta emits entering/leaving slabs per LOD (incl. coarser-ring coverage changes); cached Core key set updated from the delta at one-tile crossings, full rebuild for larger jumps or LOD>=4 changes. Equality audit vs full rebuild (45/45 crossings, 20/20 sampled; injected mismatch asserts) + 630 automation cases. 30 m/s UE, 5 pairs: BuildDesiredTiles p95 7.80 -> 4.08 ms, p99 11.46 -> 7.97 ms, worst tick 13.73 -> 12.11 ms (all pairs), near and forward readiness and arrival better in all pairs; far holes better in 4/5 (pair 5 +0.29 pp). Geometry unchanged. Source/: VoxelClipmapDesiredTiles.h, VoxelWorld.cpp, VoxelWorld.h, Tests/VoxelForgeClipmapNoOverlapTest.cpp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
aa458d3607
commit
9c70053543
6 files changed
+1314
-14
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -384,6 +384,24 @@ namespace
|
|||||||
TEXT("voxel.StreamingCoreDesiredCacheByCenter"),
|
TEXT("voxel.StreamingCoreDesiredCacheByCenter"),
|
||||||
GVoxelForgeStreamingCoreDesiredCacheByCenter,
|
GVoxelForgeStreamingCoreDesiredCacheByCenter,
|
||||||
TEXT("Reuse Core clipmap desired keys when a same-center overlay rebuild occurs; 0 queries Core each time."));
|
TEXT("Reuse Core clipmap desired keys when a same-center overlay rebuild occurs; 0 queries Core each time."));
|
||||||
|
|
||||||
|
int32 GVoxelForgeStreamingIncrementalDesiredSet = 0;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingIncrementalDesiredSet(
|
||||||
|
TEXT("voxel.StreamingIncrementalDesiredSet"),
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredSet,
|
||||||
|
TEXT("Apply exact per-LOD clipmap key deltas and reuse same-center Core desired keys; 0 uses full Core queries."));
|
||||||
|
|
||||||
|
int32 GVoxelForgeStreamingIncrementalDesiredAuditEvery = 0;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingIncrementalDesiredAuditEvery(
|
||||||
|
TEXT("voxel.StreamingIncrementalDesiredAuditEvery"),
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredAuditEvery,
|
||||||
|
TEXT("Compare the incremental desired set with a full rebuild every N center crossings; 0 disables the debug audit."));
|
||||||
|
|
||||||
|
int32 GVoxelForgeStreamingIncrementalDesiredAuditInjectMismatch = 0;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingIncrementalDesiredAuditInjectMismatch(
|
||||||
|
TEXT("voxel.StreamingIncrementalDesiredAuditInjectMismatch"),
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredAuditInjectMismatch,
|
||||||
|
TEXT("Negative control: add one key to the full desired audit reference so the next audit must fail."));
|
||||||
constexpr int32 StreamingStartupMaxMeshApplies = 24;
|
constexpr int32 StreamingStartupMaxMeshApplies = 24;
|
||||||
constexpr float StreamingStartupApplyBudgetMilliseconds = 14.0f;
|
constexpr float StreamingStartupApplyBudgetMilliseconds = 14.0f;
|
||||||
constexpr double StreamingStartupApplyMaxSeconds = 30.0;
|
constexpr double StreamingStartupApplyMaxSeconds = 30.0;
|
||||||
@@ -1161,6 +1179,11 @@ void AVoxelWorld::RegenerateAllChunks()
|
|||||||
DesiredTransitionDescendants.Reset();
|
DesiredTransitionDescendants.Reset();
|
||||||
DesiredTransitionMaxLevel = 0;
|
DesiredTransitionMaxLevel = 0;
|
||||||
DesiredStamped.Reset();
|
DesiredStamped.Reset();
|
||||||
|
CachedCoreDesiredTiles.Reset();
|
||||||
|
CachedCoreDesiredTileSet.Reset();
|
||||||
|
CachedCoreDesiredCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
||||||
|
bHasCachedCoreDesiredTiles = false;
|
||||||
|
IncrementalDesiredCenterCrossings = 0;
|
||||||
++DesiredEpoch;
|
++DesiredEpoch;
|
||||||
TransitionHold.Reset();
|
TransitionHold.Reset();
|
||||||
TransitionHoldQueue.Reset();
|
TransitionHoldQueue.Reset();
|
||||||
@@ -2316,6 +2339,25 @@ void AVoxelWorld::BeginPlay()
|
|||||||
GVoxelForgeStreamingCoreDesiredCacheByCenter =
|
GVoxelForgeStreamingCoreDesiredCacheByCenter =
|
||||||
GVoxelForgeStreamingCoreDesiredCacheByCenter != 0 ? 1 : 0;
|
GVoxelForgeStreamingCoreDesiredCacheByCenter != 0 ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingIncrementalDesiredSet="),
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredSet))
|
||||||
|
{
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredSet =
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredSet != 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingIncrementalDesiredAuditEvery="),
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredAuditEvery))
|
||||||
|
{
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredAuditEvery = FMath::Clamp(
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredAuditEvery, 0, 1000000);
|
||||||
|
}
|
||||||
|
if (FParse::Value(FCommandLine::Get(),
|
||||||
|
TEXT("voxel.StreamingIncrementalDesiredAuditInjectMismatch="),
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredAuditInjectMismatch))
|
||||||
|
{
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredAuditInjectMismatch =
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredAuditInjectMismatch != 0 ? 1 : 0;
|
||||||
|
}
|
||||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingResultBacklogCap="),
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingResultBacklogCap="),
|
||||||
CommandLineStreamingResultBacklogCap))
|
CommandLineStreamingResultBacklogCap))
|
||||||
{
|
{
|
||||||
@@ -8118,8 +8160,10 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
|||||||
|
|
||||||
VoxelForgeCoreState = State;
|
VoxelForgeCoreState = State;
|
||||||
CachedCoreDesiredTiles.Reset();
|
CachedCoreDesiredTiles.Reset();
|
||||||
|
CachedCoreDesiredTileSet.Reset();
|
||||||
bHasCachedCoreDesiredTiles = false;
|
bHasCachedCoreDesiredTiles = false;
|
||||||
CachedCoreDesiredCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
CachedCoreDesiredCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
||||||
|
IncrementalDesiredCenterCrossings = 0;
|
||||||
State->bSnapshotReady.store(true, std::memory_order_release);
|
State->bSnapshotReady.store(true, std::memory_order_release);
|
||||||
ActiveTaskCount.fetch_add(1, std::memory_order_relaxed);
|
ActiveTaskCount.fetch_add(1, std::memory_order_relaxed);
|
||||||
const double CoreCompileLaunchSeconds = bTraceCoreStartup
|
const double CoreCompileLaunchSeconds = bTraceCoreStartup
|
||||||
@@ -11081,6 +11125,8 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
|
|||||||
SupportProbeTile, &ResolvedSupportTile, &SupportUnavailableReason)
|
SupportProbeTile, &ResolvedSupportTile, &SupportUnavailableReason)
|
||||||
&& SupportUnavailableReason == nullptr;
|
&& SupportUnavailableReason == nullptr;
|
||||||
|
|
||||||
|
const uint32 PreviousDesiredStamp = DesiredStamp;
|
||||||
|
const bool bHadPreviousDesiredSet = !DesiredStamped.IsEmpty();
|
||||||
DesiredSorted.Reset();
|
DesiredSorted.Reset();
|
||||||
CriticalDesiredTiles.Reset();
|
CriticalDesiredTiles.Reset();
|
||||||
OutLeavers.Reset();
|
OutLeavers.Reset();
|
||||||
@@ -11096,15 +11142,137 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
|
|||||||
|
|
||||||
std::vector<VoxelForgeCore::TileKey> UncachedCoreDesiredTiles;
|
std::vector<VoxelForgeCore::TileKey> UncachedCoreDesiredTiles;
|
||||||
const TArray<FVoxelTileKey>* CoreDesiredTiles = nullptr;
|
const TArray<FVoxelTileKey>* CoreDesiredTiles = nullptr;
|
||||||
|
TArray<FVoxelTileKey> IncrementalCoreEntering;
|
||||||
|
TArray<FVoxelTileKey> IncrementalCoreLeaving;
|
||||||
double CoreDesiredQueryMilliseconds = 0.0;
|
double CoreDesiredQueryMilliseconds = 0.0;
|
||||||
bool bCoreDesiredCacheHit = false;
|
bool bCoreDesiredCacheHit = false;
|
||||||
if (GVoxelForgeStreamingCoreDesiredCacheByCenter != 0
|
bool bCoreDesiredIncrementalDelta = false;
|
||||||
|
bool bCoreDesiredFullFallback = false;
|
||||||
|
const bool bIncrementalDesiredSet = GVoxelForgeStreamingIncrementalDesiredSet != 0;
|
||||||
|
const bool bIncrementalCenterCrossing = bIncrementalDesiredSet
|
||||||
|
&& bHasCachedCoreDesiredTiles && CachedCoreDesiredCenter != Center;
|
||||||
|
if (bIncrementalCenterCrossing)
|
||||||
|
{
|
||||||
|
++IncrementalDesiredCenterCrossings;
|
||||||
|
}
|
||||||
|
const int32 DesiredAuditInterval = FMath::Max(
|
||||||
|
0, GVoxelForgeStreamingIncrementalDesiredAuditEvery);
|
||||||
|
const bool bShouldAuditIncrementalDesired = bIncrementalCenterCrossing
|
||||||
|
&& DesiredAuditInterval > 0
|
||||||
|
&& (IncrementalDesiredCenterCrossings % DesiredAuditInterval) == 0;
|
||||||
|
|
||||||
|
if (bIncrementalDesiredSet && bHasCachedCoreDesiredTiles
|
||||||
|
&& CachedCoreDesiredCenter == Center)
|
||||||
|
{
|
||||||
|
CoreDesiredTiles = &CachedCoreDesiredTiles;
|
||||||
|
bCoreDesiredCacheHit = true;
|
||||||
|
}
|
||||||
|
else if (bIncrementalDesiredSet && bHasCachedCoreDesiredTiles
|
||||||
|
&& bIncrementalCenterCrossing)
|
||||||
|
{
|
||||||
|
const int64 CenterDelta = FMath::Abs(static_cast<int64>(Center.X) - CachedCoreDesiredCenter.X)
|
||||||
|
+ FMath::Abs(static_cast<int64>(Center.Y) - CachedCoreDesiredCenter.Y)
|
||||||
|
+ FMath::Abs(static_cast<int64>(Center.Z) - CachedCoreDesiredCenter.Z);
|
||||||
|
if (CenterDelta == 1)
|
||||||
|
{
|
||||||
|
TArray<FVoxelTileKey> RingEntering;
|
||||||
|
TArray<FVoxelTileKey> RingLeaving;
|
||||||
|
{
|
||||||
|
FReadScopeLock Lock(State->WorldLock);
|
||||||
|
if (!State->World) return;
|
||||||
|
const VoxelForgeCore::WorldConfiguration& Configuration =
|
||||||
|
State->World->configuration();
|
||||||
|
VoxelClipmapDesiredTiles::FParameters Parameters;
|
||||||
|
Parameters.ClipRadius = Configuration.clipRadius;
|
||||||
|
Parameters.MaxClipLevel = Configuration.maxLod;
|
||||||
|
Parameters.RenderDistanceChunks = Configuration.renderDistanceChunks;
|
||||||
|
const auto GetVerticalBounds = [&](int32 CenterChunkZ,
|
||||||
|
int32& OutMinChunkZ, int32& OutMaxChunkZ)
|
||||||
|
{
|
||||||
|
bool bHasStrate = false;
|
||||||
|
int32 StrateTopChunkZ = 0;
|
||||||
|
int32 StrateBottomChunkZ = 0;
|
||||||
|
if (Configuration.clampViewToStrate && StrateManager)
|
||||||
|
{
|
||||||
|
bHasStrate = StrateManager->GetStrateChunkZBounds(
|
||||||
|
CenterChunkZ, StrateTopChunkZ, StrateBottomChunkZ);
|
||||||
|
}
|
||||||
|
VoxelClipmapDesiredTiles::ResolveVerticalBounds(
|
||||||
|
CenterChunkZ, Configuration.viewDistanceUp,
|
||||||
|
Configuration.viewDistanceDown, Configuration.strateViewMarginChunks,
|
||||||
|
Configuration.clampViewToStrate, bHasStrate,
|
||||||
|
StrateTopChunkZ, StrateBottomChunkZ, OutMinChunkZ, OutMaxChunkZ);
|
||||||
|
};
|
||||||
|
int32 PreviousMinChunkZ = 0;
|
||||||
|
int32 PreviousMaxChunkZ = 0;
|
||||||
|
int32 CurrentMinChunkZ = 0;
|
||||||
|
int32 CurrentMaxChunkZ = 0;
|
||||||
|
GetVerticalBounds(CachedCoreDesiredCenter.Z,
|
||||||
|
PreviousMinChunkZ, PreviousMaxChunkZ);
|
||||||
|
GetVerticalBounds(Center.Z, CurrentMinChunkZ, CurrentMaxChunkZ);
|
||||||
|
VoxelClipmapDesiredTiles::BuildDelta(
|
||||||
|
CachedCoreDesiredCenter, PreviousMinChunkZ, PreviousMaxChunkZ,
|
||||||
|
Center, CurrentMinChunkZ, CurrentMaxChunkZ, Parameters,
|
||||||
|
RingEntering, RingLeaving);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bTouchesCoreSurfaceOverlay = false;
|
||||||
|
for (const FVoxelTileKey& Key : RingEntering)
|
||||||
|
{
|
||||||
|
bTouchesCoreSurfaceOverlay |= Key.Level >= 4;
|
||||||
|
}
|
||||||
|
for (const FVoxelTileKey& Key : RingLeaving)
|
||||||
|
{
|
||||||
|
bTouchesCoreSurfaceOverlay |= Key.Level >= 4;
|
||||||
|
}
|
||||||
|
if (!bTouchesCoreSurfaceOverlay)
|
||||||
|
{
|
||||||
|
TSet<FVoxelTileKey> LeavingSet;
|
||||||
|
LeavingSet.Reserve(RingLeaving.Num());
|
||||||
|
for (const FVoxelTileKey& Key : RingLeaving)
|
||||||
|
{
|
||||||
|
LeavingSet.Add(Key);
|
||||||
|
CachedCoreDesiredTileSet.Remove(Key);
|
||||||
|
}
|
||||||
|
CachedCoreDesiredTiles.RemoveAll([&LeavingSet](const FVoxelTileKey& Key)
|
||||||
|
{
|
||||||
|
return LeavingSet.Contains(Key);
|
||||||
|
});
|
||||||
|
for (const FVoxelTileKey& Key : RingEntering)
|
||||||
|
{
|
||||||
|
if (!CachedCoreDesiredTileSet.Contains(Key))
|
||||||
|
{
|
||||||
|
CachedCoreDesiredTileSet.Add(Key);
|
||||||
|
CachedCoreDesiredTiles.Add(Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IncrementalCoreEntering = MoveTemp(RingEntering);
|
||||||
|
IncrementalCoreLeaving = MoveTemp(RingLeaving);
|
||||||
|
CachedCoreDesiredCenter = Center;
|
||||||
|
CoreDesiredTiles = &CachedCoreDesiredTiles;
|
||||||
|
bCoreDesiredCacheHit = true;
|
||||||
|
bCoreDesiredIncrementalDelta = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bCoreDesiredFullFallback = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bCoreDesiredFullFallback = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CoreDesiredTiles == nullptr
|
||||||
|
&& GVoxelForgeStreamingCoreDesiredCacheByCenter != 0
|
||||||
&& bHasCachedCoreDesiredTiles && CachedCoreDesiredCenter == Center)
|
&& bHasCachedCoreDesiredTiles && CachedCoreDesiredCenter == Center)
|
||||||
{
|
{
|
||||||
CoreDesiredTiles = &CachedCoreDesiredTiles;
|
CoreDesiredTiles = &CachedCoreDesiredTiles;
|
||||||
bCoreDesiredCacheHit = true;
|
bCoreDesiredCacheHit = true;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (CoreDesiredTiles == nullptr)
|
||||||
{
|
{
|
||||||
const uint64 QueryStartCycles = VoxelForgeStartupTrace::IsActive()
|
const uint64 QueryStartCycles = VoxelForgeStartupTrace::IsActive()
|
||||||
? FPlatformTime::Cycles64() : 0;
|
? FPlatformTime::Cycles64() : 0;
|
||||||
@@ -11115,14 +11283,18 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
|
|||||||
UncachedCoreDesiredTiles = State->World->desiredTiles(
|
UncachedCoreDesiredTiles = State->World->desiredTiles(
|
||||||
{CameraVoxels.X, CameraVoxels.Y, CameraVoxels.Z});
|
{CameraVoxels.X, CameraVoxels.Y, CameraVoxels.Z});
|
||||||
}
|
}
|
||||||
if (GVoxelForgeStreamingCoreDesiredCacheByCenter != 0)
|
if (GVoxelForgeStreamingCoreDesiredCacheByCenter != 0 || bIncrementalDesiredSet)
|
||||||
{
|
{
|
||||||
CachedCoreDesiredTiles.Reset();
|
CachedCoreDesiredTiles.Reset();
|
||||||
|
CachedCoreDesiredTileSet.Reset();
|
||||||
CachedCoreDesiredTiles.Reserve(static_cast<int32>(UncachedCoreDesiredTiles.size()));
|
CachedCoreDesiredTiles.Reserve(static_cast<int32>(UncachedCoreDesiredTiles.size()));
|
||||||
|
CachedCoreDesiredTileSet.Reserve(static_cast<int32>(UncachedCoreDesiredTiles.size()));
|
||||||
for (const VoxelForgeCore::TileKey& Key : UncachedCoreDesiredTiles)
|
for (const VoxelForgeCore::TileKey& Key : UncachedCoreDesiredTiles)
|
||||||
{
|
{
|
||||||
CachedCoreDesiredTiles.Emplace(
|
const FVoxelTileKey RuntimeKey(
|
||||||
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
|
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
|
||||||
|
CachedCoreDesiredTiles.Add(RuntimeKey);
|
||||||
|
CachedCoreDesiredTileSet.Add(RuntimeKey);
|
||||||
}
|
}
|
||||||
CachedCoreDesiredCenter = Center;
|
CachedCoreDesiredCenter = Center;
|
||||||
bHasCachedCoreDesiredTiles = true;
|
bHasCachedCoreDesiredTiles = true;
|
||||||
@@ -11137,11 +11309,17 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
|
|||||||
if (VoxelForgeStartupTrace::IsActive())
|
if (VoxelForgeStartupTrace::IsActive())
|
||||||
{
|
{
|
||||||
VoxelForgeStartupTrace::RecordEvent(TEXT("core_desired_cache"), FString::Printf(
|
VoxelForgeStartupTrace::RecordEvent(TEXT("core_desired_cache"), FString::Printf(
|
||||||
TEXT("\"frame\":%llu,\"center\":[%d,%d,%d],\"hit\":%d,\"keys\":%d,\"query_ms\":%s"),
|
TEXT("\"frame\":%llu,\"center\":[%d,%d,%d],\"hit\":%d,"
|
||||||
|
"\"incremental_delta\":%d,\"full_fallback\":%d,"
|
||||||
|
"\"crossing_count\":%d,\"audit_due\":%d,\"keys\":%d,"
|
||||||
|
"\"delta_entering\":%d,\"delta_leaving\":%d,\"query_ms\":%s"),
|
||||||
static_cast<unsigned long long>(GFrameCounter), Center.X, Center.Y, Center.Z,
|
static_cast<unsigned long long>(GFrameCounter), Center.X, Center.Y, Center.Z,
|
||||||
bCoreDesiredCacheHit ? 1 : 0,
|
bCoreDesiredCacheHit ? 1 : 0,
|
||||||
|
bCoreDesiredIncrementalDelta ? 1 : 0, bCoreDesiredFullFallback ? 1 : 0,
|
||||||
|
IncrementalDesiredCenterCrossings, bShouldAuditIncrementalDesired ? 1 : 0,
|
||||||
CoreDesiredTiles ? CoreDesiredTiles->Num()
|
CoreDesiredTiles ? CoreDesiredTiles->Num()
|
||||||
: static_cast<int32>(UncachedCoreDesiredTiles.size()),
|
: static_cast<int32>(UncachedCoreDesiredTiles.size()),
|
||||||
|
IncrementalCoreEntering.Num(), IncrementalCoreLeaving.Num(),
|
||||||
*FString::SanitizeFloat(static_cast<float>(CoreDesiredQueryMilliseconds))));
|
*FString::SanitizeFloat(static_cast<float>(CoreDesiredQueryMilliseconds))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11156,19 +11334,31 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
|
|||||||
TSet<FVoxelTileKey> BaseVisualTiles;
|
TSet<FVoxelTileKey> BaseVisualTiles;
|
||||||
TSet<FVoxelTileKey> FullVisualAnchorTiles;
|
TSet<FVoxelTileKey> FullVisualAnchorTiles;
|
||||||
TSet<FVoxelTileKey> CollisionAnchorTiles;
|
TSet<FVoxelTileKey> CollisionAnchorTiles;
|
||||||
auto AddCandidate = [&](const FVoxelTileKey& Key)
|
TSet<FVoxelTileKey> OverlayDesiredTiles;
|
||||||
|
TArray<FVoxelTileKey> DesiredEnteringTiles;
|
||||||
|
auto AddCandidate = [&](const FVoxelTileKey& Key, bool bOverlay)
|
||||||
{
|
{
|
||||||
|
if (bOverlay)
|
||||||
|
{
|
||||||
|
OverlayDesiredTiles.Add(Key);
|
||||||
|
}
|
||||||
if (!CandidateSet.Contains(Key))
|
if (!CandidateSet.Contains(Key))
|
||||||
{
|
{
|
||||||
CandidateSet.Add(Key);
|
CandidateSet.Add(Key);
|
||||||
CandidateKeys.Add(Key);
|
CandidateKeys.Add(Key);
|
||||||
|
const uint32* PreviousStamp = DesiredStamped.Find(Key);
|
||||||
|
if (bIncrementalDesiredSet
|
||||||
|
&& (PreviousStamp == nullptr || *PreviousStamp != PreviousDesiredStamp))
|
||||||
|
{
|
||||||
|
DesiredEnteringTiles.Add(Key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DesiredStamped.FindOrAdd(Key) = DesiredStamp;
|
DesiredStamped.FindOrAdd(Key) = DesiredStamp;
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto AddCoreDesiredTile = [&](const FVoxelTileKey& RuntimeKey)
|
const auto AddCoreDesiredTile = [&](const FVoxelTileKey& RuntimeKey)
|
||||||
{
|
{
|
||||||
AddCandidate(RuntimeKey);
|
AddCandidate(RuntimeKey, false);
|
||||||
BaseVisualTiles.Add(RuntimeKey);
|
BaseVisualTiles.Add(RuntimeKey);
|
||||||
};
|
};
|
||||||
if (CoreDesiredTiles)
|
if (CoreDesiredTiles)
|
||||||
@@ -11213,7 +11403,7 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
|
|||||||
for (int32 Dx = -XYRadius; Dx <= XYRadius; ++Dx)
|
for (int32 Dx = -XYRadius; Dx <= XYRadius; ++Dx)
|
||||||
{
|
{
|
||||||
const FVoxelTileKey Key(AnchorCenter + FIntVector(Dx, Dy, Dz), 0);
|
const FVoxelTileKey Key(AnchorCenter + FIntVector(Dx, Dy, Dz), 0);
|
||||||
AddCandidate(Key);
|
AddCandidate(Key, true);
|
||||||
if (Anchor.Policy == EVoxelAnchorPolicy::FullVisual)
|
if (Anchor.Policy == EVoxelAnchorPolicy::FullVisual)
|
||||||
{
|
{
|
||||||
FullVisualAnchorTiles.Add(Key);
|
FullVisualAnchorTiles.Add(Key);
|
||||||
@@ -11237,14 +11427,14 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
|
|||||||
It.RemoveCurrent();
|
It.RemoveCurrent();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
AddCandidate(Key);
|
AddCandidate(Key, true);
|
||||||
CollisionDesiredTiles.Add(Key);
|
CollisionDesiredTiles.Add(Key);
|
||||||
CollisionAnchorTiles.Add(Key);
|
CollisionAnchorTiles.Add(Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bRetainResolvedSupport)
|
if (bRetainResolvedSupport)
|
||||||
{
|
{
|
||||||
AddCandidate(ResolvedSupportTile);
|
AddCandidate(ResolvedSupportTile, true);
|
||||||
CollisionDesiredTiles.Add(ResolvedSupportTile);
|
CollisionDesiredTiles.Add(ResolvedSupportTile);
|
||||||
if (!BaseVisualTiles.Contains(ResolvedSupportTile)
|
if (!BaseVisualTiles.Contains(ResolvedSupportTile)
|
||||||
&& !FullVisualAnchorTiles.Contains(ResolvedSupportTile))
|
&& !FullVisualAnchorTiles.Contains(ResolvedSupportTile))
|
||||||
@@ -11304,6 +11494,95 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bShouldAuditIncrementalDesired)
|
||||||
|
{
|
||||||
|
std::vector<VoxelForgeCore::TileKey> FullReferenceCoreTiles;
|
||||||
|
const FVector CameraVoxels = WorldToLocalCm(PlayerPosition) / GetActiveVoxelSizeCm();
|
||||||
|
{
|
||||||
|
FReadScopeLock Lock(State->WorldLock);
|
||||||
|
if (!State->World) return;
|
||||||
|
FullReferenceCoreTiles = State->World->desiredTiles(
|
||||||
|
{CameraVoxels.X, CameraVoxels.Y, CameraVoxels.Z});
|
||||||
|
}
|
||||||
|
|
||||||
|
TSet<FVoxelTileKey> FullReferenceCoreSet;
|
||||||
|
FullReferenceCoreSet.Reserve(static_cast<int32>(FullReferenceCoreTiles.size()));
|
||||||
|
TSet<FVoxelTileKey> FullReferenceDesiredSet;
|
||||||
|
FullReferenceDesiredSet.Reserve(
|
||||||
|
static_cast<int32>(FullReferenceCoreTiles.size()) + OverlayDesiredTiles.Num());
|
||||||
|
for (const VoxelForgeCore::TileKey& Key : FullReferenceCoreTiles)
|
||||||
|
{
|
||||||
|
const FVoxelTileKey RuntimeKey(
|
||||||
|
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
|
||||||
|
FullReferenceCoreSet.Add(RuntimeKey);
|
||||||
|
FullReferenceDesiredSet.Add(RuntimeKey);
|
||||||
|
}
|
||||||
|
for (const FVoxelTileKey& Key : OverlayDesiredTiles)
|
||||||
|
{
|
||||||
|
FullReferenceDesiredSet.Add(Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 MissingFromIncrementalDesired = 0;
|
||||||
|
int32 ExtraInIncrementalDesired = 0;
|
||||||
|
for (const FVoxelTileKey& Key : FullReferenceDesiredSet)
|
||||||
|
{
|
||||||
|
if (!CandidateSet.Contains(Key)) ++MissingFromIncrementalDesired;
|
||||||
|
}
|
||||||
|
for (const FVoxelTileKey& Key : CandidateSet)
|
||||||
|
{
|
||||||
|
if (!FullReferenceDesiredSet.Contains(Key)) ++ExtraInIncrementalDesired;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSet<FVoxelTileKey> FullReferenceCollisionOnly;
|
||||||
|
for (const FVoxelTileKey& Key : CollisionAnchorTiles)
|
||||||
|
{
|
||||||
|
if (!FullReferenceCoreSet.Contains(Key) && !FullVisualAnchorTiles.Contains(Key))
|
||||||
|
{
|
||||||
|
FullReferenceCollisionOnly.Add(Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int32 MissingCollisionOnly = 0;
|
||||||
|
int32 ExtraCollisionOnly = 0;
|
||||||
|
for (const FVoxelTileKey& Key : FullReferenceCollisionOnly)
|
||||||
|
{
|
||||||
|
if (!CollisionOnlyTiles.Contains(Key)) ++MissingCollisionOnly;
|
||||||
|
}
|
||||||
|
for (const FVoxelTileKey& Key : CollisionOnlyTiles)
|
||||||
|
{
|
||||||
|
if (!FullReferenceCollisionOnly.Contains(Key)) ++ExtraCollisionOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GVoxelForgeStreamingIncrementalDesiredAuditInjectMismatch != 0)
|
||||||
|
{
|
||||||
|
FullReferenceDesiredSet.Add(
|
||||||
|
FVoxelTileKey(FIntVector(MAX_int32, MAX_int32, MAX_int32), 8));
|
||||||
|
++MissingFromIncrementalDesired;
|
||||||
|
}
|
||||||
|
const bool bDesiredSetsMatch = MissingFromIncrementalDesired == 0
|
||||||
|
&& ExtraInIncrementalDesired == 0
|
||||||
|
&& MissingCollisionOnly == 0 && ExtraCollisionOnly == 0;
|
||||||
|
VoxelForgeStartupTrace::RecordEvent(TEXT("incremental_desired_audit"), FString::Printf(
|
||||||
|
TEXT("\"frame\":%llu,\"center\":[%d,%d,%d],\"crossing\":%d,"
|
||||||
|
"\"core_keys\":%d,\"reference_keys\":%d,\"candidate_keys\":%d,"
|
||||||
|
"\"missing\":%d,\"extra\":%d,\"collision_only_missing\":%d,"
|
||||||
|
"\"collision_only_extra\":%d,\"pass\":%d"),
|
||||||
|
static_cast<unsigned long long>(GFrameCounter), Center.X, Center.Y, Center.Z,
|
||||||
|
IncrementalDesiredCenterCrossings, static_cast<int32>(FullReferenceCoreTiles.size()),
|
||||||
|
FullReferenceDesiredSet.Num(), CandidateSet.Num(), MissingFromIncrementalDesired,
|
||||||
|
ExtraInIncrementalDesired, MissingCollisionOnly, ExtraCollisionOnly,
|
||||||
|
bDesiredSetsMatch ? 1 : 0));
|
||||||
|
if (!bDesiredSetsMatch)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Error,
|
||||||
|
TEXT("[VoxelForgeIncrementalDesiredAudit] mismatch crossing=%d missing=%d extra=%d collision_only_missing=%d collision_only_extra=%d"),
|
||||||
|
IncrementalDesiredCenterCrossings, MissingFromIncrementalDesired,
|
||||||
|
ExtraInIncrementalDesired, MissingCollisionOnly, ExtraCollisionOnly);
|
||||||
|
FPlatformMisc::RequestExitWithStatus(
|
||||||
|
false, 1, TEXT("VoxelForge incremental desired-set equality failure"));
|
||||||
|
checkf(false, TEXT("VoxelForge incremental desired-set equality failure"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<VoxelForgeCore::TileKey> PriorityInput;
|
std::vector<VoxelForgeCore::TileKey> PriorityInput;
|
||||||
PriorityInput.reserve(static_cast<std::size_t>(CandidateKeys.Num()));
|
PriorityInput.reserve(static_cast<std::size_t>(CandidateKeys.Num()));
|
||||||
for (const FVoxelTileKey& Key : CandidateKeys)
|
for (const FVoxelTileKey& Key : CandidateKeys)
|
||||||
@@ -11384,17 +11663,50 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
|
|||||||
DesiredSorted.Insert(Key, 0);
|
DesiredSorted.Insert(Key, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BuildDesiredTransitionIndex();
|
if (bIncrementalDesiredSet && bHadPreviousDesiredSet)
|
||||||
|
{
|
||||||
|
constexpr int32 MaxSupportedTileLevel = 8;
|
||||||
|
const int32 TransitionIndexMaxLevel = FMath::Max(
|
||||||
|
DesiredTransitionMaxLevel, MaxSupportedTileLevel);
|
||||||
|
const auto ApplyTransitionDelta = [this, TransitionIndexMaxLevel](
|
||||||
|
const FVoxelTileKey& Key, bool bAdd)
|
||||||
|
{
|
||||||
|
for (int32 Level = Key.Level + 1; Level <= TransitionIndexMaxLevel; ++Level)
|
||||||
|
{
|
||||||
|
TArray<FVoxelTileKey, TInlineAllocator<8>> TouchingAncestors;
|
||||||
|
VF_AppendTouchingAncestors(Key, Level, TouchingAncestors);
|
||||||
|
for (const FVoxelTileKey& Ancestor : TouchingAncestors)
|
||||||
|
{
|
||||||
|
if (bAdd)
|
||||||
|
{
|
||||||
|
TArray<FVoxelTileKey>& Descendants =
|
||||||
|
DesiredTransitionDescendants.FindOrAdd(Ancestor);
|
||||||
|
if (!Descendants.Contains(Key)) Descendants.Add(Key);
|
||||||
|
}
|
||||||
|
else if (TArray<FVoxelTileKey>* Descendants =
|
||||||
|
DesiredTransitionDescendants.Find(Ancestor))
|
||||||
|
{
|
||||||
|
Descendants->RemoveSingleSwap(Key, EAllowShrinking::No);
|
||||||
|
if (Descendants->IsEmpty()) DesiredTransitionDescendants.Remove(Ancestor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const FVoxelTileKey& Key : OutLeavers) ApplyTransitionDelta(Key, false);
|
||||||
|
for (const FVoxelTileKey& Key : DesiredEnteringTiles) ApplyTransitionDelta(Key, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BuildDesiredTransitionIndex();
|
||||||
|
}
|
||||||
|
|
||||||
bool bRetainedSetChanged = false;
|
bool bRetainedSetChanged = false;
|
||||||
int32 RetainedTileCount = 0;
|
int32 RetainedTileCount = 0;
|
||||||
TSet<FVoxelTileKey> DesiredSet;
|
|
||||||
for (const FVoxelTileKey& Key : DesiredSorted) DesiredSet.Add(Key);
|
|
||||||
TArray<FVoxelTileKey> MissingRetainedFineTiles;
|
TArray<FVoxelTileKey> MissingRetainedFineTiles;
|
||||||
{
|
{
|
||||||
FScopeLock Lock(&State->RetainedTilesLock);
|
FScopeLock Lock(&State->RetainedTilesLock);
|
||||||
RetainedTileCount = static_cast<int32>(State->RetainedTiles.size());
|
RetainedTileCount = static_cast<int32>(State->RetainedTiles.size());
|
||||||
bRetainedSetChanged = DesiredSet.Num() != RetainedTileCount;
|
bRetainedSetChanged = DesiredSorted.Num() != RetainedTileCount;
|
||||||
for (const FVoxelTileKey& Key : DesiredSorted)
|
for (const FVoxelTileKey& Key : DesiredSorted)
|
||||||
{
|
{
|
||||||
const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level};
|
const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level};
|
||||||
@@ -14039,6 +14351,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
"\"near_reserve\":%d,\"scheduler_policy\":%d,"
|
"\"near_reserve\":%d,\"scheduler_policy\":%d,"
|
||||||
"\"forward_first\":%d,\"rebuild_on_core_region_completion\":%d,"
|
"\"forward_first\":%d,\"rebuild_on_core_region_completion\":%d,"
|
||||||
"\"core_desired_cache_by_center\":%d,"
|
"\"core_desired_cache_by_center\":%d,"
|
||||||
|
"\"incremental_desired_set\":%d,"
|
||||||
|
"\"incremental_desired_audit_every\":%d,"
|
||||||
|
"\"incremental_desired_audit_inject_mismatch\":%d,"
|
||||||
"\"camera_forward_valid\":%d,"
|
"\"camera_forward_valid\":%d,"
|
||||||
"\"near_missing_by_direction\":%s,\"near_submitted_by_direction\":%s,"
|
"\"near_missing_by_direction\":%s,\"near_submitted_by_direction\":%s,"
|
||||||
"\"near_admission_order_first\":%s,"
|
"\"near_admission_order_first\":%s,"
|
||||||
@@ -14074,6 +14389,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
GVoxelForgeStreamingFastTravelForwardFirst != 0 ? 1 : 0,
|
GVoxelForgeStreamingFastTravelForwardFirst != 0 ? 1 : 0,
|
||||||
GVoxelForgeStreamingRebuildOnCoreRegionCompletion != 0 ? 1 : 0,
|
GVoxelForgeStreamingRebuildOnCoreRegionCompletion != 0 ? 1 : 0,
|
||||||
GVoxelForgeStreamingCoreDesiredCacheByCenter != 0 ? 1 : 0,
|
GVoxelForgeStreamingCoreDesiredCacheByCenter != 0 ? 1 : 0,
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredSet != 0 ? 1 : 0,
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredAuditEvery,
|
||||||
|
GVoxelForgeStreamingIncrementalDesiredAuditInjectMismatch,
|
||||||
bCameraForwardValid ? 1 : 0,
|
bCameraForwardValid ? 1 : 0,
|
||||||
*IntArray4Json(DesiredMissingByDirection),
|
*IntArray4Json(DesiredMissingByDirection),
|
||||||
*IntArray4Json(SchedulerSubmittedByDirection),
|
*IntArray4Json(SchedulerSubmittedByDirection),
|
||||||
|
|||||||
@@ -1255,8 +1255,10 @@ public:
|
|||||||
bool bSpeedAwareTravelMode = false;
|
bool bSpeedAwareTravelMode = false;
|
||||||
bool bSpeedAwareFastMode = false;
|
bool bSpeedAwareFastMode = false;
|
||||||
TArray<FVoxelTileKey> CachedCoreDesiredTiles;
|
TArray<FVoxelTileKey> CachedCoreDesiredTiles;
|
||||||
|
TSet<FVoxelTileKey> CachedCoreDesiredTileSet;
|
||||||
FIntVector CachedCoreDesiredCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
FIntVector CachedCoreDesiredCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
||||||
bool bHasCachedCoreDesiredTiles = false;
|
bool bHasCachedCoreDesiredTiles = false;
|
||||||
|
int32 IncrementalDesiredCenterCrossings = 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,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())
|
||||||
Reference in new issue
Block a user