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
@@ -552,7 +552,37 @@ public:
|
||||
}
|
||||
|
||||
void SetSchedulerPolicy(int32 Policy)
|
||||
{SchedulerPolicy=std::clamp(Policy,-1,2);}
|
||||
{SchedulerPolicy=std::clamp(Policy,-1,3);}
|
||||
void SetSchedulerMotionSpeedMetersPerSecond(double Speed)
|
||||
{
|
||||
const double ClampedSpeed=std::max(0.0,Speed);
|
||||
int32 CurrentMode=SpeedAwareSchedulerMode.load(std::memory_order_relaxed);
|
||||
for(;;)
|
||||
{
|
||||
int32 NextMode=CurrentMode;
|
||||
if(CurrentMode==0)
|
||||
{
|
||||
if(ClampedSpeed>=12.0)NextMode=2;
|
||||
else if(ClampedSpeed>=0.75)NextMode=1;
|
||||
}
|
||||
else if(CurrentMode==1)
|
||||
{
|
||||
if(ClampedSpeed>=12.0)NextMode=2;
|
||||
else if(ClampedSpeed<=0.35)NextMode=0;
|
||||
}
|
||||
else if(ClampedSpeed<=8.0)
|
||||
{
|
||||
NextMode=ClampedSpeed<=0.35?0:1;
|
||||
}
|
||||
if(NextMode==CurrentMode
|
||||
||SpeedAwareSchedulerMode.compare_exchange_weak(
|
||||
CurrentMode,NextMode,std::memory_order_relaxed))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
SchedulerMotionSpeedMetersPerSecond.store(ClampedSpeed,std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool QueuePriorityTasks(std::vector<std::function<void()>> Tasks)
|
||||
{
|
||||
@@ -800,8 +830,7 @@ private:
|
||||
for(const auto& Pair:CancelledByCrossing)Stats.CancelledByCrossing.push_back(Pair);
|
||||
|
||||
std::vector<FVoxelTileKey> Prioritized=NewDesired.Tiles;
|
||||
const int32 ActiveSchedulerPolicy=SchedulerPolicy<0
|
||||
?(IsCoarseFirstSchedulingEnabled()?2:3):SchedulerPolicy;
|
||||
const int32 ActiveSchedulerPolicy=SchedulerPolicy<0?3:SchedulerPolicy;
|
||||
if(ActiveSchedulerPolicy<=2)
|
||||
{
|
||||
// Policy 0 retains L77 coarse-first ordering after the local support ring.
|
||||
@@ -865,6 +894,84 @@ private:
|
||||
Prioritized.swap(FairOrder);
|
||||
}
|
||||
}
|
||||
else if(ActiveSchedulerPolicy==3)
|
||||
{
|
||||
std::vector<FVoxelTileKey> WalkableRing,NearTiles,FarTiles,SpeedAwareOrder;
|
||||
WalkableRing.reserve(27);
|
||||
NearTiles.reserve(Prioritized.size());
|
||||
FarTiles.reserve(Prioritized.size());
|
||||
SpeedAwareOrder.reserve(Prioritized.size());
|
||||
for(const FVoxelTileKey& Key:Prioritized)
|
||||
{
|
||||
const bool bInWalkableRing=Key.Level==0
|
||||
&&std::abs(Key.Coord.X-NewDesired.CenterChunk.X)<=1
|
||||
&&std::abs(Key.Coord.Y-NewDesired.CenterChunk.Y)<=1
|
||||
&&std::abs(Key.Coord.Z-NewDesired.CenterChunk.Z)<=1;
|
||||
if(bInWalkableRing)WalkableRing.push_back(Key);
|
||||
else if(Key.Level<=2)NearTiles.push_back(Key);
|
||||
else FarTiles.push_back(Key);
|
||||
}
|
||||
const auto IsAhead=[&](const FVoxelTileKey& Key)
|
||||
{
|
||||
if(TravelDirection.X*TravelDirection.X+TravelDirection.Y*TravelDirection.Y<=1.0e-12f)
|
||||
return false;
|
||||
const double Scale=static_cast<double>(1<<Key.Level);
|
||||
const double OffsetX=(static_cast<double>(Key.Coord.X)+0.5)*Scale-NewDesired.CenterChunk.X;
|
||||
const double OffsetY=(static_cast<double>(Key.Coord.Y)+0.5)*Scale-NewDesired.CenterChunk.Y;
|
||||
return OffsetX*TravelDirection.X+OffsetY*TravelDirection.Y>0.0;
|
||||
};
|
||||
auto SortGroup=[&](std::vector<FVoxelTileKey>& Group,bool bNearFirst)
|
||||
{
|
||||
std::sort(Group.begin(),Group.end(),[&](const FVoxelTileKey& A,const FVoxelTileKey& B)
|
||||
{
|
||||
if(A.Level!=B.Level)return bNearFirst?A.Level<B.Level:A.Level>B.Level;
|
||||
const bool bAheadA=IsAhead(A),bAheadB=IsAhead(B);
|
||||
if(bAheadA!=bAheadB)return bAheadA;
|
||||
const double DA=StandaloneWorldMesher::TileDistanceSquaredChunks(A,NewDesired.CenterChunk);
|
||||
const double DB=StandaloneWorldMesher::TileDistanceSquaredChunks(B,NewDesired.CenterChunk);
|
||||
if(DA!=DB)return DA<DB;
|
||||
if(A.Coord.Z!=B.Coord.Z)return A.Coord.Z<B.Coord.Z;
|
||||
if(A.Coord.Y!=B.Coord.Y)return A.Coord.Y<B.Coord.Y;
|
||||
return A.Coord.X<B.Coord.X;
|
||||
});
|
||||
};
|
||||
SortGroup(WalkableRing,true);
|
||||
SortGroup(NearTiles,true);
|
||||
SortGroup(FarTiles,false);
|
||||
SpeedAwareOrder.insert(SpeedAwareOrder.end(),WalkableRing.begin(),WalkableRing.end());
|
||||
const double SpeedMetersPerSecond=SchedulerMotionSpeedMetersPerSecond.load(
|
||||
std::memory_order_relaxed);
|
||||
const int32 SpeedAwareMode=SpeedAwareSchedulerMode.load(std::memory_order_relaxed);
|
||||
if(SpeedAwareMode==2)
|
||||
{
|
||||
// Fast travel keeps parent coverage ahead of detail work.
|
||||
SpeedAwareOrder.insert(SpeedAwareOrder.end(),FarTiles.begin(),FarTiles.end());
|
||||
SpeedAwareOrder.insert(SpeedAwareOrder.end(),NearTiles.begin(),NearTiles.end());
|
||||
}
|
||||
else if(SpeedAwareMode==1)
|
||||
{
|
||||
// Walking reserves a speed-scaled share for the horizon after the support ring.
|
||||
const size_t NearPerFar=static_cast<size_t>(std::clamp(
|
||||
static_cast<int>(std::lround(6.0/(1.0+SpeedMetersPerSecond/4.0))),1,4));
|
||||
size_t NearIndex=0,FarIndex=0;
|
||||
while(NearIndex<NearTiles.size())
|
||||
{
|
||||
const size_t NearEnd=std::min(NearTiles.size(),NearIndex+NearPerFar);
|
||||
SpeedAwareOrder.insert(SpeedAwareOrder.end(),
|
||||
NearTiles.begin()+NearIndex,NearTiles.begin()+NearEnd);
|
||||
NearIndex=NearEnd;
|
||||
if(FarIndex<FarTiles.size())SpeedAwareOrder.push_back(FarTiles[FarIndex++]);
|
||||
}
|
||||
SpeedAwareOrder.insert(SpeedAwareOrder.end(),FarTiles.begin()+FarIndex,FarTiles.end());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Arrival and dwell finish near LOD0-2 before low-priority horizon work.
|
||||
SpeedAwareOrder.insert(SpeedAwareOrder.end(),NearTiles.begin(),NearTiles.end());
|
||||
SpeedAwareOrder.insert(SpeedAwareOrder.end(),FarTiles.begin(),FarTiles.end());
|
||||
}
|
||||
Prioritized.swap(SpeedAwareOrder);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::vector<VoxelForgeCore::TileKey> Unordered;Unordered.reserve(NewDesired.Tiles.size());
|
||||
@@ -1084,8 +1191,11 @@ private:
|
||||
std::unique_lock<std::mutex> Lock(Mutex);
|
||||
const auto HasEligibleTile=[&]()
|
||||
{
|
||||
return !Queue.empty()&&(SchedulerPolicy!=1||NearOutstandingJobs==0
|
||||
||Queue.front().Key.Level<=2);
|
||||
const bool bStationaryNearGate=SchedulerPolicy==3
|
||||
&&SpeedAwareSchedulerMode.load(std::memory_order_relaxed)==0
|
||||
&&NearOutstandingJobs>0;
|
||||
return !Queue.empty()&&((SchedulerPolicy!=1&&!bStationaryNearGate)
|
||||
||NearOutstandingJobs==0||Queue.front().Key.Level<=2);
|
||||
};
|
||||
Wake.wait(Lock,[&](){return bStop||!PriorityTasks.empty()||HasEligibleTile();});
|
||||
if(bStop)return;
|
||||
@@ -1250,6 +1360,9 @@ private:
|
||||
bool bHasLastCameraPosition=false;
|
||||
int32 WorkerCount=8;
|
||||
int32 SchedulerPolicy=-1;
|
||||
std::atomic<double> SchedulerMotionSpeedMetersPerSecond{0.0};
|
||||
// Match Unreal's Schmitt thresholds to keep noisy speed samples from flapping modes.
|
||||
std::atomic<int32> SpeedAwareSchedulerMode{0};
|
||||
bool bHashDensity=false;
|
||||
const FStandaloneMeshCache* MeshCache=nullptr;
|
||||
int32 MaxLevelFilter=-1;
|
||||
|
||||
Reference in new issue
Block a user