Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa458d3607 | ||
|
|
323fbe3736 |
No files matched your search
@@ -262,6 +262,12 @@ namespace
|
|||||||
GVoxelForgeCoreRegionConcurrentAdmission,
|
GVoxelForgeCoreRegionConcurrentAdmission,
|
||||||
TEXT("Allow tiles independent of a pending Core region compile to proceed; 0 restores the global streaming stop."));
|
TEXT("Allow tiles independent of a pending Core region compile to proceed; 0 restores the global streaming stop."));
|
||||||
|
|
||||||
|
int32 GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles = 1;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingFastTravelWaitForAllCriticalTiles(
|
||||||
|
TEXT("voxel.StreamingFastTravelWaitForAllCriticalTiles"),
|
||||||
|
GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles,
|
||||||
|
TEXT("Wait for the expanded critical tile set before sharing fast-travel admissions with near/far work; 0 keeps the walkable ring first and admits the normal shares."));
|
||||||
|
|
||||||
int32 GVoxelForgeStreamingResultBacklogCap = 256;
|
int32 GVoxelForgeStreamingResultBacklogCap = 256;
|
||||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingResultBacklogCap(
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingResultBacklogCap(
|
||||||
TEXT("voxel.StreamingResultBacklogCap"),
|
TEXT("voxel.StreamingResultBacklogCap"),
|
||||||
@@ -355,6 +361,29 @@ namespace
|
|||||||
TEXT("voxel.StreamingStartupApplyBoost"),
|
TEXT("voxel.StreamingStartupApplyBoost"),
|
||||||
GVoxelForgeStreamingStartupApplyBoost,
|
GVoxelForgeStreamingStartupApplyBoost,
|
||||||
TEXT("Bounded cold-view upload budget (default on): 1 allows a 14 ms / 24-mesh cap until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget."));
|
TEXT("Bounded cold-view upload budget (default on): 1 allows a 14 ms / 24-mesh cap until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget."));
|
||||||
|
float GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds = 0.0f;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingFastTravelApplyBudgetMilliseconds(
|
||||||
|
TEXT("voxel.StreamingFastTravelApplyBudgetMilliseconds"),
|
||||||
|
GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds,
|
||||||
|
TEXT("Optional visible-result apply-time cap during speed-aware fast travel; 0 keeps the configured budget."));
|
||||||
|
|
||||||
|
int32 GVoxelForgeStreamingFastTravelForwardFirst = 0;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingFastTravelForwardFirst(
|
||||||
|
TEXT("voxel.StreamingFastTravelForwardFirst"),
|
||||||
|
GVoxelForgeStreamingFastTravelForwardFirst,
|
||||||
|
TEXT("During fast travel, admit camera-forward tiles first, then nearest travel-ahead tiles; 0 uses the legacy lead-band order."));
|
||||||
|
|
||||||
|
int32 GVoxelForgeStreamingRebuildOnCoreRegionCompletion = 1;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingRebuildOnCoreRegionCompletion(
|
||||||
|
TEXT("voxel.StreamingRebuildOnCoreRegionCompletion"),
|
||||||
|
GVoxelForgeStreamingRebuildOnCoreRegionCompletion,
|
||||||
|
TEXT("Force a full desired-set rebuild after Core region compilation; 0 reuses the unchanged desired keys."));
|
||||||
|
|
||||||
|
int32 GVoxelForgeStreamingCoreDesiredCacheByCenter = 0;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingCoreDesiredCacheByCenter(
|
||||||
|
TEXT("voxel.StreamingCoreDesiredCacheByCenter"),
|
||||||
|
GVoxelForgeStreamingCoreDesiredCacheByCenter,
|
||||||
|
TEXT("Reuse Core clipmap desired keys when a same-center overlay rebuild occurs; 0 queries Core each time."));
|
||||||
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;
|
||||||
@@ -2257,6 +2286,36 @@ void AVoxelWorld::BeginPlay()
|
|||||||
GVoxelForgeCoreRegionConcurrentAdmission =
|
GVoxelForgeCoreRegionConcurrentAdmission =
|
||||||
CommandLineCoreRegionConcurrentAdmission != 0 ? 1 : 0;
|
CommandLineCoreRegionConcurrentAdmission != 0 ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingFastTravelWaitForAllCriticalTiles="),
|
||||||
|
GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles))
|
||||||
|
{
|
||||||
|
GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles =
|
||||||
|
GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles != 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingFastTravelApplyBudgetMilliseconds="),
|
||||||
|
GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds))
|
||||||
|
{
|
||||||
|
GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds = FMath::Max(
|
||||||
|
0.0f, GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds);
|
||||||
|
}
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingFastTravelForwardFirst="),
|
||||||
|
GVoxelForgeStreamingFastTravelForwardFirst))
|
||||||
|
{
|
||||||
|
GVoxelForgeStreamingFastTravelForwardFirst =
|
||||||
|
GVoxelForgeStreamingFastTravelForwardFirst != 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingRebuildOnCoreRegionCompletion="),
|
||||||
|
GVoxelForgeStreamingRebuildOnCoreRegionCompletion))
|
||||||
|
{
|
||||||
|
GVoxelForgeStreamingRebuildOnCoreRegionCompletion =
|
||||||
|
GVoxelForgeStreamingRebuildOnCoreRegionCompletion != 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingCoreDesiredCacheByCenter="),
|
||||||
|
GVoxelForgeStreamingCoreDesiredCacheByCenter))
|
||||||
|
{
|
||||||
|
GVoxelForgeStreamingCoreDesiredCacheByCenter =
|
||||||
|
GVoxelForgeStreamingCoreDesiredCacheByCenter != 0 ? 1 : 0;
|
||||||
|
}
|
||||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingResultBacklogCap="),
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingResultBacklogCap="),
|
||||||
CommandLineStreamingResultBacklogCap))
|
CommandLineStreamingResultBacklogCap))
|
||||||
{
|
{
|
||||||
@@ -2750,6 +2809,8 @@ void AVoxelWorld::BeginPlay()
|
|||||||
"\"collision_gate\":%d,"
|
"\"collision_gate\":%d,"
|
||||||
"\"streaming_result_backlog_cap\":%d,\"streaming_startup_apply_boost\":%d,"
|
"\"streaming_result_backlog_cap\":%d,\"streaming_startup_apply_boost\":%d,"
|
||||||
"\"streaming_far_apply_supplement\":%d,"
|
"\"streaming_far_apply_supplement\":%d,"
|
||||||
|
"\"streaming_fast_travel_wait_for_all_critical_tiles\":%d,"
|
||||||
|
"\"streaming_fast_travel_apply_budget_ms\":%.3f,"
|
||||||
"\"streaming_background_worker_reserve\":%d,\"core_parallel_for_background_priority\":%d,"
|
"\"streaming_background_worker_reserve\":%d,\"core_parallel_for_background_priority\":%d,"
|
||||||
"\"streaming_tile_background_low\":%d,\"streaming_tile_background_normal\":%d,"
|
"\"streaming_tile_background_low\":%d,\"streaming_tile_background_normal\":%d,"
|
||||||
"\"core_column_cache_direct_mapped\":%d,"
|
"\"core_column_cache_direct_mapped\":%d,"
|
||||||
@@ -2772,6 +2833,8 @@ void AVoxelWorld::BeginPlay()
|
|||||||
GVoxelForgeCollisionGate,
|
GVoxelForgeCollisionGate,
|
||||||
GVoxelForgeStreamingResultBacklogCap, GVoxelForgeStreamingStartupApplyBoost,
|
GVoxelForgeStreamingResultBacklogCap, GVoxelForgeStreamingStartupApplyBoost,
|
||||||
GVoxelForgeStreamingFarApplySupplement,
|
GVoxelForgeStreamingFarApplySupplement,
|
||||||
|
GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles,
|
||||||
|
GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds,
|
||||||
GVoxelForgeStreamingBackgroundWorkerReserve,
|
GVoxelForgeStreamingBackgroundWorkerReserve,
|
||||||
GVoxelForgeCoreParallelForBackgroundPriority,
|
GVoxelForgeCoreParallelForBackgroundPriority,
|
||||||
GVoxelForgeStreamingTileBackgroundLow,
|
GVoxelForgeStreamingTileBackgroundLow,
|
||||||
@@ -8054,6 +8117,9 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
|||||||
Request.parallelSignEdgeRepair = true;
|
Request.parallelSignEdgeRepair = true;
|
||||||
|
|
||||||
VoxelForgeCoreState = State;
|
VoxelForgeCoreState = State;
|
||||||
|
CachedCoreDesiredTiles.Reset();
|
||||||
|
bHasCachedCoreDesiredTiles = false;
|
||||||
|
CachedCoreDesiredCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
||||||
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
|
||||||
@@ -9889,9 +9955,16 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
const int32 MaxApplies = GVoxelForgeStep1MaxMeshApplies > 0
|
const int32 MaxApplies = GVoxelForgeStep1MaxMeshApplies > 0
|
||||||
? GVoxelForgeStep1MaxMeshApplies
|
? GVoxelForgeStep1MaxMeshApplies
|
||||||
: (bUseStartupApplyBoost ? StreamingStartupMaxMeshApplies : ConfiguredMaxApplies);
|
: (bUseStartupApplyBoost ? StreamingStartupMaxMeshApplies : ConfiguredMaxApplies);
|
||||||
const float ApplyBudgetMilliseconds = bUseStartupApplyBoost
|
float ApplyBudgetMilliseconds = bUseStartupApplyBoost
|
||||||
? StreamingStartupApplyBudgetMilliseconds
|
? StreamingStartupApplyBudgetMilliseconds
|
||||||
: (Settings ? FMath::Max(Settings->MaxMeshApplyMilliseconds, 0.0f) : 2.0f);
|
: (Settings ? FMath::Max(Settings->MaxMeshApplyMilliseconds, 0.0f) : 2.0f);
|
||||||
|
const bool bFastTravelApplyBudgetActive = bStreamingPlayerMoving && bSpeedAwareFastMode
|
||||||
|
&& GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds > 0.0f;
|
||||||
|
if (bFastTravelApplyBudgetActive)
|
||||||
|
{
|
||||||
|
ApplyBudgetMilliseconds = FMath::Min(
|
||||||
|
ApplyBudgetMilliseconds, GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds);
|
||||||
|
}
|
||||||
const double ApplyBudgetSeconds =
|
const double ApplyBudgetSeconds =
|
||||||
static_cast<double>(ApplyBudgetMilliseconds) * 0.001;
|
static_cast<double>(ApplyBudgetMilliseconds) * 0.001;
|
||||||
int32 MeshesApplied = 0;
|
int32 MeshesApplied = 0;
|
||||||
@@ -11021,14 +11094,56 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
|
|||||||
const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState;
|
const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState;
|
||||||
if (!State.IsValid() || !State->bReady.load(std::memory_order_acquire)) return;
|
if (!State.IsValid() || !State->bReady.load(std::memory_order_acquire)) return;
|
||||||
|
|
||||||
|
std::vector<VoxelForgeCore::TileKey> UncachedCoreDesiredTiles;
|
||||||
|
const TArray<FVoxelTileKey>* CoreDesiredTiles = nullptr;
|
||||||
|
double CoreDesiredQueryMilliseconds = 0.0;
|
||||||
|
bool bCoreDesiredCacheHit = false;
|
||||||
|
if (GVoxelForgeStreamingCoreDesiredCacheByCenter != 0
|
||||||
|
&& bHasCachedCoreDesiredTiles && CachedCoreDesiredCenter == Center)
|
||||||
|
{
|
||||||
|
CoreDesiredTiles = &CachedCoreDesiredTiles;
|
||||||
|
bCoreDesiredCacheHit = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const uint64 QueryStartCycles = VoxelForgeStartupTrace::IsActive()
|
||||||
|
? FPlatformTime::Cycles64() : 0;
|
||||||
const FVector CameraVoxels = WorldToLocalCm(PlayerPosition) / GetActiveVoxelSizeCm();
|
const FVector CameraVoxels = WorldToLocalCm(PlayerPosition) / GetActiveVoxelSizeCm();
|
||||||
std::vector<VoxelForgeCore::TileKey> CoreDesired;
|
|
||||||
{
|
{
|
||||||
FReadScopeLock Lock(State->WorldLock);
|
FReadScopeLock Lock(State->WorldLock);
|
||||||
if (!State->World) return;
|
if (!State->World) return;
|
||||||
CoreDesired = State->World->desiredTiles(
|
UncachedCoreDesiredTiles = State->World->desiredTiles(
|
||||||
{CameraVoxels.X, CameraVoxels.Y, CameraVoxels.Z});
|
{CameraVoxels.X, CameraVoxels.Y, CameraVoxels.Z});
|
||||||
}
|
}
|
||||||
|
if (GVoxelForgeStreamingCoreDesiredCacheByCenter != 0)
|
||||||
|
{
|
||||||
|
CachedCoreDesiredTiles.Reset();
|
||||||
|
CachedCoreDesiredTiles.Reserve(static_cast<int32>(UncachedCoreDesiredTiles.size()));
|
||||||
|
for (const VoxelForgeCore::TileKey& Key : UncachedCoreDesiredTiles)
|
||||||
|
{
|
||||||
|
CachedCoreDesiredTiles.Emplace(
|
||||||
|
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
|
||||||
|
}
|
||||||
|
CachedCoreDesiredCenter = Center;
|
||||||
|
bHasCachedCoreDesiredTiles = true;
|
||||||
|
CoreDesiredTiles = &CachedCoreDesiredTiles;
|
||||||
|
}
|
||||||
|
if (QueryStartCycles != 0)
|
||||||
|
{
|
||||||
|
CoreDesiredQueryMilliseconds = FPlatformTime::ToSeconds64(
|
||||||
|
FPlatformTime::Cycles64() - QueryStartCycles) * 1000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (VoxelForgeStartupTrace::IsActive())
|
||||||
|
{
|
||||||
|
VoxelForgeStartupTrace::RecordEvent(TEXT("core_desired_cache"), FString::Printf(
|
||||||
|
TEXT("\"frame\":%llu,\"center\":[%d,%d,%d],\"hit\":%d,\"keys\":%d,\"query_ms\":%s"),
|
||||||
|
static_cast<unsigned long long>(GFrameCounter), Center.X, Center.Y, Center.Z,
|
||||||
|
bCoreDesiredCacheHit ? 1 : 0,
|
||||||
|
CoreDesiredTiles ? CoreDesiredTiles->Num()
|
||||||
|
: static_cast<int32>(UncachedCoreDesiredTiles.size()),
|
||||||
|
*FString::SanitizeFloat(static_cast<float>(CoreDesiredQueryMilliseconds))));
|
||||||
|
}
|
||||||
|
|
||||||
const FIntVector LookaheadHeadingStep = GetStreamingHeadingStep(PlayerHeading, PlayerPawn);
|
const FIntVector LookaheadHeadingStep = GetStreamingHeadingStep(PlayerHeading, PlayerPawn);
|
||||||
const int32 LookaheadTiles = GetStreamingCollisionLookaheadTiles(
|
const int32 LookaheadTiles = GetStreamingCollisionLookaheadTiles(
|
||||||
@@ -11051,12 +11166,25 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
|
|||||||
DesiredStamped.FindOrAdd(Key) = DesiredStamp;
|
DesiredStamped.FindOrAdd(Key) = DesiredStamp;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const VoxelForgeCore::TileKey& Key : CoreDesired)
|
const auto AddCoreDesiredTile = [&](const FVoxelTileKey& RuntimeKey)
|
||||||
{
|
{
|
||||||
const FVoxelTileKey RuntimeKey(
|
|
||||||
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
|
|
||||||
AddCandidate(RuntimeKey);
|
AddCandidate(RuntimeKey);
|
||||||
BaseVisualTiles.Add(RuntimeKey);
|
BaseVisualTiles.Add(RuntimeKey);
|
||||||
|
};
|
||||||
|
if (CoreDesiredTiles)
|
||||||
|
{
|
||||||
|
for (const FVoxelTileKey& RuntimeKey : *CoreDesiredTiles)
|
||||||
|
{
|
||||||
|
AddCoreDesiredTile(RuntimeKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (const VoxelForgeCore::TileKey& Key : UncachedCoreDesiredTiles)
|
||||||
|
{
|
||||||
|
AddCoreDesiredTile(FVoxelTileKey(
|
||||||
|
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const int32 PawnCollisionRadius = FMath::Clamp(
|
const int32 PawnCollisionRadius = FMath::Clamp(
|
||||||
@@ -11478,7 +11606,6 @@ void AVoxelWorld::StartPendingCoreRegionCompile()
|
|||||||
}
|
}
|
||||||
State->RegionCompileTileCount.store(
|
State->RegionCompileTileCount.store(
|
||||||
static_cast<int32>(CompiledTileCount), std::memory_order_release);
|
static_cast<int32>(CompiledTileCount), std::memory_order_release);
|
||||||
State->bRebuildDesired.store(true, std::memory_order_release);
|
|
||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeCoreRuntime] tile_regions_compiled=%d retained=%d epoch=%u desired_epoch=%u "
|
TEXT("[VoxelForgeCoreRuntime] tile_regions_compiled=%d retained=%d epoch=%u desired_epoch=%u "
|
||||||
"wall_ms=%.3f worker_s=%.3f serial=%d"),
|
"wall_ms=%.3f worker_s=%.3f serial=%d"),
|
||||||
@@ -12381,8 +12508,16 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
CoreRegionDependentDeclines = 0;
|
CoreRegionDependentDeclines = 0;
|
||||||
CoreRegionCompileTilesInFlight.Reset();
|
CoreRegionCompileTilesInFlight.Reset();
|
||||||
if (bRegionCompileSucceeded)
|
if (bRegionCompileSucceeded)
|
||||||
|
{
|
||||||
|
if (GVoxelForgeStreamingRebuildOnCoreRegionCompletion != 0)
|
||||||
{
|
{
|
||||||
bForceDesiredRebuild = true;
|
bForceDesiredRebuild = true;
|
||||||
|
}
|
||||||
|
// Region compilation adds evaluator pages; it does not change the desired
|
||||||
|
// tile keys. The live DesiredSorted list is admitted again below every
|
||||||
|
// update. Center, anchor, and collision-gate input changes already request
|
||||||
|
// their own desired rebuild, so compiling pages must not force another full
|
||||||
|
// clipmap rebuild on the same center.
|
||||||
bool bRetainedSetMatchesDesired = false;
|
bool bRetainedSetMatchesDesired = false;
|
||||||
TArray<FVoxelTileKey> MissingRetainedFineTiles;
|
TArray<FVoxelTileKey> MissingRetainedFineTiles;
|
||||||
{
|
{
|
||||||
@@ -12475,6 +12610,40 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
const int32 AuthoredNearTaskReserve = GetNearTaskReserve(MaxTasks);
|
const int32 AuthoredNearTaskReserve = GetNearTaskReserve(MaxTasks);
|
||||||
const int32 FastTravelTotalCap = TotalAdmissionCap;
|
const int32 FastTravelTotalCap = TotalAdmissionCap;
|
||||||
const FIntVector CenterChunk = GetTileCoordAtLocalCm(WorldToLocalCm(CenterPosition));
|
const FIntVector CenterChunk = GetTileCoordAtLocalCm(WorldToLocalCm(CenterPosition));
|
||||||
|
FVector TravelHeadingLocal = GetActorTransform().InverseTransformVectorNoScale(PlayerHeading)
|
||||||
|
.GetSafeNormal();
|
||||||
|
if (TravelHeadingLocal.IsNearlyZero() && IsValid(PlayerPawn))
|
||||||
|
{
|
||||||
|
TravelHeadingLocal = GetActorTransform().InverseTransformVectorNoScale(
|
||||||
|
PlayerPawn->GetActorForwardVector()).GetSafeNormal();
|
||||||
|
}
|
||||||
|
FVector CameraForwardLocal = FVector::ZeroVector;
|
||||||
|
bool bCameraForwardValid = false;
|
||||||
|
if (IsValid(PlayerPawn))
|
||||||
|
{
|
||||||
|
if (APlayerController* PlayerController = Cast<APlayerController>(PlayerPawn->GetController());
|
||||||
|
PlayerController && PlayerController->IsLocalController())
|
||||||
|
{
|
||||||
|
FVector ViewLocation;
|
||||||
|
FRotator ViewRotation;
|
||||||
|
PlayerController->GetPlayerViewPoint(ViewLocation, ViewRotation);
|
||||||
|
CameraForwardLocal = GetActorTransform().InverseTransformVectorNoScale(
|
||||||
|
ViewRotation.Vector()).GetSafeNormal();
|
||||||
|
bCameraForwardValid = !CameraForwardLocal.IsNearlyZero();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
constexpr double StreamingForwardConeCosine = 0.573576436351046; // cos(55 degrees)
|
||||||
|
const FVector PlayerLocalCm = WorldToLocalCm(CenterPosition);
|
||||||
|
const auto GetNearTileOffset = [&](const FVoxelTileKey& Tile)
|
||||||
|
{
|
||||||
|
const double TileExtentCm = static_cast<double>(
|
||||||
|
VF_ClipLevelExtentVoxels(Tile.Level)) * GetActiveVoxelSizeCm();
|
||||||
|
const FVector TileCenter(
|
||||||
|
(static_cast<double>(Tile.Coord.X) + 0.5) * TileExtentCm,
|
||||||
|
(static_cast<double>(Tile.Coord.Y) + 0.5) * TileExtentCm,
|
||||||
|
(static_cast<double>(Tile.Coord.Z) + 0.5) * TileExtentCm);
|
||||||
|
return TileCenter - PlayerLocalCm;
|
||||||
|
};
|
||||||
FVoxelTileKey WalkableSupportTile;
|
FVoxelTileKey WalkableSupportTile;
|
||||||
const FIntVector WalkableCenter = GetPlayerSupportTile(
|
const FIntVector WalkableCenter = GetPlayerSupportTile(
|
||||||
PlayerPawn, CenterPosition, WalkableSupportTile)
|
PlayerPawn, CenterPosition, WalkableSupportTile)
|
||||||
@@ -12934,6 +13103,70 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
// Coarse work is capped below the full task budget so later near requests keep admission.
|
// Coarse work is capped below the full task budget so later near requests keep admission.
|
||||||
// Le travail grossier reste sous le budget total pour reserver des admissions aux tuiles proches.
|
// Le travail grossier reste sous le budget total pour reserver des admissions aux tuiles proches.
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
const auto GetNearDirectionClassForOffset = [&](const FVector& Offset)
|
||||||
|
{
|
||||||
|
const double Distance = Offset.Size();
|
||||||
|
if (Distance > 0.0)
|
||||||
|
{
|
||||||
|
const FVector& ConeHeading = bCameraForwardValid
|
||||||
|
? CameraForwardLocal : TravelHeadingLocal;
|
||||||
|
if (!ConeHeading.IsNearlyZero()
|
||||||
|
&& FVector::DotProduct(Offset, ConeHeading) / Distance
|
||||||
|
>= StreamingForwardConeCosine)
|
||||||
|
{
|
||||||
|
return 0; // camera cone, or travel cone when no local camera is available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const double TravelProjection = FVector::DotProduct(Offset, TravelHeadingLocal);
|
||||||
|
if (!TravelHeadingLocal.IsNearlyZero() && TravelProjection > 0.0) return 1;
|
||||||
|
if (!TravelHeadingLocal.IsNearlyZero() && TravelProjection < 0.0) return 3;
|
||||||
|
return 2; // side / direction unavailable
|
||||||
|
};
|
||||||
|
const auto GetNearDirectionClass = [&](const FVoxelTileKey& Tile)
|
||||||
|
{
|
||||||
|
return GetNearDirectionClassForOffset(GetNearTileOffset(Tile));
|
||||||
|
};
|
||||||
|
const auto SortNearTilesForwardFirst = [&](TArray<FVoxelTileKey>& Tiles)
|
||||||
|
{
|
||||||
|
struct FForwardTileEntry
|
||||||
|
{
|
||||||
|
FVoxelTileKey Key;
|
||||||
|
int32 DirectionClass = 2;
|
||||||
|
double DistanceSquared = 0.0;
|
||||||
|
};
|
||||||
|
TArray<FForwardTileEntry> Entries;
|
||||||
|
Entries.Reserve(Tiles.Num());
|
||||||
|
for (const FVoxelTileKey& Tile : Tiles)
|
||||||
|
{
|
||||||
|
const FVector Offset = GetNearTileOffset(Tile);
|
||||||
|
Entries.Add({Tile, GetNearDirectionClassForOffset(Offset), Offset.SizeSquared()});
|
||||||
|
}
|
||||||
|
std::stable_sort(Entries.GetData(), Entries.GetData() + Entries.Num(),
|
||||||
|
[](const FForwardTileEntry& A, const FForwardTileEntry& B)
|
||||||
|
{
|
||||||
|
if (A.DirectionClass != B.DirectionClass)
|
||||||
|
{
|
||||||
|
return A.DirectionClass < B.DirectionClass;
|
||||||
|
}
|
||||||
|
if (A.DirectionClass <= 1)
|
||||||
|
{
|
||||||
|
if (A.DistanceSquared != B.DistanceSquared)
|
||||||
|
{
|
||||||
|
return A.DistanceSquared < B.DistanceSquared;
|
||||||
|
}
|
||||||
|
if (A.Key.Coord.X != B.Key.Coord.X) return A.Key.Coord.X < B.Key.Coord.X;
|
||||||
|
if (A.Key.Coord.Y != B.Key.Coord.Y) return A.Key.Coord.Y < B.Key.Coord.Y;
|
||||||
|
return A.Key.Coord.Z < B.Key.Coord.Z;
|
||||||
|
}
|
||||||
|
return false; // preserve the existing deterministic order for side/back tiles
|
||||||
|
});
|
||||||
|
for (int32 Index = 0; Index < Entries.Num(); ++Index)
|
||||||
|
{
|
||||||
|
Tiles[Index] = Entries[Index].Key;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
int32 SchedulerSubmittedByDirection[4] = {};
|
||||||
|
TArray<FString> SchedulerNearAdmissionOrder;
|
||||||
const bool bSubmissionLoopRan = !bAllChunksLoaded;
|
const bool bSubmissionLoopRan = !bAllChunksLoaded;
|
||||||
const uint64 TraceSubmitTilesStartCycles = bTraceStreamingScheduler && bSubmissionLoopRan
|
const uint64 TraceSubmitTilesStartCycles = bTraceStreamingScheduler && bSubmissionLoopRan
|
||||||
? FPlatformTime::Cycles64() : 0;
|
? FPlatformTime::Cycles64() : 0;
|
||||||
@@ -12963,6 +13196,20 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
&& Tile.Level >= 0 && Tile.Level < 9)
|
&& Tile.Level >= 0 && Tile.Level < 9)
|
||||||
{
|
{
|
||||||
++SchedulerSubmittedByLevel[Tile.Level];
|
++SchedulerSubmittedByLevel[Tile.Level];
|
||||||
|
if (bTraceStreamingScheduler && Tile.Level <= 2)
|
||||||
|
{
|
||||||
|
const int32 DirectionClass = GetNearDirectionClass(Tile);
|
||||||
|
++SchedulerSubmittedByDirection[DirectionClass];
|
||||||
|
if (SchedulerNearAdmissionOrder.Num() < 12)
|
||||||
|
{
|
||||||
|
const TCHAR* Lane = Priority == EVoxelTileWorkPriority::CollisionCritical
|
||||||
|
? TEXT("critical") : TEXT("near");
|
||||||
|
SchedulerNearAdmissionOrder.Add(FString::Printf(
|
||||||
|
TEXT("\"%d,%d,%d,l%d,c%d,%s\""),
|
||||||
|
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level,
|
||||||
|
DirectionClass, Lane));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -12970,7 +13217,16 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
// PRIORITE AU SOL — la tuile de support et l'anneau XY 3x3 requis pour la collision passent avant le reste.
|
// PRIORITE AU SOL — la tuile de support et l'anneau XY 3x3 requis pour la collision passent avant le reste.
|
||||||
// They use the foreground task lane when enabled, above ordinary background streaming.
|
// They use the foreground task lane when enabled, above ordinary background streaming.
|
||||||
// Ils utilisent la voie foreground lorsqu'elle est active, avant le streaming distant.
|
// Ils utilisent la voie foreground lorsqu'elle est active, avant le streaming distant.
|
||||||
for (const FVoxelTileKey& T : CriticalDesiredTiles)
|
TArray<FVoxelTileKey> ForwardCriticalTiles;
|
||||||
|
const TArray<FVoxelTileKey>* CriticalAdmissionTiles = &CriticalDesiredTiles;
|
||||||
|
if (GVoxelForgeStreamingFastTravelForwardFirst != 0
|
||||||
|
&& bPolicy3FastTravel)
|
||||||
|
{
|
||||||
|
ForwardCriticalTiles = CriticalDesiredTiles;
|
||||||
|
SortNearTilesForwardFirst(ForwardCriticalTiles);
|
||||||
|
CriticalAdmissionTiles = &ForwardCriticalTiles;
|
||||||
|
}
|
||||||
|
for (const FVoxelTileKey& T : *CriticalAdmissionTiles)
|
||||||
{
|
{
|
||||||
if (GetGenerationAdmissionCount() >= TotalAdmissionCap)
|
if (GetGenerationAdmissionCount() >= TotalAdmissionCap)
|
||||||
{
|
{
|
||||||
@@ -13045,27 +13301,30 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
// order; this optional sort only adds the current travel heading and distance.
|
// order; this optional sort only adds the current travel heading and distance.
|
||||||
if (bPreferTravelDirection && LevelTiles.Num() > 1)
|
if (bPreferTravelDirection && LevelTiles.Num() > 1)
|
||||||
{
|
{
|
||||||
FVector Heading=FVector::ZeroVector;
|
|
||||||
FVector PlayerLocalCm=FVector::ZeroVector;
|
|
||||||
double TileSizeCm=0.0;
|
|
||||||
if(bPreferTravelDirection)
|
|
||||||
{
|
|
||||||
Heading=GetActorTransform().InverseTransformVectorNoScale(PlayerHeading);
|
|
||||||
if(Heading.IsNearlyZero()&&IsValid(PlayerPawn))
|
|
||||||
{
|
|
||||||
Heading=GetActorTransform().InverseTransformVectorNoScale(
|
|
||||||
PlayerPawn->GetActorForwardVector());
|
|
||||||
}
|
|
||||||
Heading=Heading.GetSafeNormal();
|
|
||||||
PlayerLocalCm=WorldToLocalCm(CenterPosition);
|
|
||||||
TileSizeCm=static_cast<double>(VF_ClipLevelExtentVoxels(Level))
|
|
||||||
*GetActiveVoxelSizeCm();
|
|
||||||
}
|
|
||||||
const uint64 TraceLevelSortStartCycles = bTraceStreamingScheduler
|
const uint64 TraceLevelSortStartCycles = bTraceStreamingScheduler
|
||||||
? FPlatformTime::Cycles64() : 0;
|
? FPlatformTime::Cycles64() : 0;
|
||||||
|
const bool bUseForwardFirstOrder =
|
||||||
|
GVoxelForgeStreamingFastTravelForwardFirst != 0
|
||||||
|
&& bPolicy3FastTravel && Level <= 2;
|
||||||
|
if (bUseForwardFirstOrder)
|
||||||
|
{
|
||||||
|
SortNearTilesForwardFirst(LevelTiles);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FVector Heading = GetActorTransform().InverseTransformVectorNoScale(PlayerHeading);
|
||||||
|
if (Heading.IsNearlyZero() && IsValid(PlayerPawn))
|
||||||
|
{
|
||||||
|
Heading = GetActorTransform().InverseTransformVectorNoScale(
|
||||||
|
PlayerPawn->GetActorForwardVector());
|
||||||
|
}
|
||||||
|
Heading = Heading.GetSafeNormal();
|
||||||
|
const FVector LegacyPlayerLocalCm = WorldToLocalCm(CenterPosition);
|
||||||
|
const double TileSizeCm = static_cast<double>(VF_ClipLevelExtentVoxels(Level))
|
||||||
|
* GetActiveVoxelSizeCm();
|
||||||
const bool bUseFastNearLeadOrder = bPolicy3FastTravel && Level <= 2;
|
const bool bUseFastNearLeadOrder = bPolicy3FastTravel && Level <= 2;
|
||||||
std::stable_sort(LevelTiles.GetData(),LevelTiles.GetData()+LevelTiles.Num(),
|
std::stable_sort(LevelTiles.GetData(), LevelTiles.GetData() + LevelTiles.Num(),
|
||||||
[&](const FVoxelTileKey& A,const FVoxelTileKey& B)
|
[&](const FVoxelTileKey& A, const FVoxelTileKey& B)
|
||||||
{
|
{
|
||||||
const FVector CenterA(
|
const FVector CenterA(
|
||||||
(static_cast<double>(A.Coord.X) + 0.5) * TileSizeCm,
|
(static_cast<double>(A.Coord.X) + 0.5) * TileSizeCm,
|
||||||
@@ -13075,8 +13334,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
(static_cast<double>(B.Coord.X) + 0.5) * TileSizeCm,
|
(static_cast<double>(B.Coord.X) + 0.5) * TileSizeCm,
|
||||||
(static_cast<double>(B.Coord.Y) + 0.5) * TileSizeCm,
|
(static_cast<double>(B.Coord.Y) + 0.5) * TileSizeCm,
|
||||||
(static_cast<double>(B.Coord.Z) + 0.5) * TileSizeCm);
|
(static_cast<double>(B.Coord.Z) + 0.5) * TileSizeCm);
|
||||||
const FVector OffsetA = CenterA - PlayerLocalCm;
|
const FVector OffsetA = CenterA - LegacyPlayerLocalCm;
|
||||||
const FVector OffsetB = CenterB - PlayerLocalCm;
|
const FVector OffsetB = CenterB - LegacyPlayerLocalCm;
|
||||||
if (bUseFastNearLeadOrder && !Heading.IsNearlyZero())
|
if (bUseFastNearLeadOrder && !Heading.IsNearlyZero())
|
||||||
{
|
{
|
||||||
const double ProjectionA = FVector::DotProduct(OffsetA, Heading);
|
const double ProjectionA = FVector::DotProduct(OffsetA, Heading);
|
||||||
@@ -13107,8 +13366,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
return ProjectionA < ProjectionB;
|
return ProjectionA < ProjectionB;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const bool bAheadA = !Heading.IsNearlyZero() && FVector::DotProduct(OffsetA, Heading) > 0.0;
|
const bool bAheadA = !Heading.IsNearlyZero()
|
||||||
const bool bAheadB = !Heading.IsNearlyZero() && FVector::DotProduct(OffsetB, Heading) > 0.0;
|
&& FVector::DotProduct(OffsetA, Heading) > 0.0;
|
||||||
|
const bool bAheadB = !Heading.IsNearlyZero()
|
||||||
|
&& FVector::DotProduct(OffsetB, Heading) > 0.0;
|
||||||
if (bAheadA != bAheadB) return bAheadA;
|
if (bAheadA != bAheadB) return bAheadA;
|
||||||
const double DistanceA = OffsetA.SizeSquared();
|
const double DistanceA = OffsetA.SizeSquared();
|
||||||
const double DistanceB = OffsetB.SizeSquared();
|
const double DistanceB = OffsetB.SizeSquared();
|
||||||
@@ -13117,6 +13378,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
if (A.Coord.Y != B.Coord.Y) return A.Coord.Y < B.Coord.Y;
|
if (A.Coord.Y != B.Coord.Y) return A.Coord.Y < B.Coord.Y;
|
||||||
return A.Coord.Z < B.Coord.Z;
|
return A.Coord.Z < B.Coord.Z;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
if (bTraceStreamingScheduler)
|
if (bTraceStreamingScheduler)
|
||||||
{
|
{
|
||||||
LevelSortMilliseconds += FPlatformTime::ToSeconds64(
|
LevelSortMilliseconds += FPlatformTime::ToSeconds64(
|
||||||
@@ -13231,7 +13493,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
// Policy 0 reconstructs L77. Policy 3 enters that order only after sustained fast travel.
|
// Policy 0 reconstructs L77. Policy 3 enters that order only after sustained fast travel.
|
||||||
if (bPolicy3FastTravel)
|
if (bPolicy3FastTravel)
|
||||||
{
|
{
|
||||||
if (bWalkableRingReadyForReserve && AreCriticalTilesReady())
|
const bool bWaitForExpandedCriticalSet =
|
||||||
|
GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles != 0;
|
||||||
|
if (bWalkableRingReadyForReserve
|
||||||
|
&& (!bWaitForExpandedCriticalSet || AreCriticalTilesReady()))
|
||||||
{
|
{
|
||||||
const int32 ShareAdmissionCap = FastTravelTotalCap;
|
const int32 ShareAdmissionCap = FastTravelTotalCap;
|
||||||
const int32 StartingAdmissions = GetGenerationAdmissionCount();
|
const int32 StartingAdmissions = GetGenerationAdmissionCount();
|
||||||
@@ -13451,7 +13716,25 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
int32 SettledByLevel[9] = {};
|
int32 SettledByLevel[9] = {};
|
||||||
int32 CoveredByLevel[9] = {};
|
int32 CoveredByLevel[9] = {};
|
||||||
int32 PendingByLevel[9] = {};
|
int32 PendingByLevel[9] = {};
|
||||||
|
int32 NearAheadDesired = 0;
|
||||||
|
int32 NearAheadResident = 0;
|
||||||
|
int32 NearBehindDesired = 0;
|
||||||
|
int32 NearBehindResident = 0;
|
||||||
|
int32 NearForwardConeDesired = 0;
|
||||||
|
int32 NearForwardConeResident = 0;
|
||||||
|
int32 NearViewForwardConeDesired = 0;
|
||||||
|
int32 NearViewForwardConeResident = 0;
|
||||||
|
double NearForwardWeightedDesired = 0.0;
|
||||||
|
double NearForwardWeightedResident = 0.0;
|
||||||
int32 TraceMaxDesiredLevel = 0;
|
int32 TraceMaxDesiredLevel = 0;
|
||||||
|
const FVector LocalPawn = WorldToLocalCm(CenterPosition);
|
||||||
|
FVector TraceHeading = GetActorTransform().InverseTransformVectorNoScale(PlayerHeading)
|
||||||
|
.GetSafeNormal();
|
||||||
|
if (TraceHeading.IsNearlyZero() && IsValid(PlayerPawn))
|
||||||
|
{
|
||||||
|
TraceHeading = GetActorTransform().InverseTransformVectorNoScale(
|
||||||
|
PlayerPawn->GetActorForwardVector()).GetSafeNormal();
|
||||||
|
}
|
||||||
for (const FVoxelTileKey& Tile : DesiredSorted)
|
for (const FVoxelTileKey& Tile : DesiredSorted)
|
||||||
{
|
{
|
||||||
TraceMaxDesiredLevel = FMath::Max(TraceMaxDesiredLevel, Tile.Level);
|
TraceMaxDesiredLevel = FMath::Max(TraceMaxDesiredLevel, Tile.Level);
|
||||||
@@ -13460,10 +13743,54 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
{
|
{
|
||||||
if (Tile.Level < 0 || Tile.Level > 8) continue;
|
if (Tile.Level < 0 || Tile.Level > 8) continue;
|
||||||
++DesiredByLevel[Tile.Level];
|
++DesiredByLevel[Tile.Level];
|
||||||
if (LoadedTiles.Contains(Tile)) ++ResidentByLevel[Tile.Level];
|
const bool bResident = LoadedTiles.Contains(Tile);
|
||||||
|
if (bResident) ++ResidentByLevel[Tile.Level];
|
||||||
if (LoadedTiles.Contains(Tile) || CollisionNotRequiredTiles.Contains(Tile))
|
if (LoadedTiles.Contains(Tile) || CollisionNotRequiredTiles.Contains(Tile))
|
||||||
++SettledByLevel[Tile.Level];
|
++SettledByLevel[Tile.Level];
|
||||||
if (PendingTiles.Contains(Tile)) ++PendingByLevel[Tile.Level];
|
if (PendingTiles.Contains(Tile)) ++PendingByLevel[Tile.Level];
|
||||||
|
|
||||||
|
if (Tile.Level <= 2
|
||||||
|
&& (!TraceHeading.IsNearlyZero() || bCameraForwardValid))
|
||||||
|
{
|
||||||
|
const double TileExtentCm = static_cast<double>(
|
||||||
|
VF_ClipLevelExtentVoxels(Tile.Level)) * GetActiveVoxelSizeCm();
|
||||||
|
const FVector TileCenter(
|
||||||
|
(static_cast<double>(Tile.Coord.X) + 0.5) * TileExtentCm,
|
||||||
|
(static_cast<double>(Tile.Coord.Y) + 0.5) * TileExtentCm,
|
||||||
|
(static_cast<double>(Tile.Coord.Z) + 0.5) * TileExtentCm);
|
||||||
|
const FVector Offset = TileCenter - LocalPawn;
|
||||||
|
const double Distance = Offset.Size();
|
||||||
|
const double Projection = FVector::DotProduct(Offset, TraceHeading);
|
||||||
|
const double DirectionCosine = Distance > 0.0 ? Projection / Distance : 0.0;
|
||||||
|
if (Projection > 0.0)
|
||||||
|
{
|
||||||
|
++NearAheadDesired;
|
||||||
|
if (bResident) ++NearAheadResident;
|
||||||
|
}
|
||||||
|
else if (Projection < 0.0)
|
||||||
|
{
|
||||||
|
++NearBehindDesired;
|
||||||
|
if (bResident) ++NearBehindResident;
|
||||||
|
}
|
||||||
|
if (DirectionCosine >= StreamingForwardConeCosine)
|
||||||
|
{
|
||||||
|
++NearForwardConeDesired;
|
||||||
|
if (bResident) ++NearForwardConeResident;
|
||||||
|
}
|
||||||
|
if (bCameraForwardValid && Distance > 0.0)
|
||||||
|
{
|
||||||
|
const double ViewDirectionCosine =
|
||||||
|
FVector::DotProduct(Offset, CameraForwardLocal) / Distance;
|
||||||
|
if (ViewDirectionCosine >= StreamingForwardConeCosine)
|
||||||
|
{
|
||||||
|
++NearViewForwardConeDesired;
|
||||||
|
if (bResident) ++NearViewForwardConeResident;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const double Weight = FMath::Max(0.0, DirectionCosine);
|
||||||
|
NearForwardWeightedDesired += Weight;
|
||||||
|
if (bResident) NearForwardWeightedResident += Weight;
|
||||||
|
}
|
||||||
bool bCovered = LoadedTiles.Contains(Tile);
|
bool bCovered = LoadedTiles.Contains(Tile);
|
||||||
for (int32 AncestorLevel = Tile.Level + 1;
|
for (int32 AncestorLevel = Tile.Level + 1;
|
||||||
!bCovered && AncestorLevel <= TraceMaxDesiredLevel && AncestorLevel <= 8;
|
!bCovered && AncestorLevel <= TraceMaxDesiredLevel && AncestorLevel <= 8;
|
||||||
@@ -13503,12 +13830,20 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
SettledCounts += TEXT("]");
|
SettledCounts += TEXT("]");
|
||||||
CoveredCounts += TEXT("]");
|
CoveredCounts += TEXT("]");
|
||||||
PendingCounts += TEXT("]");
|
PendingCounts += TEXT("]");
|
||||||
const FVector LocalPawn = WorldToLocalCm(CenterPosition);
|
|
||||||
const double StreamingUpdateMilliseconds = (TraceNow - StreamingUpdateStartSeconds) * 1000.0;
|
const double StreamingUpdateMilliseconds = (TraceNow - StreamingUpdateStartSeconds) * 1000.0;
|
||||||
VoxelForgeStartupTrace::RecordEvent(TEXT("streaming_sample"), FString::Printf(
|
VoxelForgeStartupTrace::RecordEvent(TEXT("streaming_sample"), FString::Printf(
|
||||||
TEXT("\"scope\":\"desired_residency_not_visual_enclosure\",\"center\":[%d,%d,%d],"
|
TEXT("\"scope\":\"desired_residency_not_visual_enclosure\",\"center\":[%d,%d,%d],"
|
||||||
"\"position_cm\":[%s,%s,%s],\"speed_cm_s\":%s,\"walkable_ready\":%d,"
|
"\"position_cm\":[%s,%s,%s],\"speed_cm_s\":%s,\"walkable_ready\":%d,"
|
||||||
"\"desired_by_lod\":%s,\"resident_by_lod\":%s,\"settled_by_lod\":%s,"
|
"\"desired_by_lod\":%s,\"resident_by_lod\":%s,\"settled_by_lod\":%s,"
|
||||||
|
"\"near_ahead_desired\":%d,\"near_ahead_resident\":%d,"
|
||||||
|
"\"near_behind_desired\":%d,\"near_behind_resident\":%d,"
|
||||||
|
"\"near_forward_cone_half_angle_deg\":55,"
|
||||||
|
"\"near_forward_cone_desired\":%d,\"near_forward_cone_resident\":%d,"
|
||||||
|
"\"near_view_forward_cone_half_angle_deg\":55,"
|
||||||
|
"\"near_view_forward_cone_desired\":%d,\"near_view_forward_cone_resident\":%d,"
|
||||||
|
"\"near_view_forward_cone_valid\":%d,"
|
||||||
|
"\"near_forward_weighted_desired\":%s,"
|
||||||
|
"\"near_forward_weighted_resident\":%s,\"near_direction_valid\":%d,"
|
||||||
"\"pending_by_lod\":%s,"
|
"\"pending_by_lod\":%s,"
|
||||||
"\"covered_by_lod\":%s,"
|
"\"covered_by_lod\":%s,"
|
||||||
"\"walkable_ready_tiles\":%d,\"walkable_required_tiles\":9,"
|
"\"walkable_ready_tiles\":%d,\"walkable_required_tiles\":9,"
|
||||||
@@ -13522,7 +13857,15 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
*FString::SanitizeFloat(LocalPawn.Z),
|
*FString::SanitizeFloat(LocalPawn.Z),
|
||||||
*FString::SanitizeFloat(PlayerHeading.Size()),
|
*FString::SanitizeFloat(PlayerHeading.Size()),
|
||||||
bWalkableRingReadyForReserve ? 1 : 0,
|
bWalkableRingReadyForReserve ? 1 : 0,
|
||||||
*DesiredCounts, *ResidentCounts, *SettledCounts, *PendingCounts, *CoveredCounts,
|
*DesiredCounts, *ResidentCounts, *SettledCounts,
|
||||||
|
NearAheadDesired, NearAheadResident, NearBehindDesired, NearBehindResident,
|
||||||
|
NearForwardConeDesired, NearForwardConeResident,
|
||||||
|
NearViewForwardConeDesired, NearViewForwardConeResident,
|
||||||
|
bCameraForwardValid ? 1 : 0,
|
||||||
|
*FString::SanitizeFloat(static_cast<float>(NearForwardWeightedDesired)),
|
||||||
|
*FString::SanitizeFloat(static_cast<float>(NearForwardWeightedResident)),
|
||||||
|
TraceHeading.IsNearlyZero() ? 0 : 1,
|
||||||
|
*PendingCounts, *CoveredCounts,
|
||||||
WalkableRingReadyCount,
|
WalkableRingReadyCount,
|
||||||
GetActiveGenerationTaskCount(), GetGenerationAdmissionCount(), MaxTasks, NearTaskReserve,
|
GetActiveGenerationTaskCount(), GetGenerationAdmissionCount(), MaxTasks, NearTaskReserve,
|
||||||
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
|
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
|
||||||
@@ -13540,6 +13883,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
int32 PendingByLevel[9] = {};
|
int32 PendingByLevel[9] = {};
|
||||||
int32 AdmissionCapByLevel[9] = {};
|
int32 AdmissionCapByLevel[9] = {};
|
||||||
int32 DeclinedRuleByLevel[9] = {};
|
int32 DeclinedRuleByLevel[9] = {};
|
||||||
|
int32 DesiredMissingByDirection[4] = {};
|
||||||
for (const FVoxelTileKey& Tile : DesiredSorted)
|
for (const FVoxelTileKey& Tile : DesiredSorted)
|
||||||
{
|
{
|
||||||
if (Tile.Level < 0 || Tile.Level > 8) continue;
|
if (Tile.Level < 0 || Tile.Level > 8) continue;
|
||||||
@@ -13554,6 +13898,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
++DesiredMissingByLevel[Tile.Level];
|
++DesiredMissingByLevel[Tile.Level];
|
||||||
|
if (Tile.Level <= 2)
|
||||||
|
{
|
||||||
|
++DesiredMissingByDirection[GetNearDirectionClass(Tile)];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13651,6 +13999,24 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
Json += TEXT("]");
|
Json += TEXT("]");
|
||||||
return Json;
|
return Json;
|
||||||
};
|
};
|
||||||
|
const auto IntArray4Json = [](const int32 Values[4])
|
||||||
|
{
|
||||||
|
FString Json = TEXT("[");
|
||||||
|
for (int32 Index = 0; Index < 4; ++Index)
|
||||||
|
{
|
||||||
|
if (Index > 0) Json += TEXT(",");
|
||||||
|
Json += FString::FromInt(Values[Index]);
|
||||||
|
}
|
||||||
|
Json += TEXT("]");
|
||||||
|
return Json;
|
||||||
|
};
|
||||||
|
FString NearAdmissionOrderJson = TEXT("[");
|
||||||
|
for (int32 Index = 0; Index < SchedulerNearAdmissionOrder.Num(); ++Index)
|
||||||
|
{
|
||||||
|
if (Index > 0) NearAdmissionOrderJson += TEXT(",");
|
||||||
|
NearAdmissionOrderJson += SchedulerNearAdmissionOrder[Index];
|
||||||
|
}
|
||||||
|
NearAdmissionOrderJson += TEXT("]");
|
||||||
const double SchedulerTraceNow = FPlatformTime::Seconds();
|
const double SchedulerTraceNow = FPlatformTime::Seconds();
|
||||||
const uint64 TotalWorkerCycles = TotalGenerationWorkerTaskCycles.load(std::memory_order_relaxed);
|
const uint64 TotalWorkerCycles = TotalGenerationWorkerTaskCycles.load(std::memory_order_relaxed);
|
||||||
const uint64 WorkerCyclesDelta = TotalWorkerCycles >= LastStreamingSchedulerWorkerCycles
|
const uint64 WorkerCyclesDelta = TotalWorkerCycles >= LastStreamingSchedulerWorkerCycles
|
||||||
@@ -13671,6 +14037,11 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
"\"max_tasks\":%d,\"ready_queue_depth\":%d,"
|
"\"max_tasks\":%d,\"ready_queue_depth\":%d,"
|
||||||
"\"total_admission_cap\":%d,\"near_admission_cap\":%d,"
|
"\"total_admission_cap\":%d,\"near_admission_cap\":%d,"
|
||||||
"\"near_reserve\":%d,\"scheduler_policy\":%d,"
|
"\"near_reserve\":%d,\"scheduler_policy\":%d,"
|
||||||
|
"\"forward_first\":%d,\"rebuild_on_core_region_completion\":%d,"
|
||||||
|
"\"core_desired_cache_by_center\":%d,"
|
||||||
|
"\"camera_forward_valid\":%d,"
|
||||||
|
"\"near_missing_by_direction\":%s,\"near_submitted_by_direction\":%s,"
|
||||||
|
"\"near_admission_order_first\":%s,"
|
||||||
"\"coarse_cap\":%d,\"first_far_level\":%d,"
|
"\"coarse_cap\":%d,\"first_far_level\":%d,"
|
||||||
"\"submission_stop_level\":%d,\"submission_stop_cap\":%d,"
|
"\"submission_stop_level\":%d,\"submission_stop_cap\":%d,"
|
||||||
"\"pending_entry\":%d,\"pending_after\":%d,\"admission_occupancy\":%d,"
|
"\"pending_entry\":%d,\"pending_after\":%d,\"admission_occupancy\":%d,"
|
||||||
@@ -13700,6 +14071,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
GVoxelForgeStreamingLegacyPendingResultCap,
|
GVoxelForgeStreamingLegacyPendingResultCap,
|
||||||
GVoxelForgeCoreRegionConcurrentAdmission, MaxTasks, ReadyQueueDepth,
|
GVoxelForgeCoreRegionConcurrentAdmission, MaxTasks, ReadyQueueDepth,
|
||||||
TotalAdmissionCap, NearAdmissionCap, NearTaskReserve, SchedulerPolicy,
|
TotalAdmissionCap, NearAdmissionCap, NearTaskReserve, SchedulerPolicy,
|
||||||
|
GVoxelForgeStreamingFastTravelForwardFirst != 0 ? 1 : 0,
|
||||||
|
GVoxelForgeStreamingRebuildOnCoreRegionCompletion != 0 ? 1 : 0,
|
||||||
|
GVoxelForgeStreamingCoreDesiredCacheByCenter != 0 ? 1 : 0,
|
||||||
|
bCameraForwardValid ? 1 : 0,
|
||||||
|
*IntArray4Json(DesiredMissingByDirection),
|
||||||
|
*IntArray4Json(SchedulerSubmittedByDirection),
|
||||||
|
*NearAdmissionOrderJson,
|
||||||
CoarseTaskCap, FirstFarLevel,
|
CoarseTaskCap, FirstFarLevel,
|
||||||
SchedulerOrderStopLevel, SchedulerOrderStopCap,
|
SchedulerOrderStopLevel, SchedulerOrderStopCap,
|
||||||
PendingTilesAtUpdateEntry, PendingTiles.Num(), AdmissionOccupancy, ActiveTaskGuard,
|
PendingTilesAtUpdateEntry, PendingTiles.Num(), AdmissionOccupancy, ActiveTaskGuard,
|
||||||
|
|||||||
@@ -1254,6 +1254,9 @@ public:
|
|||||||
float StartupTraceSpeedCmPerSecond = 0.0f;
|
float StartupTraceSpeedCmPerSecond = 0.0f;
|
||||||
bool bSpeedAwareTravelMode = false;
|
bool bSpeedAwareTravelMode = false;
|
||||||
bool bSpeedAwareFastMode = false;
|
bool bSpeedAwareFastMode = false;
|
||||||
|
TArray<FVoxelTileKey> CachedCoreDesiredTiles;
|
||||||
|
FIntVector CachedCoreDesiredCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
||||||
|
bool bHasCachedCoreDesiredTiles = false;
|
||||||
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())
|
||||||
Reference in new issue
Block a user