Files
VoxelForge/Standalone/Source/StreamingWorld.h
T
Fr0zkaandClaude Opus 5.5 ca85e876ad feat(core): ore vein records and a materialAt query, off by default (L50); research R18, R19
Versioned capsule vein records (stable ID, resource ID, endpoints, radius, seeded irregularity) resolved
into the snapshot only when requested, placed beside the route and away from passages, landings and
landmarks (forced-passage control rejected). The bound field table gains materialAt (host + two resource
IDs + Q16 weights) behind a per-tile BVH; mesh export v2 carries per-vertex materials; a measurement-only
yield computes removed resource volume (0 on already-air space). With veins on, every density, mesh,
payload and topology hash is unchanged; demo off: owner hashes identical to L47. fast/medium/full PASS.
Research R18 (cavern-graph planner) and R19 (why FloatingIslands costs ~10x SurfaceWorld).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-24 18:25:03 +02:00

1032 lines
48 KiB
C++

#pragma once
#include "WorldMesher.h"
#include "StandaloneWorkerThread.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include <condition_variable>
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <deque>
#include <functional>
#include <fstream>
#include <limits>
#include <unordered_set>
#include <utility>
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;
std::vector<uint8> TriangleSkirtFaceMasks;
uint32 SurfaceVertexCount=0;
uint32 SkirtTriangleCount=0;
uint64 DensitySamples=0;
double WorkerSeconds=0.0;
std::chrono::steady_clock::time_point QueuedAt{};
std::chrono::steady_clock::time_point StartedAt{};
std::chrono::steady_clock::time_point CompletedAt{};
uint64 DensityHash=14695981039346656037ull;
bool bHasDensityHash=false;
uint64 MeshHash=14695981039346656037ull;
bool bHasMeshHash=false;
uint64 PayloadHash=14695981039346656037ull;
bool bHasPayloadHash=false;
uint64 TopologyHash=14695981039346656037ull;
bool bHasTopologyHash=false;
uint64 MaterialHash=14695981039346656037ull;
bool bHasMaterialHash=false;
bool bGenerationFailed=false;
uint64 MeshPayloadAllocatedBytes=0;
VoxelForgeCore::TileGenerationStats CoreDiagnostics{};
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
};
inline uint64 HashStandaloneMeshPayload(const FVoxelMeshData& Mesh)
{
uint64 Hash=14695981039346656037ull;
const auto AddWord=[&](uint64 Word,int32 Bytes)
{
for(int32 I=0;I<Bytes;++I)
{
Hash^=static_cast<uint8>(Word>>(I*8));
Hash*=1099511628211ull;
}
};
const auto AddDouble=[&](double Value)
{ AddWord(std::bit_cast<uint64>(Value),8); };
AddWord(static_cast<uint64>(Mesh.Vertices.Num()),8);
for(const FVector& Position:Mesh.Vertices)
{ AddDouble(Position.X);AddDouble(Position.Y);AddDouble(Position.Z); }
AddWord(static_cast<uint64>(Mesh.Normals.Num()),8);
for(const FVector& Normal:Mesh.Normals)
{ AddDouble(Normal.X);AddDouble(Normal.Y);AddDouble(Normal.Z); }
AddWord(static_cast<uint64>(Mesh.UVs.Num()),8);
for(const auto& UV:Mesh.UVs)
{ AddDouble(static_cast<double>(UV.X));AddDouble(static_cast<double>(UV.Y)); }
AddWord(static_cast<uint64>(Mesh.Colors.Num()),8);
for(const FColor& Color:Mesh.Colors)
{ AddWord(Color.R,1);AddWord(Color.G,1);AddWord(Color.B,1);AddWord(Color.A,1); }
AddWord(static_cast<uint64>(Mesh.Triangles.Num()),8);
for(const int32 Index:Mesh.Triangles)AddWord(static_cast<uint32>(Index),4);
AddWord(static_cast<uint64>(Mesh.TriangleSkirtFaceMasks.Num()),8);
for(const uint8 Faces:Mesh.TriangleSkirtFaceMasks)AddWord(Faces,1);
AddWord(static_cast<uint32>(Mesh.NumSurfaceVertices),4);
AddWord(static_cast<uint32>(Mesh.NumSkirtTriangles),4);
AddWord(static_cast<uint32>(Mesh.NumCeilingTriangles),4);
return Hash;
}
inline uint64 HashStandaloneMeshTopology(const FVoxelMeshData& Mesh)
{
uint64 Hash=14695981039346656037ull;
const auto AddWord=[&](uint64 Word,int32 Bytes)
{
for(int32 I=0;I<Bytes;++I)
{
Hash^=static_cast<uint8>(Word>>(I*8));
Hash*=1099511628211ull;
}
};
using FTriangleKey=std::array<uint64,3>;
const auto VertexKey=[&](int32 Index)
{
uint64 VertexHash=14695981039346656037ull;
const auto AddVertexWord=[&](uint64 Word)
{
for(int32 Byte=0;Byte<8;++Byte)
{
VertexHash^=static_cast<uint8>(Word>>(Byte*8));
VertexHash*=1099511628211ull;
}
};
if(Index<0||Index>=Mesh.Vertices.Num()||Index>=Mesh.Normals.Num())
{ AddVertexWord(UINT64_MAX);return VertexHash; }
const FVector& Position=Mesh.Vertices[Index];
const FVector& Normal=Mesh.Normals[Index];
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Position.X)));
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Position.Y)));
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Position.Z)));
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Normal.X)));
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Normal.Y)));
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Normal.Z)));
return VertexHash;
};
std::vector<FTriangleKey> Triangles;
Triangles.reserve(static_cast<size_t>(Mesh.Triangles.Num()/3));
for(int32 I=0;I+2<Mesh.Triangles.Num();I+=3)
Triangles.push_back({VertexKey(Mesh.Triangles[I]),
VertexKey(Mesh.Triangles[I+1]),VertexKey(Mesh.Triangles[I+2])});
std::sort(Triangles.begin(),Triangles.end());
AddWord(static_cast<uint64>(Triangles.size()),8);
for(const FTriangleKey& Triangle:Triangles)
for(const uint64 VertexHash:Triangle)AddWord(VertexHash,8);
return Hash;
}
inline uint64 HashStandaloneCpuTileMesh(const FStandaloneCpuTile& Tile)
{
uint64 Hash=14695981039346656037ull;
const auto AddWord=[&](uint64 Word,int32 Bytes)
{
for(int32 I=0;I<Bytes;++I)
{
Hash^=static_cast<uint8>(Word>>(I*8));
Hash*=1099511628211ull;
}
};
AddWord(Tile.Vertices.size(),8);
for(const FStandaloneCpuVertex& Vertex:Tile.Vertices)
{
for(const float Value:Vertex.Position)AddWord(std::bit_cast<uint32>(Value),4);
for(const float Value:Vertex.Normal)AddWord(std::bit_cast<uint32>(Value),4);
}
AddWord(Tile.Indices.size(),8);
for(const uint32 Index:Tile.Indices)AddWord(Index,4);
return Hash;
}
// Far parent repairs and shell stitching are implemented by VoxelForgeCore.
struct FStandaloneMeshCacheMetadata
{
uint64 SourceDataHash=0;
int32 Seed=0;
int32 ClipRadius=3;
int32 FullResClipLevels=2;
int32 CoarseTileCells=16;
int32 StrateContentCutMinLevel=9;
int32 SkirtCells=0;
int32 LODOctaveDrop=0;
int32 bGenerateSkirts=0;
uint32 AlgorithmVersion=1;
uint32 TunnelMode=0;
uint32 VoxelSizeCm=25;
};
class FStandaloneMeshCache
{
public:
static constexpr uint32 CurrentVersion=1;
void Clear(){Tiles.clear();bLoaded=false;BytesLoaded=0;}
bool IsLoaded()const{return bLoaded;}
size_t Num()const{return Tiles.size();}
uint64 GetBytesLoaded()const{return BytesLoaded;}
const FStandaloneCpuTile* Find(const FVoxelTileKey& Key)const
{
const auto It=Tiles.find(Key);
return It==Tiles.end()?nullptr:&It->second;
}
static bool Save(const std::string& Path,const FStandaloneMeshCacheMetadata& Metadata,
std::vector<FStandaloneCpuTile>& InTiles,std::string& Error)
{
std::vector<FStandaloneCpuTile*> Ordered;
Ordered.reserve(InTiles.size());
for(FStandaloneCpuTile& Tile:InTiles)Ordered.push_back(&Tile);
std::sort(Ordered.begin(),Ordered.end(),[](const FStandaloneCpuTile* A,const FStandaloneCpuTile* B)
{
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;
});
std::ofstream File(Path,std::ios::binary|std::ios::trunc);
if(!File){Error="could not open mesh cache for writing: "+Path;return false;}
struct FHeader
{
char Magic[8];uint32 Version;uint32 Endian;uint64 SourceDataHash;
int32 Seed,ClipRadius,FullResClipLevels,CoarseTileCells,StrateContentCutMinLevel;
int32 SkirtCells,LODOctaveDrop,bGenerateSkirts;
uint32 AlgorithmVersion,TunnelMode,VoxelSizeCm;
uint64 TileCount;
};
static_assert(sizeof(FHeader)==80);
const FHeader Header{{'V','F','M','E','S','H','1','\0'},1,0x01020304u,
Metadata.SourceDataHash,Metadata.Seed,Metadata.ClipRadius,Metadata.FullResClipLevels,
Metadata.CoarseTileCells,Metadata.StrateContentCutMinLevel,Metadata.SkirtCells,
Metadata.LODOctaveDrop,Metadata.bGenerateSkirts,Metadata.AlgorithmVersion,
Metadata.TunnelMode,Metadata.VoxelSizeCm,static_cast<uint64>(Ordered.size())};
File.write(reinterpret_cast<const char*>(&Header),sizeof(Header));
struct FRecord
{
int32 Level,X,Y,Z;uint64 Samples,DensityHash;uint32 VertexCount,IndexCount;
};
static_assert(sizeof(FRecord)==40);
for(const FStandaloneCpuTile* Tile:Ordered)
{
if(Tile->Vertices.size()>UINT32_MAX||Tile->Indices.size()>UINT32_MAX
||Tile->Indices.size()%3!=0)
{Error="mesh cache tile exceeds the 32-bit record limits";return false;}
const FRecord Record{Tile->Key.Level,Tile->Key.Coord.X,Tile->Key.Coord.Y,
Tile->Key.Coord.Z,Tile->DensitySamples,Tile->DensityHash,
static_cast<uint32>(Tile->Vertices.size()),static_cast<uint32>(Tile->Indices.size())};
File.write(reinterpret_cast<const char*>(&Record),sizeof(Record));
if(!Tile->Vertices.empty())File.write(reinterpret_cast<const char*>(Tile->Vertices.data()),
static_cast<std::streamsize>(Tile->Vertices.size()*sizeof(FStandaloneCpuVertex)));
if(!Tile->Indices.empty())File.write(reinterpret_cast<const char*>(Tile->Indices.data()),
static_cast<std::streamsize>(Tile->Indices.size()*sizeof(uint32)));
if(!File){Error="mesh cache write failed: "+Path;return false;}
}
File.flush();
if(!File){Error="mesh cache flush failed: "+Path;return false;}
return true;
}
bool Load(const std::string& Path,const FStandaloneMeshCacheMetadata& Expected,std::string& Error)
{
Clear();
std::ifstream File(Path,std::ios::binary);
if(!File){Error="cache file is missing or unreadable";return false;}
struct FHeader
{
char Magic[8];uint32 Version;uint32 Endian;uint64 SourceDataHash;
int32 Seed,ClipRadius,FullResClipLevels,CoarseTileCells,StrateContentCutMinLevel;
int32 SkirtCells,LODOctaveDrop,bGenerateSkirts;
uint32 AlgorithmVersion,TunnelMode,VoxelSizeCm;
uint64 TileCount;
};
static_assert(sizeof(FHeader)==80);
FHeader H{};File.read(reinterpret_cast<char*>(&H),sizeof(H));
const char ExpectedMagic[8]={'V','F','M','E','S','H','1','\0'};
if(!File||std::memcmp(H.Magic,ExpectedMagic,sizeof(ExpectedMagic))!=0
||H.Version!=1||H.Endian!=0x01020304u||H.TileCount>1000000)
{Error="mesh cache header is invalid";return false;}
if(H.SourceDataHash!=Expected.SourceDataHash||H.Seed!=Expected.Seed
||H.ClipRadius!=Expected.ClipRadius||H.FullResClipLevels!=Expected.FullResClipLevels
||H.CoarseTileCells!=Expected.CoarseTileCells
||H.StrateContentCutMinLevel!=Expected.StrateContentCutMinLevel
||H.SkirtCells!=Expected.SkirtCells||H.LODOctaveDrop!=Expected.LODOctaveDrop
||H.bGenerateSkirts!=Expected.bGenerateSkirts
||H.AlgorithmVersion!=Expected.AlgorithmVersion||H.TunnelMode!=Expected.TunnelMode
||H.VoxelSizeCm!=Expected.VoxelSizeCm)
{Error="mesh cache metadata does not match this world configuration";return false;}
struct FRecord
{
int32 Level,X,Y,Z;uint64 Samples,DensityHash;uint32 VertexCount,IndexCount;
};
static_assert(sizeof(FRecord)==40);
std::unordered_map<FVoxelTileKey,FStandaloneCpuTile,FStandaloneTileKeyHash> Loaded;
Loaded.reserve(static_cast<size_t>(H.TileCount)*2+1);
for(uint64 I=0;I<H.TileCount;++I)
{
FRecord Record{};File.read(reinterpret_cast<char*>(&Record),sizeof(Record));
if(!File||Record.Level<0||Record.Level>8||Record.VertexCount>10000000
||Record.IndexCount>60000000)
{Error="mesh cache contains an invalid tile record";return false;}
FStandaloneCpuTile Tile;Tile.Key=FVoxelTileKey(FIntVector(Record.X,Record.Y,Record.Z),Record.Level);
Tile.DensitySamples=Record.Samples;Tile.DensityHash=Record.DensityHash;Tile.bHasDensityHash=true;
Tile.Vertices.resize(Record.VertexCount);Tile.Indices.resize(Record.IndexCount);
if(!Tile.Vertices.empty())File.read(reinterpret_cast<char*>(Tile.Vertices.data()),
static_cast<std::streamsize>(Tile.Vertices.size()*sizeof(FStandaloneCpuVertex)));
if(!Tile.Indices.empty())File.read(reinterpret_cast<char*>(Tile.Indices.data()),
static_cast<std::streamsize>(Tile.Indices.size()*sizeof(uint32)));
if(!File){Error="mesh cache ended inside tile geometry";return false;}
for(const uint32 Index:Tile.Indices)
if(Index>=Tile.Vertices.size())
{Error="mesh cache contains an out-of-range mesh index";return false;}
for(const FStandaloneCpuVertex& Vertex:Tile.Vertices)
for(const float Value:Vertex.Position)
if(!std::isfinite(Value))
{Error="mesh cache contains a non-finite vertex";return false;}
for(const FStandaloneCpuVertex& Vertex:Tile.Vertices)
for(const float Value:Vertex.Normal)
if(!std::isfinite(Value))
{Error="mesh cache contains a non-finite normal";return false;}
if(!Loaded.emplace(Tile.Key,std::move(Tile)).second)
{Error="mesh cache contains duplicate tile keys";return false;}
}
const std::streamoff End=File.tellg();
if(End<0){Error="could not determine mesh cache size";return false;}
BytesLoaded=static_cast<uint64>(End);Tiles.swap(Loaded);bLoaded=true;
return true;
}
private:
std::unordered_map<FVoxelTileKey,FStandaloneCpuTile,FStandaloneTileKeyHash> Tiles;
bool bLoaded=false;
uint64 BytesLoaded=0;
};
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;
size_t PriorityRank=0;
std::chrono::steady_clock::time_point QueuedAt{};
std::chrono::steady_clock::time_point StartedAt{};
};
struct FStandaloneTileJobHeapOrder
{
bool operator()(const FStandaloneTileJob& A,const FStandaloneTileJob& B)const
{
if(A.PriorityRank!=B.PriorityRank)return A.PriorityRank>B.PriorityRank;
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:
static void SetCoarseFirstSchedulingEnabled(bool bEnabled)
{bCoarseFirstSchedulingEnabled.store(bEnabled,std::memory_order_relaxed);}
static bool IsCoarseFirstSchedulingEnabled()
{return bCoarseFirstSchedulingEnabled.load(std::memory_order_relaxed);}
explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=8,
bool bInHashDensity=false,
const FStandaloneMeshCache* InMeshCache=nullptr,
int32 InMaxLevelFilter=-1,
bool bInSkipUvAndColors=false,
bool bInSkipMaterialGroups=false,
bool bInSkipUV=false,
bool bInSkipColors=false,
VoxelForgeCore::World* InCoreWorld=nullptr,
bool bInCaptureColumnCoordinates=false,
int32 InLod8HorizonInteriorStepVoxels=0,
bool bInSharedColumnCache=true)
:World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,16)),bHashDensity(bInHashDensity),
MeshCache(InMeshCache),MaxLevelFilter(InMaxLevelFilter),
CoreWorld(InCoreWorld),bSkipUvAndColors(bInSkipUvAndColors),bSkipMaterialGroups(bInSkipMaterialGroups),
bSkipUV(bInSkipUV),bSkipColors(bInSkipColors),
bCaptureColumnCoordinates(bInCaptureColumnCoordinates),
bSharedColumnCache(bInSharedColumnCache),
Lod8HorizonInteriorStepVoxels(std::max(0,InLod8HorizonInteriorStepVoxels))
{
VoxelGenLOD::InitializeConsoleSwitches();
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
}
FStandaloneStreamingGenerator(const FStandaloneStreamingGenerator&)=delete;
FStandaloneStreamingGenerator& operator=(const FStandaloneStreamingGenerator&)=delete;
~FStandaloneStreamingGenerator(){Stop();}
void EnableColdTimelineTrace(std::chrono::steady_clock::time_point Origin,
const std::atomic<uint64>* FrameOrdinal)
{
TimelineOrigin=Origin;
TimelineFrameOrdinal=FrameOrdinal;
bColdTimelineTrace=true;
}
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();});
bHasLastCameraPosition=false;TravelDirection=FVector::ZeroVector;
return UpdateDesired(CameraPositionVoxels,ResidentKeys,CrossingIndex);
}
FStandaloneStreamingUpdate StartWithDesired(const FStandaloneDesiredTileSet& InitialDesired,
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();});
bHasLastCameraPosition=false;TravelDirection=FVector::ZeroVector;
return UpdateDesiredSet(InitialDesired,ResidentKeys,CrossingIndex);
}
void SetDesiredTileFilter(const std::vector<FVoxelTileKey>& Keys)
{
TileFilter.clear();TileFilter.reserve(Keys.size()*2+1);
for(const FVoxelTileKey& Key:Keys)TileFilter.insert(Key);
bUseTileFilter=true;
}
bool QueuePriorityTasks(std::vector<std::function<void()>> Tasks)
{
if(Tasks.empty())return true;
{
std::lock_guard<std::mutex> Lock(Mutex);
if(bStop||Workers.empty())return false;
for(auto& Task:Tasks)PriorityTasks.push_back(std::move(Task));
}
Wake.notify_all();
return true;
}
void ClearPriorityTasks()
{
std::lock_guard<std::mutex> Lock(Mutex);
PriorityTasks.clear();
}
void Stop()
{
{
std::lock_guard<std::mutex> Lock(Mutex);
bStop=true;
Queue.clear();PriorityTasks.clear();
}
Wake.notify_all();
for(FStandaloneWorkerThread& Worker:Workers)if(Worker.joinable())Worker.join();
Workers.clear();
std::lock_guard<std::mutex> Lock(Mutex);
Ready.clear();Outstanding.clear();Desired.clear();Running.clear();
}
FStandaloneStreamingUpdate UpdateDesired(
const FVector& CameraPositionVoxels,const std::vector<FVoxelTileKey>& ResidentKeys,
uint64 CrossingIndex=0)
{
if(bHasLastCameraPosition)
{
const FVector Delta=CameraPositionVoxels-LastCameraPositionVoxels;
const double LengthSquared=static_cast<double>(Delta.X)*Delta.X
+static_cast<double>(Delta.Y)*Delta.Y;
if(LengthSquared>1.0e-12)
{
const double InverseLength=1.0/std::sqrt(LengthSquared);
TravelDirection=FVector(static_cast<float>(Delta.X*InverseLength),
static_cast<float>(Delta.Y*InverseLength),0.0f);
}
else TravelDirection=FVector::ZeroVector;
}
else
TravelDirection=FVector::ZeroVector;
LastCameraPositionVoxels=CameraPositionVoxels;
bHasLastCameraPosition=true;
FStandaloneDesiredTileSet NewDesired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraPositionVoxels);
if(CoreWorld)
{
const auto CoreTiles=CoreWorld->desiredTiles({CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z});
NewDesired.Tiles.clear();NewDesired.Tiles.reserve(CoreTiles.size());
for(const auto& Key:CoreTiles)
NewDesired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
}
return UpdateDesiredSet(std::move(NewDesired),ResidentKeys,CrossingIndex);
}
FStandaloneStreamingUpdate UpdateDesiredWithSet(
FStandaloneDesiredTileSet NewDesired,const std::vector<FVoxelTileKey>& ResidentKeys,
uint64 CrossingIndex=0)
{
return UpdateDesiredSet(std::move(NewDesired),ResidentKeys,CrossingIndex);
}
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;
}
FStandaloneStreamingUpdate RestrictDesiredTileFilterKeepingStarted(
const std::vector<FVoxelTileKey>& BaseFilter,
const std::vector<FVoxelTileKey>& CandidateKeys,
const std::vector<FVoxelTileKey>& ResidentKeys,
std::vector<FVoxelTileKey>& RetainedCandidateKeys)
{
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Candidates;
Candidates.reserve(CandidateKeys.size()*2+1);
for(const FVoxelTileKey& Key:CandidateKeys)Candidates.insert(Key);
FStandaloneStreamingUpdate Stats;
std::lock_guard<std::mutex> Lock(Mutex);
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Retained;
Retained.reserve(CandidateKeys.size()*2+1);
for(const FVoxelTileKey& Key:Running)
if(Candidates.contains(Key))Retained.insert(Key);
for(const FStandaloneCpuTile& Tile:Ready)
if(Candidates.contains(Tile.Key))Retained.insert(Tile.Key);
for(const FVoxelTileKey& Key:ResidentKeys)
if(Candidates.contains(Key))Retained.insert(Key);
TileFilter.clear();
TileFilter.reserve((BaseFilter.size()+Retained.size())*2+1);
for(const FVoxelTileKey& Key:BaseFilter)TileFilter.insert(Key);
for(const FVoxelTileKey& Key:Retained)TileFilter.insert(Key);
bUseTileFilter=true;
FStandaloneDesiredTileSet NewDesired=CurrentDesired;
NewDesired.Tiles.erase(std::remove_if(NewDesired.Tiles.begin(),NewDesired.Tiles.end(),
[&](const FVoxelTileKey& Key){return !TileFilter.contains(Key);}),NewDesired.Tiles.end());
Stats.CenterChunk=NewDesired.CenterChunk;
Stats.DesiredTiles=static_cast<int32>(NewDesired.Tiles.size());
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> ResidentSet;
ResidentSet.reserve(ResidentKeys.size()*2+1);
for(const FVoxelTileKey& Key:ResidentKeys)ResidentSet.insert(Key);
for(const FVoxelTileKey& Key:NewDesired.Tiles)
{
if(Key.Level<World->FullResClipLevels)++Stats.FullResTiles;
if(ResidentSet.contains(Key))++Stats.RetainedResidentTiles;
}
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;
if(bColdTimelineTrace)TraceTileEvent("cancelled",Job.Key,FClock::now(),Job.QueuedAt);
Outstanding.erase(Job.Key);++Stats.CancelledQueuedJobs;
++CancelledByCrossing[Job.RequestedCrossing];return true;
});
Queue.erase(QueueEnd,Queue.end());
std::make_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
Stats.CancelledByCrossing.reserve(CancelledByCrossing.size());
for(const auto& Pair:CancelledByCrossing)Stats.CancelledByCrossing.push_back(Pair);
CurrentDesired=std::move(NewDesired);
RetainedCandidateKeys.assign(Retained.begin(),Retained.end());
std::sort(RetainedCandidateKeys.begin(),RetainedCandidateKeys.end(),
[](const FVoxelTileKey& A,const FVoxelTileKey& B)
{
if(A.Level!=B.Level)return A.Level<B.Level;
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;
});
return Stats;
}
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;}
uint64 GetCacheHits()const{return CacheHitCount.load(std::memory_order_relaxed);}
void SetMesherStageProfilingEnabled(bool Enabled)
{bProfileMesherStages=Enabled;}
void SetUniformBlockProofAuditEnabled(bool Enabled)
{bAuditUniformBlockProofs=Enabled;}
FStandaloneCpuTile GenerateDiagnostic(const FVoxelTileKey& Key,
std::uint32_t GlobalOctaveDrop=0,std::uint32_t CellStride=1,
bool ForceGenerate=false)
{
FStandaloneTileJob Job;Job.Key=Key;Job.StartedAt=FClock::now();Job.QueuedAt=Job.StartedAt;
return Generate(Job,GlobalOctaveDrop,CellStride,ForceGenerate);
}
private:
FStandaloneStreamingUpdate UpdateDesiredSet(FStandaloneDesiredTileSet NewDesired,
const std::vector<FVoxelTileKey>& ResidentKeys,uint64 CrossingIndex)
{
if(MaxLevelFilter>=0)
NewDesired.Tiles.erase(std::remove_if(NewDesired.Tiles.begin(),NewDesired.Tiles.end(),
[&](const FVoxelTileKey& Key){return Key.Level>MaxLevelFilter;}),NewDesired.Tiles.end());
if(bUseTileFilter)
NewDesired.Tiles.erase(std::remove_if(NewDesired.Tiles.begin(),NewDesired.Tiles.end(),
[&](const FVoxelTileKey& Key){return !TileFilter.contains(Key);}),NewDesired.Tiles.end());
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;
if(bColdTimelineTrace)TraceTileEvent("cancelled",Job.Key,FClock::now(),Job.QueuedAt);
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);
std::vector<FVoxelTileKey> Prioritized=NewDesired.Tiles;
if(IsCoarseFirstSchedulingEnabled())
{
// The local walkable ring leads, the coarse enclosure follows, then target-detail work.
// L'anneau praticable local passe d'abord, puis l'enveloppe grossiere, puis le detail cible.
const int32 FirstCoarseLevel=FMath::Max(2,World->FullResClipLevels);
auto Tier=[&](const FVoxelTileKey& Key)
{
if(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)return 0;
return Key.Level>=FirstCoarseLevel?1:2;
};
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;
};
std::sort(Prioritized.begin(),Prioritized.end(),[&](const FVoxelTileKey& A,const FVoxelTileKey& B)
{
const int32 TierA=Tier(A),TierB=Tier(B);
if(TierA!=TierB)return TierA<TierB;
if(TierA==1&&A.Level!=B.Level)return 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.Level!=B.Level)return A.Level<B.Level;
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;
});
}
else
{
std::vector<VoxelForgeCore::TileKey> Unordered;Unordered.reserve(NewDesired.Tiles.size());
for(const FVoxelTileKey& Key:NewDesired.Tiles)
Unordered.push_back({{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level});
const auto CorePrioritized=VoxelForgeCore::World::prioritizeTiles(Unordered,
{NewDesired.CenterChunk.X,NewDesired.CenterChunk.Y,NewDesired.CenterChunk.Z});
Prioritized.clear();Prioritized.reserve(CorePrioritized.size());
for(const auto& Key:CorePrioritized)
Prioritized.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
}
std::unordered_map<FVoxelTileKey,size_t,FStandaloneTileKeyHash> PriorityRanks;
PriorityRanks.reserve(Prioritized.size()*2+1);
for(size_t Rank=0;Rank<Prioritized.size();++Rank)
{
PriorityRanks.emplace(Prioritized[Rank],Rank);
}
for(FStandaloneTileJob& Job:Queue)Job.PriorityRank=PriorityRanks.at(Job.Key);
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;
FStandaloneTileJob Job;
Job.Key=Key;Job.BandZMin=BandMin;Job.BandZMax=BandMax;
Job.RequestedCrossing=CrossingIndex;Job.PriorityRank=PriorityRanks.at(Key);
Job.QueuedAt=FClock::now();
Queue.push_back(Job);
if(bColdTimelineTrace)TraceTileEvent("queued",Key,Job.QueuedAt);
Outstanding.insert(Key);++Stats.NewJobs;
}
CurrentDesired=std::move(NewDesired);
std::make_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
Wake.notify_all();
return Stats;
}
FStandaloneCpuTile Generate(const FStandaloneTileJob& Job,
std::uint32_t DiagnosticGlobalOctaveDrop=0,std::uint32_t DiagnosticCellStride=1,
bool ForceGenerate=false)
{
const auto StartTime=Job.StartedAt;
FStandaloneCpuTile Out;Out.Key=Job.Key;Out.RequestedCrossing=Job.RequestedCrossing;
Out.QueuedAt=Job.QueuedAt;Out.StartedAt=Job.StartedAt;
if(MeshCache&&!ForceGenerate)
{
if(const FStandaloneCpuTile* Cached=MeshCache->Find(Job.Key))
{
Out=*Cached;Out.RequestedCrossing=Job.RequestedCrossing;
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
Out.CompletedAt=FClock::now();
if(bHashDensity)
{Out.MeshHash=HashStandaloneCpuTileMesh(Out);Out.bHasMeshHash=true;}
CacheHitCount.fetch_add(1,std::memory_order_relaxed);
return Out;
}
}
UVoxelMarchingCubesMesher Mesher;
Mesher.SetGenerator(World->Generator.get());
Mesher.bStandaloneSkipUvAndColors=bSkipUvAndColors;
Mesher.bStandaloneSkipUV=bSkipUV;
Mesher.bStandaloneSkipColors=bSkipColors;
Mesher.bStandaloneSkipMaterialGroups=bSkipMaterialGroups;
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{};
if(CoreWorld)
{
VoxelForgeCore::TileGenerationOptions Options;
Options.bandMinZVoxel=Job.BandZMin;Options.bandMaxZVoxel=Job.BandZMax;
Options.hashDensity=bHashDensity;
Options.skipUvAndColors=bSkipUvAndColors;
Options.skipMaterialGroups=bSkipMaterialGroups;Options.skipUv=bSkipUV;Options.skipColors=bSkipColors;
Options.captureColumnCoordinates=bCaptureColumnCoordinates;
Options.useSharedColumnCache=bSharedColumnCache;
Options.lod8HorizonInteriorStepVoxels=Lod8HorizonInteriorStepVoxels;
Options.profileMesherStages=bProfileMesherStages&&!ForceGenerate;
Options.auditUniformBlockProofs=bAuditUniformBlockProofs&&!ForceGenerate;
Options.diagnosticGlobalOctaveDrop=DiagnosticGlobalOctaveDrop;
Options.diagnosticCellStride=std::max<std::uint32_t>(1,DiagnosticCellStride);
Options.diagnosticColumnInterpolationStride=
std::max<std::uint32_t>(1,DiagnosticCellStride);
std::string Error;
auto Result=CoreWorld->generateTile(
{{Job.Key.Coord.X,Job.Key.Coord.Y,Job.Key.Coord.Z},Job.Key.Level},Options,Error);
if(!Result)
{
std::fprintf(stderr,"[VoxelForgeCore] tile generation failed: %s\n",Error.c_str());
Out.bGenerationFailed=true;
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
Out.CompletedAt=FClock::now();return Out;
}
Out.DensitySamples=Result->densitySampleCount;
Out.CoreDiagnostics=Result->diagnostics;
Out.SurfaceVertexCount=Result->groups.surfaceVertexCount;
Out.SkirtTriangleCount=Result->groups.skirtTriangleCount;
Out.MeshPayloadAllocatedBytes=Result->meshPayloadAllocatedBytes;
Out.DensityHash=Result->densityHash;Out.MeshHash=Result->meshHash;
Out.PayloadHash=Result->payloadHash;Out.TopologyHash=Result->topologyHash;
Out.MaterialHash=Result->materialHash;
Out.bHasDensityHash=Result->hasDensityHash;Out.bHasMeshHash=Result->hasMeshHash;
Out.bHasPayloadHash=Result->hasPayloadHash;Out.bHasTopologyHash=Result->hasTopologyHash;
Out.bHasMaterialHash=Result->hasMaterialHash;
Out.Vertices.reserve(Result->vertices.size());
for(const auto& Vertex:Result->vertices)
Out.Vertices.push_back({
{static_cast<float>(Vertex.position.x*0.01),static_cast<float>(Vertex.position.y*0.01),
static_cast<float>(Vertex.position.z*0.01)},
{static_cast<float>(Vertex.normal.x),static_cast<float>(Vertex.normal.y),
static_cast<float>(Vertex.normal.z)},
Vertex.hostMaterialId,Vertex.resourceMaterialIds,Vertex.resourceWeightsQ16});
Out.Indices=std::move(Result->indices);
Out.TriangleSkirtFaceMasks=std::move(Result->skirtFaceMasks);
if(bProfile)
{
const VoxelDensityProfile::FSnapshot After=VoxelDensityProfile::SnapshotCurrentThread();
for(size_t I=0;I<Out.ProfileCycles.size();++I)
Out.ProfileCycles[I]=After.Cycles[I]>=Before.Cycles[I]?After.Cycles[I]-Before.Cycles[I]:0;
}
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
Out.CompletedAt=FClock::now();return Out;
}
std::vector<float> DensityValues;
FVoxelMeshData Mesh=Mesher.GenerateMesh(Job.Key.OriginVoxels(),Step,Cells,nullptr,
Job.BandZMin,Job.BandZMax,&SampleCount,nullptr,nullptr,
bHashDensity?&DensityValues:nullptr);
if(bHashDensity)
{
Out.bHasDensityHash=true;
for(const float Density:DensityValues)
{
Out.DensityHash^=std::bit_cast<uint32>(Density);
Out.DensityHash*=1099511628211ull;
}
}
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.SurfaceVertexCount=static_cast<uint32>(std::clamp(Mesh.NumSurfaceVertices,0,Mesh.Vertices.Num()));
Out.SkirtTriangleCount=static_cast<uint32>(std::max(0,Mesh.NumSkirtTriangles));
Out.MeshPayloadAllocatedBytes=static_cast<uint64>(Mesh.Vertices.GetAllocatedSize())
+static_cast<uint64>(Mesh.Normals.GetAllocatedSize())
+static_cast<uint64>(Mesh.UVs.GetAllocatedSize())
+static_cast<uint64>(Mesh.Colors.GetAllocatedSize())
+static_cast<uint64>(Mesh.TriangleSkirtFaceMasks.GetAllocatedSize())
+static_cast<uint64>(Mesh.Triangles.GetAllocatedSize());
if(bHashDensity)
{
Out.PayloadHash=HashStandaloneMeshPayload(Mesh);Out.bHasPayloadHash=true;
Out.TopologyHash=HashStandaloneMeshTopology(Mesh);Out.bHasTopologyHash=true;
}
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.TriangleSkirtFaceMasks.assign(Mesh.TriangleSkirtFaceMasks.begin(),
Mesh.TriangleSkirtFaceMasks.end());
Out.TriangleSkirtFaceMasks.resize(Out.Indices.size()/3,0);
if(bHashDensity)
{Out.MeshHash=HashStandaloneCpuTileMesh(Out);Out.bHasMeshHash=true;}
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
Out.CompletedAt=FClock::now();
return Out;
}
void WorkerLoop()
{
constexpr uint32 PriorityTaskBurst=3;
uint32 ConsecutivePriorityTasks=0;
for(;;)
{
FStandaloneTileJob Job;
std::function<void()> PriorityTask;
{
std::unique_lock<std::mutex> Lock(Mutex);
Wake.wait(Lock,[&](){return bStop||!PriorityTasks.empty()||!Queue.empty();});
if(bStop)return;
const bool bPreferPriority=!PriorityTasks.empty()
&&(Queue.empty()||ConsecutivePriorityTasks<PriorityTaskBurst);
if(bPreferPriority)
{
PriorityTask=std::move(PriorityTasks.front());PriorityTasks.pop_front();
++ConsecutivePriorityTasks;
}
else
{
std::pop_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
Job=Queue.back();Queue.pop_back();Job.StartedAt=FClock::now();
Running.insert(Job.Key);ConsecutivePriorityTasks=0;
}
}
if(PriorityTask)
{
PriorityTask();
continue;
}
if(bColdTimelineTrace)TraceTileEvent("started",Job.Key,Job.StartedAt,Job.QueuedAt,Job.StartedAt);
FStandaloneCpuTile Tile=Generate(Job);
if(bColdTimelineTrace)TraceTileEvent("finished",Tile.Key,Tile.CompletedAt,
Tile.QueuedAt,Tile.StartedAt,Tile.CompletedAt,Tile.WorkerSeconds*1000.0);
{
std::lock_guard<std::mutex> Lock(Mutex);
if(bStop)return;
Running.erase(Tile.Key);
Ready.push_back(std::move(Tile));
}
}
}
using FClock=std::chrono::steady_clock;
double TimelineMilliseconds(FClock::time_point At)const
{
return At.time_since_epoch().count()==0?-1.0:
std::chrono::duration<double,std::milli>(At-TimelineOrigin).count();
}
uint64 TimelineFrame()const
{
return TimelineFrameOrdinal?TimelineFrameOrdinal->load(std::memory_order_relaxed):0;
}
void TraceTileEvent(const char* Event,const FVoxelTileKey& Key,FClock::time_point At,
FClock::time_point Queued={},FClock::time_point Started={},FClock::time_point Finished={},
double WorkerMs=-1.0)const
{
std::printf("[CpuTileTimeline] event=%s key=(%d,%d,%d,lod%d) at_ms=%.3f frame=%llu queued_ms=%.3f started_ms=%.3f finished_ms=%.3f worker_ms=%.3f\n",
Event,Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,TimelineMilliseconds(At),
static_cast<unsigned long long>(TimelineFrame()),TimelineMilliseconds(Queued),
TimelineMilliseconds(Started),TimelineMilliseconds(Finished),WorkerMs);
}
FStandaloneOwnerWorld* World=nullptr;
inline static std::atomic<bool> bCoarseFirstSchedulingEnabled{true};
FVector LastCameraPositionVoxels=FVector::ZeroVector;
FVector TravelDirection=FVector::ZeroVector;
bool bHasLastCameraPosition=false;
int32 WorkerCount=8;
bool bHashDensity=false;
const FStandaloneMeshCache* MeshCache=nullptr;
int32 MaxLevelFilter=-1;
VoxelForgeCore::World* CoreWorld=nullptr;
bool bSkipUvAndColors=false;
bool bSkipMaterialGroups=false;
bool bSkipUV=false;
bool bSkipColors=false;
bool bCaptureColumnCoordinates=false;
bool bSharedColumnCache=true;
int32 Lod8HorizonInteriorStepVoxels=0;
bool bProfileMesherStages=false;
bool bAuditUniformBlockProofs=false;
std::atomic<uint64> CacheHitCount{0};
mutable std::mutex Mutex;
std::condition_variable Wake;
bool bStop=true;
std::vector<FStandaloneWorkerThread> Workers;
std::deque<std::function<void()>> PriorityTasks;
std::vector<FStandaloneTileJob> Queue;
std::vector<FStandaloneCpuTile> Ready;
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Running;
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Desired;
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Outstanding;
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> TileFilter;
bool bUseTileFilter=false;
FStandaloneDesiredTileSet CurrentDesired;
int32 FullResTileCount=0;
bool bColdTimelineTrace=false;
FClock::time_point TimelineOrigin{};
const std::atomic<uint64>* TimelineFrameOrdinal=nullptr;
};