standalone: per-tile speed at plugin level, streaming like the game, softer lighting
Cause found by profiling: room/SDF graph-cache churn (exact XY bounds + sample order rebuilt it constantly). Region grouping (one LOD3 tile: 1,084 -> 229 builds), taller-tile traversal for LOD6 (36,480 -> 12,160), extent-dependent cache slots, lambdas instead of recursive std::function. Serial ms/tile, 64 nearest tiles per level (before -> after): L0 50.4 -> 50.6, L1 112 -> 110, L2 24 -> 22, L3 223 -> 73, L4 269 -> 134, L5 393 -> 127, L6 283 -> 145. Hashes of all levels unchanged; parity 3,436/3,436 bit-identical. Full clipmap, 16 workers BelowNormal: full detail 66.6 s, all 12,054 tiles 175 s. In-stream L0 tail is high (median 83 ms, mean 198 ms, p95 669 ms) vs 51 ms serial. Streaming: recentre keeps the intersection, generates only missing tiles nearest/finest first; one crossing = exactly 441 new tiles in 6.1 s, no stale results. Lighting: normals already come from the density gradient; the striping was the squared headlamp; broad linear fill + warmer palette. Daytime CPU rule documented in Standalone/README.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
3f300b912a
commit
81b7d6d0ff
24 files changed
+1096
-196
No files matched your search
@@ -3,6 +3,7 @@
|
||||
#include "WorldMesher.h"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <array>
|
||||
#include <unordered_set>
|
||||
|
||||
struct FStandaloneTileKeyHash
|
||||
@@ -30,6 +31,7 @@ struct FStandaloneCpuTile
|
||||
std::vector<uint32> Indices;
|
||||
uint64 DensitySamples=0;
|
||||
double WorkerSeconds=0.0;
|
||||
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
|
||||
};
|
||||
|
||||
struct FStandaloneStreamingUpdate
|
||||
@@ -40,6 +42,7 @@ struct FStandaloneStreamingUpdate
|
||||
int32 NewJobs=0;
|
||||
int32 CancelledQueuedJobs=0;
|
||||
int32 FullResTiles=0;
|
||||
std::vector<std::pair<uint64,uint64>> CancelledByCrossing;
|
||||
};
|
||||
|
||||
struct FStandaloneTileJob
|
||||
@@ -48,6 +51,20 @@ struct FStandaloneTileJob
|
||||
int32 BandZMin=MIN_int32;
|
||||
int32 BandZMax=MAX_int32;
|
||||
uint64 RequestedCrossing=0;
|
||||
double PriorityDistanceSquared=0.0;
|
||||
};
|
||||
|
||||
struct FStandaloneTileJobHeapOrder
|
||||
{
|
||||
bool operator()(const FStandaloneTileJob& A,const FStandaloneTileJob& B)const
|
||||
{
|
||||
if(A.PriorityDistanceSquared!=B.PriorityDistanceSquared)
|
||||
return A.PriorityDistanceSquared>B.PriorityDistanceSquared;
|
||||
if(A.Key.Level!=B.Key.Level)return A.Key.Level>B.Key.Level;
|
||||
if(A.Key.Coord.Z!=B.Key.Coord.Z)return A.Key.Coord.Z>B.Key.Coord.Z;
|
||||
if(A.Key.Coord.Y!=B.Key.Coord.Y)return A.Key.Coord.Y>B.Key.Coord.Y;
|
||||
return A.Key.Coord.X>B.Key.Coord.X;
|
||||
}
|
||||
};
|
||||
|
||||
namespace StandaloneWorldMesher
|
||||
@@ -99,6 +116,7 @@ public:
|
||||
explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=6)
|
||||
:World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,32))
|
||||
{
|
||||
VoxelGenLOD::InitializeConsoleSwitches();
|
||||
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
|
||||
}
|
||||
FStandaloneStreamingGenerator(const FStandaloneStreamingGenerator&)=delete;
|
||||
@@ -153,12 +171,20 @@ public:
|
||||
Desired.clear();Desired.reserve(NewDesired.Tiles.size()*2+1);
|
||||
for(const FVoxelTileKey& Key:NewDesired.Tiles)Desired.insert(Key);
|
||||
FullResTileCount=Stats.FullResTiles;
|
||||
std::unordered_map<uint64,uint64> CancelledByCrossing;
|
||||
auto QueueEnd=std::remove_if(Queue.begin(),Queue.end(),[&](const FStandaloneTileJob& Job)
|
||||
{
|
||||
if(Desired.contains(Job.Key))return false;
|
||||
Outstanding.erase(Job.Key);++Stats.CancelledQueuedJobs;return true;
|
||||
Outstanding.erase(Job.Key);++Stats.CancelledQueuedJobs;
|
||||
++CancelledByCrossing[Job.RequestedCrossing];return true;
|
||||
});
|
||||
Queue.erase(QueueEnd,Queue.end());
|
||||
Stats.CancelledByCrossing.reserve(CancelledByCrossing.size());
|
||||
for(const auto& Pair:CancelledByCrossing)Stats.CancelledByCrossing.push_back(Pair);
|
||||
|
||||
for(FStandaloneTileJob& Job:Queue)
|
||||
Job.PriorityDistanceSquared=StandaloneWorldMesher::TileDistanceSquaredChunks(
|
||||
Job.Key,NewDesired.CenterChunk);
|
||||
|
||||
const int32 BandMin=World->StrateContentCutMinLevel<9&&NewDesired.bHasStrate
|
||||
? NewDesired.StrateBottomChunkZ*CHUNK_SIZE:MIN_int32;
|
||||
@@ -167,16 +193,12 @@ public:
|
||||
for(const FVoxelTileKey& Key:NewDesired.Tiles)
|
||||
{
|
||||
if(Resident.contains(Key)||Outstanding.contains(Key))continue;
|
||||
Queue.push_back({Key,BandMin,BandMax,CrossingIndex});
|
||||
Queue.push_back({Key,BandMin,BandMax,CrossingIndex,
|
||||
StandaloneWorldMesher::TileDistanceSquaredChunks(Key,NewDesired.CenterChunk)});
|
||||
Outstanding.insert(Key);++Stats.NewJobs;
|
||||
}
|
||||
CurrentDesired=std::move(NewDesired);
|
||||
std::sort(Queue.begin(),Queue.end(),[&](const FStandaloneTileJob& A,const FStandaloneTileJob& B)
|
||||
{
|
||||
const double DA=StandaloneWorldMesher::TileDistanceSquaredChunks(A.Key,CurrentDesired.CenterChunk);
|
||||
const double DB=StandaloneWorldMesher::TileDistanceSquaredChunks(B.Key,CurrentDesired.CenterChunk);
|
||||
return DA==DB?A.Key.Level<B.Key.Level:DA<DB;
|
||||
});
|
||||
std::make_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
|
||||
Wake.notify_all();
|
||||
return Stats;
|
||||
}
|
||||
@@ -215,8 +237,21 @@ private:
|
||||
const int32 Cells=Level<World->FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World->CoarseTileCells);
|
||||
const int32 Step=FMath::Max(1,Extent/Cells);
|
||||
int64 SampleCount=0;
|
||||
const bool bProfile=VoxelDensityProfile::GetMode()!=VoxelDensityProfile::EMode::Disabled;
|
||||
const VoxelDensityProfile::FSnapshot Before=bProfile
|
||||
? VoxelDensityProfile::SnapshotCurrentThread():VoxelDensityProfile::FSnapshot{};
|
||||
FVoxelMeshData Mesh=Mesher.GenerateMesh(Job.Key.OriginVoxels(),Step,Cells,nullptr,
|
||||
Job.BandZMin,Job.BandZMax,&SampleCount);
|
||||
if(bProfile)
|
||||
{
|
||||
const VoxelDensityProfile::FSnapshot After=VoxelDensityProfile::SnapshotCurrentThread();
|
||||
for(size_t I=0;I<Out.ProfileCycles.size();++I)
|
||||
{
|
||||
const size_t Bucket=I;
|
||||
Out.ProfileCycles[I]=After.Cycles[Bucket]>=Before.Cycles[Bucket]
|
||||
?After.Cycles[Bucket]-Before.Cycles[Bucket]:0;
|
||||
}
|
||||
}
|
||||
Out.DensitySamples=static_cast<uint64>(FMath::Max<int64>(0,SampleCount));
|
||||
Out.Vertices.reserve(static_cast<size_t>(Mesh.Vertices.Num()));
|
||||
for(int32 I=0;I<Mesh.Vertices.Num();++I)
|
||||
@@ -241,7 +276,8 @@ private:
|
||||
std::unique_lock<std::mutex> Lock(Mutex);
|
||||
Wake.wait(Lock,[&](){return bStop||!Queue.empty();});
|
||||
if(bStop)return;
|
||||
Job=Queue.front();Queue.erase(Queue.begin());
|
||||
std::pop_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
|
||||
Job=Queue.back();Queue.pop_back();
|
||||
}
|
||||
FStandaloneCpuTile Tile=Generate(Job);
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user