standalone: 64 m full-detail override, detailed round tunnels, optional mesh cache -- generation itself NOT faster
ClipRadius override 3 (64 m at 25 cm). Uncached full detail 20.3 s on 8 workers (512 tiles at ~51 ms serial each should be ~3-4 s: the in-stream inefficiency is unsolved); full clipmap 170.6 s. The 1 s / 5 s targets were met only from an on-disk mesh cache (.vfmesh, not committed) of already-generated tiles: a revisit speed-up, not generation speed. Round tunnels gained deterministic multi-scale wall relief and lost the larger mouth wedges (1.54% of lattice signs changed, mean 0.20 m). Hashes stable across runs and 1 vs 6 workers. 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
81b7d6d0ff
commit
5441c7da5c
181 files changed
+1674
-51
No files matched your search
@@ -3,7 +3,12 @@
|
||||
#include "WorldMesher.h"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <unordered_set>
|
||||
|
||||
struct FStandaloneTileKeyHash
|
||||
@@ -31,9 +36,172 @@ struct FStandaloneCpuTile
|
||||
std::vector<uint32> Indices;
|
||||
uint64 DensitySamples=0;
|
||||
double WorkerSeconds=0.0;
|
||||
uint64 DensityHash=14695981039346656037ull;
|
||||
bool bHasDensityHash=false;
|
||||
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -113,8 +281,12 @@ namespace StandaloneWorldMesher
|
||||
class FStandaloneStreamingGenerator
|
||||
{
|
||||
public:
|
||||
explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=6)
|
||||
:World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,32))
|
||||
explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=8,
|
||||
bool bInHashDensity=false,
|
||||
const FStandaloneMeshCache* InMeshCache=nullptr,
|
||||
int32 InMaxLevelFilter=-1)
|
||||
:World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,8)),bHashDensity(bInHashDensity),
|
||||
MeshCache(InMeshCache),MaxLevelFilter(InMaxLevelFilter)
|
||||
{
|
||||
VoxelGenLOD::InitializeConsoleSwitches();
|
||||
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
|
||||
@@ -155,6 +327,9 @@ public:
|
||||
uint64 CrossingIndex=0)
|
||||
{
|
||||
FStandaloneDesiredTileSet NewDesired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraPositionVoxels);
|
||||
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());
|
||||
FStandaloneStreamingUpdate Stats;
|
||||
Stats.CenterChunk=NewDesired.CenterChunk;
|
||||
Stats.DesiredTiles=static_cast<int32>(NewDesired.Tiles.size());
|
||||
@@ -221,12 +396,23 @@ public:
|
||||
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);}
|
||||
|
||||
private:
|
||||
FStandaloneCpuTile Generate(const FStandaloneTileJob& Job)
|
||||
{
|
||||
const auto StartTime=FClock::now();
|
||||
FStandaloneCpuTile Out;Out.Key=Job.Key;Out.RequestedCrossing=Job.RequestedCrossing;
|
||||
if(MeshCache)
|
||||
{
|
||||
if(const FStandaloneCpuTile* Cached=MeshCache->Find(Job.Key))
|
||||
{
|
||||
Out=*Cached;Out.RequestedCrossing=Job.RequestedCrossing;
|
||||
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
|
||||
CacheHitCount.fetch_add(1,std::memory_order_relaxed);
|
||||
return Out;
|
||||
}
|
||||
}
|
||||
UVoxelMarchingCubesMesher Mesher;
|
||||
Mesher.SetGenerator(World->Generator.get());
|
||||
Mesher.bGenerateSkirts=World->bGenerateSkirts;
|
||||
@@ -240,8 +426,19 @@ private:
|
||||
const bool bProfile=VoxelDensityProfile::GetMode()!=VoxelDensityProfile::EMode::Disabled;
|
||||
const VoxelDensityProfile::FSnapshot Before=bProfile
|
||||
? VoxelDensityProfile::SnapshotCurrentThread():VoxelDensityProfile::FSnapshot{};
|
||||
std::vector<float> DensityValues;
|
||||
FVoxelMeshData Mesh=Mesher.GenerateMesh(Job.Key.OriginVoxels(),Step,Cells,nullptr,
|
||||
Job.BandZMin,Job.BandZMax,&SampleCount);
|
||||
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();
|
||||
@@ -290,7 +487,11 @@ private:
|
||||
|
||||
using FClock=std::chrono::steady_clock;
|
||||
FStandaloneOwnerWorld* World=nullptr;
|
||||
int32 WorkerCount=6;
|
||||
int32 WorkerCount=8;
|
||||
bool bHashDensity=false;
|
||||
const FStandaloneMeshCache* MeshCache=nullptr;
|
||||
int32 MaxLevelFilter=-1;
|
||||
std::atomic<uint64> CacheHitCount{0};
|
||||
mutable std::mutex Mutex;
|
||||
std::condition_variable Wake;
|
||||
bool bStop=true;
|
||||
|
||||
Reference in new issue
Block a user