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
305 lines
12 KiB
C++
305 lines
12 KiB
C++
#pragma once
|
|
|
|
#include "WorldMesher.h"
|
|
|
|
#include <condition_variable>
|
|
#include <array>
|
|
#include <unordered_set>
|
|
|
|
struct FStandaloneTileKeyHash
|
|
{
|
|
std::size_t operator()(const FVoxelTileKey& Key) const noexcept
|
|
{ return static_cast<std::size_t>(GetTypeHash(Key)); }
|
|
};
|
|
|
|
struct FStandaloneDesiredTileSet
|
|
{
|
|
std::vector<FVoxelTileKey> Tiles;
|
|
FIntVector CenterChunk=FIntVector::ZeroValue;
|
|
int32 MinimumChunkZ=MIN_int32;
|
|
int32 MaximumChunkZ=MAX_int32;
|
|
int32 StrateTopChunkZ=0;
|
|
int32 StrateBottomChunkZ=0;
|
|
bool bHasStrate=false;
|
|
};
|
|
|
|
struct FStandaloneCpuTile
|
|
{
|
|
FVoxelTileKey Key;
|
|
uint64 RequestedCrossing=0;
|
|
std::vector<FStandaloneCpuVertex> Vertices;
|
|
std::vector<uint32> Indices;
|
|
uint64 DensitySamples=0;
|
|
double WorkerSeconds=0.0;
|
|
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
|
|
};
|
|
|
|
struct FStandaloneStreamingUpdate
|
|
{
|
|
FIntVector CenterChunk=FIntVector::ZeroValue;
|
|
int32 DesiredTiles=0;
|
|
int32 RetainedResidentTiles=0;
|
|
int32 NewJobs=0;
|
|
int32 CancelledQueuedJobs=0;
|
|
int32 FullResTiles=0;
|
|
std::vector<std::pair<uint64,uint64>> CancelledByCrossing;
|
|
};
|
|
|
|
struct FStandaloneTileJob
|
|
{
|
|
FVoxelTileKey Key;
|
|
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
|
|
{
|
|
inline double TileDistanceSquaredChunks(const FVoxelTileKey& Tile,const FIntVector& Center)
|
|
{
|
|
const double Scale=static_cast<double>(1<<Tile.Level);
|
|
const double DX=(static_cast<double>(Tile.Coord.X)+0.5)*Scale-Center.X;
|
|
const double DY=(static_cast<double>(Tile.Coord.Y)+0.5)*Scale-Center.Y;
|
|
const double DZ=(static_cast<double>(Tile.Coord.Z)+0.5)*Scale-Center.Z;
|
|
return DX*DX+DY*DY+DZ*DZ;
|
|
}
|
|
|
|
inline FStandaloneDesiredTileSet BuildDesiredSet(
|
|
const FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels)
|
|
{
|
|
FStandaloneDesiredTileSet Desired;
|
|
Desired.CenterChunk=CameraChunk(CameraPositionVoxels);
|
|
if(World.Settings->bClampViewToStrate)
|
|
Desired.bHasStrate=World.Manager->GetStrateChunkZBounds(
|
|
Desired.CenterChunk.Z,Desired.StrateTopChunkZ,Desired.StrateBottomChunkZ);
|
|
VoxelClipmapDesiredTiles::ResolveVerticalBounds(
|
|
Desired.CenterChunk.Z,World.Settings->ViewDistanceUp,World.Settings->ViewDistanceDown,
|
|
World.Settings->StrateViewMarginChunks,World.Settings->bClampViewToStrate,
|
|
Desired.bHasStrate,Desired.StrateTopChunkZ,Desired.StrateBottomChunkZ,
|
|
Desired.MinimumChunkZ,Desired.MaximumChunkZ);
|
|
VoxelClipmapDesiredTiles::FParameters Parameters;
|
|
Parameters.ClipRadius=World.ClipRadius;
|
|
Parameters.MaxClipLevel=FMath::Clamp(World.MaxClipLevel,0,8);
|
|
Parameters.RenderDistanceChunks=World.RenderDistanceChunks;
|
|
TArray<FVoxelTileKey> Tiles;
|
|
VoxelClipmapDesiredTiles::Build(Desired.CenterChunk,Desired.MinimumChunkZ,
|
|
Desired.MaximumChunkZ,Parameters,Tiles);
|
|
Desired.Tiles.reserve(static_cast<size_t>(Tiles.Num()));
|
|
for(const FVoxelTileKey& Tile:Tiles)Desired.Tiles.push_back(Tile);
|
|
std::sort(Desired.Tiles.begin(),Desired.Tiles.end(),[&](const FVoxelTileKey& A,const FVoxelTileKey& B)
|
|
{
|
|
const double DA=TileDistanceSquaredChunks(A,Desired.CenterChunk);
|
|
const double DB=TileDistanceSquaredChunks(B,Desired.CenterChunk);
|
|
return DA==DB?A.Level<B.Level:DA<DB;
|
|
});
|
|
return Desired;
|
|
}
|
|
}
|
|
|
|
class FStandaloneStreamingGenerator
|
|
{
|
|
public:
|
|
explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=6)
|
|
:World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,32))
|
|
{
|
|
VoxelGenLOD::InitializeConsoleSwitches();
|
|
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
|
|
}
|
|
FStandaloneStreamingGenerator(const FStandaloneStreamingGenerator&)=delete;
|
|
FStandaloneStreamingGenerator& operator=(const FStandaloneStreamingGenerator&)=delete;
|
|
~FStandaloneStreamingGenerator(){Stop();}
|
|
|
|
FStandaloneStreamingUpdate Start(const FVector& CameraPositionVoxels,
|
|
const std::vector<FVoxelTileKey>& ResidentKeys={},
|
|
uint64 CrossingIndex=0)
|
|
{
|
|
Stop();
|
|
{
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
bStop=false;
|
|
}
|
|
for(int32 I=0;I<WorkerCount;++I)Workers.emplace_back([this](){WorkerLoop();});
|
|
return UpdateDesired(CameraPositionVoxels,ResidentKeys,CrossingIndex);
|
|
}
|
|
|
|
void Stop()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
bStop=true;
|
|
Queue.clear();
|
|
}
|
|
Wake.notify_all();
|
|
for(std::thread& Worker:Workers)if(Worker.joinable())Worker.join();
|
|
Workers.clear();
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
Ready.clear();Outstanding.clear();Desired.clear();
|
|
}
|
|
|
|
FStandaloneStreamingUpdate UpdateDesired(
|
|
const FVector& CameraPositionVoxels,const std::vector<FVoxelTileKey>& ResidentKeys,
|
|
uint64 CrossingIndex=0)
|
|
{
|
|
FStandaloneDesiredTileSet NewDesired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraPositionVoxels);
|
|
FStandaloneStreamingUpdate Stats;
|
|
Stats.CenterChunk=NewDesired.CenterChunk;
|
|
Stats.DesiredTiles=static_cast<int32>(NewDesired.Tiles.size());
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Resident;
|
|
Resident.reserve(ResidentKeys.size()*2+1);
|
|
for(const FVoxelTileKey& Key:ResidentKeys)Resident.insert(Key);
|
|
for(const FVoxelTileKey& Key:NewDesired.Tiles)
|
|
{
|
|
if(Key.Level<World->FullResClipLevels)++Stats.FullResTiles;
|
|
if(Resident.contains(Key))++Stats.RetainedResidentTiles;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
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;
|
|
++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;
|
|
const int32 BandMax=World->StrateContentCutMinLevel<9&&NewDesired.bHasStrate
|
|
? (NewDesired.StrateTopChunkZ+1)*CHUNK_SIZE:MAX_int32;
|
|
for(const FVoxelTileKey& Key:NewDesired.Tiles)
|
|
{
|
|
if(Resident.contains(Key)||Outstanding.contains(Key))continue;
|
|
Queue.push_back({Key,BandMin,BandMax,CrossingIndex,
|
|
StandaloneWorldMesher::TileDistanceSquaredChunks(Key,NewDesired.CenterChunk)});
|
|
Outstanding.insert(Key);++Stats.NewJobs;
|
|
}
|
|
CurrentDesired=std::move(NewDesired);
|
|
std::make_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
|
|
Wake.notify_all();
|
|
return Stats;
|
|
}
|
|
|
|
std::vector<FStandaloneCpuTile> TakeReady()
|
|
{
|
|
std::vector<FStandaloneCpuTile> Out;
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
Out.swap(Ready);
|
|
for(const FStandaloneCpuTile& Tile:Out)Outstanding.erase(Tile.Key);
|
|
return Out;
|
|
}
|
|
|
|
bool IsDesired(const FVoxelTileKey& Key)const
|
|
{std::lock_guard<std::mutex> Lock(Mutex);return Desired.contains(Key);}
|
|
int32 DesiredCount()const
|
|
{std::lock_guard<std::mutex> Lock(Mutex);return static_cast<int32>(Desired.size());}
|
|
int32 FullResDesiredCount()const
|
|
{std::lock_guard<std::mutex> Lock(Mutex);return FullResTileCount;}
|
|
FIntVector CenterChunk()const
|
|
{std::lock_guard<std::mutex> Lock(Mutex);return CurrentDesired.CenterChunk;}
|
|
int32 GetWorkerCount()const{return WorkerCount;}
|
|
|
|
private:
|
|
FStandaloneCpuTile Generate(const FStandaloneTileJob& Job)
|
|
{
|
|
const auto StartTime=FClock::now();
|
|
FStandaloneCpuTile Out;Out.Key=Job.Key;Out.RequestedCrossing=Job.RequestedCrossing;
|
|
UVoxelMarchingCubesMesher Mesher;
|
|
Mesher.SetGenerator(World->Generator.get());
|
|
Mesher.bGenerateSkirts=World->bGenerateSkirts;
|
|
Mesher.SkirtCells=static_cast<float>(World->SkirtCells);
|
|
Mesher.LODOctaveDrop=World->LODOctaveDrop;
|
|
const int32 Level=Job.Key.Level;
|
|
const int32 Extent=Job.Key.ExtentVoxels();
|
|
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)
|
|
{
|
|
const FVector& P=Mesh.Vertices[I];const FVector& N=Mesh.Normals[I];
|
|
Out.Vertices.push_back({
|
|
{static_cast<float>(P.X*0.01),static_cast<float>(P.Y*0.01),static_cast<float>(P.Z*0.01)},
|
|
{static_cast<float>(N.X),static_cast<float>(N.Y),static_cast<float>(N.Z)}});
|
|
}
|
|
Out.Indices.reserve(static_cast<size_t>(Mesh.Triangles.Num()));
|
|
for(const int32 Index:Mesh.Triangles)Out.Indices.push_back(static_cast<uint32>(Index));
|
|
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
|
|
return Out;
|
|
}
|
|
|
|
void WorkerLoop()
|
|
{
|
|
for(;;)
|
|
{
|
|
FStandaloneTileJob Job;
|
|
{
|
|
std::unique_lock<std::mutex> Lock(Mutex);
|
|
Wake.wait(Lock,[&](){return bStop||!Queue.empty();});
|
|
if(bStop)return;
|
|
std::pop_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
|
|
Job=Queue.back();Queue.pop_back();
|
|
}
|
|
FStandaloneCpuTile Tile=Generate(Job);
|
|
{
|
|
std::lock_guard<std::mutex> Lock(Mutex);
|
|
if(bStop)return;
|
|
Ready.push_back(std::move(Tile));
|
|
}
|
|
}
|
|
}
|
|
|
|
using FClock=std::chrono::steady_clock;
|
|
FStandaloneOwnerWorld* World=nullptr;
|
|
int32 WorkerCount=6;
|
|
mutable std::mutex Mutex;
|
|
std::condition_variable Wake;
|
|
bool bStop=true;
|
|
std::vector<std::thread> Workers;
|
|
std::vector<FStandaloneTileJob> Queue;
|
|
std::vector<FStandaloneCpuTile> Ready;
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Desired;
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Outstanding;
|
|
FStandaloneDesiredTileSet CurrentDesired;
|
|
int32 FullResTileCount=0;
|
|
};
|