streaming: speed-aware scheduler with hysteresis (L80); arrival near-first, fast travel keeps the horizon
Policy 3 (headless + Unreal): walkable ring always first; at rest/arrival LOD0-2 before far; walking interleaves near:far ~4:1 toward heading; fast travel admits far coarse tiles right after the walkable ring. Hysteresis: walking 0.75/0.35 m/s, fast 12/8 m/s. Headless, far holes while moving 30 m/s: L77 9.0% / L79 77.2% / L80 8.9%. Cold LOD0-2 ready (one clean protocol, medians): SW 1.30, FI 3.61, TN 3.07, VS 1.43 s. Unreal 30 m/s traces end 2,233/2,237 loaded (commandlet readiness exit 1) for all policies. Passage zero-padding cut: ~50% fewer passage iterations on active tiles, max 6.6 cm surface displacement, no sign flips; not adopted yet (padding sweep next). World unchanged (v213). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
2c8a26c0d0
commit
b06397df9c
10 files changed
+830
-25
No files matched your search
@@ -208,11 +208,11 @@ namespace
|
||||
GVoxelForgeStreamingCoarseFirst,
|
||||
TEXT("Prioritize walkable tiles, then finish LOD0-2 before far clipmap work; controls far-level order."));
|
||||
|
||||
int32 GVoxelForgeStreamingSchedulerPolicy = 2;
|
||||
int32 GVoxelForgeStreamingSchedulerPolicy = 3;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingSchedulerPolicy(
|
||||
TEXT("voxel.StreamingSchedulerPolicy"),
|
||||
GVoxelForgeStreamingSchedulerPolicy,
|
||||
TEXT("Streaming order: 0=L77 coarse-first, 1=L78 strict near-first, 2=L79 fair far share."));
|
||||
TEXT("Streaming order: 0=L77 coarse-first, 1=L78 strict near-first, 2=L79 fair share, 3=L80 speed-aware."));
|
||||
|
||||
int32 GVoxelForgeStreamingDynamicNearReserve = 1;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingDynamicNearReserve(
|
||||
@@ -2931,6 +2931,7 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
||||
HeadlessStreamingTestDistanceCm = 0.0;
|
||||
HeadlessStreamingTestSpeedCmPerSecond = 0.0f;
|
||||
HeadlessStreamingTestStartSeconds = 0.0f;
|
||||
HeadlessStreamingTestMoveDurationSeconds = 0.0f;
|
||||
HeadlessStreamingTestExitSeconds = 0.0f;
|
||||
HeadlessStreamingTestTeleportAtSeconds = 0.0f;
|
||||
HeadlessStreamingTestTeleportDistanceCm = 0.0f;
|
||||
@@ -3140,10 +3141,12 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
||||
|| FParse::Param(CommandLine, TEXT("voxel.TestMove"));
|
||||
float Speed = 800.0f;
|
||||
float StartSeconds = 20.0f;
|
||||
float MoveDurationSeconds = 0.0f;
|
||||
float ExitSeconds = 0.0f;
|
||||
int32 ExitOnSteadyValue = 0;
|
||||
FParse::Value(CommandLine, TEXT("voxel.TestMoveSpeedCmPerSecond="), Speed);
|
||||
FParse::Value(CommandLine, TEXT("voxel.TestMoveStartSeconds="), StartSeconds);
|
||||
FParse::Value(CommandLine, TEXT("voxel.TestMoveDurationSeconds="), MoveDurationSeconds);
|
||||
FParse::Value(CommandLine, TEXT("voxel.TestExitSeconds="), ExitSeconds);
|
||||
FParse::Value(CommandLine, TEXT("voxel.TestTeleportAfterSeconds="),
|
||||
HeadlessStreamingTestTeleportAtSeconds);
|
||||
@@ -3313,6 +3316,7 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
||||
}
|
||||
HeadlessStreamingTestSpeedCmPerSecond = FMath::Max(Speed, 0.0f);
|
||||
HeadlessStreamingTestStartSeconds = FMath::Max(StartSeconds, 0.0f);
|
||||
HeadlessStreamingTestMoveDurationSeconds = FMath::Max(MoveDurationSeconds, 0.0f);
|
||||
bHeadlessStreamingTestMovement = HeadlessStreamingTestSpeedCmPerSecond > 0.0f;
|
||||
}
|
||||
|
||||
@@ -3323,10 +3327,11 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
||||
HeadlessStreamingTestBeginSeconds = FPlatformTime::Seconds();
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStreamingTest] clean=1 movement=%d speed_cm_s=%.3f start_s=%.3f "
|
||||
"exit_s=%.3f direction=(%.3f,%.3f,%.3f)"),
|
||||
"move_duration_s=%.3f exit_s=%.3f direction=(%.3f,%.3f,%.3f)"),
|
||||
bHeadlessStreamingTestMovement ? 1 : 0,
|
||||
HeadlessStreamingTestSpeedCmPerSecond,
|
||||
HeadlessStreamingTestStartSeconds,
|
||||
HeadlessStreamingTestMoveDurationSeconds,
|
||||
HeadlessStreamingTestExitSeconds,
|
||||
HeadlessStreamingTestDirection.X,
|
||||
HeadlessStreamingTestDirection.Y,
|
||||
@@ -4394,6 +4399,26 @@ void AVoxelWorld::AdvanceHeadlessStreamingTest(
|
||||
return;
|
||||
}
|
||||
|
||||
if (HeadlessStreamingTestMoveDurationSeconds > 0.0f
|
||||
&& ElapsedSeconds - static_cast<double>(HeadlessStreamingTestStartSeconds)
|
||||
>= static_cast<double>(HeadlessStreamingTestMoveDurationSeconds))
|
||||
{
|
||||
bHeadlessStreamingTestMovement = false;
|
||||
InOutPlayerHeading = FVector::ZeroVector;
|
||||
HeadlessStreamingTestLastElapsedSeconds = ElapsedSeconds;
|
||||
if (VoxelForgeStartupTrace::IsActive())
|
||||
{
|
||||
VoxelForgeStartupTrace::RecordEvent(TEXT("streaming_test_move_stopped"),
|
||||
FString::Printf(TEXT("\"move_duration_s\":%s,\"elapsed_s\":%s"),
|
||||
*FString::SanitizeFloat(HeadlessStreamingTestMoveDurationSeconds),
|
||||
*FString::SanitizeFloat(ElapsedSeconds)));
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStreamingTest] movement stopped after %.3f s"),
|
||||
HeadlessStreamingTestMoveDurationSeconds);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bHeadlessStreamingTestTeleportDone
|
||||
&& HeadlessStreamingTestTeleportAtSeconds > 0.0f
|
||||
&& ElapsedSeconds - static_cast<double>(HeadlessStreamingTestStartSeconds)
|
||||
@@ -11941,7 +11966,35 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
RecordSchedulerSkip(TEXT("core_region_compile_pending"));
|
||||
return;
|
||||
}
|
||||
const bool bPawnMoving = PlayerHeading.Size() >= 50.0f;
|
||||
const int32 SchedulerPolicy = FMath::Clamp(GVoxelForgeStreamingSchedulerPolicy, 0, 3);
|
||||
const float SchedulerSpeedCmPerSecond = PlayerHeading.Size();
|
||||
if (SchedulerPolicy == 3)
|
||||
{
|
||||
// Schmitt thresholds keep a noisy velocity estimate from switching modes every frame.
|
||||
if (!bSpeedAwareTravelMode && SchedulerSpeedCmPerSecond >= 75.0f)
|
||||
{
|
||||
bSpeedAwareTravelMode = true;
|
||||
}
|
||||
else if (bSpeedAwareTravelMode && SchedulerSpeedCmPerSecond <= 35.0f)
|
||||
{
|
||||
bSpeedAwareTravelMode = false;
|
||||
}
|
||||
if (!bSpeedAwareFastMode && SchedulerSpeedCmPerSecond >= 1200.0f)
|
||||
{
|
||||
bSpeedAwareFastMode = true;
|
||||
}
|
||||
else if (bSpeedAwareFastMode && SchedulerSpeedCmPerSecond <= 800.0f)
|
||||
{
|
||||
bSpeedAwareFastMode = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bSpeedAwareTravelMode = false;
|
||||
bSpeedAwareFastMode = false;
|
||||
}
|
||||
const bool bPawnMoving = SchedulerPolicy == 3
|
||||
? bSpeedAwareTravelMode : SchedulerSpeedCmPerSecond >= 50.0f;
|
||||
bStreamingPlayerMoving = bPawnMoving;
|
||||
// Match the near reserve to the added far queue so queued horizon tiles cannot spend that headroom.
|
||||
// Aligner la reserve proche sur la file lointaine pour que l'horizon ne consomme pas cette marge.
|
||||
@@ -11955,7 +12008,6 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
const int32 NearAdmissionCap = bPawnMoving ? MaxTasks : TotalAdmissionCap;
|
||||
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve) + ReadyQueueDepth;
|
||||
constexpr int32 FirstFarLevel = 3;
|
||||
const int32 SchedulerPolicy = FMath::Clamp(GVoxelForgeStreamingSchedulerPolicy, 0, 2);
|
||||
const int32 FirstCoarseLevel = FMath::Max(
|
||||
2, Settings ? Settings->FullResClipLevels : 2);
|
||||
const bool bUseCoarseFirstOrder = GVoxelForgeStreamingCoarseFirst != 0
|
||||
@@ -12458,10 +12510,15 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
return true;
|
||||
};
|
||||
|
||||
// Policy 0 reconstructs L77's coarse-first admission order for the moving-camera control.
|
||||
if (SchedulerPolicy == 0)
|
||||
// Policy 0 reconstructs L77. Policy 3 enters that order only after sustained fast travel.
|
||||
const bool bPolicy3FastTravel = SchedulerPolicy == 3 && bSpeedAwareFastMode;
|
||||
if (SchedulerPolicy == 0 || bPolicy3FastTravel)
|
||||
{
|
||||
if (bUseCoarseFirstOrder)
|
||||
const bool bPolicy3MayAdmit = !bPolicy3FastTravel
|
||||
|| (bWalkableRingReadyForReserve && AreCriticalTilesReady());
|
||||
const bool bCoarseFirstForPolicy = bPolicy3FastTravel
|
||||
? bWalkableRingReadyForReserve : bUseCoarseFirstOrder;
|
||||
if (bPolicy3MayAdmit && bCoarseFirstForPolicy)
|
||||
{
|
||||
for (int32 Level = MaxDesiredLevel; Level >= FirstCoarseLevel; --Level)
|
||||
{
|
||||
@@ -12477,7 +12534,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
true);
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (bPolicy3MayAdmit)
|
||||
{
|
||||
SubmitLevel(0, EVoxelTileWorkPriority::LOD0, NearAdmissionCap, false);
|
||||
for (int32 Level = 1; Level <= MaxDesiredLevel; ++Level)
|
||||
@@ -12488,6 +12545,63 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (SchedulerPolicy == 3)
|
||||
{
|
||||
// The collision support ring stays first. At rest, finish LOD0-2 before
|
||||
// admitting low-priority horizon work. While walking, reserve a speed-scaled
|
||||
// fraction of available task slots for far parents on every update.
|
||||
const bool bNearAdmissionReady = AreCriticalTilesReady()
|
||||
&& bWalkableRingReadyForReserve;
|
||||
if (bNearAdmissionReady)
|
||||
{
|
||||
if (!bSpeedAwareTravelMode)
|
||||
{
|
||||
if (!AreNearLODTilesReady())
|
||||
{
|
||||
for (int32 Level = 0; Level <= 2 && Level <= MaxDesiredLevel; ++Level)
|
||||
{
|
||||
const EVoxelTileWorkPriority Priority = Level == 0
|
||||
? EVoxelTileWorkPriority::LOD0 : EVoxelTileWorkPriority::Horizon;
|
||||
SubmitLevel(Level, Priority, NearAdmissionCap, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int32 Level = FirstFarLevel; Level <= MaxDesiredLevel; ++Level)
|
||||
{
|
||||
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
|
||||
CoarseTaskCap, true);
|
||||
if (GetGenerationAdmissionCount() >= CoarseTaskCap) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const int32 ShareAdmissionCap = FMath::Max(NearAdmissionCap, CoarseTaskCap);
|
||||
const int32 StartingAdmissions = GetGenerationAdmissionCount();
|
||||
const int32 AvailableAdmissions = FMath::Max(
|
||||
0, ShareAdmissionCap - StartingAdmissions);
|
||||
const double SpeedMetersPerSecond = SchedulerSpeedCmPerSecond / 100.0;
|
||||
const int32 NearPerFar = FMath::Clamp(
|
||||
FMath::RoundToInt(6.0 / (1.0 + SpeedMetersPerSecond / 4.0)), 1, 4);
|
||||
const int32 NearShareAdmissions =
|
||||
(AvailableAdmissions * NearPerFar) / (NearPerFar + 1);
|
||||
const int32 NearShareCap = StartingAdmissions + NearShareAdmissions;
|
||||
for (int32 Level = 0; Level <= 2 && Level <= MaxDesiredLevel; ++Level)
|
||||
{
|
||||
const EVoxelTileWorkPriority Priority = Level == 0
|
||||
? EVoxelTileWorkPriority::LOD0 : EVoxelTileWorkPriority::Horizon;
|
||||
SubmitLevel(Level, Priority, NearShareCap, true);
|
||||
}
|
||||
for (int32 Level = MaxDesiredLevel; Level >= FirstFarLevel; --Level)
|
||||
{
|
||||
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
|
||||
ShareAdmissionCap, true);
|
||||
if (GetGenerationAdmissionCount() >= ShareAdmissionCap) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const bool bNearAdmissionReady = AreCriticalTilesReady()
|
||||
@@ -12587,16 +12701,35 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
{
|
||||
int32 DesiredByLevel[9] = {};
|
||||
int32 ResidentByLevel[9] = {};
|
||||
int32 CoveredByLevel[9] = {};
|
||||
int32 PendingByLevel[9] = {};
|
||||
int32 TraceMaxDesiredLevel = 0;
|
||||
for (const FVoxelTileKey& Tile : DesiredSorted)
|
||||
{
|
||||
TraceMaxDesiredLevel = FMath::Max(TraceMaxDesiredLevel, Tile.Level);
|
||||
}
|
||||
for (const FVoxelTileKey& Tile : DesiredSorted)
|
||||
{
|
||||
if (Tile.Level < 0 || Tile.Level > 8) continue;
|
||||
++DesiredByLevel[Tile.Level];
|
||||
if (LoadedTiles.Contains(Tile)) ++ResidentByLevel[Tile.Level];
|
||||
if (PendingTiles.Contains(Tile)) ++PendingByLevel[Tile.Level];
|
||||
bool bCovered = LoadedTiles.Contains(Tile);
|
||||
for (int32 AncestorLevel = Tile.Level + 1;
|
||||
!bCovered && AncestorLevel <= TraceMaxDesiredLevel && AncestorLevel <= 8;
|
||||
++AncestorLevel)
|
||||
{
|
||||
const int32 Divisor = VF_ClipLevelScale(AncestorLevel - Tile.Level);
|
||||
const FVoxelTileKey Ancestor(
|
||||
VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord, Divisor),
|
||||
AncestorLevel);
|
||||
bCovered = LoadedTiles.Contains(Ancestor);
|
||||
}
|
||||
if (bCovered) ++CoveredByLevel[Tile.Level];
|
||||
}
|
||||
FString DesiredCounts = TEXT("[");
|
||||
FString ResidentCounts = TEXT("[");
|
||||
FString CoveredCounts = TEXT("[");
|
||||
FString PendingCounts = TEXT("[");
|
||||
for (int32 Level = 0; Level <= 8; ++Level)
|
||||
{
|
||||
@@ -12604,14 +12737,17 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
{
|
||||
DesiredCounts += TEXT(",");
|
||||
ResidentCounts += TEXT(",");
|
||||
CoveredCounts += TEXT(",");
|
||||
PendingCounts += TEXT(",");
|
||||
}
|
||||
DesiredCounts += FString::FromInt(DesiredByLevel[Level]);
|
||||
ResidentCounts += FString::FromInt(ResidentByLevel[Level]);
|
||||
CoveredCounts += FString::FromInt(CoveredByLevel[Level]);
|
||||
PendingCounts += FString::FromInt(PendingByLevel[Level]);
|
||||
}
|
||||
DesiredCounts += TEXT("]");
|
||||
ResidentCounts += TEXT("]");
|
||||
CoveredCounts += TEXT("]");
|
||||
PendingCounts += TEXT("]");
|
||||
const FVector LocalPawn = WorldToLocalCm(CenterPosition);
|
||||
const double StreamingUpdateMilliseconds = (TraceNow - StreamingUpdateStartSeconds) * 1000.0;
|
||||
@@ -12619,6 +12755,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
TEXT("\"scope\":\"desired_residency_not_visual_enclosure\",\"center\":[%d,%d,%d],"
|
||||
"\"position_cm\":[%s,%s,%s],\"speed_cm_s\":%s,\"walkable_ready\":%d,"
|
||||
"\"desired_by_lod\":%s,\"resident_by_lod\":%s,\"pending_by_lod\":%s,"
|
||||
"\"covered_by_lod\":%s,"
|
||||
"\"active_generation\":%d,\"admission_occupancy\":%d,"
|
||||
"\"task_cap\":%d,\"near_reserve\":%d,"
|
||||
"\"coarse_first\":%d,\"dynamic_near_reserve\":%d,"
|
||||
@@ -12629,7 +12766,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
*FString::SanitizeFloat(LocalPawn.Z),
|
||||
*FString::SanitizeFloat(PlayerHeading.Size()),
|
||||
bWalkableRingReadyForReserve ? 1 : 0,
|
||||
*DesiredCounts, *ResidentCounts, *PendingCounts,
|
||||
*DesiredCounts, *ResidentCounts, *PendingCounts, *CoveredCounts,
|
||||
GetActiveGenerationTaskCount(), GetGenerationAdmissionCount(), MaxTasks, NearTaskReserve,
|
||||
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
|
||||
GVoxelForgeStreamingLegacyPendingResultCap,
|
||||
|
||||
@@ -1240,6 +1240,8 @@ public:
|
||||
bool bStartupApplyConfigTraced = false;
|
||||
uint64 LastApplyFrameCycles = 0;
|
||||
float StartupTraceSpeedCmPerSecond = 0.0f;
|
||||
bool bSpeedAwareTravelMode = false;
|
||||
bool bSpeedAwareFastMode = false;
|
||||
TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
|
||||
TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile
|
||||
// Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this
|
||||
@@ -1347,6 +1349,7 @@ public:
|
||||
double HeadlessStreamingTestDistanceCm = 0.0;
|
||||
float HeadlessStreamingTestSpeedCmPerSecond = 0.0f;
|
||||
float HeadlessStreamingTestStartSeconds = 0.0f;
|
||||
float HeadlessStreamingTestMoveDurationSeconds = 0.0f;
|
||||
float HeadlessStreamingTestExitSeconds = 0.0f;
|
||||
float HeadlessStreamingTestTeleportAtSeconds = 0.0f;
|
||||
float HeadlessStreamingTestTeleportDistanceCm = 0.0f;
|
||||
|
||||
Reference in new issue
Block a user