wip(assembler): round L1 -- Linux baseline on abyss: Linux world identical to Windows
Headless Linux benchmark/hash tool (Clang 19 / GCC 14) sharing the viewer's plan builder and streaming pipeline (moved verbatim into StreamBenchmarkPipeline.h / WorldAssemblyPrototype.h). Owner Round17 view: density/mesh/payload/topology hashes equal the Windows S13 reference at W1/W8/W12 (Clang, 3 runs each) and W8 (GCC), with -ffp-contract=off. No output change. Baseline (i9-12900H, CPUs 0-11, powersave/balance_performance): whole view W8 3.562 s median, W12 3.512 s, W1 16.555 s; peak RSS < 605 MiB. Review: the default-stack crash is 17.3 MiB of static TLS per thread (glibc puts it in the thread stack; HeightfieldColumnCache 6.8 MB), not recursion -- open, after S14. MSVC compile of the viewer refactor not verified (no MSVC here). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ec2afe9c93
commit
1276fd11e3
11 files changed
+1308
-760
No files matched your search
@@ -1,5 +1,7 @@
|
||||
#include "OwnerWorldLoader.h"
|
||||
#include "StreamingWorld.h"
|
||||
#include "StreamBenchmarkPipeline.h"
|
||||
#include "WorldAssemblyPrototype.h"
|
||||
#include "StandaloneCoreScheduler.h"
|
||||
#include "../Core/src/StandaloneSnapshotAdapter.h"
|
||||
#include "../Research/OwnerShapeWorld/gpu_field_port/GpuFarViewer.h"
|
||||
@@ -52,6 +54,9 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
using StandaloneStreamBenchmark::FHashedTile;
|
||||
using StandaloneStreamBenchmark::HashTileRows;
|
||||
|
||||
struct V3 { float X=0.0f,Y=0.0f,Z=0.0f; };
|
||||
V3 Add(V3 A,V3 B){return {A.X+B.X,A.Y+B.Y,A.Z+B.Z};}
|
||||
V3 Sub(V3 A,V3 B){return {A.X-B.X,A.Y-B.Y,A.Z-B.Z};}
|
||||
@@ -6932,354 +6937,6 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
return TotalSamples>0&&LegacyToDetailed.Unmatched+DetailedToLegacy.Unmatched==0?0:10;
|
||||
}
|
||||
|
||||
void HashWord(uint64& Hash,uint64 Word,int32 Bytes=8)
|
||||
{
|
||||
for(int32 I=0;I<Bytes;++I)
|
||||
{
|
||||
Hash^=static_cast<uint8>(Word>>(I*8));
|
||||
Hash*=1099511628211ull;
|
||||
}
|
||||
}
|
||||
|
||||
struct FHashedTile
|
||||
{
|
||||
FVoxelTileKey Key;
|
||||
uint64 Hash=14695981039346656037ull;
|
||||
};
|
||||
|
||||
bool TileKeyLessForHash(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;
|
||||
}
|
||||
|
||||
uint64 HashTileRows(std::vector<FHashedTile>& Rows)
|
||||
{
|
||||
std::sort(Rows.begin(),Rows.end(),[](const FHashedTile& A,const FHashedTile& B)
|
||||
{return TileKeyLessForHash(A.Key,B.Key);});
|
||||
uint64 Hash=14695981039346656037ull;
|
||||
for(const FHashedTile& Row:Rows)
|
||||
{
|
||||
HashWord(Hash,static_cast<uint32>(Row.Key.Level),4);
|
||||
HashWord(Hash,static_cast<uint32>(Row.Key.Coord.X),4);
|
||||
HashWord(Hash,static_cast<uint32>(Row.Key.Coord.Y),4);
|
||||
HashWord(Hash,static_cast<uint32>(Row.Key.Coord.Z),4);
|
||||
HashWord(Hash,Row.Hash);
|
||||
}
|
||||
return Hash;
|
||||
}
|
||||
|
||||
int RunPipelinedStreamingBenchmark(std::shared_ptr<FStandaloneOwnerWorld> OwnerWorld,
|
||||
const VoxelForgeCore::ResolvedWorldSnapshot& CoreSnapshot,
|
||||
const FVector& CameraPositionVoxels,int32 WorkerCount,int32 MaxLevel,
|
||||
VoxelForgeCore::DetailVariant DetailVariant,const VoxelForgeCore::JobScheduler& Scheduler,
|
||||
bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,bool bSkipUV,bool bSkipColors,
|
||||
std::chrono::steady_clock::time_point ColdStart,bool bProfileColumnOverlap,
|
||||
int32 FarHorizonLOD8InteriorStepVoxels)
|
||||
{
|
||||
using Clock=std::chrono::steady_clock;
|
||||
FStandaloneOwnerWorld& World=*OwnerWorld;
|
||||
const bool bRoundTunnels=World.Generator->bStandaloneResearchRoundTunnels;
|
||||
const bool bEnhancedTunnelWalls=World.Generator->bStandaloneResearchEnhancedWalls;
|
||||
VoxelForgeCore::World Shape(CoreSnapshot);
|
||||
FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels);
|
||||
const auto CoreTiles=Shape.desiredTiles(
|
||||
{CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z});
|
||||
Desired.Tiles.clear();Desired.Tiles.reserve(CoreTiles.size());
|
||||
for(const auto& Key:CoreTiles)
|
||||
Desired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
|
||||
if(MaxLevel>=0)
|
||||
Desired.Tiles.erase(std::remove_if(Desired.Tiles.begin(),Desired.Tiles.end(),
|
||||
[&](const FVoxelTileKey& Tile){return Tile.Level>MaxLevel;}),Desired.Tiles.end());
|
||||
FIntVector FineMin(INT32_MAX,INT32_MAX,INT32_MAX),FineMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
||||
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
{
|
||||
const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
|
||||
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
|
||||
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
|
||||
if(Tile.Level<World.FullResClipLevels)
|
||||
{
|
||||
FineMin.X=std::min(FineMin.X,Origin.X);FineMin.Y=std::min(FineMin.Y,Origin.Y);FineMin.Z=std::min(FineMin.Z,Origin.Z);
|
||||
FineMax.X=std::max(FineMax.X,Origin.X+Extent);FineMax.Y=std::max(FineMax.Y,Origin.Y+Extent);FineMax.Z=std::max(FineMax.Z,Origin.Z+Extent);
|
||||
}
|
||||
}
|
||||
if(FineMin.X==INT32_MAX||ClipMin.X==INT32_MAX)
|
||||
{std::fprintf(stderr,"[StreamPipeline] no desired detail or clipmap tiles.\n");return 7;}
|
||||
|
||||
const FIntVector CenterChunk=Desired.CenterChunk;
|
||||
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Neighborhood;
|
||||
Neighborhood.reserve(32);
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
if(Tile.Level==0&&std::abs(Tile.Coord.X-CenterChunk.X)<=1
|
||||
&&std::abs(Tile.Coord.Y-CenterChunk.Y)<=1&&std::abs(Tile.Coord.Z-CenterChunk.Z)<=1)
|
||||
Neighborhood.insert(Tile);
|
||||
if(Neighborhood.size()!=27)
|
||||
{std::fprintf(stderr,"[StreamPipeline] expected 27 player-neighborhood tiles, found %zu.\n",Neighborhood.size());return 7;}
|
||||
|
||||
// The initial desired set now owns all clipmap metadata needed by this Core-only run.
|
||||
// Release the separate compatibility-world generation graph before compiling Core.
|
||||
World.Generator.reset();
|
||||
World.Manager.reset();
|
||||
World.DefinitionStorage.clear();
|
||||
World.BiomeStorage.clear();
|
||||
World.OperationStorage.clear();
|
||||
World.DensitySamples.clear();
|
||||
|
||||
std::string BuildError;
|
||||
const bool bIncludeFarWorld=MaxLevel<0||MaxLevel>=World.FullResClipLevels;
|
||||
const auto ShapeBuildStart=Clock::now();
|
||||
VoxelForgeCore::ShapeBuildRequest ShapeRequest;
|
||||
ShapeRequest.minVoxel={FineMin.X,FineMin.Y,FineMin.Z};
|
||||
ShapeRequest.maxVoxel={FineMax.X,FineMax.Y,FineMax.Z};
|
||||
ShapeRequest.clipMinVoxel={ClipMin.X,ClipMin.Y,ClipMin.Z};
|
||||
ShapeRequest.clipMaxVoxel={ClipMax.X,ClipMax.Y,ClipMax.Z};
|
||||
ShapeRequest.workerLimit=static_cast<uint32>(std::clamp(WorkerCount,1,16));
|
||||
ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld;
|
||||
ShapeRequest.roundTunnels=bRoundTunnels;
|
||||
ShapeRequest.enhancedTunnelWalls=bEnhancedTunnelWalls;
|
||||
if(!Shape.compile(ShapeRequest,Scheduler,BuildError))
|
||||
{std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",BuildError.c_str());return 2;}
|
||||
const auto ShapeReady=Clock::now();
|
||||
const double ShapeSeconds=std::chrono::duration<double>(ShapeReady-ShapeBuildStart).count();
|
||||
FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
|
||||
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap,
|
||||
FarHorizonLOD8InteriorStepVoxels);
|
||||
const auto StreamStart=Clock::now();
|
||||
const FStandaloneStreamingUpdate Update=Streamer.StartWithDesired(Desired);
|
||||
if(Update.DesiredTiles!=static_cast<int32>(Desired.Tiles.size()))
|
||||
{
|
||||
Streamer.Stop();
|
||||
std::fprintf(stderr,"[StreamPipeline] expected %zu tiles but queued %d.\n",
|
||||
Desired.Tiles.size(),Update.DesiredTiles);return 7;
|
||||
}
|
||||
|
||||
std::vector<FHashedTile> DensityRows,MeshRows,PayloadRows,TopologyRows;
|
||||
int32 Finished=0,NeighborhoodFinished=0,FullDetailFinished=0;
|
||||
int32 FullDetailExpected=0;
|
||||
struct FColumnXY
|
||||
{
|
||||
std::int64_t X=0,Y=0;
|
||||
bool operator==(const FColumnXY&)const=default;
|
||||
};
|
||||
struct FColumnXYHash
|
||||
{
|
||||
std::size_t operator()(const FColumnXY& Value)const noexcept
|
||||
{
|
||||
const std::size_t X=std::hash<std::int64_t>{}(Value.X);
|
||||
const std::size_t Y=std::hash<std::int64_t>{}(Value.Y);
|
||||
return X^(Y+static_cast<std::size_t>(0x9E3779B9u)+(X<<6)+(X>>2));
|
||||
}
|
||||
};
|
||||
struct FFirstColumnUse
|
||||
{
|
||||
FVoxelTileKey Tile;
|
||||
std::uint64_t WorkerHash=0;
|
||||
};
|
||||
std::array<std::unordered_map<FColumnXY,std::vector<FFirstColumnUse>,FColumnXYHash>,9> ColumnUsesByLOD;
|
||||
std::array<std::uint64_t,9> ColumnDuplicateEventsByLOD{},NeighbourDuplicateEventsByLOD{};
|
||||
std::array<std::uint64_t,9> CrossWorkerNeighbourDuplicateEventsByLOD{};
|
||||
std::array<std::uint64_t,9> ColumnCaptureTilesByLOD{},ExpectedTilesByLOD{};
|
||||
std::array<bool,9> ColumnCaptureCompleteByLOD{};
|
||||
ColumnCaptureCompleteByLOD.fill(true);
|
||||
std::array<double,9> WorkerSecondsByLOD{};
|
||||
std::array<double,9> ColumnEvalMsByLOD{},MarchingExclusiveMsByLOD{};
|
||||
std::array<double,9> GroundRefineExclusiveMsByLOD{},CapRefineExclusiveMsByLOD{};
|
||||
std::array<double,9> MeshAssemblyMsByLOD{};
|
||||
std::array<std::uint64_t,9> TileCountByLOD{};
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
if(Tile.Level>=0&&Tile.Level<static_cast<int32>(ExpectedTilesByLOD.size()))
|
||||
++ExpectedTilesByLOD[static_cast<std::size_t>(Tile.Level)];
|
||||
const auto AreSameLODNeighbours=[](const FVoxelTileKey& A,const FVoxelTileKey& B)
|
||||
{
|
||||
if(A.Level!=B.Level)return false;
|
||||
const std::int64_t DX=std::abs(static_cast<std::int64_t>(A.Coord.X)-B.Coord.X);
|
||||
const std::int64_t DY=std::abs(static_cast<std::int64_t>(A.Coord.Y)-B.Coord.Y);
|
||||
const std::int64_t DZ=std::abs(static_cast<std::int64_t>(A.Coord.Z)-B.Coord.Z);
|
||||
return std::max({DX,DY,DZ})<=1&&(DX+DY+DZ)>0;
|
||||
};
|
||||
uint64 NativeSolidBoxes=0,NativeAirBoxes=0,NativeMixedBoxes=0,NativeUnknownBoxes=0;
|
||||
uint64 NativeFieldCalls=0,NativeColumnHits=0,NativeColumnMisses=0,NativeMeshingSkips=0;
|
||||
double NativeClassificationMilliseconds=0.0,NativeColumnComputeMilliseconds=0.0;
|
||||
double NativeMeshingMilliseconds=0.0;
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
if(Tile.Level<World.FullResClipLevels)++FullDetailExpected;
|
||||
Clock::time_point NeighborhoodReady{},FullDetailReady{},WholeViewReady{};
|
||||
bool bNeighborhoodReported=false,bFullDetailReported=false;
|
||||
while(Finished<Update.DesiredTiles)
|
||||
{
|
||||
std::vector<FStandaloneCpuTile> Ready=Streamer.TakeReady();
|
||||
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
|
||||
for(const FStandaloneCpuTile& Tile:Ready)
|
||||
{
|
||||
++Finished;
|
||||
if(Tile.Key.Level>=0&&Tile.Key.Level<static_cast<int32>(TileCountByLOD.size()))
|
||||
{
|
||||
const std::size_t LOD=static_cast<std::size_t>(Tile.Key.Level);
|
||||
++TileCountByLOD[LOD];
|
||||
WorkerSecondsByLOD[LOD]+=Tile.WorkerSeconds;
|
||||
ColumnEvalMsByLOD[LOD]+=Tile.CoreDiagnostics.columnEvaluationExclusiveMilliseconds;
|
||||
MarchingExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.marchingCubesExclusiveMilliseconds;
|
||||
GroundRefineExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.groundRefinementExclusiveMilliseconds;
|
||||
CapRefineExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.capRefinementExclusiveMilliseconds;
|
||||
MeshAssemblyMsByLOD[LOD]+=Tile.CoreDiagnostics.meshAssemblyMilliseconds;
|
||||
}
|
||||
if(bProfileColumnOverlap&&Tile.Key.Level>=0
|
||||
&&Tile.Key.Level<static_cast<int32>(ColumnCaptureTilesByLOD.size()))
|
||||
{
|
||||
const std::size_t LOD=static_cast<std::size_t>(Tile.Key.Level);
|
||||
++ColumnCaptureTilesByLOD[LOD];
|
||||
ColumnCaptureCompleteByLOD[LOD]=ColumnCaptureCompleteByLOD[LOD]
|
||||
&&Tile.CoreDiagnostics.columnCoordinateCaptureComplete;
|
||||
std::vector<std::array<std::int64_t,2>> Coordinates=
|
||||
Tile.CoreDiagnostics.evaluatedColumnCoordinates;
|
||||
std::sort(Coordinates.begin(),Coordinates.end());
|
||||
Coordinates.erase(std::unique(Coordinates.begin(),Coordinates.end()),Coordinates.end());
|
||||
for(const auto& Coordinate:Coordinates)
|
||||
{
|
||||
const FColumnXY XY{Coordinate[0],Coordinate[1]};
|
||||
auto [Uses,Inserted]=ColumnUsesByLOD[LOD].try_emplace(XY);
|
||||
std::vector<FFirstColumnUse>& PriorUses=Uses->second;
|
||||
if(Inserted)
|
||||
{
|
||||
PriorUses.push_back({Tile.Key,Tile.CoreDiagnostics.columnEvaluationWorkerHash});
|
||||
continue;
|
||||
}
|
||||
++ColumnDuplicateEventsByLOD[LOD];
|
||||
bool bHasNeighbourDuplicate=false,bHasCrossWorkerNeighbourDuplicate=false;
|
||||
for(const FFirstColumnUse& Prior:PriorUses)
|
||||
{
|
||||
if(!AreSameLODNeighbours(Prior.Tile,Tile.Key))continue;
|
||||
bHasNeighbourDuplicate=true;
|
||||
bHasCrossWorkerNeighbourDuplicate=bHasCrossWorkerNeighbourDuplicate
|
||||
||Prior.WorkerHash!=Tile.CoreDiagnostics.columnEvaluationWorkerHash;
|
||||
}
|
||||
if(bHasNeighbourDuplicate)++NeighbourDuplicateEventsByLOD[LOD];
|
||||
if(bHasCrossWorkerNeighbourDuplicate)
|
||||
++CrossWorkerNeighbourDuplicateEventsByLOD[LOD];
|
||||
PriorUses.push_back({Tile.Key,Tile.CoreDiagnostics.columnEvaluationWorkerHash});
|
||||
}
|
||||
}
|
||||
WholeViewReady=std::max(WholeViewReady,Tile.CompletedAt);
|
||||
if(Neighborhood.contains(Tile.Key))
|
||||
{
|
||||
++NeighborhoodFinished;
|
||||
NeighborhoodReady=std::max(NeighborhoodReady,Tile.CompletedAt);
|
||||
}
|
||||
if(Tile.Key.Level<World.FullResClipLevels)
|
||||
{
|
||||
++FullDetailFinished;
|
||||
FullDetailReady=std::max(FullDetailReady,Tile.CompletedAt);
|
||||
NativeSolidBoxes+=Tile.CoreDiagnostics.solidBoxes;
|
||||
NativeAirBoxes+=Tile.CoreDiagnostics.airBoxes;
|
||||
NativeMixedBoxes+=Tile.CoreDiagnostics.mixedBoxes;
|
||||
NativeUnknownBoxes+=Tile.CoreDiagnostics.unknownBoxes;
|
||||
NativeFieldCalls+=Tile.CoreDiagnostics.fieldCalls;
|
||||
NativeColumnHits+=Tile.CoreDiagnostics.columnCacheHits;
|
||||
NativeColumnMisses+=Tile.CoreDiagnostics.columnCacheMisses;
|
||||
NativeMeshingSkips+=Tile.CoreDiagnostics.meshingSkipped?1u:0u;
|
||||
NativeClassificationMilliseconds+=Tile.CoreDiagnostics.classificationMilliseconds;
|
||||
NativeColumnComputeMilliseconds+=Tile.CoreDiagnostics.columnComputeMilliseconds;
|
||||
NativeMeshingMilliseconds+=Tile.CoreDiagnostics.meshingMilliseconds;
|
||||
}
|
||||
if(bHashDensity)
|
||||
{
|
||||
if(Tile.bHasDensityHash)DensityRows.push_back({Tile.Key,Tile.DensityHash});
|
||||
if(Tile.bHasMeshHash)MeshRows.push_back({Tile.Key,Tile.MeshHash});
|
||||
if(Tile.bHasPayloadHash)PayloadRows.push_back({Tile.Key,Tile.PayloadHash});
|
||||
if(Tile.bHasTopologyHash)TopologyRows.push_back({Tile.Key,Tile.TopologyHash});
|
||||
}
|
||||
}
|
||||
if(!bNeighborhoodReported&&NeighborhoodFinished==static_cast<int32>(Neighborhood.size()))
|
||||
{
|
||||
std::printf("[PipelineMilestone] neighborhood_ready=%.3fs generated=%d/27\n",
|
||||
std::chrono::duration<double>(NeighborhoodReady-ColdStart).count(),NeighborhoodFinished);
|
||||
bNeighborhoodReported=true;
|
||||
}
|
||||
if(!bFullDetailReported&&FullDetailFinished==FullDetailExpected)
|
||||
{
|
||||
std::printf("[PipelineMilestone] full_detail_ready=%.3fs generated=%d/%d\n",
|
||||
std::chrono::duration<double>(FullDetailReady-ColdStart).count(),
|
||||
FullDetailFinished,FullDetailExpected);
|
||||
bFullDetailReported=true;
|
||||
}
|
||||
}
|
||||
Streamer.Stop();
|
||||
bool bColumnOverlapCapturePassing=true;
|
||||
if(bProfileColumnOverlap)
|
||||
{
|
||||
for(std::size_t LOD=0;LOD<ColumnCaptureTilesByLOD.size();++LOD)
|
||||
{
|
||||
const bool bLODComplete=ColumnCaptureCompleteByLOD[LOD]
|
||||
&&ColumnCaptureTilesByLOD[LOD]==ExpectedTilesByLOD[LOD];
|
||||
bColumnOverlapCapturePassing=bColumnOverlapCapturePassing&&bLODComplete;
|
||||
std::printf("[WorldAssemblerLODColumnOverlap] lod=%zu tiles=%llu expected_tiles=%llu unique_xy_columns=%zu repeated_tile_evaluations=%llu neighbouring_duplicate_evaluations=%llu cross_worker_neighbouring_duplicates=%llu capture_complete=%d\n",
|
||||
LOD,static_cast<unsigned long long>(ColumnCaptureTilesByLOD[LOD]),
|
||||
static_cast<unsigned long long>(ExpectedTilesByLOD[LOD]),
|
||||
ColumnUsesByLOD[LOD].size(),
|
||||
static_cast<unsigned long long>(ColumnDuplicateEventsByLOD[LOD]),
|
||||
static_cast<unsigned long long>(NeighbourDuplicateEventsByLOD[LOD]),
|
||||
static_cast<unsigned long long>(CrossWorkerNeighbourDuplicateEventsByLOD[LOD]),
|
||||
bLODComplete?1:0);
|
||||
}
|
||||
}
|
||||
for(std::size_t LOD=0;LOD<TileCountByLOD.size();++LOD)
|
||||
{
|
||||
std::printf("[WorldAssemblerLODWorker] lod=%zu tiles=%llu worker_sum_s=%.6f column_eval_exclusive_ms=%.3f mc_exclusive_ms=%.3f ground_refine_exclusive_ms=%.3f cap_refine_exclusive_ms=%.3f mesh_assembly_ms=%.3f\n",
|
||||
LOD,static_cast<unsigned long long>(TileCountByLOD[LOD]),WorkerSecondsByLOD[LOD],
|
||||
ColumnEvalMsByLOD[LOD],MarchingExclusiveMsByLOD[LOD],
|
||||
GroundRefineExclusiveMsByLOD[LOD],CapRefineExclusiveMsByLOD[LOD],MeshAssemblyMsByLOD[LOD]);
|
||||
}
|
||||
const double NeighborhoodSeconds=std::chrono::duration<double>(NeighborhoodReady-ColdStart).count();
|
||||
const double NeighborhoodMeshSeconds=std::chrono::duration<double>(NeighborhoodReady-ShapeReady).count();
|
||||
const double FullDetailSeconds=std::chrono::duration<double>(FullDetailReady-ColdStart).count();
|
||||
const double WholeViewSeconds=std::chrono::duration<double>(WholeViewReady-ColdStart).count();
|
||||
const double StreamSeconds=std::chrono::duration<double>(WholeViewReady-StreamStart).count();
|
||||
std::printf("[StreamPipeline] workers=%d shape=%.3fs neighborhood_mesh=%.3fs neighborhood_ready=%.3fs full_detail_ready=%.3fs whole_view_ready=%.3fs neighborhood_tiles=%d/27 full_detail_tiles=%d/%d whole_tiles=%d/%zu stream_wall=%.3fs cache_hits=0 full_payload=1\n",
|
||||
WorkerCount,ShapeSeconds,NeighborhoodMeshSeconds,NeighborhoodSeconds,
|
||||
FullDetailSeconds,WholeViewSeconds,NeighborhoodFinished,
|
||||
FullDetailFinished,FullDetailExpected,Finished,Desired.Tiles.size(),StreamSeconds);
|
||||
std::printf("[WorldAssemblerTilePipeline] full_detail_tiles=%d solid_boxes=%llu air_boxes=%llu mixed_boxes=%llu unknown_boxes=%llu whole_tile_skips=%llu field_calls=%llu column_cache_hits=%llu column_cache_misses=%llu column_compute_worker_ms=%.3f classification_worker_ms=%.3f meshing_worker_ms=%.3f\n",
|
||||
FullDetailExpected,static_cast<unsigned long long>(NativeSolidBoxes),
|
||||
static_cast<unsigned long long>(NativeAirBoxes),
|
||||
static_cast<unsigned long long>(NativeMixedBoxes),
|
||||
static_cast<unsigned long long>(NativeUnknownBoxes),
|
||||
static_cast<unsigned long long>(NativeMeshingSkips),
|
||||
static_cast<unsigned long long>(NativeFieldCalls),
|
||||
static_cast<unsigned long long>(NativeColumnHits),
|
||||
static_cast<unsigned long long>(NativeColumnMisses),
|
||||
NativeColumnComputeMilliseconds,NativeClassificationMilliseconds,
|
||||
NativeMeshingMilliseconds);
|
||||
|
||||
bool bHashesPassing=true;
|
||||
if(bHashDensity)
|
||||
{
|
||||
bHashesPassing=DensityRows.size()==Desired.Tiles.size()
|
||||
&&MeshRows.size()==Desired.Tiles.size()
|
||||
&&PayloadRows.size()==Desired.Tiles.size()
|
||||
&&TopologyRows.size()==Desired.Tiles.size();
|
||||
std::vector<FHashedTile> FullDensity,FullMesh,FullPayload,FullTopology;
|
||||
for(const FHashedTile& Row:DensityRows)if(Row.Key.Level<World.FullResClipLevels)FullDensity.push_back(Row);
|
||||
for(const FHashedTile& Row:MeshRows)if(Row.Key.Level<World.FullResClipLevels)FullMesh.push_back(Row);
|
||||
for(const FHashedTile& Row:PayloadRows)if(Row.Key.Level<World.FullResClipLevels)FullPayload.push_back(Row);
|
||||
for(const FHashedTile& Row:TopologyRows)if(Row.Key.Level<World.FullResClipLevels)FullTopology.push_back(Row);
|
||||
std::printf("[PipelineFullDetailHash] tiles=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
|
||||
FullDetailExpected,static_cast<unsigned long long>(HashTileRows(FullDensity)),
|
||||
static_cast<unsigned long long>(HashTileRows(FullMesh)),
|
||||
static_cast<unsigned long long>(HashTileRows(FullPayload)),
|
||||
static_cast<unsigned long long>(HashTileRows(FullTopology)));
|
||||
std::printf("[PipelineWholeViewHash] tiles=%zu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
|
||||
Desired.Tiles.size(),static_cast<unsigned long long>(HashTileRows(DensityRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(MeshRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(PayloadRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(TopologyRows)));
|
||||
}
|
||||
return NeighborhoodFinished==static_cast<int32>(Neighborhood.size())
|
||||
&&FullDetailFinished==FullDetailExpected&&Finished==Update.DesiredTiles&&bHashesPassing
|
||||
&&(!bProfileColumnOverlap||bColumnOverlapCapturePassing)?0:6;
|
||||
}
|
||||
int RunStreamingBenchmark(FStandaloneOwnerWorld& World,VoxelForgeCore::World* CoreWorld,
|
||||
const FVector& CameraPositionVoxels,
|
||||
int32 WorkerCount,int32 MaxLevel,bool bRoundTunnels,
|
||||
@@ -8132,414 +7789,6 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
&&analyticReverseCovered==1&&packedAnalyticRenderSame?0:12;
|
||||
}
|
||||
|
||||
std::int64_t WorldAssemblyFixed(double voxels)
|
||||
{return static_cast<std::int64_t>(std::llround(voxels*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));}
|
||||
|
||||
std::int64_t WorldAssemblyQ16(double value)
|
||||
{return static_cast<std::int64_t>(std::llround(value*65536.0));}
|
||||
|
||||
std::int64_t WorldAssemblyWavelength(double frequency,double fallbackVoxels)
|
||||
{
|
||||
const double wavelength=std::isfinite(frequency)&&frequency>0.0?1.0/frequency:fallbackVoxels;
|
||||
return std::max<std::int64_t>(1,WorldAssemblyFixed(wavelength));
|
||||
}
|
||||
|
||||
uint64 WorldAssemblyFeatureId(uint32 seed,int64 x,int64 y,uint32 kind)
|
||||
{
|
||||
uint64 hash=14695981039346656037ull;
|
||||
const auto add=[&](uint64 value,int bytes)
|
||||
{
|
||||
for(int i=0;i<bytes;++i){hash^=static_cast<uint8>(value>>(8*i));hash*=1099511628211ull;}
|
||||
};
|
||||
add(seed,4);add(static_cast<uint64>(x),8);add(static_cast<uint64>(y),8);add(kind,4);
|
||||
return hash?hash:1;
|
||||
}
|
||||
|
||||
bool BuildFiniteStrateRoute(VoxelForgeCore::WorldAssemblyRecipeV1& recipe,
|
||||
VoxelForgeCore::WorldAssemblyStratumPlanV1& stratum,std::uint64_t& tested,
|
||||
std::uint64_t& gradeRejected,std::uint64_t& sweepRejected,std::string& error)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
constexpr std::int64_t PlayerRadiusQ=348; // 0.34 m in Core's 0.25 m voxel scale.
|
||||
constexpr std::int64_t PlayerHeightQ=1802; // 1.76 m in Core's 0.25 m voxel scale.
|
||||
constexpr std::int64_t HalfHeightQ=PlayerHeightQ/2;
|
||||
constexpr std::int64_t StepQ=128; // 0.5 Core voxel, finer than the density lattice.
|
||||
constexpr std::int64_t ProbeRadiusQ=174;
|
||||
constexpr std::int64_t SupportDepthQ=256;
|
||||
constexpr std::int64_t GradeQ16=17560; // tan(15 degrees), rounded upward.
|
||||
constexpr std::int64_t RouteRadiusQ=4*WorldAssemblySubvoxelsPerVoxel;
|
||||
struct Direction {std::int32_t x,y;};
|
||||
constexpr std::array<Direction,8> directions{{
|
||||
{65536,0},{46341,46341},{0,65536},{-46341,46341},
|
||||
{-65536,0},{-46341,-46341},{0,-65536},{46341,-46341}}};
|
||||
struct Candidate
|
||||
{
|
||||
std::int64_t score=0;
|
||||
std::int32_t centerX=0,centerY=0,direction=0;
|
||||
std::vector<WorldAssemblyRoutePointV1> points;
|
||||
};
|
||||
|
||||
auto& constraints=stratum.routeConstraints;
|
||||
constraints.version=1;
|
||||
constraints.capsuleRadiusSubvoxels=PlayerRadiusQ;
|
||||
constraints.capsuleHeightSubvoxels=PlayerHeightQ;
|
||||
constraints.maximumGradeQ16=GradeQ16;
|
||||
constraints.sweepSpacingSubvoxels=StepQ;
|
||||
constraints.supportProbeRadiusSubvoxels=ProbeRadiusQ;
|
||||
constraints.supportSearchDepthSubvoxels=SupportDepthQ;
|
||||
constraints.minimumClearanceSubvoxels=8;
|
||||
constraints.minimumSupportProbes=3;
|
||||
|
||||
const Heightfield2D finalField(recipe,stratum);
|
||||
if(!finalField.isValid())
|
||||
{error="route planner received an invalid final composed field";return false;}
|
||||
std::vector<Candidate> candidates;
|
||||
constexpr std::int32_t halfLength=128;
|
||||
constexpr std::int32_t waypointSpacing=8;
|
||||
for(std::int32_t centerY=-256;centerY<=256;centerY+=64)
|
||||
for(std::int32_t centerX=-256;centerX<=256;centerX+=64)
|
||||
for(std::int32_t directionIndex=0;directionIndex<static_cast<std::int32_t>(directions.size());++directionIndex)
|
||||
{
|
||||
Candidate candidate;
|
||||
candidate.centerX=centerX;candidate.centerY=centerY;candidate.direction=directionIndex;
|
||||
candidate.points.reserve(33);
|
||||
std::int64_t previousGround=0,previousX=0,previousY=0;
|
||||
bool gradePass=true;
|
||||
for(std::int32_t index=0;index<=32;++index)
|
||||
{
|
||||
const std::int64_t along=(index-16)*waypointSpacing;
|
||||
const std::int64_t x=centerX+std::llround(
|
||||
static_cast<double>(along)*directions[directionIndex].x/65536.0);
|
||||
const std::int64_t y=centerY+std::llround(
|
||||
static_cast<double>(along)*directions[directionIndex].y/65536.0);
|
||||
const auto column=finalField.point(WorldAssemblyFixed(static_cast<double>(x)),
|
||||
WorldAssemblyFixed(static_cast<double>(y)),0);
|
||||
const std::int64_t horizontalQ=static_cast<std::int64_t>(std::llround(
|
||||
std::hypot(static_cast<double>(x-previousX),static_cast<double>(y-previousY))
|
||||
*WorldAssemblySubvoxelsPerVoxel));
|
||||
if(index>0)
|
||||
{
|
||||
const std::int64_t riseQ=std::abs(column.groundHeightSubvoxels-previousGround);
|
||||
if(horizontalQ==0||riseQ*65536>horizontalQ*GradeQ16)
|
||||
{gradePass=false;break;}
|
||||
candidate.score+=riseQ*65536/std::max<std::int64_t>(1,horizontalQ);
|
||||
}
|
||||
WorldAssemblyRoutePointV1 point;
|
||||
point.positionSubvoxels={WorldAssemblyFixed(static_cast<double>(x)),
|
||||
WorldAssemblyFixed(static_cast<double>(y)),column.groundHeightSubvoxels+HalfHeightQ};
|
||||
point.radiusSubvoxels=RouteRadiusQ;
|
||||
point.stableFeatureId=WorldAssemblyFeatureId(recipe.seed,
|
||||
static_cast<std::int64_t>(x),static_cast<std::int64_t>(y),
|
||||
0x52540000u+static_cast<std::uint32_t>(index));
|
||||
candidate.points.push_back(point);
|
||||
previousGround=column.groundHeightSubvoxels;previousX=x;previousY=y;
|
||||
}
|
||||
++tested;
|
||||
if(gradePass)candidates.push_back(std::move(candidate));
|
||||
else ++gradeRejected;
|
||||
}
|
||||
std::stable_sort(candidates.begin(),candidates.end(),[](const Candidate& a,const Candidate& b)
|
||||
{
|
||||
if(a.score!=b.score)return a.score<b.score;
|
||||
if(a.centerY!=b.centerY)return a.centerY<b.centerY;
|
||||
if(a.centerX!=b.centerX)return a.centerX<b.centerX;
|
||||
return a.direction<b.direction;
|
||||
});
|
||||
std::uint64_t sweepAttempt=0,repairAttempt=0;
|
||||
VoxelForgeCore::WorldAssemblyRouteAuditResult lastAudit{};
|
||||
for(auto& candidate:candidates)
|
||||
{
|
||||
stratum.route=candidate.points;
|
||||
std::int64_t liftQ=0;
|
||||
constexpr std::int64_t RepairStepQ=64;
|
||||
constexpr std::int64_t MaximumRepairLiftQ=SupportDepthQ-RepairStepQ;
|
||||
for(;;)
|
||||
{
|
||||
if(liftQ>0)
|
||||
for(std::size_t pointIndex=0;pointIndex<stratum.route.size();++pointIndex)
|
||||
stratum.route[pointIndex].positionSubvoxels[2]
|
||||
=candidate.points[pointIndex].positionSubvoxels[2]+liftQ;
|
||||
const Heightfield2D candidateField(recipe,stratum);
|
||||
const auto audit=AuditHeightfieldRoute(candidateField);
|
||||
lastAudit=audit;
|
||||
if(audit.passed)
|
||||
{
|
||||
if(liftQ>0)
|
||||
std::printf("[WorldAssemblerRouteRepaired] uniform_lift_q=%lld support_limit_q=%lld\n",
|
||||
static_cast<long long>(liftQ),static_cast<long long>(SupportDepthQ));
|
||||
return true;
|
||||
}
|
||||
if(audit.blockedCapsuleSamples==0||liftQ>=MaximumRepairLiftQ)break;
|
||||
const std::int64_t needed=std::max<std::int64_t>(1,
|
||||
constraints.minimumClearanceSubvoxels-audit.minimumClearanceSubvoxels);
|
||||
const std::int64_t steps=(needed+RepairStepQ-1)/RepairStepQ;
|
||||
const std::int64_t nextLift=std::max(liftQ+RepairStepQ,liftQ+steps*RepairStepQ);
|
||||
if(nextLift>MaximumRepairLiftQ)break;
|
||||
liftQ=nextLift;
|
||||
++repairAttempt;
|
||||
}
|
||||
if(sweepAttempt<3)
|
||||
std::printf("[WorldAssemblerRouteCandidateRejected] ordinal=%llu blocked=%llu unsupported=%llu grade_failures=%llu minimum_clearance_q=%lld\n",
|
||||
static_cast<unsigned long long>(sweepAttempt),
|
||||
static_cast<unsigned long long>(lastAudit.blockedCapsuleSamples),
|
||||
static_cast<unsigned long long>(lastAudit.unsupportedCenterSamples),
|
||||
static_cast<unsigned long long>(lastAudit.gradeFailures),
|
||||
static_cast<long long>(lastAudit.minimumClearanceSubvoxels));
|
||||
++sweepAttempt;
|
||||
++sweepRejected;
|
||||
}
|
||||
std::printf("[WorldAssemblerRouteSearchFailed] tested=%llu grade_rejected=%llu sweepable_candidates=%llu sweep_rejected=%llu last_blocked=%llu last_unsupported=%llu last_grade_failures=%llu\n",
|
||||
static_cast<unsigned long long>(tested),static_cast<unsigned long long>(gradeRejected),
|
||||
static_cast<unsigned long long>(candidates.size()),static_cast<unsigned long long>(sweepRejected),
|
||||
static_cast<unsigned long long>(lastAudit.blockedCapsuleSamples),
|
||||
static_cast<unsigned long long>(lastAudit.unsupportedCenterSamples),
|
||||
static_cast<unsigned long long>(lastAudit.gradeFailures));
|
||||
std::printf("[WorldAssemblerRouteRepairSummary] repair_attempts=%llu\n",
|
||||
static_cast<unsigned long long>(repairAttempt));
|
||||
stratum.route.clear();
|
||||
error="finite-strate route candidates failed the composed-field capsule and support sweep";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AttachRound1WorldAssemblyPrototype(FStandaloneOwnerWorld& owner,VFJson::Value& root,
|
||||
std::string& error,const std::string& manifestOutputPath)
|
||||
{
|
||||
if(!owner.Manager||!owner.Generator)
|
||||
{error="world assembler prototype needs the resolved standalone owner world";return false;}
|
||||
int32 surfaceChunkZ=0,topChunkZ=0,bottomChunkZ=0;
|
||||
bool foundSurface=false;
|
||||
for(int32 chunkZ=1024;chunkZ>=-4096;--chunkZ)
|
||||
{
|
||||
if(owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,chunkZ))
|
||||
!=ECaveGeneratorType::SurfaceWorld)continue;
|
||||
surfaceChunkZ=chunkZ;
|
||||
foundSurface=owner.Manager->GetStrateChunkZBounds(chunkZ,topChunkZ,bottomChunkZ);
|
||||
break;
|
||||
}
|
||||
if(!foundSurface)
|
||||
{error="world assembler prototype could not find a finite SurfaceWorld strate";return false;}
|
||||
const FIntVector chunk(0,0,surfaceChunkZ);
|
||||
const FSurfaceGenerationParams surface=owner.Manager->GetSurfaceParamsForChunk(chunk);
|
||||
const FBiomeContext biomeContext=owner.Manager->GetBiomeContextForChunk(chunk);
|
||||
const std::int64_t bottomVoxel=static_cast<std::int64_t>(bottomChunkZ)*CHUNK_SIZE;
|
||||
const std::int64_t heightVoxels=static_cast<std::int64_t>(topChunkZ-bottomChunkZ+1)*CHUNK_SIZE;
|
||||
const FBiomeMapParams& map=biomeContext.Map;
|
||||
const UVoxelStrateDefinition* definition=owner.Manager->GetStrateForChunk(chunk);
|
||||
|
||||
VoxelForgeCore::WorldAssemblyManifest manifest;
|
||||
auto& recipe=manifest.recipe;
|
||||
recipe.seed=static_cast<uint32>(owner.Seed);
|
||||
recipe.biomeCellSizeSubvoxels=WorldAssemblyFixed(map.CellSize);
|
||||
recipe.biomeWarpSubvoxels=WorldAssemblyFixed(map.WarpStrength);
|
||||
recipe.biomeWarpWavelengthSubvoxels=WorldAssemblyWavelength(map.WarpFrequency,556.0);
|
||||
recipe.biomeBorderBlendSubvoxels=WorldAssemblyFixed(map.BorderBlend);
|
||||
recipe.biomeClimateReliefWavelengthSubvoxels=WorldAssemblyWavelength(map.ReliefFrequency,3333.0);
|
||||
recipe.biomeClimateMoistureWavelengthSubvoxels=WorldAssemblyWavelength(map.MoistureFrequency,2500.0);
|
||||
recipe.biomeClimateReliefContrastQ16=WorldAssemblyQ16(map.ReliefContrast);
|
||||
const auto makeProfile=[&](uint32 stableId,const FSurfaceGenerationParams& params,
|
||||
const FBiomeResolved* climate)
|
||||
{
|
||||
VoxelForgeCore::WorldAssemblyBiomeProfileV1 profile;
|
||||
profile.stableId=stableId;
|
||||
profile.baseHeightSubvoxels=WorldAssemblyFixed(static_cast<double>(bottomVoxel)
|
||||
+static_cast<double>(heightVoxels)*params.BaseGroundRelative);
|
||||
profile.continentAmplitudeSubvoxels=WorldAssemblyFixed(params.ElevationRange);
|
||||
profile.continentWavelengthSubvoxels=WorldAssemblyWavelength(params.ContinentFrequency,1000.0);
|
||||
profile.mountainAmplitudeSubvoxels=WorldAssemblyFixed(params.ElevationRange*params.MountainStrength);
|
||||
profile.mountainWavelengthSubvoxels=WorldAssemblyWavelength(params.MountainFrequency,500.0);
|
||||
profile.detailAmplitudeSubvoxels=WorldAssemblyFixed(params.SurfaceRoughness);
|
||||
profile.detailWavelengthSubvoxels=WorldAssemblyWavelength(params.DetailFrequency,25.0);
|
||||
profile.reliefMinimumQ16=climate?WorldAssemblyQ16(climate->ReliefMin):0;
|
||||
profile.reliefMaximumQ16=climate?WorldAssemblyQ16(climate->ReliefMax):65536;
|
||||
profile.moistureMinimumQ16=climate?WorldAssemblyQ16(climate->MoistureMin):0;
|
||||
profile.moistureMaximumQ16=climate?WorldAssemblyQ16(climate->MoistureMax):65536;
|
||||
profile.reliefStrengthQ16=WorldAssemblyQ16(params.ReliefStrength);
|
||||
profile.reliefContrastQ16=WorldAssemblyQ16(params.ReliefContrast);
|
||||
profile.reliefWavelengthSubvoxels=WorldAssemblyWavelength(params.ReliefFrequency,667.0);
|
||||
profile.heightWarpStrengthSubvoxels=WorldAssemblyFixed(params.HeightWarpStrength);
|
||||
profile.heightWarpWavelengthSubvoxels=WorldAssemblyWavelength(params.HeightWarpFrequency,125.0);
|
||||
profile.beachWidthSubvoxels=WorldAssemblyFixed(params.BeachWidth);
|
||||
profile.cliffStrengthQ16=WorldAssemblyQ16(params.CliffStrength);
|
||||
profile.cliffSlopeThresholdQ16=std::max<std::int64_t>(1,WorldAssemblyQ16(params.CliffSlopeThreshold));
|
||||
profile.cliffSharpnessQ16=WorldAssemblyQ16(params.CliffSharpness);
|
||||
profile.cliffSampleDistanceSubvoxels=WorldAssemblyFixed(std::max(0.5f,params.CliffSampleDist));
|
||||
const double structuralAbsBound=static_cast<double>(params.ElevationRange)
|
||||
+static_cast<double>(params.ElevationRange)*params.MountainStrength
|
||||
+2.0*params.SurfaceRoughness;
|
||||
const double cliffAbsBound=2.0*structuralAbsBound*params.CliffStrength*params.CliffSharpness;
|
||||
profile.cliffMaximumDisplacementSubvoxels=WorldAssemblyFixed(params.CliffStrength>0.0f
|
||||
?std::max(1.0,cliffAbsBound):0.0);
|
||||
profile.hasCapProfile=true;
|
||||
profile.capHeightSubvoxels=WorldAssemblyFixed(static_cast<double>(bottomVoxel)
|
||||
+static_cast<double>(heightVoxels)*params.CeilingRelative);
|
||||
profile.capWarpStrengthSubvoxels=WorldAssemblyFixed(params.CeilingWarpStrength);
|
||||
profile.capWarpWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingWarpFrequency,100.0);
|
||||
profile.capUndulationSubvoxels=WorldAssemblyFixed(params.CeilingUndulation);
|
||||
profile.capWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingUndulationFrequency,500.0);
|
||||
profile.capDetailAmplitudeSubvoxels=WorldAssemblyFixed(params.CeilingRoughness);
|
||||
profile.capDetailWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingRoughnessFrequency,25.0);
|
||||
profile.capRidgeAmplitudeSubvoxels=WorldAssemblyFixed(params.CeilingRidgeStrength);
|
||||
profile.capRidgeWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingRidgeFrequency,50.0);
|
||||
return profile;
|
||||
};
|
||||
if(biomeContext.IsValid()&&definition)
|
||||
{
|
||||
for(const FBiomeResolved& resolved:biomeContext.Biomes)
|
||||
{
|
||||
const UVoxelBiomeDefinition* biome=definition->Biomes.IsValidIndex(resolved.Index)
|
||||
?definition->Biomes[resolved.Index]:nullptr;
|
||||
FSurfaceGenerationParams params=surface;
|
||||
if(biome&&biome->bOverrideTerrain&&biome->GeneratorType==ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
params=biome->SurfaceParams;
|
||||
params.StrateTopWorldZ=surface.StrateTopWorldZ;
|
||||
params.StrateBottomWorldZ=surface.StrateBottomWorldZ;
|
||||
params.BoundarySealThickness=surface.BoundarySealThickness;
|
||||
params.BaseDensity=surface.BaseDensity;
|
||||
params.WaterLevelRelative=surface.WaterLevelRelative;
|
||||
}
|
||||
recipe.biomes.push_back(makeProfile(0x1001u+static_cast<uint32>(resolved.Index),params,&resolved));
|
||||
}
|
||||
}
|
||||
if(recipe.biomes.empty())recipe.biomes.push_back(makeProfile(0x1001u,surface,nullptr));
|
||||
|
||||
VoxelForgeCore::WorldAssemblyStratumPlanV1 stratum;
|
||||
stratum.strateIndex=owner.Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(surfaceChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
|
||||
stratum.topChunkZ=topChunkZ;
|
||||
stratum.bottomChunkZ=bottomChunkZ;
|
||||
const double strateBottom=static_cast<double>(bottomVoxel);
|
||||
const double strateHeight=static_cast<double>(heightVoxels);
|
||||
stratum.capHeightSubvoxels=WorldAssemblyFixed(strateBottom+strateHeight*surface.CeilingRelative);
|
||||
stratum.capUndulationSubvoxels=WorldAssemblyFixed(surface.CeilingUndulation);
|
||||
stratum.capWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingUndulationFrequency,250.0);
|
||||
stratum.capDetailAmplitudeSubvoxels=WorldAssemblyFixed(surface.CeilingRoughness);
|
||||
stratum.capDetailWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingRoughnessFrequency,25.0);
|
||||
stratum.capRidgeAmplitudeSubvoxels=WorldAssemblyFixed(surface.CeilingRidgeStrength);
|
||||
stratum.capRidgeWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingRidgeFrequency,50.0);
|
||||
stratum.waterSurfaceSubvoxels=WorldAssemblyFixed(strateBottom+strateHeight*surface.WaterLevelRelative);
|
||||
stratum.waterEnabled=definition&&definition->bHasWater&&surface.WaterLevelRelative>0.0f;
|
||||
|
||||
// Select a deterministic steep edge in the resolved owner field for a bounded shelf.
|
||||
// Selection d'une pente deterministe dans le champ resolu pour une saillie bornee.
|
||||
constexpr int probeSide=49,probeStep=32;
|
||||
std::array<float,probeSide*probeSide> heights{};
|
||||
std::array<uint8,probeSide*probeSide> valid{};
|
||||
for(int y=0;y<probeSide;++y)
|
||||
for(int x=0;x<probeSide;++x)
|
||||
{
|
||||
const float px=static_cast<float>((x-probeSide/2)*probeStep);
|
||||
const float py=static_cast<float>((y-probeSide/2)*probeStep);
|
||||
float ceiling=0.0f;
|
||||
const std::size_t index=static_cast<std::size_t>(y)*probeSide+x;
|
||||
valid[index]=owner.Generator->GetSurfaceHeightAt(px,py,topChunkZ,heights[index],ceiling)?1u:0u;
|
||||
}
|
||||
double steepest=0.0;int cliffX=0,cliffY=0;bool xAxis=true;int downhill=1;
|
||||
for(int y=1;y<probeSide-1;++y)
|
||||
for(int x=1;x<probeSide-1;++x)
|
||||
{
|
||||
const std::size_t center=static_cast<std::size_t>(y)*probeSide+x;
|
||||
if(!valid[center]||!valid[center-1]||!valid[center+1]||!valid[center-probeSide]||!valid[center+probeSide])continue;
|
||||
const double dx=static_cast<double>(heights[center+1]-heights[center-1]);
|
||||
const double dy=static_cast<double>(heights[center+probeSide]-heights[center-probeSide]);
|
||||
const double slope=(std::abs(dx)+std::abs(dy))/(4.0*probeStep);
|
||||
if(slope>steepest){steepest=slope;cliffX=(x-probeSide/2)*probeStep;cliffY=(y-probeSide/2)*probeStep;
|
||||
xAxis=std::abs(dx)>=std::abs(dy);downhill=(xAxis?dx:dy)>=0.0?-1:1;}
|
||||
}
|
||||
if(steepest>0.05)
|
||||
{
|
||||
float ground=0.0f,ceiling=0.0f;
|
||||
if(owner.Generator->GetSurfaceHeightAt(static_cast<float>(cliffX),static_cast<float>(cliffY),
|
||||
topChunkZ,ground,ceiling))
|
||||
{
|
||||
VoxelForgeCore::WorldAssemblyFeatureV1 shelf;
|
||||
shelf.stableId=WorldAssemblyFeatureId(recipe.seed,cliffX,cliffY,0x4f564552u);
|
||||
shelf.type=4;
|
||||
constexpr int reach=72,width=36,height=64,feather=8;
|
||||
const int dirX=xAxis?downhill:0,dirY=xAxis?0:downhill;
|
||||
const int minX=xAxis?(dirX>0?cliffX-feather:cliffX-reach-feather)
|
||||
:cliffX-width-feather;
|
||||
const int maxX=xAxis?(dirX>0?cliffX+reach+feather:cliffX+feather)
|
||||
:cliffX+width+feather;
|
||||
const int minY=xAxis?cliffY-width-feather
|
||||
:(dirY>0?cliffY-feather:cliffY-reach-feather);
|
||||
const int maxY=xAxis?cliffY+width+feather
|
||||
:(dirY>0?cliffY+reach+feather:cliffY+feather);
|
||||
VoxelForgeCore::Heightfield2D baseField(recipe,stratum);
|
||||
double lowestGround=std::numeric_limits<double>::infinity();
|
||||
double highestGround=-std::numeric_limits<double>::infinity();
|
||||
for(int sampleY=minY;sampleY<=maxY;sampleY+=4)
|
||||
for(int sampleX=minX;sampleX<=maxX;sampleX+=4)
|
||||
{
|
||||
const auto sample=baseField.point(WorldAssemblyFixed(sampleX),
|
||||
WorldAssemblyFixed(sampleY),0);
|
||||
const double heightQ=static_cast<double>(sample.groundHeightSubvoxels)
|
||||
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
lowestGround=std::min(lowestGround,heightQ);
|
||||
highestGround=std::max(highestGround,heightQ);
|
||||
}
|
||||
if(!std::isfinite(lowestGround)||!std::isfinite(highestGround))
|
||||
{error="world assembler cliff shelf could not bound its terrain columns";return false;}
|
||||
shelf.minimumSubvoxels={WorldAssemblyFixed(minX),WorldAssemblyFixed(minY),
|
||||
WorldAssemblyFixed(std::max<double>(bottomVoxel,lowestGround-4.0))};
|
||||
shelf.maximumSubvoxels={WorldAssemblyFixed(maxX),WorldAssemblyFixed(maxY),
|
||||
WorldAssemblyFixed(std::min<double>((topChunkZ+1)*CHUNK_SIZE,
|
||||
highestGround+height+4.0))};
|
||||
shelf.parametersSubvoxels={WorldAssemblyFixed(dirX),WorldAssemblyFixed(dirY),
|
||||
WorldAssemblyFixed(height),WorldAssemblyFixed(reach)};
|
||||
stratum.features.push_back(shelf);
|
||||
}
|
||||
}
|
||||
std::uint64_t routeCandidates=0,routeGradeRejected=0,routeSweepRejected=0;
|
||||
if(!BuildFiniteStrateRoute(recipe,stratum,routeCandidates,routeGradeRejected,
|
||||
routeSweepRejected,error))return false;
|
||||
std::printf("[WorldAssemblerRoutePlan] candidates=%llu grade_rejected=%llu sweep_rejected=%llu accepted_points=%zu capsule_m=0.34x1.76 corridor_radius_vox=4 support_probe_points=5 minimum_support_probes=%u status=PASS\n",
|
||||
static_cast<unsigned long long>(routeCandidates),
|
||||
static_cast<unsigned long long>(routeGradeRejected),
|
||||
static_cast<unsigned long long>(routeSweepRejected),stratum.route.size(),
|
||||
stratum.routeConstraints.minimumSupportProbes);
|
||||
manifest.plan.strates.push_back(std::move(stratum));
|
||||
if(!manifest.finalize(error))return false;
|
||||
const std::string manifestText=manifest.ToJson();
|
||||
const auto roundTrip=VoxelForgeCore::WorldAssemblyManifest::FromJson(manifestText,error);
|
||||
if(!roundTrip||!roundTrip->validate(error)||roundTrip->ToJson()!=manifestText
|
||||
||roundTrip->contentHash!=manifest.contentHash)
|
||||
{if(error.empty())error="world assembly canonical plan serialization did not round-trip";return false;}
|
||||
std::printf("[WorldAssemblerPlanSerialization] bytes=%zu canonical_round_trip=true route_points=%zu plan_hash=%016llx manifest_hash=%016llx status=PASS\n",
|
||||
manifestText.size(),manifest.plan.strates.front().route.size(),
|
||||
static_cast<unsigned long long>(manifest.plan.contentHash),
|
||||
static_cast<unsigned long long>(manifest.contentHash));
|
||||
if(!manifestOutputPath.empty())
|
||||
{
|
||||
const std::filesystem::path outputPath(manifestOutputPath);
|
||||
std::error_code directoryError;
|
||||
if(!outputPath.parent_path().empty())
|
||||
std::filesystem::create_directories(outputPath.parent_path(),directoryError);
|
||||
if(directoryError)
|
||||
{error="could not create the world assembly manifest output directory";return false;}
|
||||
std::ofstream output(outputPath,std::ios::binary);
|
||||
if(!output)
|
||||
{error="could not open the world assembly manifest output path";return false;}
|
||||
output.write(manifestText.data(),static_cast<std::streamsize>(manifestText.size()));
|
||||
if(!output)
|
||||
{error="could not write the world assembly manifest payload";return false;}
|
||||
}
|
||||
VFJson::Value manifestJson;
|
||||
if(!VFJson::Parser(manifestText).Parse(manifestJson,error))return false;
|
||||
auto* rootObject=std::get_if<VFJson::Value::Object>(&root.Data);
|
||||
if(!rootObject)
|
||||
{error="owner snapshot root is not a JSON object";return false;}
|
||||
(*rootObject)["world_assembly"]=std::move(manifestJson);
|
||||
std::printf("[WorldAssemblerPrototype] recipe_version=%u evaluator_version=%u strata=%zu biomes=%zu strate_index=%d chunk_z=%d..%d serialized_bytes=%zu recipe_hash=%016llx plan_hash=%016llx manifest_hash=%016llx source=owner_surface_params_and_biome_map route_points=%zu features=%zu\n",
|
||||
manifest.recipe.schemaVersion,manifest.recipe.evaluatorVersion,manifest.plan.strates.size(),
|
||||
manifest.recipe.biomes.size(),manifest.plan.strates.front().strateIndex,topChunkZ,bottomChunkZ,
|
||||
manifestText.size(),static_cast<unsigned long long>(manifest.recipe.contentHash),
|
||||
static_cast<unsigned long long>(manifest.plan.contentHash),
|
||||
static_cast<unsigned long long>(manifest.contentHash),manifest.plan.strates.front().route.size(),
|
||||
manifest.plan.strates.front().features.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunWorldAssemblerCapAudit(FStandaloneOwnerWorld& owner,
|
||||
const VoxelForgeCore::ResolvedWorldSnapshot& snapshot,VoxelForgeCore::World& world,
|
||||
VoxelForgeCore::Vec3d walkCameraVoxels)
|
||||
@@ -9916,7 +9165,7 @@ int main(int Argc,char** Argv)
|
||||
if(App.bWorldAssemblerPrototype)
|
||||
{
|
||||
const auto PlanStart=std::chrono::steady_clock::now();
|
||||
const bool bPlanReady=AttachRound1WorldAssemblyPrototype(*App.World,App.OwnerJsonRoot,
|
||||
const bool bPlanReady=StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(*App.World,App.OwnerJsonRoot,
|
||||
Error,WorldAssemblyManifestOutputPath);
|
||||
const double PlanMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-PlanStart).count();
|
||||
@@ -9978,7 +9227,7 @@ int main(int Argc,char** Argv)
|
||||
if(bStreamBenchmark)
|
||||
{
|
||||
if(bStreamPipeline)
|
||||
return RunPipelinedStreamingBenchmark(App.World,App.Snapshot,
|
||||
return StandaloneStreamBenchmark::RunPipelinedStreamingBenchmark(App.World,App.Snapshot,
|
||||
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),
|
||||
App.WorkerCount,StreamBenchmarkMaxLevel,App.ShapeDetailVariant,App.CoreScheduler,
|
||||
bStreamHash||!BakeMeshCachePath.empty(),bSkipUvAndColors,bSkipMaterialGroups,
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
#include "OwnerWorldLoader.h"
|
||||
#include "StandaloneCoreScheduler.h"
|
||||
#include "StreamBenchmarkPipeline.h"
|
||||
#include "WorldAssemblyPrototype.h"
|
||||
#include "../Core/src/StandaloneSnapshotAdapter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct FOptions
|
||||
{
|
||||
std::filesystem::path DataPath;
|
||||
int WorkerCount=8;
|
||||
int ClipRadius=3;
|
||||
int MaxClipLevel=8;
|
||||
int MaxLevel=-1;
|
||||
int FarHorizonLOD8InteriorStepVoxels=0;
|
||||
bool WorldAssemblerPrototype=false;
|
||||
bool StreamBenchmark=false;
|
||||
bool StreamPipeline=false;
|
||||
bool StreamHash=false;
|
||||
bool ProfileColumnOverlap=false;
|
||||
bool ForceGenerate=false;
|
||||
bool HasCamera=false;
|
||||
FVector CameraMeters=FVector::ZeroVector;
|
||||
};
|
||||
|
||||
bool ParseInteger(std::string_view text,int& output)
|
||||
{
|
||||
if(text.empty())return false;
|
||||
char* end=nullptr;
|
||||
const std::string value(text);
|
||||
const long parsed=std::strtol(value.c_str(),&end,10);
|
||||
if(end==value.c_str()||*end!='\0'||parsed<INT32_MIN||parsed>INT32_MAX)return false;
|
||||
output=static_cast<int>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseCamera(std::string_view text,FVector& meters)
|
||||
{
|
||||
const std::string values(text);
|
||||
float x=0.0f,y=0.0f,z=0.0f,yaw=0.0f,pitch=0.0f;
|
||||
if(std::sscanf(values.c_str(),"%f,%f,%f,%f,%f",&x,&y,&z,&yaw,&pitch)!=5)
|
||||
return false;
|
||||
meters={x,y,z};
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseOptions(int argc,char** argv,FOptions& options)
|
||||
{
|
||||
const std::filesystem::path executable=std::filesystem::absolute(argv[0]);
|
||||
options.DataPath=executable.parent_path().parent_path()/"data/owner_world.json";
|
||||
for(int index=1;index<argc;++index)
|
||||
{
|
||||
const std::string_view arg(argv[index]);
|
||||
const auto readInt=[&](std::string_view prefix,int& value)
|
||||
{return arg.starts_with(prefix)&&ParseInteger(arg.substr(prefix.size()),value);};
|
||||
if(arg=="--help"||arg=="-h")
|
||||
{
|
||||
std::printf("Usage: %s [owner_world.json] --world-assembler-prototype --stream-benchmark --stream-pipeline [options]\n"
|
||||
" --workers=N --clip-radius=N --max-clip-level=N --max-level=N\n"
|
||||
" --camera=X,Y,Z,YAW,PITCH (position in metres) --stream-hash\n"
|
||||
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
||||
" --process-affinity=MASK --process-priority=Normal --frame-limit=FPS (viewer compatibility only)\n",
|
||||
argv[0]);
|
||||
return false;
|
||||
}
|
||||
if(arg=="--world-assembler-prototype")options.WorldAssemblerPrototype=true;
|
||||
else if(arg=="--stream-benchmark")options.StreamBenchmark=true;
|
||||
else if(arg=="--stream-pipeline")options.StreamPipeline=true;
|
||||
else if(arg=="--stream-hash")options.StreamHash=true;
|
||||
else if(arg=="--profile-column-overlap")options.ProfileColumnOverlap=true;
|
||||
else if(arg=="--force-generate")options.ForceGenerate=true;
|
||||
else if(arg.starts_with("--camera="))
|
||||
{
|
||||
if(!ParseCamera(arg.substr(9),options.CameraMeters))
|
||||
{std::fprintf(stderr,"Invalid --camera value: %.*s\n",static_cast<int>(arg.size()),arg.data());return false;}
|
||||
options.HasCamera=true;
|
||||
}
|
||||
else if(arg.starts_with("--workers="))
|
||||
{if(!readInt("--workers=",options.WorkerCount))return false;}
|
||||
else if(arg.starts_with("--clip-radius="))
|
||||
{if(!readInt("--clip-radius=",options.ClipRadius))return false;}
|
||||
else if(arg.starts_with("--max-clip-level="))
|
||||
{if(!readInt("--max-clip-level=",options.MaxClipLevel))return false;}
|
||||
else if(arg.starts_with("--max-level="))
|
||||
{if(!readInt("--max-level=",options.MaxLevel))return false;}
|
||||
else if(arg.starts_with("--far-horizon-lod8-step="))
|
||||
{if(!readInt("--far-horizon-lod8-step=",options.FarHorizonLOD8InteriorStepVoxels))return false;}
|
||||
else if(arg.starts_with("--process-affinity=")||arg.starts_with("--process-priority=")
|
||||
||arg.starts_with("--frame-limit="))
|
||||
{
|
||||
// These viewer switches are accepted for command-line parity. The caller pins
|
||||
// this headless process with taskset, and a headless run has no frame limiter.
|
||||
}
|
||||
else if(!arg.empty()&&arg.front()!='-')options.DataPath=std::filesystem::path(arg);
|
||||
else
|
||||
{std::fprintf(stderr,"Unknown option: %.*s\n",static_cast<int>(arg.size()),arg.data());return false;}
|
||||
}
|
||||
options.WorkerCount=std::clamp(options.WorkerCount,1,16);
|
||||
options.ClipRadius=std::clamp(options.ClipRadius,1,64);
|
||||
options.MaxClipLevel=std::clamp(options.MaxClipLevel,0,8);
|
||||
options.FarHorizonLOD8InteriorStepVoxels=std::max(0,options.FarHorizonLOD8InteriorStepVoxels);
|
||||
if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline)
|
||||
{
|
||||
std::fprintf(stderr,"This headless target runs the world-assembler streaming pipeline; pass --world-assembler-prototype --stream-benchmark --stream-pipeline.\n");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc,char** argv)
|
||||
{
|
||||
using Clock=std::chrono::steady_clock;
|
||||
const auto coldStart=Clock::now();
|
||||
FOptions options;
|
||||
if(!ParseOptions(argc,argv,options))return 2;
|
||||
std::printf("[HeadlessConfig] data=%s workers=%d clip_radius=%d max_clip_level=%d max_level=%d hash=%s column_overlap=%s force_generate=%s\n",
|
||||
options.DataPath.string().c_str(),options.WorkerCount,options.ClipRadius,options.MaxClipLevel,
|
||||
options.MaxLevel,options.StreamHash?"on":"off",options.ProfileColumnOverlap?"on":"off",
|
||||
options.ForceGenerate?"on":"off");
|
||||
std::printf("[ColdTimeline] stage=owner_data_load begin_ms=%.3f\n",
|
||||
std::chrono::duration<double,std::milli>(Clock::now()-coldStart).count());
|
||||
|
||||
std::string error;
|
||||
auto ownerWorld=std::make_shared<FStandaloneOwnerWorld>();
|
||||
VFJson::Value ownerJsonRoot;
|
||||
{
|
||||
std::ifstream jsonFile(options.DataPath,std::ios::binary);
|
||||
if(!jsonFile)
|
||||
{std::fprintf(stderr,"Owner data load failed: cannot open %s\n",options.DataPath.string().c_str());return 2;}
|
||||
std::string ownerJsonText{std::istreambuf_iterator<char>(jsonFile),std::istreambuf_iterator<char>{}};
|
||||
if(!jsonFile&&!jsonFile.eof())
|
||||
{std::fprintf(stderr,"Owner data load failed: read error for %s\n",options.DataPath.string().c_str());return 2;}
|
||||
if(!VFJson::Parser(std::move(ownerJsonText)).Parse(ownerJsonRoot,error))
|
||||
{std::fprintf(stderr,"Owner data load failed: %s\n",error.c_str());return 2;}
|
||||
}
|
||||
if(!StandaloneOwnerLoader::Resolve(ownerJsonRoot,*ownerWorld,error))
|
||||
{std::fprintf(stderr,"Owner data load failed: %s\n",error.c_str());return 2;}
|
||||
std::printf("[ColdTimeline] stage=owner_data_load end_ms=%.3f\n",
|
||||
std::chrono::duration<double,std::milli>(Clock::now()-coldStart).count());
|
||||
|
||||
const int seed=ownerWorld->Seed;
|
||||
const int exportedMaxClipLevel=ownerWorld->MaxClipLevel;
|
||||
ownerWorld->ClipRadius=options.ClipRadius;
|
||||
ownerWorld->Settings->ClipRadius=options.ClipRadius;
|
||||
ownerWorld->MaxClipLevel=options.MaxClipLevel;
|
||||
ownerWorld->Settings->MaxClipLevel=options.MaxClipLevel;
|
||||
if(exportedMaxClipLevel!=options.MaxClipLevel)
|
||||
std::printf("Standalone max clip level override: exported=%d -> max_level=%d.\n",
|
||||
exportedMaxClipLevel,options.MaxClipLevel);
|
||||
|
||||
FVector cameraVoxels=ownerWorld->ViewerStartVoxels;
|
||||
if(options.HasCamera)
|
||||
cameraVoxels=FVector(options.CameraMeters.X*4.0f,options.CameraMeters.Y*4.0f,options.CameraMeters.Z*4.0f);
|
||||
|
||||
const auto planStart=Clock::now();
|
||||
if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
||||
*ownerWorld,ownerJsonRoot,error,{}))
|
||||
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;}
|
||||
std::printf("[WorldAssemblerPlanTiming] total_ms=%.3f status=PASS\n",
|
||||
std::chrono::duration<double,std::milli>(Clock::now()-planStart).count());
|
||||
|
||||
auto snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(ownerJsonRoot,seed,
|
||||
options.ClipRadius,options.MaxClipLevel,false,false,error);
|
||||
if(!snapshot)
|
||||
{std::fprintf(stderr,"Shape interval build failed: %s\n",error.c_str());return 2;}
|
||||
|
||||
FStandaloneCoreScheduler scheduler;
|
||||
return StandaloneStreamBenchmark::RunPipelinedStreamingBenchmark(ownerWorld,snapshot,
|
||||
cameraVoxels,options.WorkerCount,options.MaxLevel,VoxelForgeCore::DetailVariant::coherent,
|
||||
scheduler,options.StreamHash,false,false,false,false,coldStart,
|
||||
options.ProfileColumnOverlap,options.FarHorizonLOD8InteriorStepVoxels);
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
#pragma once
|
||||
|
||||
#include "OwnerWorldLoader.h"
|
||||
#include "StreamingWorld.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace StandaloneStreamBenchmark
|
||||
{
|
||||
inline void HashWord(uint64& Hash,uint64 Word,int32 Bytes=8)
|
||||
{
|
||||
for(int32 I=0;I<Bytes;++I)
|
||||
{
|
||||
Hash^=static_cast<uint8>(Word>>(I*8));
|
||||
Hash*=1099511628211ull;
|
||||
}
|
||||
}
|
||||
|
||||
struct FHashedTile
|
||||
{
|
||||
FVoxelTileKey Key;
|
||||
uint64 Hash=14695981039346656037ull;
|
||||
};
|
||||
|
||||
inline bool TileKeyLessForHash(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;
|
||||
}
|
||||
|
||||
inline uint64 HashTileRows(std::vector<FHashedTile>& Rows)
|
||||
{
|
||||
std::sort(Rows.begin(),Rows.end(),[](const FHashedTile& A,const FHashedTile& B)
|
||||
{return TileKeyLessForHash(A.Key,B.Key);});
|
||||
uint64 Hash=14695981039346656037ull;
|
||||
for(const FHashedTile& Row:Rows)
|
||||
{
|
||||
HashWord(Hash,static_cast<uint32>(Row.Key.Level),4);
|
||||
HashWord(Hash,static_cast<uint32>(Row.Key.Coord.X),4);
|
||||
HashWord(Hash,static_cast<uint32>(Row.Key.Coord.Y),4);
|
||||
HashWord(Hash,static_cast<uint32>(Row.Key.Coord.Z),4);
|
||||
HashWord(Hash,Row.Hash);
|
||||
}
|
||||
return Hash;
|
||||
}
|
||||
|
||||
inline int RunPipelinedStreamingBenchmark(std::shared_ptr<FStandaloneOwnerWorld> OwnerWorld,
|
||||
const VoxelForgeCore::ResolvedWorldSnapshot& CoreSnapshot,
|
||||
const FVector& CameraPositionVoxels,int32 WorkerCount,int32 MaxLevel,
|
||||
VoxelForgeCore::DetailVariant DetailVariant,const VoxelForgeCore::JobScheduler& Scheduler,
|
||||
bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,bool bSkipUV,bool bSkipColors,
|
||||
std::chrono::steady_clock::time_point ColdStart,bool bProfileColumnOverlap,
|
||||
int32 FarHorizonLOD8InteriorStepVoxels)
|
||||
{
|
||||
using Clock=std::chrono::steady_clock;
|
||||
FStandaloneOwnerWorld& World=*OwnerWorld;
|
||||
const bool bRoundTunnels=World.Generator->bStandaloneResearchRoundTunnels;
|
||||
const bool bEnhancedTunnelWalls=World.Generator->bStandaloneResearchEnhancedWalls;
|
||||
VoxelForgeCore::World Shape(CoreSnapshot);
|
||||
FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels);
|
||||
const auto CoreTiles=Shape.desiredTiles(
|
||||
{CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z});
|
||||
Desired.Tiles.clear();Desired.Tiles.reserve(CoreTiles.size());
|
||||
for(const auto& Key:CoreTiles)
|
||||
Desired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
|
||||
if(MaxLevel>=0)
|
||||
Desired.Tiles.erase(std::remove_if(Desired.Tiles.begin(),Desired.Tiles.end(),
|
||||
[&](const FVoxelTileKey& Tile){return Tile.Level>MaxLevel;}),Desired.Tiles.end());
|
||||
FIntVector FineMin(INT32_MAX,INT32_MAX,INT32_MAX),FineMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
||||
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
{
|
||||
const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
|
||||
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
|
||||
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
|
||||
if(Tile.Level<World.FullResClipLevels)
|
||||
{
|
||||
FineMin.X=std::min(FineMin.X,Origin.X);FineMin.Y=std::min(FineMin.Y,Origin.Y);FineMin.Z=std::min(FineMin.Z,Origin.Z);
|
||||
FineMax.X=std::max(FineMax.X,Origin.X+Extent);FineMax.Y=std::max(FineMax.Y,Origin.Y+Extent);FineMax.Z=std::max(FineMax.Z,Origin.Z+Extent);
|
||||
}
|
||||
}
|
||||
if(FineMin.X==INT32_MAX||ClipMin.X==INT32_MAX)
|
||||
{std::fprintf(stderr,"[StreamPipeline] no desired detail or clipmap tiles.\n");return 7;}
|
||||
|
||||
const FIntVector CenterChunk=Desired.CenterChunk;
|
||||
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Neighborhood;
|
||||
Neighborhood.reserve(32);
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
if(Tile.Level==0&&std::abs(Tile.Coord.X-CenterChunk.X)<=1
|
||||
&&std::abs(Tile.Coord.Y-CenterChunk.Y)<=1&&std::abs(Tile.Coord.Z-CenterChunk.Z)<=1)
|
||||
Neighborhood.insert(Tile);
|
||||
if(Neighborhood.size()!=27)
|
||||
{std::fprintf(stderr,"[StreamPipeline] expected 27 player-neighborhood tiles, found %zu.\n",Neighborhood.size());return 7;}
|
||||
|
||||
// The initial desired set now owns all clipmap metadata needed by this Core-only run.
|
||||
// Release the separate compatibility-world generation graph before compiling Core.
|
||||
World.Generator.reset();
|
||||
World.Manager.reset();
|
||||
World.DefinitionStorage.clear();
|
||||
World.BiomeStorage.clear();
|
||||
World.OperationStorage.clear();
|
||||
World.DensitySamples.clear();
|
||||
|
||||
std::string BuildError;
|
||||
const bool bIncludeFarWorld=MaxLevel<0||MaxLevel>=World.FullResClipLevels;
|
||||
const auto ShapeBuildStart=Clock::now();
|
||||
VoxelForgeCore::ShapeBuildRequest ShapeRequest;
|
||||
ShapeRequest.minVoxel={FineMin.X,FineMin.Y,FineMin.Z};
|
||||
ShapeRequest.maxVoxel={FineMax.X,FineMax.Y,FineMax.Z};
|
||||
ShapeRequest.clipMinVoxel={ClipMin.X,ClipMin.Y,ClipMin.Z};
|
||||
ShapeRequest.clipMaxVoxel={ClipMax.X,ClipMax.Y,ClipMax.Z};
|
||||
ShapeRequest.workerLimit=static_cast<uint32>(std::clamp(WorkerCount,1,16));
|
||||
ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld;
|
||||
ShapeRequest.roundTunnels=bRoundTunnels;
|
||||
ShapeRequest.enhancedTunnelWalls=bEnhancedTunnelWalls;
|
||||
if(!Shape.compile(ShapeRequest,Scheduler,BuildError))
|
||||
{std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",BuildError.c_str());return 2;}
|
||||
const auto ShapeReady=Clock::now();
|
||||
const double ShapeSeconds=std::chrono::duration<double>(ShapeReady-ShapeBuildStart).count();
|
||||
FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
|
||||
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap,
|
||||
FarHorizonLOD8InteriorStepVoxels);
|
||||
const auto StreamStart=Clock::now();
|
||||
const FStandaloneStreamingUpdate Update=Streamer.StartWithDesired(Desired);
|
||||
if(Update.DesiredTiles!=static_cast<int32>(Desired.Tiles.size()))
|
||||
{
|
||||
Streamer.Stop();
|
||||
std::fprintf(stderr,"[StreamPipeline] expected %zu tiles but queued %d.\n",
|
||||
Desired.Tiles.size(),Update.DesiredTiles);return 7;
|
||||
}
|
||||
|
||||
std::vector<FHashedTile> DensityRows,MeshRows,PayloadRows,TopologyRows;
|
||||
int32 Finished=0,NeighborhoodFinished=0,FullDetailFinished=0;
|
||||
int32 FullDetailExpected=0;
|
||||
struct FColumnXY
|
||||
{
|
||||
std::int64_t X=0,Y=0;
|
||||
bool operator==(const FColumnXY&)const=default;
|
||||
};
|
||||
struct FColumnXYHash
|
||||
{
|
||||
std::size_t operator()(const FColumnXY& Value)const noexcept
|
||||
{
|
||||
const std::size_t X=std::hash<std::int64_t>{}(Value.X);
|
||||
const std::size_t Y=std::hash<std::int64_t>{}(Value.Y);
|
||||
return X^(Y+static_cast<std::size_t>(0x9E3779B9u)+(X<<6)+(X>>2));
|
||||
}
|
||||
};
|
||||
struct FFirstColumnUse
|
||||
{
|
||||
FVoxelTileKey Tile;
|
||||
std::uint64_t WorkerHash=0;
|
||||
};
|
||||
std::array<std::unordered_map<FColumnXY,std::vector<FFirstColumnUse>,FColumnXYHash>,9> ColumnUsesByLOD;
|
||||
std::array<std::uint64_t,9> ColumnDuplicateEventsByLOD{},NeighbourDuplicateEventsByLOD{};
|
||||
std::array<std::uint64_t,9> CrossWorkerNeighbourDuplicateEventsByLOD{};
|
||||
std::array<std::uint64_t,9> ColumnCaptureTilesByLOD{},ExpectedTilesByLOD{};
|
||||
std::array<bool,9> ColumnCaptureCompleteByLOD{};
|
||||
ColumnCaptureCompleteByLOD.fill(true);
|
||||
std::array<double,9> WorkerSecondsByLOD{};
|
||||
std::array<double,9> ColumnEvalMsByLOD{},MarchingExclusiveMsByLOD{};
|
||||
std::array<double,9> GroundRefineExclusiveMsByLOD{},CapRefineExclusiveMsByLOD{};
|
||||
std::array<double,9> MeshAssemblyMsByLOD{};
|
||||
std::array<std::uint64_t,9> TileCountByLOD{};
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
if(Tile.Level>=0&&Tile.Level<static_cast<int32>(ExpectedTilesByLOD.size()))
|
||||
++ExpectedTilesByLOD[static_cast<std::size_t>(Tile.Level)];
|
||||
const auto AreSameLODNeighbours=[](const FVoxelTileKey& A,const FVoxelTileKey& B)
|
||||
{
|
||||
if(A.Level!=B.Level)return false;
|
||||
const std::int64_t DX=std::abs(static_cast<std::int64_t>(A.Coord.X)-B.Coord.X);
|
||||
const std::int64_t DY=std::abs(static_cast<std::int64_t>(A.Coord.Y)-B.Coord.Y);
|
||||
const std::int64_t DZ=std::abs(static_cast<std::int64_t>(A.Coord.Z)-B.Coord.Z);
|
||||
return std::max({DX,DY,DZ})<=1&&(DX+DY+DZ)>0;
|
||||
};
|
||||
uint64 NativeSolidBoxes=0,NativeAirBoxes=0,NativeMixedBoxes=0,NativeUnknownBoxes=0;
|
||||
uint64 NativeFieldCalls=0,NativeColumnHits=0,NativeColumnMisses=0,NativeMeshingSkips=0;
|
||||
double NativeClassificationMilliseconds=0.0,NativeColumnComputeMilliseconds=0.0;
|
||||
double NativeMeshingMilliseconds=0.0;
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
if(Tile.Level<World.FullResClipLevels)++FullDetailExpected;
|
||||
Clock::time_point NeighborhoodReady{},FullDetailReady{},WholeViewReady{};
|
||||
bool bNeighborhoodReported=false,bFullDetailReported=false;
|
||||
while(Finished<Update.DesiredTiles)
|
||||
{
|
||||
std::vector<FStandaloneCpuTile> Ready=Streamer.TakeReady();
|
||||
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
|
||||
for(const FStandaloneCpuTile& Tile:Ready)
|
||||
{
|
||||
++Finished;
|
||||
if(Tile.Key.Level>=0&&Tile.Key.Level<static_cast<int32>(TileCountByLOD.size()))
|
||||
{
|
||||
const std::size_t LOD=static_cast<std::size_t>(Tile.Key.Level);
|
||||
++TileCountByLOD[LOD];
|
||||
WorkerSecondsByLOD[LOD]+=Tile.WorkerSeconds;
|
||||
ColumnEvalMsByLOD[LOD]+=Tile.CoreDiagnostics.columnEvaluationExclusiveMilliseconds;
|
||||
MarchingExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.marchingCubesExclusiveMilliseconds;
|
||||
GroundRefineExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.groundRefinementExclusiveMilliseconds;
|
||||
CapRefineExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.capRefinementExclusiveMilliseconds;
|
||||
MeshAssemblyMsByLOD[LOD]+=Tile.CoreDiagnostics.meshAssemblyMilliseconds;
|
||||
}
|
||||
if(bProfileColumnOverlap&&Tile.Key.Level>=0
|
||||
&&Tile.Key.Level<static_cast<int32>(ColumnCaptureTilesByLOD.size()))
|
||||
{
|
||||
const std::size_t LOD=static_cast<std::size_t>(Tile.Key.Level);
|
||||
++ColumnCaptureTilesByLOD[LOD];
|
||||
ColumnCaptureCompleteByLOD[LOD]=ColumnCaptureCompleteByLOD[LOD]
|
||||
&&Tile.CoreDiagnostics.columnCoordinateCaptureComplete;
|
||||
std::vector<std::array<std::int64_t,2>> Coordinates=
|
||||
Tile.CoreDiagnostics.evaluatedColumnCoordinates;
|
||||
std::sort(Coordinates.begin(),Coordinates.end());
|
||||
Coordinates.erase(std::unique(Coordinates.begin(),Coordinates.end()),Coordinates.end());
|
||||
for(const auto& Coordinate:Coordinates)
|
||||
{
|
||||
const FColumnXY XY{Coordinate[0],Coordinate[1]};
|
||||
auto [Uses,Inserted]=ColumnUsesByLOD[LOD].try_emplace(XY);
|
||||
std::vector<FFirstColumnUse>& PriorUses=Uses->second;
|
||||
if(Inserted)
|
||||
{
|
||||
PriorUses.push_back({Tile.Key,Tile.CoreDiagnostics.columnEvaluationWorkerHash});
|
||||
continue;
|
||||
}
|
||||
++ColumnDuplicateEventsByLOD[LOD];
|
||||
bool bHasNeighbourDuplicate=false,bHasCrossWorkerNeighbourDuplicate=false;
|
||||
for(const FFirstColumnUse& Prior:PriorUses)
|
||||
{
|
||||
if(!AreSameLODNeighbours(Prior.Tile,Tile.Key))continue;
|
||||
bHasNeighbourDuplicate=true;
|
||||
bHasCrossWorkerNeighbourDuplicate=bHasCrossWorkerNeighbourDuplicate
|
||||
||Prior.WorkerHash!=Tile.CoreDiagnostics.columnEvaluationWorkerHash;
|
||||
}
|
||||
if(bHasNeighbourDuplicate)++NeighbourDuplicateEventsByLOD[LOD];
|
||||
if(bHasCrossWorkerNeighbourDuplicate)
|
||||
++CrossWorkerNeighbourDuplicateEventsByLOD[LOD];
|
||||
PriorUses.push_back({Tile.Key,Tile.CoreDiagnostics.columnEvaluationWorkerHash});
|
||||
}
|
||||
}
|
||||
WholeViewReady=std::max(WholeViewReady,Tile.CompletedAt);
|
||||
if(Neighborhood.contains(Tile.Key))
|
||||
{
|
||||
++NeighborhoodFinished;
|
||||
NeighborhoodReady=std::max(NeighborhoodReady,Tile.CompletedAt);
|
||||
}
|
||||
if(Tile.Key.Level<World.FullResClipLevels)
|
||||
{
|
||||
++FullDetailFinished;
|
||||
FullDetailReady=std::max(FullDetailReady,Tile.CompletedAt);
|
||||
NativeSolidBoxes+=Tile.CoreDiagnostics.solidBoxes;
|
||||
NativeAirBoxes+=Tile.CoreDiagnostics.airBoxes;
|
||||
NativeMixedBoxes+=Tile.CoreDiagnostics.mixedBoxes;
|
||||
NativeUnknownBoxes+=Tile.CoreDiagnostics.unknownBoxes;
|
||||
NativeFieldCalls+=Tile.CoreDiagnostics.fieldCalls;
|
||||
NativeColumnHits+=Tile.CoreDiagnostics.columnCacheHits;
|
||||
NativeColumnMisses+=Tile.CoreDiagnostics.columnCacheMisses;
|
||||
NativeMeshingSkips+=Tile.CoreDiagnostics.meshingSkipped?1u:0u;
|
||||
NativeClassificationMilliseconds+=Tile.CoreDiagnostics.classificationMilliseconds;
|
||||
NativeColumnComputeMilliseconds+=Tile.CoreDiagnostics.columnComputeMilliseconds;
|
||||
NativeMeshingMilliseconds+=Tile.CoreDiagnostics.meshingMilliseconds;
|
||||
}
|
||||
if(bHashDensity)
|
||||
{
|
||||
if(Tile.bHasDensityHash)DensityRows.push_back({Tile.Key,Tile.DensityHash});
|
||||
if(Tile.bHasMeshHash)MeshRows.push_back({Tile.Key,Tile.MeshHash});
|
||||
if(Tile.bHasPayloadHash)PayloadRows.push_back({Tile.Key,Tile.PayloadHash});
|
||||
if(Tile.bHasTopologyHash)TopologyRows.push_back({Tile.Key,Tile.TopologyHash});
|
||||
}
|
||||
}
|
||||
if(!bNeighborhoodReported&&NeighborhoodFinished==static_cast<int32>(Neighborhood.size()))
|
||||
{
|
||||
std::printf("[PipelineMilestone] neighborhood_ready=%.3fs generated=%d/27\n",
|
||||
std::chrono::duration<double>(NeighborhoodReady-ColdStart).count(),NeighborhoodFinished);
|
||||
bNeighborhoodReported=true;
|
||||
}
|
||||
if(!bFullDetailReported&&FullDetailFinished==FullDetailExpected)
|
||||
{
|
||||
std::printf("[PipelineMilestone] full_detail_ready=%.3fs generated=%d/%d\n",
|
||||
std::chrono::duration<double>(FullDetailReady-ColdStart).count(),
|
||||
FullDetailFinished,FullDetailExpected);
|
||||
bFullDetailReported=true;
|
||||
}
|
||||
}
|
||||
Streamer.Stop();
|
||||
bool bColumnOverlapCapturePassing=true;
|
||||
if(bProfileColumnOverlap)
|
||||
{
|
||||
for(std::size_t LOD=0;LOD<ColumnCaptureTilesByLOD.size();++LOD)
|
||||
{
|
||||
const bool bLODComplete=ColumnCaptureCompleteByLOD[LOD]
|
||||
&&ColumnCaptureTilesByLOD[LOD]==ExpectedTilesByLOD[LOD];
|
||||
bColumnOverlapCapturePassing=bColumnOverlapCapturePassing&&bLODComplete;
|
||||
std::printf("[WorldAssemblerLODColumnOverlap] lod=%zu tiles=%llu expected_tiles=%llu unique_xy_columns=%zu repeated_tile_evaluations=%llu neighbouring_duplicate_evaluations=%llu cross_worker_neighbouring_duplicates=%llu capture_complete=%d\n",
|
||||
LOD,static_cast<unsigned long long>(ColumnCaptureTilesByLOD[LOD]),
|
||||
static_cast<unsigned long long>(ExpectedTilesByLOD[LOD]),
|
||||
ColumnUsesByLOD[LOD].size(),
|
||||
static_cast<unsigned long long>(ColumnDuplicateEventsByLOD[LOD]),
|
||||
static_cast<unsigned long long>(NeighbourDuplicateEventsByLOD[LOD]),
|
||||
static_cast<unsigned long long>(CrossWorkerNeighbourDuplicateEventsByLOD[LOD]),
|
||||
bLODComplete?1:0);
|
||||
}
|
||||
}
|
||||
for(std::size_t LOD=0;LOD<TileCountByLOD.size();++LOD)
|
||||
{
|
||||
std::printf("[WorldAssemblerLODWorker] lod=%zu tiles=%llu worker_sum_s=%.6f column_eval_exclusive_ms=%.3f mc_exclusive_ms=%.3f ground_refine_exclusive_ms=%.3f cap_refine_exclusive_ms=%.3f mesh_assembly_ms=%.3f\n",
|
||||
LOD,static_cast<unsigned long long>(TileCountByLOD[LOD]),WorkerSecondsByLOD[LOD],
|
||||
ColumnEvalMsByLOD[LOD],MarchingExclusiveMsByLOD[LOD],
|
||||
GroundRefineExclusiveMsByLOD[LOD],CapRefineExclusiveMsByLOD[LOD],MeshAssemblyMsByLOD[LOD]);
|
||||
}
|
||||
const double NeighborhoodSeconds=std::chrono::duration<double>(NeighborhoodReady-ColdStart).count();
|
||||
const double NeighborhoodMeshSeconds=std::chrono::duration<double>(NeighborhoodReady-ShapeReady).count();
|
||||
const double FullDetailSeconds=std::chrono::duration<double>(FullDetailReady-ColdStart).count();
|
||||
const double WholeViewSeconds=std::chrono::duration<double>(WholeViewReady-ColdStart).count();
|
||||
const double StreamSeconds=std::chrono::duration<double>(WholeViewReady-StreamStart).count();
|
||||
std::printf("[StreamPipeline] workers=%d shape=%.3fs neighborhood_mesh=%.3fs neighborhood_ready=%.3fs full_detail_ready=%.3fs whole_view_ready=%.3fs neighborhood_tiles=%d/27 full_detail_tiles=%d/%d whole_tiles=%d/%zu stream_wall=%.3fs cache_hits=0 full_payload=1\n",
|
||||
WorkerCount,ShapeSeconds,NeighborhoodMeshSeconds,NeighborhoodSeconds,
|
||||
FullDetailSeconds,WholeViewSeconds,NeighborhoodFinished,
|
||||
FullDetailFinished,FullDetailExpected,Finished,Desired.Tiles.size(),StreamSeconds);
|
||||
std::printf("[WorldAssemblerTilePipeline] full_detail_tiles=%d solid_boxes=%llu air_boxes=%llu mixed_boxes=%llu unknown_boxes=%llu whole_tile_skips=%llu field_calls=%llu column_cache_hits=%llu column_cache_misses=%llu column_compute_worker_ms=%.3f classification_worker_ms=%.3f meshing_worker_ms=%.3f\n",
|
||||
FullDetailExpected,static_cast<unsigned long long>(NativeSolidBoxes),
|
||||
static_cast<unsigned long long>(NativeAirBoxes),
|
||||
static_cast<unsigned long long>(NativeMixedBoxes),
|
||||
static_cast<unsigned long long>(NativeUnknownBoxes),
|
||||
static_cast<unsigned long long>(NativeMeshingSkips),
|
||||
static_cast<unsigned long long>(NativeFieldCalls),
|
||||
static_cast<unsigned long long>(NativeColumnHits),
|
||||
static_cast<unsigned long long>(NativeColumnMisses),
|
||||
NativeColumnComputeMilliseconds,NativeClassificationMilliseconds,
|
||||
NativeMeshingMilliseconds);
|
||||
|
||||
bool bHashesPassing=true;
|
||||
if(bHashDensity)
|
||||
{
|
||||
bHashesPassing=DensityRows.size()==Desired.Tiles.size()
|
||||
&&MeshRows.size()==Desired.Tiles.size()
|
||||
&&PayloadRows.size()==Desired.Tiles.size()
|
||||
&&TopologyRows.size()==Desired.Tiles.size();
|
||||
std::vector<FHashedTile> FullDensity,FullMesh,FullPayload,FullTopology;
|
||||
for(const FHashedTile& Row:DensityRows)if(Row.Key.Level<World.FullResClipLevels)FullDensity.push_back(Row);
|
||||
for(const FHashedTile& Row:MeshRows)if(Row.Key.Level<World.FullResClipLevels)FullMesh.push_back(Row);
|
||||
for(const FHashedTile& Row:PayloadRows)if(Row.Key.Level<World.FullResClipLevels)FullPayload.push_back(Row);
|
||||
for(const FHashedTile& Row:TopologyRows)if(Row.Key.Level<World.FullResClipLevels)FullTopology.push_back(Row);
|
||||
std::printf("[PipelineFullDetailHash] tiles=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
|
||||
FullDetailExpected,static_cast<unsigned long long>(HashTileRows(FullDensity)),
|
||||
static_cast<unsigned long long>(HashTileRows(FullMesh)),
|
||||
static_cast<unsigned long long>(HashTileRows(FullPayload)),
|
||||
static_cast<unsigned long long>(HashTileRows(FullTopology)));
|
||||
std::printf("[PipelineWholeViewHash] tiles=%zu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
|
||||
Desired.Tiles.size(),static_cast<unsigned long long>(HashTileRows(DensityRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(MeshRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(PayloadRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(TopologyRows)));
|
||||
}
|
||||
return NeighborhoodFinished==static_cast<int32>(Neighborhood.size())
|
||||
&&FullDetailFinished==FullDetailExpected&&Finished==Update.DesiredTiles&&bHashesPassing
|
||||
&&(!bProfileColumnOverlap||bColumnOverlapCapturePassing)?0:6;
|
||||
}
|
||||
} // namespace StandaloneStreamBenchmark
|
||||
@@ -0,0 +1,427 @@
|
||||
#pragma once
|
||||
|
||||
#include "OwnerWorldLoader.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace StandaloneWorldAssemblyPrototype
|
||||
{
|
||||
inline std::int64_t WorldAssemblyFixed(double voxels)
|
||||
{return static_cast<std::int64_t>(std::llround(voxels*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));}
|
||||
|
||||
inline std::int64_t WorldAssemblyQ16(double value)
|
||||
{return static_cast<std::int64_t>(std::llround(value*65536.0));}
|
||||
|
||||
inline std::int64_t WorldAssemblyWavelength(double frequency,double fallbackVoxels)
|
||||
{
|
||||
const double wavelength=std::isfinite(frequency)&&frequency>0.0?1.0/frequency:fallbackVoxels;
|
||||
return std::max<std::int64_t>(1,WorldAssemblyFixed(wavelength));
|
||||
}
|
||||
|
||||
inline uint64 WorldAssemblyFeatureId(uint32 seed,int64 x,int64 y,uint32 kind)
|
||||
{
|
||||
uint64 hash=14695981039346656037ull;
|
||||
const auto add=[&](uint64 value,int bytes)
|
||||
{
|
||||
for(int i=0;i<bytes;++i){hash^=static_cast<uint8>(value>>(8*i));hash*=1099511628211ull;}
|
||||
};
|
||||
add(seed,4);add(static_cast<uint64>(x),8);add(static_cast<uint64>(y),8);add(kind,4);
|
||||
return hash?hash:1;
|
||||
}
|
||||
|
||||
inline bool BuildFiniteStrateRoute(VoxelForgeCore::WorldAssemblyRecipeV1& recipe,
|
||||
VoxelForgeCore::WorldAssemblyStratumPlanV1& stratum,std::uint64_t& tested,
|
||||
std::uint64_t& gradeRejected,std::uint64_t& sweepRejected,std::string& error)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
constexpr std::int64_t PlayerRadiusQ=348; // 0.34 m in Core's 0.25 m voxel scale.
|
||||
constexpr std::int64_t PlayerHeightQ=1802; // 1.76 m in Core's 0.25 m voxel scale.
|
||||
constexpr std::int64_t HalfHeightQ=PlayerHeightQ/2;
|
||||
constexpr std::int64_t StepQ=128; // 0.5 Core voxel, finer than the density lattice.
|
||||
constexpr std::int64_t ProbeRadiusQ=174;
|
||||
constexpr std::int64_t SupportDepthQ=256;
|
||||
constexpr std::int64_t GradeQ16=17560; // tan(15 degrees), rounded upward.
|
||||
constexpr std::int64_t RouteRadiusQ=4*WorldAssemblySubvoxelsPerVoxel;
|
||||
struct Direction {std::int32_t x,y;};
|
||||
constexpr std::array<Direction,8> directions{{
|
||||
{65536,0},{46341,46341},{0,65536},{-46341,46341},
|
||||
{-65536,0},{-46341,-46341},{0,-65536},{46341,-46341}}};
|
||||
struct Candidate
|
||||
{
|
||||
std::int64_t score=0;
|
||||
std::int32_t centerX=0,centerY=0,direction=0;
|
||||
std::vector<WorldAssemblyRoutePointV1> points;
|
||||
};
|
||||
|
||||
auto& constraints=stratum.routeConstraints;
|
||||
constraints.version=1;
|
||||
constraints.capsuleRadiusSubvoxels=PlayerRadiusQ;
|
||||
constraints.capsuleHeightSubvoxels=PlayerHeightQ;
|
||||
constraints.maximumGradeQ16=GradeQ16;
|
||||
constraints.sweepSpacingSubvoxels=StepQ;
|
||||
constraints.supportProbeRadiusSubvoxels=ProbeRadiusQ;
|
||||
constraints.supportSearchDepthSubvoxels=SupportDepthQ;
|
||||
constraints.minimumClearanceSubvoxels=8;
|
||||
constraints.minimumSupportProbes=3;
|
||||
|
||||
const Heightfield2D finalField(recipe,stratum);
|
||||
if(!finalField.isValid())
|
||||
{error="route planner received an invalid final composed field";return false;}
|
||||
std::vector<Candidate> candidates;
|
||||
constexpr std::int32_t halfLength=128;
|
||||
constexpr std::int32_t waypointSpacing=8;
|
||||
for(std::int32_t centerY=-256;centerY<=256;centerY+=64)
|
||||
for(std::int32_t centerX=-256;centerX<=256;centerX+=64)
|
||||
for(std::int32_t directionIndex=0;directionIndex<static_cast<std::int32_t>(directions.size());++directionIndex)
|
||||
{
|
||||
Candidate candidate;
|
||||
candidate.centerX=centerX;candidate.centerY=centerY;candidate.direction=directionIndex;
|
||||
candidate.points.reserve(33);
|
||||
std::int64_t previousGround=0,previousX=0,previousY=0;
|
||||
bool gradePass=true;
|
||||
for(std::int32_t index=0;index<=32;++index)
|
||||
{
|
||||
const std::int64_t along=(index-16)*waypointSpacing;
|
||||
const std::int64_t x=centerX+std::llround(
|
||||
static_cast<double>(along)*directions[directionIndex].x/65536.0);
|
||||
const std::int64_t y=centerY+std::llround(
|
||||
static_cast<double>(along)*directions[directionIndex].y/65536.0);
|
||||
const auto column=finalField.point(WorldAssemblyFixed(static_cast<double>(x)),
|
||||
WorldAssemblyFixed(static_cast<double>(y)),0);
|
||||
const std::int64_t horizontalQ=static_cast<std::int64_t>(std::llround(
|
||||
std::hypot(static_cast<double>(x-previousX),static_cast<double>(y-previousY))
|
||||
*WorldAssemblySubvoxelsPerVoxel));
|
||||
if(index>0)
|
||||
{
|
||||
const std::int64_t riseQ=std::abs(column.groundHeightSubvoxels-previousGround);
|
||||
if(horizontalQ==0||riseQ*65536>horizontalQ*GradeQ16)
|
||||
{gradePass=false;break;}
|
||||
candidate.score+=riseQ*65536/std::max<std::int64_t>(1,horizontalQ);
|
||||
}
|
||||
WorldAssemblyRoutePointV1 point;
|
||||
point.positionSubvoxels={WorldAssemblyFixed(static_cast<double>(x)),
|
||||
WorldAssemblyFixed(static_cast<double>(y)),column.groundHeightSubvoxels+HalfHeightQ};
|
||||
point.radiusSubvoxels=RouteRadiusQ;
|
||||
point.stableFeatureId=WorldAssemblyFeatureId(recipe.seed,
|
||||
static_cast<std::int64_t>(x),static_cast<std::int64_t>(y),
|
||||
0x52540000u+static_cast<std::uint32_t>(index));
|
||||
candidate.points.push_back(point);
|
||||
previousGround=column.groundHeightSubvoxels;previousX=x;previousY=y;
|
||||
}
|
||||
++tested;
|
||||
if(gradePass)candidates.push_back(std::move(candidate));
|
||||
else ++gradeRejected;
|
||||
}
|
||||
std::stable_sort(candidates.begin(),candidates.end(),[](const Candidate& a,const Candidate& b)
|
||||
{
|
||||
if(a.score!=b.score)return a.score<b.score;
|
||||
if(a.centerY!=b.centerY)return a.centerY<b.centerY;
|
||||
if(a.centerX!=b.centerX)return a.centerX<b.centerX;
|
||||
return a.direction<b.direction;
|
||||
});
|
||||
std::uint64_t sweepAttempt=0,repairAttempt=0;
|
||||
VoxelForgeCore::WorldAssemblyRouteAuditResult lastAudit{};
|
||||
for(auto& candidate:candidates)
|
||||
{
|
||||
stratum.route=candidate.points;
|
||||
std::int64_t liftQ=0;
|
||||
constexpr std::int64_t RepairStepQ=64;
|
||||
constexpr std::int64_t MaximumRepairLiftQ=SupportDepthQ-RepairStepQ;
|
||||
for(;;)
|
||||
{
|
||||
if(liftQ>0)
|
||||
for(std::size_t pointIndex=0;pointIndex<stratum.route.size();++pointIndex)
|
||||
stratum.route[pointIndex].positionSubvoxels[2]
|
||||
=candidate.points[pointIndex].positionSubvoxels[2]+liftQ;
|
||||
const Heightfield2D candidateField(recipe,stratum);
|
||||
const auto audit=AuditHeightfieldRoute(candidateField);
|
||||
lastAudit=audit;
|
||||
if(audit.passed)
|
||||
{
|
||||
if(liftQ>0)
|
||||
std::printf("[WorldAssemblerRouteRepaired] uniform_lift_q=%lld support_limit_q=%lld\n",
|
||||
static_cast<long long>(liftQ),static_cast<long long>(SupportDepthQ));
|
||||
return true;
|
||||
}
|
||||
if(audit.blockedCapsuleSamples==0||liftQ>=MaximumRepairLiftQ)break;
|
||||
const std::int64_t needed=std::max<std::int64_t>(1,
|
||||
constraints.minimumClearanceSubvoxels-audit.minimumClearanceSubvoxels);
|
||||
const std::int64_t steps=(needed+RepairStepQ-1)/RepairStepQ;
|
||||
const std::int64_t nextLift=std::max(liftQ+RepairStepQ,liftQ+steps*RepairStepQ);
|
||||
if(nextLift>MaximumRepairLiftQ)break;
|
||||
liftQ=nextLift;
|
||||
++repairAttempt;
|
||||
}
|
||||
if(sweepAttempt<3)
|
||||
std::printf("[WorldAssemblerRouteCandidateRejected] ordinal=%llu blocked=%llu unsupported=%llu grade_failures=%llu minimum_clearance_q=%lld\n",
|
||||
static_cast<unsigned long long>(sweepAttempt),
|
||||
static_cast<unsigned long long>(lastAudit.blockedCapsuleSamples),
|
||||
static_cast<unsigned long long>(lastAudit.unsupportedCenterSamples),
|
||||
static_cast<unsigned long long>(lastAudit.gradeFailures),
|
||||
static_cast<long long>(lastAudit.minimumClearanceSubvoxels));
|
||||
++sweepAttempt;
|
||||
++sweepRejected;
|
||||
}
|
||||
std::printf("[WorldAssemblerRouteSearchFailed] tested=%llu grade_rejected=%llu sweepable_candidates=%llu sweep_rejected=%llu last_blocked=%llu last_unsupported=%llu last_grade_failures=%llu\n",
|
||||
static_cast<unsigned long long>(tested),static_cast<unsigned long long>(gradeRejected),
|
||||
static_cast<unsigned long long>(candidates.size()),static_cast<unsigned long long>(sweepRejected),
|
||||
static_cast<unsigned long long>(lastAudit.blockedCapsuleSamples),
|
||||
static_cast<unsigned long long>(lastAudit.unsupportedCenterSamples),
|
||||
static_cast<unsigned long long>(lastAudit.gradeFailures));
|
||||
std::printf("[WorldAssemblerRouteRepairSummary] repair_attempts=%llu\n",
|
||||
static_cast<unsigned long long>(repairAttempt));
|
||||
stratum.route.clear();
|
||||
error="finite-strate route candidates failed the composed-field capsule and support sweep";
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool AttachRound1WorldAssemblyPrototype(FStandaloneOwnerWorld& owner,VFJson::Value& root,
|
||||
std::string& error,const std::string& manifestOutputPath)
|
||||
{
|
||||
if(!owner.Manager||!owner.Generator)
|
||||
{error="world assembler prototype needs the resolved standalone owner world";return false;}
|
||||
int32 surfaceChunkZ=0,topChunkZ=0,bottomChunkZ=0;
|
||||
bool foundSurface=false;
|
||||
for(int32 chunkZ=1024;chunkZ>=-4096;--chunkZ)
|
||||
{
|
||||
if(owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,chunkZ))
|
||||
!=ECaveGeneratorType::SurfaceWorld)continue;
|
||||
surfaceChunkZ=chunkZ;
|
||||
foundSurface=owner.Manager->GetStrateChunkZBounds(chunkZ,topChunkZ,bottomChunkZ);
|
||||
break;
|
||||
}
|
||||
if(!foundSurface)
|
||||
{error="world assembler prototype could not find a finite SurfaceWorld strate";return false;}
|
||||
const FIntVector chunk(0,0,surfaceChunkZ);
|
||||
const FSurfaceGenerationParams surface=owner.Manager->GetSurfaceParamsForChunk(chunk);
|
||||
const FBiomeContext biomeContext=owner.Manager->GetBiomeContextForChunk(chunk);
|
||||
const std::int64_t bottomVoxel=static_cast<std::int64_t>(bottomChunkZ)*CHUNK_SIZE;
|
||||
const std::int64_t heightVoxels=static_cast<std::int64_t>(topChunkZ-bottomChunkZ+1)*CHUNK_SIZE;
|
||||
const FBiomeMapParams& map=biomeContext.Map;
|
||||
const UVoxelStrateDefinition* definition=owner.Manager->GetStrateForChunk(chunk);
|
||||
|
||||
VoxelForgeCore::WorldAssemblyManifest manifest;
|
||||
auto& recipe=manifest.recipe;
|
||||
recipe.seed=static_cast<uint32>(owner.Seed);
|
||||
recipe.biomeCellSizeSubvoxels=WorldAssemblyFixed(map.CellSize);
|
||||
recipe.biomeWarpSubvoxels=WorldAssemblyFixed(map.WarpStrength);
|
||||
recipe.biomeWarpWavelengthSubvoxels=WorldAssemblyWavelength(map.WarpFrequency,556.0);
|
||||
recipe.biomeBorderBlendSubvoxels=WorldAssemblyFixed(map.BorderBlend);
|
||||
recipe.biomeClimateReliefWavelengthSubvoxels=WorldAssemblyWavelength(map.ReliefFrequency,3333.0);
|
||||
recipe.biomeClimateMoistureWavelengthSubvoxels=WorldAssemblyWavelength(map.MoistureFrequency,2500.0);
|
||||
recipe.biomeClimateReliefContrastQ16=WorldAssemblyQ16(map.ReliefContrast);
|
||||
const auto makeProfile=[&](uint32 stableId,const FSurfaceGenerationParams& params,
|
||||
const FBiomeResolved* climate)
|
||||
{
|
||||
VoxelForgeCore::WorldAssemblyBiomeProfileV1 profile;
|
||||
profile.stableId=stableId;
|
||||
profile.baseHeightSubvoxels=WorldAssemblyFixed(static_cast<double>(bottomVoxel)
|
||||
+static_cast<double>(heightVoxels)*params.BaseGroundRelative);
|
||||
profile.continentAmplitudeSubvoxels=WorldAssemblyFixed(params.ElevationRange);
|
||||
profile.continentWavelengthSubvoxels=WorldAssemblyWavelength(params.ContinentFrequency,1000.0);
|
||||
profile.mountainAmplitudeSubvoxels=WorldAssemblyFixed(params.ElevationRange*params.MountainStrength);
|
||||
profile.mountainWavelengthSubvoxels=WorldAssemblyWavelength(params.MountainFrequency,500.0);
|
||||
profile.detailAmplitudeSubvoxels=WorldAssemblyFixed(params.SurfaceRoughness);
|
||||
profile.detailWavelengthSubvoxels=WorldAssemblyWavelength(params.DetailFrequency,25.0);
|
||||
profile.reliefMinimumQ16=climate?WorldAssemblyQ16(climate->ReliefMin):0;
|
||||
profile.reliefMaximumQ16=climate?WorldAssemblyQ16(climate->ReliefMax):65536;
|
||||
profile.moistureMinimumQ16=climate?WorldAssemblyQ16(climate->MoistureMin):0;
|
||||
profile.moistureMaximumQ16=climate?WorldAssemblyQ16(climate->MoistureMax):65536;
|
||||
profile.reliefStrengthQ16=WorldAssemblyQ16(params.ReliefStrength);
|
||||
profile.reliefContrastQ16=WorldAssemblyQ16(params.ReliefContrast);
|
||||
profile.reliefWavelengthSubvoxels=WorldAssemblyWavelength(params.ReliefFrequency,667.0);
|
||||
profile.heightWarpStrengthSubvoxels=WorldAssemblyFixed(params.HeightWarpStrength);
|
||||
profile.heightWarpWavelengthSubvoxels=WorldAssemblyWavelength(params.HeightWarpFrequency,125.0);
|
||||
profile.beachWidthSubvoxels=WorldAssemblyFixed(params.BeachWidth);
|
||||
profile.cliffStrengthQ16=WorldAssemblyQ16(params.CliffStrength);
|
||||
profile.cliffSlopeThresholdQ16=std::max<std::int64_t>(1,WorldAssemblyQ16(params.CliffSlopeThreshold));
|
||||
profile.cliffSharpnessQ16=WorldAssemblyQ16(params.CliffSharpness);
|
||||
profile.cliffSampleDistanceSubvoxels=WorldAssemblyFixed(std::max(0.5f,params.CliffSampleDist));
|
||||
const double structuralAbsBound=static_cast<double>(params.ElevationRange)
|
||||
+static_cast<double>(params.ElevationRange)*params.MountainStrength
|
||||
+2.0*params.SurfaceRoughness;
|
||||
const double cliffAbsBound=2.0*structuralAbsBound*params.CliffStrength*params.CliffSharpness;
|
||||
profile.cliffMaximumDisplacementSubvoxels=WorldAssemblyFixed(params.CliffStrength>0.0f
|
||||
?std::max(1.0,cliffAbsBound):0.0);
|
||||
profile.hasCapProfile=true;
|
||||
profile.capHeightSubvoxels=WorldAssemblyFixed(static_cast<double>(bottomVoxel)
|
||||
+static_cast<double>(heightVoxels)*params.CeilingRelative);
|
||||
profile.capWarpStrengthSubvoxels=WorldAssemblyFixed(params.CeilingWarpStrength);
|
||||
profile.capWarpWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingWarpFrequency,100.0);
|
||||
profile.capUndulationSubvoxels=WorldAssemblyFixed(params.CeilingUndulation);
|
||||
profile.capWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingUndulationFrequency,500.0);
|
||||
profile.capDetailAmplitudeSubvoxels=WorldAssemblyFixed(params.CeilingRoughness);
|
||||
profile.capDetailWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingRoughnessFrequency,25.0);
|
||||
profile.capRidgeAmplitudeSubvoxels=WorldAssemblyFixed(params.CeilingRidgeStrength);
|
||||
profile.capRidgeWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingRidgeFrequency,50.0);
|
||||
return profile;
|
||||
};
|
||||
if(biomeContext.IsValid()&&definition)
|
||||
{
|
||||
for(const FBiomeResolved& resolved:biomeContext.Biomes)
|
||||
{
|
||||
const UVoxelBiomeDefinition* biome=definition->Biomes.IsValidIndex(resolved.Index)
|
||||
?definition->Biomes[resolved.Index]:nullptr;
|
||||
FSurfaceGenerationParams params=surface;
|
||||
if(biome&&biome->bOverrideTerrain&&biome->GeneratorType==ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
params=biome->SurfaceParams;
|
||||
params.StrateTopWorldZ=surface.StrateTopWorldZ;
|
||||
params.StrateBottomWorldZ=surface.StrateBottomWorldZ;
|
||||
params.BoundarySealThickness=surface.BoundarySealThickness;
|
||||
params.BaseDensity=surface.BaseDensity;
|
||||
params.WaterLevelRelative=surface.WaterLevelRelative;
|
||||
}
|
||||
recipe.biomes.push_back(makeProfile(0x1001u+static_cast<uint32>(resolved.Index),params,&resolved));
|
||||
}
|
||||
}
|
||||
if(recipe.biomes.empty())recipe.biomes.push_back(makeProfile(0x1001u,surface,nullptr));
|
||||
|
||||
VoxelForgeCore::WorldAssemblyStratumPlanV1 stratum;
|
||||
stratum.strateIndex=owner.Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(surfaceChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
|
||||
stratum.topChunkZ=topChunkZ;
|
||||
stratum.bottomChunkZ=bottomChunkZ;
|
||||
const double strateBottom=static_cast<double>(bottomVoxel);
|
||||
const double strateHeight=static_cast<double>(heightVoxels);
|
||||
stratum.capHeightSubvoxels=WorldAssemblyFixed(strateBottom+strateHeight*surface.CeilingRelative);
|
||||
stratum.capUndulationSubvoxels=WorldAssemblyFixed(surface.CeilingUndulation);
|
||||
stratum.capWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingUndulationFrequency,250.0);
|
||||
stratum.capDetailAmplitudeSubvoxels=WorldAssemblyFixed(surface.CeilingRoughness);
|
||||
stratum.capDetailWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingRoughnessFrequency,25.0);
|
||||
stratum.capRidgeAmplitudeSubvoxels=WorldAssemblyFixed(surface.CeilingRidgeStrength);
|
||||
stratum.capRidgeWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingRidgeFrequency,50.0);
|
||||
stratum.waterSurfaceSubvoxels=WorldAssemblyFixed(strateBottom+strateHeight*surface.WaterLevelRelative);
|
||||
stratum.waterEnabled=definition&&definition->bHasWater&&surface.WaterLevelRelative>0.0f;
|
||||
|
||||
// Select a deterministic steep edge in the resolved owner field for a bounded shelf.
|
||||
// Selection d'une pente deterministe dans le champ resolu pour une saillie bornee.
|
||||
constexpr int probeSide=49,probeStep=32;
|
||||
std::array<float,probeSide*probeSide> heights{};
|
||||
std::array<uint8,probeSide*probeSide> valid{};
|
||||
for(int y=0;y<probeSide;++y)
|
||||
for(int x=0;x<probeSide;++x)
|
||||
{
|
||||
const float px=static_cast<float>((x-probeSide/2)*probeStep);
|
||||
const float py=static_cast<float>((y-probeSide/2)*probeStep);
|
||||
float ceiling=0.0f;
|
||||
const std::size_t index=static_cast<std::size_t>(y)*probeSide+x;
|
||||
valid[index]=owner.Generator->GetSurfaceHeightAt(px,py,topChunkZ,heights[index],ceiling)?1u:0u;
|
||||
}
|
||||
double steepest=0.0;int cliffX=0,cliffY=0;bool xAxis=true;int downhill=1;
|
||||
for(int y=1;y<probeSide-1;++y)
|
||||
for(int x=1;x<probeSide-1;++x)
|
||||
{
|
||||
const std::size_t center=static_cast<std::size_t>(y)*probeSide+x;
|
||||
if(!valid[center]||!valid[center-1]||!valid[center+1]||!valid[center-probeSide]||!valid[center+probeSide])continue;
|
||||
const double dx=static_cast<double>(heights[center+1]-heights[center-1]);
|
||||
const double dy=static_cast<double>(heights[center+probeSide]-heights[center-probeSide]);
|
||||
const double slope=(std::abs(dx)+std::abs(dy))/(4.0*probeStep);
|
||||
if(slope>steepest){steepest=slope;cliffX=(x-probeSide/2)*probeStep;cliffY=(y-probeSide/2)*probeStep;
|
||||
xAxis=std::abs(dx)>=std::abs(dy);downhill=(xAxis?dx:dy)>=0.0?-1:1;}
|
||||
}
|
||||
if(steepest>0.05)
|
||||
{
|
||||
float ground=0.0f,ceiling=0.0f;
|
||||
if(owner.Generator->GetSurfaceHeightAt(static_cast<float>(cliffX),static_cast<float>(cliffY),
|
||||
topChunkZ,ground,ceiling))
|
||||
{
|
||||
VoxelForgeCore::WorldAssemblyFeatureV1 shelf;
|
||||
shelf.stableId=WorldAssemblyFeatureId(recipe.seed,cliffX,cliffY,0x4f564552u);
|
||||
shelf.type=4;
|
||||
constexpr int reach=72,width=36,height=64,feather=8;
|
||||
const int dirX=xAxis?downhill:0,dirY=xAxis?0:downhill;
|
||||
const int minX=xAxis?(dirX>0?cliffX-feather:cliffX-reach-feather)
|
||||
:cliffX-width-feather;
|
||||
const int maxX=xAxis?(dirX>0?cliffX+reach+feather:cliffX+feather)
|
||||
:cliffX+width+feather;
|
||||
const int minY=xAxis?cliffY-width-feather
|
||||
:(dirY>0?cliffY-feather:cliffY-reach-feather);
|
||||
const int maxY=xAxis?cliffY+width+feather
|
||||
:(dirY>0?cliffY+reach+feather:cliffY+feather);
|
||||
VoxelForgeCore::Heightfield2D baseField(recipe,stratum);
|
||||
double lowestGround=std::numeric_limits<double>::infinity();
|
||||
double highestGround=-std::numeric_limits<double>::infinity();
|
||||
for(int sampleY=minY;sampleY<=maxY;sampleY+=4)
|
||||
for(int sampleX=minX;sampleX<=maxX;sampleX+=4)
|
||||
{
|
||||
const auto sample=baseField.point(WorldAssemblyFixed(sampleX),
|
||||
WorldAssemblyFixed(sampleY),0);
|
||||
const double heightQ=static_cast<double>(sample.groundHeightSubvoxels)
|
||||
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
lowestGround=std::min(lowestGround,heightQ);
|
||||
highestGround=std::max(highestGround,heightQ);
|
||||
}
|
||||
if(!std::isfinite(lowestGround)||!std::isfinite(highestGround))
|
||||
{error="world assembler cliff shelf could not bound its terrain columns";return false;}
|
||||
shelf.minimumSubvoxels={WorldAssemblyFixed(minX),WorldAssemblyFixed(minY),
|
||||
WorldAssemblyFixed(std::max<double>(bottomVoxel,lowestGround-4.0))};
|
||||
shelf.maximumSubvoxels={WorldAssemblyFixed(maxX),WorldAssemblyFixed(maxY),
|
||||
WorldAssemblyFixed(std::min<double>((topChunkZ+1)*CHUNK_SIZE,
|
||||
highestGround+height+4.0))};
|
||||
shelf.parametersSubvoxels={WorldAssemblyFixed(dirX),WorldAssemblyFixed(dirY),
|
||||
WorldAssemblyFixed(height),WorldAssemblyFixed(reach)};
|
||||
stratum.features.push_back(shelf);
|
||||
}
|
||||
}
|
||||
std::uint64_t routeCandidates=0,routeGradeRejected=0,routeSweepRejected=0;
|
||||
if(!BuildFiniteStrateRoute(recipe,stratum,routeCandidates,routeGradeRejected,
|
||||
routeSweepRejected,error))return false;
|
||||
std::printf("[WorldAssemblerRoutePlan] candidates=%llu grade_rejected=%llu sweep_rejected=%llu accepted_points=%zu capsule_m=0.34x1.76 corridor_radius_vox=4 support_probe_points=5 minimum_support_probes=%u status=PASS\n",
|
||||
static_cast<unsigned long long>(routeCandidates),
|
||||
static_cast<unsigned long long>(routeGradeRejected),
|
||||
static_cast<unsigned long long>(routeSweepRejected),stratum.route.size(),
|
||||
stratum.routeConstraints.minimumSupportProbes);
|
||||
manifest.plan.strates.push_back(std::move(stratum));
|
||||
if(!manifest.finalize(error))return false;
|
||||
const std::string manifestText=manifest.ToJson();
|
||||
const auto roundTrip=VoxelForgeCore::WorldAssemblyManifest::FromJson(manifestText,error);
|
||||
if(!roundTrip||!roundTrip->validate(error)||roundTrip->ToJson()!=manifestText
|
||||
||roundTrip->contentHash!=manifest.contentHash)
|
||||
{if(error.empty())error="world assembly canonical plan serialization did not round-trip";return false;}
|
||||
std::printf("[WorldAssemblerPlanSerialization] bytes=%zu canonical_round_trip=true route_points=%zu plan_hash=%016llx manifest_hash=%016llx status=PASS\n",
|
||||
manifestText.size(),manifest.plan.strates.front().route.size(),
|
||||
static_cast<unsigned long long>(manifest.plan.contentHash),
|
||||
static_cast<unsigned long long>(manifest.contentHash));
|
||||
if(!manifestOutputPath.empty())
|
||||
{
|
||||
const std::filesystem::path outputPath(manifestOutputPath);
|
||||
std::error_code directoryError;
|
||||
if(!outputPath.parent_path().empty())
|
||||
std::filesystem::create_directories(outputPath.parent_path(),directoryError);
|
||||
if(directoryError)
|
||||
{error="could not create the world assembly manifest output directory";return false;}
|
||||
std::ofstream output(outputPath,std::ios::binary);
|
||||
if(!output)
|
||||
{error="could not open the world assembly manifest output path";return false;}
|
||||
output.write(manifestText.data(),static_cast<std::streamsize>(manifestText.size()));
|
||||
if(!output)
|
||||
{error="could not write the world assembly manifest payload";return false;}
|
||||
}
|
||||
VFJson::Value manifestJson;
|
||||
if(!VFJson::Parser(manifestText).Parse(manifestJson,error))return false;
|
||||
auto* rootObject=std::get_if<VFJson::Value::Object>(&root.Data);
|
||||
if(!rootObject)
|
||||
{error="owner snapshot root is not a JSON object";return false;}
|
||||
(*rootObject)["world_assembly"]=std::move(manifestJson);
|
||||
std::printf("[WorldAssemblerPrototype] recipe_version=%u evaluator_version=%u strata=%zu biomes=%zu strate_index=%d chunk_z=%d..%d serialized_bytes=%zu recipe_hash=%016llx plan_hash=%016llx manifest_hash=%016llx source=owner_surface_params_and_biome_map route_points=%zu features=%zu\n",
|
||||
manifest.recipe.schemaVersion,manifest.recipe.evaluatorVersion,manifest.plan.strates.size(),
|
||||
manifest.recipe.biomes.size(),manifest.plan.strates.front().strateIndex,topChunkZ,bottomChunkZ,
|
||||
manifestText.size(),static_cast<unsigned long long>(manifest.recipe.contentHash),
|
||||
static_cast<unsigned long long>(manifest.plan.contentHash),
|
||||
static_cast<unsigned long long>(manifest.contentHash),manifest.plan.strates.front().route.size(),
|
||||
manifest.plan.strates.front().features.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace StandaloneWorldAssemblyPrototype
|
||||
Reference in new issue
Block a user