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:
Fr0zkaandClaude Opus 5.5 committed 2026-10-27 21:39:21 +01:00
1 parent 2c8a26c0d0
commit b06397df9c
10 files changed
+830 -25

No files matched your search

+147 -10
View File
@@ -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,
+3
View File
@@ -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;