wip(assembler): round L11 -- fix 24 m+ crossing crash; near ring ready 2x sooner at spawn

Crash: crossing tiles were coalesced into one huge rectangle, and BuildSpatialBins tried to
resize to ~4.3e17 bytes (std::bad_alloc; the L7 32 m failure had the same cause). Analytic
regions now skip unread dense bins and compile in world-aligned 128-voxel pages, published
together; out-of-range keys error before allocation. 24 m, 32 m and 1 km crossings pass
(1 km: 72 pages, peak RSS 700 MiB).
Spawn: World::isTileShapeFree proves LOD0 SurfaceWorld tiles that never read the cave shape;
those start before shape compile, and all others wait. Reviewer W12 no-hash: near ring 0.337 ->
0.159 s, whole view +2.1%. All SurfaceWorld/FloatingIslands hashes unchanged (W1/W8/W12, both
compilers, forced-overlap run). Unreal VoxelWorld.cpp adapter changed but NOT built here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-20 00:28:58 +02:00
1 parent b370908174
commit b1cfaf53b8
8 files changed
+808 -59

No files matched your search

+40 -4
View File
@@ -146,6 +146,10 @@ public:
FVoxelForgeCoreTaskScheduler Scheduler; FVoxelForgeCoreTaskScheduler Scheduler;
std::unordered_set<VoxelForgeCore::TileKey, VoxelForgeCore::TileKeyHash> RetainedTiles; std::unordered_set<VoxelForgeCore::TileKey, VoxelForgeCore::TileKeyHash> RetainedTiles;
FCriticalSection RetainedTilesLock; FCriticalSection RetainedTilesLock;
FCriticalSection ShapeReadinessLock;
std::unordered_set<VoxelForgeCore::TileKey, VoxelForgeCore::TileKeyHash> ShapeFreeTiles;
std::unordered_set<VoxelForgeCore::TileKey, VoxelForgeCore::TileKeyHash> ShapeDependentTiles;
std::atomic<bool> bSnapshotReady{false};
std::atomic<bool> bReady{false}; std::atomic<bool> bReady{false};
std::atomic<bool> bInitialFailed{false}; std::atomic<bool> bInitialFailed{false};
std::atomic<bool> bRegionCompileInFlight{false}; std::atomic<bool> bRegionCompileInFlight{false};
@@ -7689,6 +7693,7 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
Request.includeFarWorld = true; Request.includeFarWorld = true;
VoxelForgeCoreState = State; VoxelForgeCoreState = State;
State->bSnapshotReady.store(true, std::memory_order_release);
ActiveTaskCount.fetch_add(1, std::memory_order_relaxed); ActiveTaskCount.fetch_add(1, std::memory_order_relaxed);
UE::Tasks::Launch(TEXT("VoxelForgeCoreCompile"), UE::Tasks::Launch(TEXT("VoxelForgeCoreCompile"),
[this, State, Request, InitialDesired, ActiveCoreClipRadius, ActiveCoreMaxClipLevel, [this, State, Request, InitialDesired, ActiveCoreClipRadius, ActiveCoreMaxClipLevel,
@@ -7708,10 +7713,17 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
std::string Error; std::string Error;
bool bCompiled = false; bool bCompiled = false;
{
FWriteScopeLock Lock(State->WorldLock);
bCompiled = State->World->compile(Request, State->Scheduler, Error); bCompiled = State->World->compile(Request, State->Scheduler, Error);
if (bCompiled) if (bCompiled)
{
// The initial compile builds into local state and atomically publishes an immutable
// shape. Proven native-only tiles may hold shared WorldLock readers during this work;
// take the writer only for region retention after publication.
// La compilation initiale construit un etat local puis publie une forme immuable.
// Les tuiles natives validees gardent le verrou lecteur partage; le verrou ecrivain
// ne sert qu'a retenir les regions apres publication.
FWriteScopeLock Lock(State->WorldLock);
if (State->World)
{ {
State->World->retainTileRegions(InitialDesired); State->World->retainTileRegions(InitialDesired);
FScopeLock RetainedLock(&State->RetainedTilesLock); FScopeLock RetainedLock(&State->RetainedTilesLock);
@@ -13019,9 +13031,33 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
return; return;
} }
} }
if (PendingTiles.Contains(Tile) || !IsVoxelForgeCoreReady()) return; if (PendingTiles.Contains(Tile)) return;
const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState; const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState;
if (!State.IsValid() || State->bRegionCompileFailed.load(std::memory_order_acquire)) return; if (!State.IsValid()||State->bInitialFailed.load(std::memory_order_acquire)
||State->bRegionCompileFailed.load(std::memory_order_acquire)
||State->bShuttingDown.load(std::memory_order_acquire))return;
const bool bShapeReady=State->bReady.load(std::memory_order_acquire);
if(!bShapeReady)
{
if(!State->bSnapshotReady.load(std::memory_order_acquire))return;
const VoxelForgeCore::TileKey CoreKey{
{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level};
bool bShapeFree=false;
{
FScopeLock ReadinessLock(&State->ShapeReadinessLock);
if(State->ShapeFreeTiles.contains(CoreKey))bShapeFree=true;
else if(State->ShapeDependentTiles.contains(CoreKey))return;
else
{
FReadScopeLock WorldReadLock(State->WorldLock);
if(!State->World)return;
bShapeFree=State->World->isTileShapeFree(CoreKey);
if(bShapeFree)State->ShapeFreeTiles.insert(CoreKey);
else State->ShapeDependentTiles.insert(CoreKey);
}
}
if(!bShapeFree)return;
}
if (IsStreamingResultBacklogCapped()) if (IsStreamingResultBacklogCapped())
{ {
StreamingResultBacklogCapStops.fetch_add(1, std::memory_order_relaxed); StreamingResultBacklogCapStops.fetch_add(1, std::memory_order_relaxed);
@@ -1082,6 +1082,11 @@ namespace VoxelForgeCore
std::uint32_t diagnosticGlobalOctaveDrop = 0; std::uint32_t diagnosticGlobalOctaveDrop = 0;
std::uint32_t diagnosticCellStride = 1; std::uint32_t diagnosticCellStride = 1;
std::uint32_t diagnosticColumnInterpolationStride = 1; std::uint32_t diagnosticColumnInterpolationStride = 1;
// Diagnostic negative control only: deliberately bypass the native-only readiness
// proof and require the sampling guard to reject any shape-dependent tile.
// Controle negatif diagnostique: contourne volontairement la preuve native seule
// et exige que la garde rejette toute tuile dependante de la forme.
bool diagnosticForceNativeOnly = false;
}; };
class VFCORE_API World class VFCORE_API World
@@ -1099,11 +1104,20 @@ namespace VoxelForgeCore
// True only when a fine tile needs a separately compiled region before generation. // True only when a fine tile needs a separately compiled region before generation.
// Vrai seulement si une tuile fine doit attendre la compilation d'une region dediee. // Vrai seulement si une tuile fine doit attendre la compilation d'une region dediee.
bool needsTileRegion(const TileKey& tile) const; bool needsTileRegion(const TileKey& tile) const;
// Conservative precompile proof: true only for a tile that can sample the native
// SurfaceWorld heightfield throughout its padded mesh domain without compiled shape data.
// Preuve prudente avant compilation: vrai seulement si toute la zone maillee avec marge
// utilise le champ SurfaceWorld natif, sans lire la forme compilee.
bool isTileShapeFree(const TileKey& tile) const;
// Compile missing detail regions for entering fine tiles. Generated tile keys map to // Compile missing detail regions for entering fine tiles. Generated tile keys map to
// shared world-coordinate regions. The default analyticOnly path coalesces each entering // bounded, world-aligned pages. The default analyticOnly path builds each page's
// LOD slab, builds its primitives/bins, and evaluates density on demand without // primitives/bins and evaluates density on demand without rasterizing unused lattice
// rasterizing unused lattice volumes. Set false to use fixed absolute packed pages. The // volumes. Set false to use fixed absolute packed pages. The optional count is newly
// optional count is newly covered tile keys; regions stay resident until retainTileRegions. // covered tile keys; regions stay resident until retainTileRegions.
// Compile les regions de detail manquantes. Les tuiles entrantes sont regroupees en pages
// bornees alignees au monde; analyticOnly construit les primitives et bins sans rasteriser
// de volume inutilise. Sinon, utilise les pages compactes fixes; le compteur indique les
// nouvelles tuiles couvertes et retainTileRegions libere les anciennes regions.
bool compileTileRegion(std::span<const TileKey> tiles, std::uint32_t workerLimit, bool compileTileRegion(std::span<const TileKey> tiles, std::uint32_t workerLimit,
const JobScheduler& scheduler, std::string& error, const JobScheduler& scheduler, std::string& error,
std::size_t* outCompiledTiles = nullptr, std::size_t* outCompiledTiles = nullptr,
+159 -7
View File
@@ -487,7 +487,13 @@ public:
const auto Stage=Clock::now(); const auto Stage=Clock::now();
BuildLayerBounds();BuildOwnerPrimitives(); BuildLayerBounds();BuildOwnerPrimitives();
if(RoundTunnelsEnabled)BuildRoundTunnelPrimitives(); if(RoundTunnelsEnabled)BuildRoundTunnelPrimitives();
BuildSpatialBins(LineBinSize); // Analytic regions never build packed interval lines. Their exact-density fallback only
// reads the sparse 3D primitive bins; allocating the dense per-axis line-bin grids here
// scales with the region's XY bounding rectangle and is both unnecessary and unbounded.
// Les regions analytiques n'utilisent pas les intervalles compactes; leur repli exact lit
// les bins 3D clairsemes. Les grilles denses par axe croitraient avec la surface XY sans
// servir ce chemin.
BuildSpatialBins(LineBinSize,false);
Stats.ShapeSeconds=std::chrono::duration<double>(Clock::now()-Stage).count(); Stats.ShapeSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
Stats.PrimitiveCount=Primitives.size(); Stats.PrimitiveCount=Primitives.size();
if(Primitives.empty()) if(Primitives.empty())
@@ -497,6 +503,59 @@ public:
return true; return true;
} }
void InitializeNativeSurfaceOnly(FStandaloneOwnerWorld& World,
const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly,
const FIntVector& InMinimum,const FIntVector& InMaximum)
{
Owner=&World;WorldAssembly=InWorldAssembly;
NativeOnlyMinimum=InMinimum;NativeOnlyMaximum=InMaximum;
bNativeOnly=true;bAnalyticOnly=true;Built=true;
NativeOnlyViolations.store(0,std::memory_order_relaxed);
}
bool BuildShapeDependencyProof(FStandaloneOwnerWorld& World,
const FIntVector& MinVoxels,const FIntVector& MaxVoxels,bool bInRoundTunnels)
{
Owner=&World;WorkerCount=1;RoundTunnelsEnabled=bInRoundTunnels;
bAnalyticOnly=true;bNativeOnly=false;Stats={};
for(int32 Axis=0;Axis<3;++Axis)
{
Min[Axis]=(&MinVoxels.X)[Axis]-2;Max[Axis]=(&MaxVoxels.X)[Axis]+2;
ClipMin[Axis]=(&MinVoxels.X)[Axis];ClipMax[Axis]=(&MaxVoxels.X)[Axis];
Size[Axis]=Max[Axis]-Min[Axis]+1;
if(Size[Axis]<=0||Size[Axis]>4096||ClipMax[Axis]<ClipMin[Axis])return false;
}
BuildLayerBounds();BuildOwnerPrimitives();
if(RoundTunnelsEnabled)BuildRoundTunnelPrimitives();
return true;
}
bool HasShapeDependencyInBox(const FIntVector& Origin,int32 Step,int32 Cells) const
{
const int32 Pad=std::max(Step,1);
const int32 BMin[3]={Origin.X-Pad,Origin.Y-Pad,Origin.Z-Pad};
const int32 BMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step,
Origin.Z+(Cells+1)*Step};
for(const FPrimitive& Primitive:Primitives)
{
bool bOverlaps=true;
for(int32 Axis=0;Axis<3;++Axis)
bOverlaps=bOverlaps&&Primitive.Max[Axis]+DetailBand>=BMin[Axis]
&&Primitive.Min[Axis]-DetailBand<=BMax[Axis];
if(bOverlaps)return true;
}
for(const FInterval& Range:LayerRanges)
if(Range.Lo<=BMax[2]&&Range.Hi>=BMin[2]
&&(Range.Lo>=BMin[2]||Range.Hi<=BMax[2]))return true;
return false;
}
bool IsNativeOnly() const { return bNativeOnly; }
uint32 GetNativeOnlyViolationCount() const
{ return NativeOnlyViolations.load(std::memory_order_relaxed); }
void RecordNativeOnlyViolation() const
{ NativeOnlyViolations.fetch_add(1,std::memory_order_relaxed); }
const FBuildStats& GetStats() const { return Stats; } const FBuildStats& GetStats() const { return Stats; }
bool IsBuilt() const { return Built; } bool IsBuilt() const { return Built; }
bool HasFarWorld() const { return FarBuilt; } bool HasFarWorld() const { return FarBuilt; }
@@ -951,6 +1010,15 @@ public:
// edge interpolation and normals to the existing mesher. // edge interpolation and normals to the existing mesher.
float DensityAt(float X,float Y,float Z) const float DensityAt(float X,float Y,float Z) const
{ {
if(bNativeOnly)
{
if(!NativeOnlyContains(X,Y,Z))
{RecordNativeOnlyViolation();return 0.0f;}
float NativeDensity=0.0f;
if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
{RecordNativeOnlyViolation();return 0.0f;}
return NativeDensity;
}
float IslandDensity=0.0f; float IslandDensity=0.0f;
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity)) if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
{ {
@@ -975,6 +1043,17 @@ public:
float DensityAtLattice(int32 X,int32 Y,int32 Z) const float DensityAtLattice(int32 X,int32 Y,int32 Z) const
{ {
if(bNativeOnly)
{
if(!NativeOnlyContains(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z)))
{RecordNativeOnlyViolation();return 0.0f;}
float NativeDensity=0.0f;
if(!NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z),NativeDensity))
{RecordNativeOnlyViolation();return 0.0f;}
return NativeDensity;
}
float IslandDensity=0.0f; float IslandDensity=0.0f;
if(NativeFloatingIslandDensityAt(static_cast<float>(X),static_cast<float>(Y), if(NativeFloatingIslandDensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z),IslandDensity)) static_cast<float>(Z),IslandDensity))
@@ -1003,6 +1082,15 @@ public:
float DensityAtUnclamped(float X,float Y,float Z) const float DensityAtUnclamped(float X,float Y,float Z) const
{ {
if(bNativeOnly)
{
if(!NativeOnlyContains(X,Y,Z))
{RecordNativeOnlyViolation();return 0.0f;}
float NativeDensity=0.0f;
if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
{RecordNativeOnlyViolation();return 0.0f;}
return NativeDensity;
}
float IslandDensity=0.0f; float IslandDensity=0.0f;
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity)) if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
{ {
@@ -1023,6 +1111,8 @@ public:
VoxelForgeCore::DetailFieldSample DetailSampleAt(float X,float Y,float Z) const VoxelForgeCore::DetailFieldSample DetailSampleAt(float X,float Y,float Z) const
{ {
VoxelForgeCore::DetailFieldSample Sample; VoxelForgeCore::DetailFieldSample Sample;
if(bNativeOnly)
{RecordNativeOnlyViolation();return Sample;}
AnalyticDensityAt(X,Y,Z,false,&Sample.displacement, AnalyticDensityAt(X,Y,Z,false,&Sample.displacement,
&Sample.caveInfluence,&Sample.floorInfluence); &Sample.caveInfluence,&Sample.floorInfluence);
return Sample; return Sample;
@@ -1040,6 +1130,12 @@ public:
{ {
Count=std::clamp(Count,0,8); Count=std::clamp(Count,0,8);
if(Count==0)return; if(Count==0)return;
if(bNativeOnly)
{
for(int32 Lane=0;Lane<Count;++Lane)
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
return;
}
bool bHasLegacySurfaceSample=false; bool bHasLegacySurfaceSample=false;
bool bHasNativeIslandSample=false; bool bHasNativeIslandSample=false;
for(int32 Lane=0;Lane<Count;++Lane) for(int32 Lane=0;Lane<Count;++Lane)
@@ -1202,10 +1298,17 @@ public:
{ {
if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
||!std::isfinite(X)||!std::isfinite(Y))return false; ||!std::isfinite(X)||!std::isfinite(Y))return false;
const std::int64_t chunkMinimum=static_cast<std::int64_t>(ChunkZ)*CHUNK_SIZE;
const std::int64_t chunkMaximum=chunkMinimum+CHUNK_SIZE;
if(bNativeOnly&&(!NativeOnlyContainsColumn(X,Y)
||chunkMaximum<NativeOnlyMinimum.Z||chunkMinimum>NativeOnlyMaximum.Z
||Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
!=ECaveGeneratorType::SurfaceWorld))
{RecordNativeOnlyViolation();return false;}
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ)) if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
!=ECaveGeneratorType::SurfaceWorld)return false; !=ECaveGeneratorType::SurfaceWorld)return false;
const int32 StrateIndex=Owner->Manager->GetStrateIndex( const int32 StrateIndex=Owner->Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE)); static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
const auto* Plan=FindWorldAssemblyStratum(StrateIndex,ChunkZ); const auto* Plan=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
if(!Plan)return false; if(!Plan)return false;
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
@@ -1250,6 +1353,17 @@ public:
!=ECaveGeneratorType::SurfaceWorld)return false; !=ECaveGeneratorType::SurfaceWorld)return false;
Plan=Candidate; Plan=Candidate;
} }
if(bNativeOnly)
{
const int32 BMinX=Origin.X-Pad,BMinY=Origin.Y-Pad;
const int32 BMaxX=Origin.X+(Cells+1)*Step;
const int32 BMaxY=Origin.Y+(Cells+1)*Step;
if(BMinX<NativeOnlyMinimum.X||BMinY<NativeOnlyMinimum.Y
||BMin<NativeOnlyMinimum.Z||BMaxX>NativeOnlyMaximum.X
||BMaxY>NativeOnlyMaximum.Y||BMax>NativeOnlyMaximum.Z)
{RecordNativeOnlyViolation();return false;}
}
if(bNativeOnly&&!Plan)RecordNativeOnlyViolation();
return Plan!=nullptr; return Plan!=nullptr;
} }
@@ -1281,7 +1395,7 @@ public:
{ {
bSurfaceOverlap=true; bSurfaceOverlap=true;
const int32 StrateIndex=Owner->Manager->GetStrateIndex( const int32 StrateIndex=Owner->Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE)); static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
const auto* Candidate=FindWorldAssemblyStratum(StrateIndex,ChunkZ); const auto* Candidate=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
if(!Candidate||(SurfacePlan&&SurfacePlan!=Candidate)) if(!Candidate||(SurfacePlan&&SurfacePlan!=Candidate))
bBadPlan=true; bBadPlan=true;
@@ -1306,7 +1420,7 @@ public:
{ {
bIslandOverlap=true; bIslandOverlap=true;
const int32 StrateIndex=Owner->Manager->GetStrateIndex( const int32 StrateIndex=Owner->Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE)); static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
const auto* Candidate=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ); const auto* Candidate=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ);
if(!Candidate||(IslandPlan&&IslandPlan!=Candidate))bBadPlan=true; if(!Candidate||(IslandPlan&&IslandPlan!=Candidate))bBadPlan=true;
IslandPlan=Candidate; IslandPlan=Candidate;
@@ -1327,6 +1441,19 @@ public:
else ++Counts.MixedBoxes; else ++Counts.MixedBoxes;
return Result; return Result;
}; };
if(bNativeOnly)
{
const bool bInsideProofBox=BMin[0]>=NativeOnlyMinimum.X
&&BMin[1]>=NativeOnlyMinimum.Y&&BMin[2]>=NativeOnlyMinimum.Z
&&BMax[0]<=NativeOnlyMaximum.X&&BMax[1]<=NativeOnlyMaximum.Y
&&BMax[2]<=NativeOnlyMaximum.Z;
if(!bInsideProofBox||!bSurfaceOverlap||bIslandOverlap||bOtherStrateOverlap
||bBadPlan||!EditBrushes.empty())
{
RecordNativeOnlyViolation();
return Finish(EVoxelTileClass::Mixed,true);
}
}
// A box crossing recipes cannot use one field interval; unknown composition stays Mixed. // A box crossing recipes cannot use one field interval; unknown composition stays Mixed.
// Une boite qui traverse plusieurs recettes reste Mixed, car l'intervalle d'un seul champ ne suffit pas. // Une boite qui traverse plusieurs recettes reste Mixed, car l'intervalle d'un seul champ ne suffit pas.
if(bBadPlan||(bSurfaceOverlap&&(bIslandOverlap||bOtherStrateOverlap)) if(bBadPlan||(bSurfaceOverlap&&(bIslandOverlap||bOtherStrateOverlap))
@@ -1341,7 +1468,7 @@ public:
// for SurfaceWorld and FloatingIslands. Legacy analytic primitives/layer bounds cannot // for SurfaceWorld and FloatingIslands. Legacy analytic primitives/layer bounds cannot
// modify that field; only the explicit edit transaction above can. Keep their guards on // modify that field; only the explicit edit transaction above can. Keep their guards on
// the legacy classifier path, where those overlays do participate in density. // the legacy classifier path, where those overlays do participate in density.
if(!bUseNativeLatticeProof||(!bSurfaceOverlap&&!bIslandOverlap)) if(!bNativeOnly&&(!bUseNativeLatticeProof||(!bSurfaceOverlap&&!bIslandOverlap)))
{ {
for(int32 BZ=FloorDiv(BMin[2],ActiveSpatialBinSize);BZ<=FloorDiv(BMax[2],ActiveSpatialBinSize);++BZ) for(int32 BZ=FloorDiv(BMin[2],ActiveSpatialBinSize);BZ<=FloorDiv(BMax[2],ActiveSpatialBinSize);++BZ)
for(int32 BY=FloorDiv(BMin[1],ActiveSpatialBinSize);BY<=FloorDiv(BMax[1],ActiveSpatialBinSize);++BY) for(int32 BY=FloorDiv(BMin[1],ActiveSpatialBinSize);BY<=FloorDiv(BMax[1],ActiveSpatialBinSize);++BY)
@@ -1880,6 +2007,21 @@ public:
} }
private: private:
bool NativeOnlyContainsColumn(float X,float Y) const
{
return std::isfinite(X)&&std::isfinite(Y)
&&X>=NativeOnlyMinimum.X&&X<=NativeOnlyMaximum.X
&&Y>=NativeOnlyMinimum.Y&&Y<=NativeOnlyMaximum.Y;
}
bool NativeOnlyContains(float X,float Y,float Z) const
{
return std::isfinite(X)&&std::isfinite(Y)&&std::isfinite(Z)
&&X>=NativeOnlyMinimum.X&&X<=NativeOnlyMaximum.X
&&Y>=NativeOnlyMinimum.Y&&Y<=NativeOnlyMaximum.Y
&&Z>=NativeOnlyMinimum.Z&&Z<=NativeOnlyMaximum.Z;
}
static FClassificationThreadStats& ThreadClassificationStats() static FClassificationThreadStats& ThreadClassificationStats()
{ {
static thread_local FClassificationThreadStats Stats; static thread_local FClassificationThreadStats Stats;
@@ -1914,7 +2056,7 @@ private:
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ)) if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
!=ECaveGeneratorType::SurfaceWorld)return false; !=ECaveGeneratorType::SurfaceWorld)return false;
const int32 StrateIndex=Owner->Manager->GetStrateIndex( const int32 StrateIndex=Owner->Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE)); static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
const auto* Plan=FindWorldAssemblyStratum(StrateIndex,ChunkZ); const auto* Plan=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
if(!Plan)return false; if(!Plan)return false;
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
@@ -1942,7 +2084,7 @@ private:
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ)) if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
!=ECaveGeneratorType::FloatingIslands)return false; !=ECaveGeneratorType::FloatingIslands)return false;
const int32 StrateIndex=Owner->Manager->GetStrateIndex( const int32 StrateIndex=Owner->Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE)); static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
const auto* Plan=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ); const auto* Plan=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ);
if(!Plan)return false; if(!Plan)return false;
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
@@ -4675,6 +4817,12 @@ public:
int32 Count,float* OutDensity) const int32 Count,float* OutDensity) const
{ {
if(Count<=0||OutDensity==nullptr)return; if(Count<=0||OutDensity==nullptr)return;
if(bNativeOnly)
{
for(int32 Lane=0;Lane<Count;++Lane)
OutDensity[Lane]=DensityAtLattice(X,Y,ZStart+Lane*ZStep);
return;
}
bool bHasLegacySurfaceSample=false; bool bHasLegacySurfaceSample=false;
for(int32 Lane=0;Lane<Count;++Lane) for(int32 Lane=0;Lane<Count;++Lane)
bHasLegacySurfaceSample|=UsesLegacySurfaceField(static_cast<float>(X), bHasLegacySurfaceSample|=UsesLegacySurfaceField(static_cast<float>(X),
@@ -5211,6 +5359,10 @@ private:
size_t WordsPerZ=0; size_t WordsPerZ=0;
EDetailVariant DetailVariant=EDetailVariant::Coherent; EDetailVariant DetailVariant=EDetailVariant::Coherent;
bool Built=false; bool Built=false;
bool bNativeOnly=false;
FIntVector NativeOnlyMinimum=FIntVector::ZeroValue;
FIntVector NativeOnlyMaximum=FIntVector::ZeroValue;
mutable std::atomic<uint32> NativeOnlyViolations{0};
bool FarBuilt=false; bool FarBuilt=false;
std::vector<FInterval> LayerRanges; std::vector<FInterval> LayerRanges;
std::vector<FLayerField> LayerFields; std::vector<FLayerField> LayerFields;
+291 -27
View File
@@ -13,6 +13,8 @@
#include <set> #include <set>
#include <functional> #include <functional>
#include <thread> #include <thread>
#include <new>
#include <stdexcept>
namespace vfcore::detail namespace vfcore::detail
{ {
@@ -943,6 +945,7 @@ namespace VoxelForgeCore
std::shared_ptr<const WorldAssemblyManifest> worldAssembly; std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
WorldConfiguration configuration; WorldConfiguration configuration;
std::unique_ptr<FStandaloneShapeWorld> shape; std::unique_ptr<FStandaloneShapeWorld> shape;
std::atomic<bool> shapeReady{false};
std::unique_ptr<world_assembly_detail::FSharedHeightfieldColumnCache> std::unique_ptr<world_assembly_detail::FSharedHeightfieldColumnCache>
sharedHeightfieldColumns; sharedHeightfieldColumns;
std::unique_ptr<floating_island_detail::FSharedIslandColumnCache> std::unique_ptr<floating_island_detail::FSharedIslandColumnCache>
@@ -996,9 +999,136 @@ namespace VoxelForgeCore
World::World(World&&) noexcept=default; World::World(World&&) noexcept=default;
World& World::operator=(World&&) noexcept=default; World& World::operator=(World&&) noexcept=default;
namespace
{
template<class TImpl>
bool NativeOnlyBoundsForTile(const TImpl& impl,const TileKey& key,
FIntVector& minimum,FIntVector& maximum)
{
if(!impl.owner||key.lod!=0||key.lod>=impl.configuration.fullResolutionLodCount)
return false;
constexpr std::int64_t TileExtent=CHUNK_SIZE;
const std::int64_t skirt=impl.owner->CoreVoxelSizeMicrons>0
?static_cast<std::int64_t>(std::ceil(std::abs(
static_cast<double>(impl.owner->SkirtCells)
*static_cast<double>(impl.owner->OwnerVoxelSizeMicrons)
/static_cast<double>(impl.owner->CoreVoxelSizeMicrons)))):0;
const std::int64_t pad=std::max<std::int64_t>(8,skirt+4);
const std::int64_t coordinates[3]={key.coordinate.x,key.coordinate.y,key.coordinate.z};
std::int64_t low[3]{},high[3]{};
for(int axis=0;axis<3;++axis)
{
const std::int64_t origin=coordinates[axis]*TileExtent;
low[axis]=origin-pad;
high[axis]=origin+TileExtent+pad;
if(low[axis]<std::numeric_limits<std::int32_t>::min()+4ll
||high[axis]>std::numeric_limits<std::int32_t>::max()-4ll)return false;
}
minimum=FIntVector(static_cast<int32>(low[0]),static_cast<int32>(low[1]),
static_cast<int32>(low[2]));
maximum=FIntVector(static_cast<int32>(high[0]),static_cast<int32>(high[1]),
static_cast<int32>(high[2]));
return true;
}
bool IntersectsBox(const std::array<std::int64_t,3>& minimum,
const std::array<std::int64_t,3>& maximum,
const std::int64_t tileMinimum[3],const std::int64_t tileMaximum[3])
{
for(int axis=0;axis<3;++axis)
if(maximum[axis]<tileMinimum[axis]||minimum[axis]>tileMaximum[axis])
return false;
return true;
}
bool RouteTouchesBox(const WorldAssemblyStratumPlanV1& plan,
const std::int64_t tileMinimum[3],const std::int64_t tileMaximum[3])
{
if(plan.route.empty())return false;
const std::int64_t constraintRadius=std::max<std::int64_t>(0,
plan.routeConstraints.capsuleRadiusSubvoxels)
+std::max<std::int64_t>(0,plan.routeConstraints.capsuleHeightSubvoxels)/2;
for(std::size_t index=0;index<plan.route.size();++index)
{
const auto& point=plan.route[index];
const std::int64_t radius=std::max(constraintRadius,
std::max<std::int64_t>(0,point.radiusSubvoxels));
std::array<std::int64_t,3> low{},high{};
for(int axis=0;axis<3;++axis)
{
low[axis]=point.positionSubvoxels[axis]-radius;
high[axis]=point.positionSubvoxels[axis]+radius;
}
if(IntersectsBox(low,high,tileMinimum,tileMaximum))return true;
if(index+1>=plan.route.size())continue;
const auto& next=plan.route[index+1];
const std::int64_t segmentRadius=std::max(radius,
std::max(constraintRadius,std::max<std::int64_t>(0,next.radiusSubvoxels)));
for(int axis=0;axis<3;++axis)
{
low[axis]=std::min(point.positionSubvoxels[axis],next.positionSubvoxels[axis])
-segmentRadius;
high[axis]=std::max(point.positionSubvoxels[axis],next.positionSubvoxels[axis])
+segmentRadius;
}
if(IntersectsBox(low,high,tileMinimum,tileMaximum))return true;
}
return false;
}
bool OwnerPassageTouchesBox(const FStandaloneOwnerWorld& owner,
const FIntVector& minimum,const FIntVector& maximum)
{
const double ownerToCore=owner.OwnerVoxelSizeMicrons>0
&&owner.CoreVoxelSizeMicrons>0
?static_cast<double>(owner.OwnerVoxelSizeMicrons)
/static_cast<double>(owner.CoreVoxelSizeMicrons):1.0;
const double padding=(std::max(8.0,static_cast<double>(owner.SkirtCells)+4.0)
/ownerToCore)+64.0;
const double tileMinimum[3]={static_cast<double>(minimum.X),
static_cast<double>(minimum.Y),static_cast<double>(minimum.Z)};
const double tileMaximum[3]={static_cast<double>(maximum.X),
static_cast<double>(maximum.Y),static_cast<double>(maximum.Z)};
const auto SegmentTouches=[&](const FVector& a,const FVector& b,double radius)
{
for(int axis=0;axis<3;++axis)
{
const double pointA=(&a.X)[axis]*ownerToCore;
const double pointB=(&b.X)[axis]*ownerToCore;
const double expansion=(radius+padding)*ownerToCore;
if(std::max(pointA,pointB)+expansion<tileMinimum[axis]
||std::min(pointA,pointB)-expansion>tileMaximum[axis])return false;
}
return true;
};
for(const FVoxelPassage& passage:owner.Manager->GetPassages())
{
const auto& points=passage.ControlPoints;
if(points.Num()<2)
{
if(SegmentTouches(passage.UpperPoint,passage.LowerPoint,passage.Radius))
return true;
continue;
}
for(int32 index=0;index+1<points.Num();++index)
{
const double radius=std::max({static_cast<double>(passage.Radius),
passage.ControlRadii.IsValidIndex(index)
?static_cast<double>(passage.ControlRadii[index]):0.0,
passage.ControlRadii.IsValidIndex(index+1)
?static_cast<double>(passage.ControlRadii[index+1]):0.0});
if(SegmentTouches(points[index],points[index+1],radius))return true;
}
}
return false;
}
}
bool World::compile(const ShapeBuildRequest& request,const JobScheduler& scheduler,std::string& error) bool World::compile(const ShapeBuildRequest& request,const JobScheduler& scheduler,std::string& error)
{ {
if(!impl_->owner){error="resolved world snapshot is empty";return false;} if(!impl_->owner){error="resolved world snapshot is empty";return false;}
if(impl_->shapeReady.load(std::memory_order_acquire))
{error="world shape is already compiled";return false;}
if(!::vfcore::fp_contract::selfTest()) if(!::vfcore::fp_contract::selfTest())
{ {
error="Core startup refused: floating-point contraction self-test detected a fused multiply-add; this compiler/flag combination is unsupported for world generation"; error="Core startup refused: floating-point contraction self-test detected a fused multiply-add; this compiler/flag combination is unsupported for world generation";
@@ -1018,13 +1148,74 @@ namespace VoxelForgeCore
std::unique_lock lock(impl_->tileRegionMutex); std::unique_lock lock(impl_->tileRegionMutex);
impl_->tileRegions.clear(); impl_->tileRegions.clear();
impl_->tileRegionCount.store(0,std::memory_order_release); impl_->tileRegionCount.store(0,std::memory_order_release);
impl_->shapeReady.store(true,std::memory_order_release);
} }
return ok; return ok;
} }
bool World::isTileShapeFree(const TileKey& key) const
{
if(!impl_||!impl_->owner||!impl_->worldAssembly||!impl_->owner->Manager
||!impl_->owner->Generator||!impl_->editLog.empty())return false;
FIntVector boundsMin,boundsMax;
if(!NativeOnlyBoundsForTile(*impl_,key,boundsMin,boundsMax))return false;
const std::int64_t tileMinimum[3]={boundsMin.X,boundsMin.Y,boundsMin.Z};
const std::int64_t tileMaximum[3]={boundsMax.X,boundsMax.Y,boundsMax.Z};
const int32 firstSurfaceChunk=FloorDiv(boundsMin.Z,CHUNK_SIZE);
const int32 lastSurfaceChunk=FloorDiv(boundsMax.Z,CHUNK_SIZE);
const WorldAssemblyStratumPlanV1* selectedPlan=nullptr;
for(int32 chunkZ=firstSurfaceChunk;chunkZ<=lastSurfaceChunk;++chunkZ)
{
if(impl_->owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,chunkZ))
!=ECaveGeneratorType::SurfaceWorld)return false;
const int32 strateIndex=impl_->owner->Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(chunkZ)*CHUNK_SIZE
+CHUNK_SIZE*0.5)*VOXEL_SIZE));
const auto found=std::find_if(impl_->worldAssembly->plan.strates.begin(),
impl_->worldAssembly->plan.strates.end(),[strateIndex,chunkZ](const auto& plan)
{return plan.strateIndex==strateIndex&&chunkZ<=plan.topChunkZ
&&chunkZ>=plan.bottomChunkZ;});
if(found==impl_->worldAssembly->plan.strates.end()
||(selectedPlan&&selectedPlan!=&*found))return false;
selectedPlan=&*found;
}
if(!selectedPlan||!Heightfield2D(impl_->worldAssembly->recipe,*selectedPlan).isValid())
return false;
const double coreVoxelsPerOwnerChunk=static_cast<double>(CHUNK_SIZE)
*static_cast<double>(impl_->owner->OwnerVoxelSizeMicrons)
/static_cast<double>(std::max(1u,impl_->owner->CoreVoxelSizeMicrons));
const int32 firstIslandChunk=FMath::FloorToInt(
static_cast<double>(boundsMin.Z)/coreVoxelsPerOwnerChunk);
const int32 lastIslandChunk=FMath::FloorToInt(
static_cast<double>(boundsMax.Z)/coreVoxelsPerOwnerChunk);
for(int32 chunkZ=firstIslandChunk;chunkZ<=lastIslandChunk;++chunkZ)
if(impl_->owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,chunkZ))
==ECaveGeneratorType::FloatingIslands)return false;
constexpr std::int64_t scale=WorldAssemblySubvoxelsPerVoxel;
const std::int64_t subMinimum[3]={tileMinimum[0]*scale,tileMinimum[1]*scale,
tileMinimum[2]*scale};
const std::int64_t subMaximum[3]={tileMaximum[0]*scale,tileMaximum[1]*scale,
tileMaximum[2]*scale};
for(const WorldAssemblyFeatureV1& feature:selectedPlan->features)
if(IntersectsBox(feature.minimumSubvoxels,feature.maximumSubvoxels,
subMinimum,subMaximum))return false;
if(RouteTouchesBox(*selectedPlan,subMinimum,subMaximum))return false;
if(OwnerPassageTouchesBox(*impl_->owner,boundsMin,boundsMax))return false;
FStandaloneShapeWorld proofGeometry;
if(!proofGeometry.BuildShapeDependencyProof(*impl_->owner,boundsMin,boundsMax,
impl_->owner->Generator->bStandaloneResearchRoundTunnels))return false;
const FIntVector tileOrigin=ToNative(key).OriginVoxels();
if(proofGeometry.HasShapeDependencyInBox(tileOrigin,1,CHUNK_SIZE))return false;
return true;
}
bool World::needsTileRegion(const TileKey& key) const bool World::needsTileRegion(const TileKey& key) const
{ {
if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt() if(!impl_||!impl_->owner||!impl_->shapeReady.load(std::memory_order_acquire)
||!impl_->shape||!impl_->shape->IsBuilt()
||key.lod<0||key.lod>=impl_->configuration.fullResolutionLodCount) ||key.lod<0||key.lod>=impl_->configuration.fullResolutionLodCount)
return false; return false;
if(CoveredByBaseShape(impl_->buildRequest,impl_->configuration,key))return false; if(CoveredByBaseShape(impl_->buildRequest,impl_->configuration,key))return false;
@@ -1039,10 +1230,15 @@ namespace VoxelForgeCore
{ {
if(outCompiledTiles)*outCompiledTiles=0; if(outCompiledTiles)*outCompiledTiles=0;
if(outStats)*outStats={}; if(outStats)*outStats={};
if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt()) try
{
if(!impl_||!impl_->owner||!impl_->shapeReady.load(std::memory_order_acquire)
||!impl_->shape||!impl_->shape->IsBuilt())
{error="the base world must be compiled before adding tile regions";return false;} {error="the base world must be compiled before adding tile regions";return false;}
if(tiles.empty())return true; if(tiles.empty())return true;
constexpr std::int64_t RegionPadding=10;
struct FPendingRegion struct FPendingRegion
{ {
TileKey pageKey; TileKey pageKey;
@@ -1061,12 +1257,27 @@ namespace VoxelForgeCore
const TileKey& key=tiles[inputIndex]; const TileKey& key=tiles[inputIndex];
if(key.lod<0||key.lod>8) if(key.lod<0||key.lod>8)
{error="tile region contains an invalid LOD";return false;} {error="tile region contains an invalid LOD";return false;}
if(key.lod>=impl_->configuration.fullResolutionLodCount if(key.lod>=impl_->configuration.fullResolutionLodCount)continue;
||CoveredByBaseShape(impl_->buildRequest,impl_->configuration,key))continue; const std::int64_t extent=static_cast<std::int64_t>(CHUNK_SIZE)
*(std::int64_t{1}<<key.lod);
const std::int32_t coordinates[3]={key.coordinate.x,key.coordinate.y,
key.coordinate.z};
for(int axis=0;axis<3;++axis)
{
const std::int64_t origin=static_cast<std::int64_t>(coordinates[axis])*extent;
if(origin<std::numeric_limits<std::int32_t>::min()+RegionPadding
||origin+extent>std::numeric_limits<std::int32_t>::max()-RegionPadding)
{error="tile region coordinate is out of range";return false;}
}
if(CoveredByBaseShape(impl_->buildRequest,impl_->configuration,key))continue;
if(impl_->tileRegions.contains(key)||!seenTiles.insert(key).second)continue; if(impl_->tileRegions.contains(key)||!seenTiles.insert(key).second)continue;
// Analytic regions evaluate only requested fine tiles and can safely coalesce a // Keep every region's bounds spatially finite. A large camera step can bring in
// whole entering LOD slab. Packed pages retain their fixed absolute parent cells. // disjoint tiles across the whole view; coalescing an entire LOD slab makes the
const TileKey pageKey=analyticOnly?TileKey{{0,0,0},key.lod}:ShapePageKey(key); // dense axis-bin grid grow with the bounding rectangle of all of them at once.
// Garder chaque region spatialement bornee: un grand deplacement peut amener des
// tuiles disjointes; fusionner tout un niveau ferait croitre les bins avec son
// grand rectangle englobant.
const TileKey pageKey=ShapePageKey(key);
auto [found,inserted]=regionByPage.try_emplace(pageKey,pendingRegions.size()); auto [found,inserted]=regionByPage.try_emplace(pageKey,pendingRegions.size());
if(inserted)pendingRegions.push_back({pageKey,{}}); if(inserted)pendingRegions.push_back({pageKey,{}});
pendingRegions[found->second].tiles.push_back(key); pendingRegions[found->second].tiles.push_back(key);
@@ -1173,12 +1384,16 @@ namespace VoxelForgeCore
} }
} }
{ {
std::unique_lock lock(impl_->tileRegionMutex); std::unordered_map<TileKey,std::shared_ptr<FStandaloneShapeWorld>,TileKeyHash> publish;
publish.reserve(stats.tileKeysCovered);
for(std::size_t i=0;i<pendingRegions.size();++i) for(std::size_t i=0;i<pendingRegions.size();++i)
{
for(const TileKey& key:pendingRegions[i].tiles) for(const TileKey& key:pendingRegions[i].tiles)
if(impl_->tileRegions.try_emplace(key,compiledRegions[i]).second)++compiledTileCount; publish.try_emplace(key,compiledRegions[i]);
} const std::size_t candidates=publish.size();
std::unique_lock lock(impl_->tileRegionMutex);
impl_->tileRegions.reserve(impl_->tileRegions.size()+candidates);
impl_->tileRegions.merge(publish);
compiledTileCount=candidates-publish.size();
impl_->tileRegionCount.store(impl_->tileRegions.size(),std::memory_order_release); impl_->tileRegionCount.store(impl_->tileRegions.size(),std::memory_order_release);
} }
stats.wallSeconds=std::chrono::duration<double>(Clock::now()-compileStart).count(); stats.wallSeconds=std::chrono::duration<double>(Clock::now()-compileStart).count();
@@ -1186,6 +1401,21 @@ namespace VoxelForgeCore
if(outStats)*outStats=stats; if(outStats)*outStats=stats;
return true; return true;
} }
catch(const std::bad_alloc&)
{
if(outCompiledTiles)*outCompiledTiles=0;
if(outStats)*outStats={};
error="region OOM";
return false;
}
catch(const std::length_error&)
{
if(outCompiledTiles)*outCompiledTiles=0;
if(outStats)*outStats={};
error="region size";
return false;
}
}
void World::retainTileRegions(std::span<const TileKey> desiredTiles) void World::retainTileRegions(std::span<const TileKey> desiredTiles)
{ {
@@ -1201,14 +1431,17 @@ namespace VoxelForgeCore
} }
void World::buildFarWorld() void World::buildFarWorld()
{ if(impl_->shape)impl_->shape->BuildFarWorld(); } { if(impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape)impl_->shape->BuildFarWorld(); }
bool World::isCompiled() const noexcept { return impl_&&impl_->shape&&impl_->shape->IsBuilt(); } bool World::isCompiled() const noexcept
bool World::hasFarWorld() const noexcept { return impl_&&impl_->shape&&impl_->shape->HasFarWorld(); } { return impl_&&impl_->shapeReady.load(std::memory_order_acquire); }
bool World::hasFarWorld() const noexcept
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)
&&impl_->shape&&impl_->shape->HasFarWorld(); }
ShapeBuildStats World::buildStats() const ShapeBuildStats World::buildStats() const
{ {
ShapeBuildStats out; ShapeBuildStats out;
if(!impl_||!impl_->shape)return out; if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape)return out;
const auto& stats=impl_->shape->GetStats(); const auto& stats=impl_->shape->GetStats();
out.shapeSeconds=stats.ShapeSeconds;out.intervalSeconds=stats.IntervalSeconds; out.shapeSeconds=stats.ShapeSeconds;out.intervalSeconds=stats.IntervalSeconds;
out.surfaceSeconds=stats.SurfaceSeconds;out.farShapeSeconds=stats.FarShapeSeconds; out.surfaceSeconds=stats.SurfaceSeconds;out.farShapeSeconds=stats.FarShapeSeconds;
@@ -1218,18 +1451,19 @@ namespace VoxelForgeCore
} }
float World::densityAt(Vec3d position) const float World::densityAt(Vec3d position) const
{ return impl_&&impl_->shape?impl_->shape->DensityAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; } { return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
float World::unclampedDensityAt(Vec3d position) const float World::unclampedDensityAt(Vec3d position) const
{ return impl_&&impl_->shape?impl_->shape->DensityAtUnclamped(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; } { return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAtUnclamped(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
DetailFieldSample World::detailSampleAt(Vec3d position) const DetailFieldSample World::detailSampleAt(Vec3d position) const
{ return impl_&&impl_->shape?impl_->shape->DetailSampleAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):DetailFieldSample{}; } { return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DetailSampleAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):DetailFieldSample{}; }
float World::densityAtLattice(Vec3i position) const float World::densityAtLattice(Vec3i position) const
{ return impl_&&impl_->shape?impl_->shape->DensityAtLattice(position.x,position.y,position.z):0.0f; } { return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAtLattice(position.x,position.y,position.z):0.0f; }
bool World::isSolidAt(Vec3d position) const { return densityAt(position)<0.0f; } bool World::isSolidAt(Vec3d position) const { return densityAt(position)<0.0f; }
bool World::isSolidAlongSegment(Vec3d start,Vec3d end,double spacing) const bool World::isSolidAlongSegment(Vec3d start,Vec3d end,double spacing) const
{ {
if(!impl_||!impl_->shape||!std::isfinite(spacing)||spacing<=0.0)return false; if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape
||!std::isfinite(spacing)||spacing<=0.0)return false;
const double dx=end.x-start.x,dy=end.y-start.y,dz=end.z-start.z; const double dx=end.x-start.x,dy=end.y-start.y,dz=end.z-start.z;
const double length=std::sqrt(dx*dx+dy*dy+dz*dz); const double length=std::sqrt(dx*dx+dy*dy+dz*dz);
const std::size_t intervals=std::max<std::size_t>(1,static_cast<std::size_t>(std::ceil(length/spacing))); const std::size_t intervals=std::max<std::size_t>(1,static_cast<std::size_t>(std::ceil(length/spacing)));
@@ -1243,7 +1477,7 @@ namespace VoxelForgeCore
TileClass World::classifyBox(Vec3i origin,std::int32_t step,std::int32_t cells) const TileClass World::classifyBox(Vec3i origin,std::int32_t step,std::int32_t cells) const
{ {
if(!impl_||!impl_->shape)return TileClass::mixed; if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape)return TileClass::mixed;
const EVoxelTileClass result=impl_->shape->ClassifyBox(ToNative(origin),step,cells); const EVoxelTileClass result=impl_->shape->ClassifyBox(ToNative(origin),step,cells);
return result==EVoxelTileClass::AllSolid?TileClass::allSolid: return result==EVoxelTileClass::AllSolid?TileClass::allSolid:
result==EVoxelTileClass::AllAir?TileClass::allAir:TileClass::mixed; result==EVoxelTileClass::AllAir?TileClass::allAir:TileClass::mixed;
@@ -1365,20 +1599,38 @@ namespace VoxelForgeCore
// Les tuiles proches n'avaient aucun conflit; garder leur acces simple et associer les LOD grossiers. // Les tuiles proches n'avaient aucun conflit; garder leur acces simple et associer les LOD grossiers.
Heightfield2D::setThreadColumnCacheLegacyDirectMapped( Heightfield2D::setThreadColumnCacheLegacyDirectMapped(
options.legacyDirectMappedColumnCache||key.lod<=2); options.legacyDirectMappedColumnCache||key.lod<=2);
if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt()) if(!impl_||!impl_->owner)
{ error="world must be compiled before generating tiles";return std::nullopt; } { error="resolved world snapshot is empty";return std::nullopt; }
const bool bShapeReady=impl_->shapeReady.load(std::memory_order_acquire);
const bool bNativeOnly=!bShapeReady||options.diagnosticForceNativeOnly;
if(!bShapeReady&&!options.diagnosticForceNativeOnly&&!isTileShapeFree(key))
{error="tile is shape-dependent and must wait for shape compilation";return std::nullopt;}
if(!bNativeOnly&&(!impl_->shape||!impl_->shape->IsBuilt()))
{error="world shape publication is incomplete";return std::nullopt;}
FScopedSharedColumnCaches sharedColumnCaches(options.useSharedColumnCache FScopedSharedColumnCaches sharedColumnCaches(options.useSharedColumnCache
?impl_->sharedHeightfieldColumns.get():nullptr, ?impl_->sharedHeightfieldColumns.get():nullptr,
options.useSharedColumnCache?impl_->sharedIslandColumns.get():nullptr); options.useSharedColumnCache?impl_->sharedIslandColumns.get():nullptr);
const auto& world=*impl_->owner; const auto& world=*impl_->owner;
FStandaloneShapeWorld nativeOnlyShape;
FIntVector nativeOnlyMinimum,nativeOnlyMaximum;
std::shared_ptr<FStandaloneShapeWorld> page; std::shared_ptr<FStandaloneShapeWorld> page;
if(impl_->tileRegionCount.load(std::memory_order_acquire)!=0) if(bNativeOnly)
{
if(!NativeOnlyBoundsForTile(*impl_,key,nativeOnlyMinimum,nativeOnlyMaximum))
{error="shape-free guard: tile has no bounded native-only domain";return std::nullopt;}
if(!options.diagnosticForceNativeOnly&&!isTileShapeFree(key))
{error="shape-free guard: tile failed the native-only readiness proof";return std::nullopt;}
nativeOnlyShape.InitializeNativeSurfaceOnly(*impl_->owner,impl_->worldAssembly.get(),
nativeOnlyMinimum,nativeOnlyMaximum);
}
else if(impl_->tileRegionCount.load(std::memory_order_acquire)!=0)
{ {
std::shared_lock lock(impl_->tileRegionMutex); std::shared_lock lock(impl_->tileRegionMutex);
if(const auto found=impl_->tileRegions.find(key);found!=impl_->tileRegions.end()) if(const auto found=impl_->tileRegions.find(key);found!=impl_->tileRegions.end())
page=found->second; page=found->second;
} }
const FStandaloneShapeWorld* tileShapeContext=page?page.get():impl_->shape.get(); const FStandaloneShapeWorld* tileShapeContext=bNativeOnly?&nativeOnlyShape:
(page?page.get():impl_->shape.get());
UVoxelMarchingCubesMesher mesher; UVoxelMarchingCubesMesher mesher;
mesher.SetGenerator(world.Generator.get()); mesher.SetGenerator(world.Generator.get());
mesher.SetGeometryVoxelSizeCm(static_cast<float>(world.CoreVoxelSizeMicrons)/10000.0f); mesher.SetGeometryVoxelSizeCm(static_cast<float>(world.CoreVoxelSizeMicrons)/10000.0f);
@@ -1435,6 +1687,12 @@ namespace VoxelForgeCore
} }
if(!sharpFeatures.IsEmpty()) if(!sharpFeatures.IsEmpty())
{ {
if(bNativeOnly)
{
nativeOnlyShape.RecordNativeOnlyViolation();
error="shape-free guard: tile intersects a shape feature";
return std::nullopt;
}
// Streamed analytic pages omit the native assembly plan. Use the immutable base // Streamed analytic pages omit the native assembly plan. Use the immutable base
// shape only for tagged tiles so their mesher samples the same field as the tags. // shape only for tagged tiles so their mesher samples the same field as the tags.
// Les pages analytiques omettent le plan natif ; utiliser la base uniquement pour // Les pages analytiques omettent le plan natif ; utiliser la base uniquement pour
@@ -1493,7 +1751,7 @@ namespace VoxelForgeCore
out.diagnostics.featureRefinedResolution=bFeatureRefinedResolution; out.diagnostics.featureRefinedResolution=bFeatureRefinedResolution;
const bool bHasWorldAssembly=impl_->worldAssembly!=nullptr; const bool bHasWorldAssembly=impl_->worldAssembly!=nullptr;
const bool bNeedsFarShell=bHasWorldAssembly&&key.lod>=7 const bool bNeedsFarShell=bHasWorldAssembly&&key.lod>=7
&&(tileShape.HasFarWorld()||impl_->shape->HasFarWorld()); &&(tileShape.HasFarWorld()||(bShapeReady&&impl_->shape->HasFarWorld()));
bool bOverlapsPlayerEdit=false; bool bOverlapsPlayerEdit=false;
if(tileShape.HasEditTransaction()) if(tileShape.HasEditTransaction())
{ {
@@ -1767,6 +2025,11 @@ namespace VoxelForgeCore
Heightfield2D::setThreadColumnOctaveDrop(PreviousOctaveDrop); Heightfield2D::setThreadColumnOctaveDrop(PreviousOctaveDrop);
Heightfield2D::setThreadColumnInterpolationStrideSubvoxels(PreviousInterpolationStride); Heightfield2D::setThreadColumnInterpolationStrideSubvoxels(PreviousInterpolationStride);
} }
if(bNativeOnly&&tileShape.GetNativeOnlyViolationCount()!=0)
{
error="shape-free guard caught a mesher read outside the native SurfaceWorld field";
return std::nullopt;
}
const double MarchingCubesWallMilliseconds=std::chrono::duration<double,std::milli>( const double MarchingCubesWallMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-MarchingCubesStart).count(); std::chrono::steady_clock::now()-MarchingCubesStart).count();
const double ColumnMillisecondsAfterMeshing= const double ColumnMillisecondsAfterMeshing=
@@ -1801,7 +2064,8 @@ namespace VoxelForgeCore
std::chrono::steady_clock::now()-MeshingStart).count(); std::chrono::steady_clock::now()-MeshingStart).count();
out.diagnostics.farHorizonVersion=key.lod>=4?options.farHorizonVersion:0; out.diagnostics.farHorizonVersion=key.lod>=4?options.farHorizonVersion:0;
const auto FarShellStart=std::chrono::steady_clock::now(); const auto FarShellStart=std::chrono::steady_clock::now();
const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:*impl_->shape; const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:
(bShapeReady?*impl_->shape:tileShape);
out.diagnostics.farShellRepairSkipped=bSkipFarShellRepair; out.diagnostics.farShellRepairSkipped=bSkipFarShellRepair;
if(farShape.HasFarWorld()&&!bSkipFarShellRepair) if(farShape.HasFarWorld()&&!bSkipFarShellRepair)
{ {
@@ -0,0 +1,74 @@
# VoxelForge L11 — Linux report
## 24 m crossing crash
The crash is real. The original binary threw `std::bad_alloc` after base shape compilation with both 6 GiB and 16 GiB `RLIMIT_AS`; the 16 GiB run peaked at 476,128 KiB RSS and 1,542,284 KiB virtual size. The allocation trace identifies a 432,345,551,342,665,824-byte resize in `BuildSpatialBins`, reached from `BuildAnalyticRegion` during `World::compileTileRegion`. The crossing code had coalesced disjoint entering tiles into a large LOD rectangle and tried to allocate dense per-axis bins that the analytic fallback never reads. L10's 263.57 MiB figure is the proxies' reserved virtual capacity; RSS samples record resident usage separately.
Analytic regions now skip those dense bins and compile in world-aligned 128-voxel pages. Pages are built privately and published together; coordinate, `bad_alloc`, and size failures return errors before partial pages are published. On the final L11 binary, 24 m and 32 m both passed (16 pages each); 1,000 m passed with 2,260 entering keys, 72 pages, 63 ms region-build wall time, 700,232 KiB peak RSS, and 2,578,588 KiB peak virtual size. The fixed 24 m run peaked at 533,420 KiB RSS and 2,578,588 KiB virtual size. Crossing pages build no packed interval nodes (`lattice_nodes=0`) and do not expand the immutable base sign/proxy arrays; they add analytic primitive/bin data. `retainTileRegions` prunes pages outside the desired set. The largest empirically verified step is 1,000 m. Work follows the destination's entering tile set; once the old and new views are disjoint, its page count stays bounded by the active view as travel distance increases. Keys outside Core's signed 32-bit voxel range are rejected with `tile region coordinate is out of range` before page construction. The earlier L7 32 m / 160-million-node failure was the same unbounded region geometry.
| Step | Entering keys | Pages | Added interval nodes | Base proxy nodes / virtual capacity |
|---:|---:|---:|---:|---:|
| 24 m | 432 | 16 | 0 | 185,024 / 263.57 MiB |
| 1,000 m | 2,260 | 72 | 0 | 185,024 / 263.57 MiB |
## Shape readiness
`isTileShapeFree` accepts only LOD0 tiles whose padded mesh box maps to one valid SurfaceWorld plan, excludes FloatingIslands, plan features, routes, passages, edits, and overlaps with owner shape primitives or detail bounds. This covers the MC skirt and sample margin. Higher LODs are conservatively shape-dependent; the accepted path cannot use far parents. The native-only context is initialized without building sign arrays, proxies, owner primitive vectors, or spatial bins. Native sampling and classification paths check the padded bounds, reject unsupported fallbacks, and count any attempted shape-dependent read; tile generation then fails instead of returning a partial mesh.
The SurfaceWorld spawn view proved 27/27 near-ring tiles shape-free. A deliberately mis-marked distant tile `(0,0,100000,lod0)` reached the sampler and was rejected by the guard. After shape publication, native-only and full-shape generation matched all four hashes for all 27 tiles. Shape-dependent tiles waited for the release-published shape; tile jobs continue through the existing Epoch and MPSC result path, and published shape/page data is immutable. The FloatingIslands spawn proof accepted 0/27 tiles, so all waited for its shape. Slow-shape delay defaults to zero; audit and negative-control work is opt-in.
## No-hash timing pairs
Five interleaved W12 pairs, camera `0,-35,-487.94`, seed 0, Clang `-ffp-contract=off`, hashing off. Times are from process start. The L9 executable does not emit a first-tile completion event and starts tile workers only after shape compilation; its first-tile cell is therefore unavailable.
| Pair | First tile L9 / L11 | Near ring L9 / L11 | Whole view L9 / L11 |
|---:|---:|---:|---:|
| 1 | — / 153 ms | 343 / 166 ms | 1,439 / 1,429 ms |
| 2 | — / 160 ms | 424 / 173 ms | 1,526 / 1,447 ms |
| 3 | — / 145 ms | 347 / 163 ms | 1,447 / 1,443 ms |
| 4 | — / 146 ms | 353 / 170 ms | 1,457 / 1,449 ms |
| 5 | — / 153 ms | 376 / 173 ms | 1,478 / 1,464 ms |
| Median | — / 153 ms | 353 / 170 ms | 1,457 / 1,447 ms |
The delayed maximum-overlap W12 run held shape compilation for 1.5 s: first tile 133 ms, near ring 166 ms, shape ready 1.696 s. Shape-free and full-shape near-ring hashes matched, and whole-view, full-detail, per-LOD, and exact-query hashes matched the baseline.
## Hashes
All 28 configurations passed against L10's committed `b370908` baseline: SurfaceWorld and FloatingIslands, seed 0, W1/W8/W12, Clang `-ffp-contract=off` and GCC default, with five W12 repeats per compiler and recipe. Each run matched all 12 rows: full-detail, whole-view, nine LODs, and exact queries. The delayed-overlap run also matched.
| Recipe | Full-detail density / mesh / payload / topology | Whole-view density / mesh / payload / topology | Exact-query density & unclamped / detail / collision |
|---|---|---|---|
| SurfaceWorld | `0x0ee0579bd15cbbe4` / `0x32e5d4964d610fee` / `0x1932b96ee4b1d910` / `0x69743c6bad3447dc` | `0xc10697a8f48380c7` / `0xe75c9119a1498cd5` / `0x49a9a8d1835a6c8c` / `0xedcb23288b5e8724` | `0xe4d4a8eb95b35933` / `0xefcba20c0c70c415` / `0xe604823a249029bf` |
| FloatingIslands | `0x6d842d2878b08ef6` / `0x4b868a3184f08035` / `0x2cedf2236788752d` / `0xeac17d5e0cb5044a` | `0x5be4d8cde79b2a76` / `0x96f75041f6861b3a` / `0x3700a516cc89d50c` / `0x6b99948d6eab4f21` | `0x61daded9c303becb` / `0xefcba20c0c70c415` / `0x84d37463be9b0573` |
The per-LOD D/M/P/T rows match exactly in every configuration; the complete comparison is recorded in `Saved/Linux/l11_hash_audit.txt`.
## Failed and retried runs
- The 24 m reproduction failed with `std::bad_alloc` under both address-space caps (`l11-cross24-as6g.log`, `l11-cross24-as16g.log`); the allocation trace above isolated the oversized bin resize. Final-binary 24 m, 32 m, and 1,000 m reruns passed (`l11-final-cross-24m.log`, `l11-final-cross-32m.log`, `l11-final-cross-1000m.log`).
- The first readiness proof admitted 512 LOD0 keys (`l11-readiness-audit.log`, `l11-readiness-audit2.log`). Although the 27 near-ring equivalence check passed, distant owner-shape tiles changed the whole-view density hash. Tightening the proof to test owner primitive bounds reduced early admission to the 27 safe near-ring tiles and restored every hash (`l11-readiness-audit-final.log`).
- One rebuilt audit invocation omitted the owner JSON path and exited 2 before generation (`l11-readiness-audit-path-error.log`; `Standalone/bin/data/owner_world.json` was missing). It was rerun with `Standalone/data/owner_world.json` and passed.
- The required hash matrix and five timing pairs had no failed runs. All monitored run processes stayed below the 6,000,000,000-byte RSS ceiling; governor remained `powersave`.
The standalone Core/headless builds passed with Clang and GCC. The Unreal adapter change was source-reviewed but could not be built here because UnrealBuildTool/UnrealEditor are not installed on this Linux machine.
At spawn on SurfaceWorld, the near ring is ready at a 170 ms median versus 353 ms on L9, and the first L11 tile is ready at 153 ms median before shape publication; L9 gates tile work on shape compilation and does not report that milestone. For fast movement, destination-page work stays bounded through the tested 1 km crossing, while invalid Core coordinates return an error before allocation.
## Orchestrator review (Claude)
- **Re-run by me, 5 interleaved W12 pairs, no hashing.** Near ring (`neighborhood_ready`) L9
0.338/0.338/0.337/0.326/0.334 s vs L11 0.159/0.159/0.167/0.169/0.159 s (median 0.337 → 0.159 s,
−53%). Whole view 1.337 → 1.365 s median (+2.1%, 4 of 5 pairs slower): a small cost, accepted for a
2× faster spawn ring. W12 hash runs give density `c10697a8f48380c7`.
- **Crossings re-run by me on L11** (`--region-crossing --camera-step=S,0,0`): 24 m gives 432 tiles,
0.867 s wall, 10.05 worker-s, rc 0; 1,000 m gives 2,260 tiles, 5.90 s wall, 70.4 worker-s, rc 0. The L9
binary lacks the option, so the original crash rests on Luna's reproduction and allocation trace.
- **New finding, for L12:** the 24 m crossing costs ~23 ms of work per tile, against ~2 ms for L10's 16 m
crossing (288 tiles, 0.633 worker-s). Tiles outside the base shape bounds, served from analytic
region pages, look about 10× more expensive. That is a moving-player cost.
- **Concurrency read:** `World::compile` builds the shape privately, then sets `impl_->shape` and
`buildRequest` before a release store of `shapeReady`. Every shape reader acquire-checks
`shapeReady` first (short-circuit), and shape-free tiles use a private native-only context. Sound.
- **Unreal:** `Source/VoxelForge/Private/VoxelWorld.cpp` changed (compile outside the writer lock;
proven shape-free tiles start before `bReady`). It is NOT compiled here, so it must be built on the
owner's PC before it is trusted in game.
+17 -1
View File
@@ -9,6 +9,7 @@
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <ctime>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
@@ -41,6 +42,9 @@ namespace
bool ScreenSpaceGate=false; bool ScreenSpaceGate=false;
bool AuditUniformBlockProofs=false; bool AuditUniformBlockProofs=false;
bool ProfileLodWork=false; bool ProfileLodWork=false;
bool RegionCrossing=false;
bool AuditTileShapeReadiness=false;
int DiagnosticSlowShapeCompileMs=0;
bool HasSeedOverride=false; bool HasSeedOverride=false;
int SeedOverride=0; int SeedOverride=0;
bool HasRenderDistanceOverride=false; bool HasRenderDistanceOverride=false;
@@ -106,6 +110,7 @@ namespace
" --workers=N --clip-radius=N --max-clip-level=N --max-level=N\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" " --camera=X,Y,Z,YAW,PITCH (position in metres) --stream-hash\n"
" --camera-step=X,Y,Z (optional second view step in metres)\n" " --camera-step=X,Y,Z (optional second view step in metres)\n"
" --region-crossing (compile entering Core regions at the camera step)\n"
" --column-cache=shared|thread-local (default: shared)\n" " --column-cache=shared|thread-local (default: shared)\n"
" --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n" " --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n"
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n" " --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
@@ -115,6 +120,8 @@ namespace
" --screen-space-gate (measure display error and historical negative controls)\n" " --screen-space-gate (measure display error and historical negative controls)\n"
" --audit-uniform-block-proofs (fill and check every interval-certified block)\n" " --audit-uniform-block-proofs (fill and check every interval-certified block)\n"
" --profile-lod-work (report per-LOD density sample counts)\n" " --profile-lod-work (report per-LOD density sample counts)\n"
" --audit-tile-shape-readiness (run a deliberately mis-marked tile against the native-only guard)\n"
" --diagnostic-slow-shape-compile-ms=N (diagnostic overlap control; default 0)\n"
" --render-distance-chunks=N (0 disables the extra far ring)\n" " --render-distance-chunks=N (0 disables the extra far ring)\n"
" --tls-probe=N (Linux worker TLS/stack measurement; N=0..12)\n" " --tls-probe=N (Linux worker TLS/stack measurement; N=0..12)\n"
" --tls-probe-touch=none|surface-world|floating-islands (first-use probe)\n" " --tls-probe-touch=none|surface-world|floating-islands (first-use probe)\n"
@@ -132,6 +139,8 @@ namespace
else if(arg=="--screen-space-gate")options.ScreenSpaceGate=true; else if(arg=="--screen-space-gate")options.ScreenSpaceGate=true;
else if(arg=="--audit-uniform-block-proofs")options.AuditUniformBlockProofs=true; else if(arg=="--audit-uniform-block-proofs")options.AuditUniformBlockProofs=true;
else if(arg=="--profile-lod-work")options.ProfileLodWork=true; else if(arg=="--profile-lod-work")options.ProfileLodWork=true;
else if(arg=="--region-crossing")options.RegionCrossing=true;
else if(arg=="--audit-tile-shape-readiness")options.AuditTileShapeReadiness=true;
else if(arg=="--auto-surface-camera")options.AutoSurfaceCamera=true; else if(arg=="--auto-surface-camera")options.AutoSurfaceCamera=true;
else if(arg.starts_with("--recipe=")) else if(arg.starts_with("--recipe="))
{ {
@@ -199,6 +208,8 @@ namespace
{if(!readInt("--max-level=",options.MaxLevel))return false;} {if(!readInt("--max-level=",options.MaxLevel))return false;}
else if(arg.starts_with("--far-horizon-lod8-step=")) else if(arg.starts_with("--far-horizon-lod8-step="))
{if(!readInt("--far-horizon-lod8-step=",options.FarHorizonLOD8InteriorStepVoxels))return false;} {if(!readInt("--far-horizon-lod8-step=",options.FarHorizonLOD8InteriorStepVoxels))return false;}
else if(arg.starts_with("--diagnostic-slow-shape-compile-ms="))
{if(!readInt("--diagnostic-slow-shape-compile-ms=",options.DiagnosticSlowShapeCompileMs))return false;}
else if(arg.starts_with("--process-affinity=")||arg.starts_with("--process-priority=") else if(arg.starts_with("--process-affinity=")||arg.starts_with("--process-priority=")
||arg.starts_with("--frame-limit=")) ||arg.starts_with("--frame-limit="))
{ {
@@ -213,8 +224,11 @@ namespace
options.ClipRadius=std::clamp(options.ClipRadius,1,64); options.ClipRadius=std::clamp(options.ClipRadius,1,64);
options.MaxClipLevel=std::clamp(options.MaxClipLevel,0,8); options.MaxClipLevel=std::clamp(options.MaxClipLevel,0,8);
options.FarHorizonLOD8InteriorStepVoxels=std::max(0,options.FarHorizonLOD8InteriorStepVoxels); options.FarHorizonLOD8InteriorStepVoxels=std::max(0,options.FarHorizonLOD8InteriorStepVoxels);
options.DiagnosticSlowShapeCompileMs=std::clamp(options.DiagnosticSlowShapeCompileMs,0,30000);
if(!options.TlsProbeRequested&&options.TlsProbeTouch!="none") if(!options.TlsProbeRequested&&options.TlsProbeTouch!="none")
{std::fprintf(stderr,"--tls-probe-touch requires --tls-probe=N\n");return false;} {std::fprintf(stderr,"--tls-probe-touch requires --tls-probe=N\n");return false;}
if(options.RegionCrossing&&!options.HasCameraStep)
{std::fprintf(stderr,"--region-crossing requires --camera-step=X,Y,Z\n");return false;}
if(options.TlsProbeRequested)return true; if(options.TlsProbeRequested)return true;
if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline) if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline)
{ {
@@ -354,5 +368,7 @@ int main(int argc,char** argv)
options.ProfileColumnOverlap,options.FarHorizonLOD8InteriorStepVoxels, options.ProfileColumnOverlap,options.FarHorizonLOD8InteriorStepVoxels,
options.MeshCoverage,options.MeshExportPath,options.CameraYawDegrees, options.MeshCoverage,options.MeshExportPath,options.CameraYawDegrees,
options.CameraPitchDegrees,options.ScreenSpaceGate,options.UseSharedColumnCache, options.CameraPitchDegrees,options.ScreenSpaceGate,options.UseSharedColumnCache,
cameraStepVoxels,options.AuditUniformBlockProofs,options.ProfileLodWork); cameraStepVoxels,options.AuditUniformBlockProofs,options.ProfileLodWork,
options.RegionCrossing,options.AuditTileShapeReadiness,
options.DiagnosticSlowShapeCompileMs);
} }
+207 -16
View File
@@ -9,10 +9,13 @@
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <ctime>
#include <cstdio> #include <cstdio>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <future>
#include <limits> #include <limits>
#include <numeric>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
@@ -139,7 +142,9 @@ namespace StandaloneStreamBenchmark
const std::filesystem::path& MeshExportPath={},float CameraYawDegrees=0.0f, const std::filesystem::path& MeshExportPath={},float CameraYawDegrees=0.0f,
float CameraPitchDegrees=0.0f,bool bScreenSpaceGate=false, float CameraPitchDegrees=0.0f,bool bScreenSpaceGate=false,
bool bUseSharedColumnCache=true,FVector CameraStepVoxels=FVector::ZeroVector, bool bUseSharedColumnCache=true,FVector CameraStepVoxels=FVector::ZeroVector,
bool bAuditUniformBlockProofs=false,bool bReportLodWorkProfile=false) bool bAuditUniformBlockProofs=false,bool bReportLodWorkProfile=false,
bool bRegionCrossing=false,
bool bAuditTileShapeReadiness=false,int DiagnosticSlowShapeCompileMs=0)
{ {
using Clock=std::chrono::steady_clock; using Clock=std::chrono::steady_clock;
FStandaloneOwnerWorld& World=*OwnerWorld; FStandaloneOwnerWorld& World=*OwnerWorld;
@@ -223,7 +228,7 @@ namespace StandaloneStreamBenchmark
} }
}; };
AccumulateBounds(Desired.Tiles); AccumulateBounds(Desired.Tiles);
if(bHasCameraStep)AccumulateBounds(MovedDesired.Tiles); if(bHasCameraStep&&!bRegionCrossing)AccumulateBounds(MovedDesired.Tiles);
if(FineMin.X==INT32_MAX||ClipMin.X==INT32_MAX) if(FineMin.X==INT32_MAX||ClipMin.X==INT32_MAX)
{std::fprintf(stderr,"[StreamPipeline] no desired detail or clipmap tiles.\n");return 7;} {std::fprintf(stderr,"[StreamPipeline] no desired detail or clipmap tiles.\n");return 7;}
@@ -237,6 +242,46 @@ namespace StandaloneStreamBenchmark
if(Neighborhood.size()!=27) if(Neighborhood.size()!=27)
{std::fprintf(stderr,"[StreamPipeline] expected 27 player-neighborhood tiles, found %zu.\n",Neighborhood.size());return 7;} {std::fprintf(stderr,"[StreamPipeline] expected 27 player-neighborhood tiles, found %zu.\n",Neighborhood.size());return 7;}
FStandaloneDesiredTileSet ShapeFreeDesired=Desired;
ShapeFreeDesired.Tiles.clear();
std::vector<FVoxelTileKey> ShapeFreeTiles;
ShapeFreeTiles.reserve(Neighborhood.size());
int32 ShapeFreeNeighborhood=0;
for(const FVoxelTileKey& Tile:Desired.Tiles)
{
if(!Neighborhood.contains(Tile))continue;
const VoxelForgeCore::TileKey CoreKey{{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level};
if(!Shape.isTileShapeFree(CoreKey))continue;
ShapeFreeDesired.Tiles.push_back(Tile);
ShapeFreeTiles.push_back(Tile);
if(Neighborhood.contains(Tile))++ShapeFreeNeighborhood;
}
if(bAuditTileShapeReadiness)
{
FVoxelTileKey DeliberatelyWrong(FIntVector(0,0,31250),0);
for(int32 Z=100000;Z<100100;++Z)
{
DeliberatelyWrong=FVoxelTileKey(FIntVector(0,0,Z),0);
if(!Shape.isTileShapeFree({{0,0,Z},0}))break;
}
VoxelForgeCore::TileGenerationOptions NegativeControlOptions;
NegativeControlOptions.diagnosticForceNativeOnly=true;
std::string NegativeControlError;
const auto MisMarked=Shape.generateTile(
{{DeliberatelyWrong.Coord.X,DeliberatelyWrong.Coord.Y,DeliberatelyWrong.Coord.Z},0},
NegativeControlOptions,NegativeControlError);
const bool bGuardCaught=!MisMarked
&&NegativeControlError.find("shape-free guard")!=std::string::npos;
std::printf("[ShapeReadinessNegativeControl] deliberately_mismarked=(%d,%d,%d,lod0) result=%s guard_caught=%d error=%s\n",
DeliberatelyWrong.Coord.X,DeliberatelyWrong.Coord.Y,DeliberatelyWrong.Coord.Z,
MisMarked?"generated":"rejected",bGuardCaught?1:0,
NegativeControlError.c_str());
if(!bGuardCaught)
{std::fprintf(stderr,"[StreamPipeline] shape-free guard did not reject the deliberately mis-marked tile.\n");return 9;}
}
std::printf("[ShapeReadiness] shape_free_desired=%zu/%zu near_ring=%d/27 proof=single_SurfaceWorld_plan_no_features_routes_passages_edits_or_owner_primitive_overlap\n",
ShapeFreeTiles.size(),Desired.Tiles.size(),ShapeFreeNeighborhood);
// The initial desired set now owns all clipmap metadata needed by this Core-only run. // 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. // Release the separate compatibility-world generation graph before compiling Core.
World.Generator.reset(); World.Generator.reset();
@@ -245,10 +290,7 @@ namespace StandaloneStreamBenchmark
World.BiomeStorage.clear(); World.BiomeStorage.clear();
World.OperationStorage.clear(); World.OperationStorage.clear();
World.DensitySamples.clear(); World.DensitySamples.clear();
std::string BuildError;
const bool bIncludeFarWorld=MaxLevel<0||MaxLevel>=World.FullResClipLevels; const bool bIncludeFarWorld=MaxLevel<0||MaxLevel>=World.FullResClipLevels;
const auto ShapeBuildStart=Clock::now();
VoxelForgeCore::ShapeBuildRequest ShapeRequest; VoxelForgeCore::ShapeBuildRequest ShapeRequest;
ShapeRequest.minVoxel={FineMin.X,FineMin.Y,FineMin.Z}; ShapeRequest.minVoxel={FineMin.X,FineMin.Y,FineMin.Z};
ShapeRequest.maxVoxel={FineMax.X,FineMax.Y,FineMax.Z}; ShapeRequest.maxVoxel={FineMax.X,FineMax.Y,FineMax.Z};
@@ -258,21 +300,82 @@ namespace StandaloneStreamBenchmark
ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld; ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld;
ShapeRequest.roundTunnels=bRoundTunnels; ShapeRequest.roundTunnels=bRoundTunnels;
ShapeRequest.enhancedTunnelWalls=bEnhancedTunnelWalls; 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, FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap, bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap,
FarHorizonLOD8InteriorStepVoxels,bUseSharedColumnCache); FarHorizonLOD8InteriorStepVoxels,bUseSharedColumnCache);
Streamer.SetUniformBlockProofAuditEnabled(bAuditUniformBlockProofs); Streamer.SetUniformBlockProofAuditEnabled(bAuditUniformBlockProofs);
if(bScreenSpaceGate)Streamer.SetMesherStageProfilingEnabled(true); if(bScreenSpaceGate)Streamer.SetMesherStageProfilingEnabled(true);
const auto StreamStart=Clock::now(); const auto StreamStart=Clock::now();
const FStandaloneStreamingUpdate Update=Streamer.StartWithDesired(Desired); const FStandaloneStreamingUpdate EarlyUpdate=Streamer.StartWithDesired(ShapeFreeDesired);
if(EarlyUpdate.DesiredTiles!=static_cast<int32>(ShapeFreeDesired.Tiles.size()))
{
Streamer.Stop();
std::fprintf(stderr,"[StreamPipeline] expected %zu shape-free tiles but queued %d.\n",
ShapeFreeDesired.Tiles.size(),EarlyUpdate.DesiredTiles);return 7;
}
const auto ShapeBuildStart=Clock::now();
auto ShapeBuildFuture=std::async(std::launch::async,
[&Shape,&Scheduler,ShapeRequest,DiagnosticSlowShapeCompileMs]() mutable
{
if(DiagnosticSlowShapeCompileMs>0)
std::this_thread::sleep_for(std::chrono::milliseconds(DiagnosticSlowShapeCompileMs));
std::string Error;
const bool bCompiled=Shape.compile(ShapeRequest,Scheduler,Error);
return std::pair<bool,std::string>{bCompiled,std::move(Error)};
});
const auto [bShapeCompiled,BuildError]=ShapeBuildFuture.get();
if(!bShapeCompiled)
{
Streamer.Stop();
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();
if(bAuditTileShapeReadiness)
{
VoxelForgeCore::TileGenerationOptions CompiledOptions;
VoxelForgeCore::TileGenerationOptions NativeOnlyOptions;
CompiledOptions.hashDensity=true;
NativeOnlyOptions.hashDensity=true;
NativeOnlyOptions.diagnosticForceNativeOnly=true;
int32 Compared=0,Mismatched=0;
for(const FVoxelTileKey& Tile:ShapeFreeTiles)
{
if(!Neighborhood.contains(Tile))continue;
const VoxelForgeCore::TileKey CoreKey{{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level};
std::string CompiledError,NativeOnlyError;
const auto Compiled=Shape.generateTile(CoreKey,CompiledOptions,CompiledError);
const auto NativeOnly=Shape.generateTile(CoreKey,NativeOnlyOptions,NativeOnlyError);
++Compared;
const bool bSame=Compiled&&NativeOnly
&&Compiled->densityHash==NativeOnly->densityHash
&&Compiled->meshHash==NativeOnly->meshHash
&&Compiled->payloadHash==NativeOnly->payloadHash
&&Compiled->topologyHash==NativeOnly->topologyHash;
if(!bSame)
{
++Mismatched;
std::printf("[ShapeReadinessEquivalence] key=(%d,%d,%d,lod%d) match=0 compiled=%s native_only=%s density=%s mesh=%s\n",
Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z,Tile.Level,
Compiled?"ok":CompiledError.c_str(),NativeOnly?"ok":NativeOnlyError.c_str(),
Compiled&&NativeOnly&&Compiled->densityHash==NativeOnly->densityHash?"same":"different",
Compiled&&NativeOnly&&Compiled->meshHash==NativeOnly->meshHash?"same":"different");
}
}
std::printf("[ShapeReadinessEquivalence] compared_near_ring=%d mismatches=%d status=%s\n",
Compared,Mismatched,Mismatched==0?"PASS":"FAIL");
if(Mismatched!=0)
{
Streamer.Stop();
std::fprintf(stderr,"[StreamPipeline] native-only tile generation differs from compiled generation.\n");
return 9;
}
}
const FStandaloneStreamingUpdate Update=Streamer.UpdateDesiredWithSet(Desired,{},0);
if(Update.DesiredTiles!=static_cast<int32>(Desired.Tiles.size())) if(Update.DesiredTiles!=static_cast<int32>(Desired.Tiles.size()))
{ {
Streamer.Stop(); Streamer.Stop();
std::fprintf(stderr,"[StreamPipeline] expected %zu tiles but queued %d.\n", std::fprintf(stderr,"[StreamPipeline] expected %zu tiles after shape publication but queued %d.\n",
Desired.Tiles.size(),Update.DesiredTiles);return 7; Desired.Tiles.size(),Update.DesiredTiles);return 7;
} }
FSceneExportWriter SceneExport; FSceneExportWriter SceneExport;
@@ -355,8 +458,8 @@ namespace StandaloneStreamBenchmark
std::array<std::optional<FStandaloneCpuTile>,9> GateSampleTiles{}; std::array<std::optional<FStandaloneCpuTile>,9> GateSampleTiles{};
std::array<double,9> GateSampleDistances{}; std::array<double,9> GateSampleDistances{};
GateSampleDistances.fill(std::numeric_limits<double>::infinity()); GateSampleDistances.fill(std::numeric_limits<double>::infinity());
Clock::time_point NeighborhoodReady{},FullDetailReady{},WholeViewReady{}; Clock::time_point FirstTileReady{},NeighborhoodReady{},FullDetailReady{},WholeViewReady{};
bool bNeighborhoodReported=false,bFullDetailReported=false; bool bFirstTileReported=false,bNeighborhoodReported=false,bFullDetailReported=false;
uint64 BlockProofBlocksChecked=0,BlockProofSamplesChecked=0; uint64 BlockProofBlocksChecked=0,BlockProofSamplesChecked=0;
uint64 BlockProofFallbackBlocks=0,BlockProofUnknownBlocks=0,BlockProofViolations=0; uint64 BlockProofFallbackBlocks=0,BlockProofUnknownBlocks=0,BlockProofViolations=0;
while(Finished<Update.DesiredTiles) while(Finished<Update.DesiredTiles)
@@ -365,6 +468,12 @@ namespace StandaloneStreamBenchmark
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;} if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
for(const FStandaloneCpuTile& Tile:Ready) for(const FStandaloneCpuTile& Tile:Ready)
{ {
if(Tile.bGenerationFailed)
{
Streamer.Stop();
std::fprintf(stderr,"[StreamPipeline] a tile failed generation; refusing a partial result.\n");
return 9;
}
if(bExportRequested)SceneExport.Add(Tile); if(bExportRequested)SceneExport.Add(Tile);
++Finished; ++Finished;
if(bReportMeshCoverage) if(bReportMeshCoverage)
@@ -426,6 +535,8 @@ namespace StandaloneStreamBenchmark
} }
} }
WholeViewReady=std::max(WholeViewReady,Tile.CompletedAt); WholeViewReady=std::max(WholeViewReady,Tile.CompletedAt);
if(FirstTileReady.time_since_epoch().count()==0||Tile.CompletedAt<FirstTileReady)
FirstTileReady=Tile.CompletedAt;
if(Neighborhood.contains(Tile.Key)) if(Neighborhood.contains(Tile.Key))
{ {
++NeighborhoodFinished; ++NeighborhoodFinished;
@@ -455,6 +566,12 @@ namespace StandaloneStreamBenchmark
if(Tile.bHasTopologyHash)TopologyRows.push_back({Tile.Key,Tile.TopologyHash}); if(Tile.bHasTopologyHash)TopologyRows.push_back({Tile.Key,Tile.TopologyHash});
} }
} }
if(!bFirstTileReported&&FirstTileReady.time_since_epoch().count()!=0)
{
std::printf("[PipelineMilestone] first_tile_ready=%.3fs\n",
std::chrono::duration<double>(FirstTileReady-ColdStart).count());
bFirstTileReported=true;
}
if(!bNeighborhoodReported&&NeighborhoodFinished==static_cast<int32>(Neighborhood.size())) if(!bNeighborhoodReported&&NeighborhoodFinished==static_cast<int32>(Neighborhood.size()))
{ {
std::printf("[PipelineMilestone] neighborhood_ready=%.3fs generated=%d/27\n", std::printf("[PipelineMilestone] neighborhood_ready=%.3fs generated=%d/27\n",
@@ -472,9 +589,55 @@ namespace StandaloneStreamBenchmark
if(bHasCameraStep) if(bHasCameraStep)
{ {
const auto MovingStart=Clock::now(); const auto MovingStart=Clock::now();
double RegionCompileWallSeconds=0.0;
double RegionCompileCpuSeconds=0.0;
std::size_t RegionCompileRequested=0,RegionCompileCovered=0;
VoxelForgeCore::TileRegionCompileStats RegionCompileStats;
if(bRegionCrossing)
{
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> InitialKeys;
InitialKeys.reserve(Desired.Tiles.size()*2+1);
for(const FVoxelTileKey& Key:Desired.Tiles)InitialKeys.insert(Key);
std::vector<VoxelForgeCore::TileKey> EnteringCoreTiles;
EnteringCoreTiles.reserve(MovedDesired.Tiles.size());
for(const FVoxelTileKey& Key:MovedDesired.Tiles)
{
if(InitialKeys.contains(Key))continue;
EnteringCoreTiles.push_back({{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level});
}
RegionCompileRequested=EnteringCoreTiles.size();
const auto RegionCompileStart=Clock::now();
const std::clock_t RegionCompileCpuStart=std::clock();
std::string RegionCompileError;
if(!Shape.compileTileRegion(EnteringCoreTiles,
static_cast<uint32>(std::clamp(WorkerCount,1,16)),Scheduler,
RegionCompileError,&RegionCompileCovered,&RegionCompileStats,true))
{
Streamer.Stop();
std::fprintf(stderr,"[MovingRegionCompile] failed: %s\n",RegionCompileError.c_str());
return 2;
}
RegionCompileWallSeconds=std::chrono::duration<double>(
Clock::now()-RegionCompileStart).count();
RegionCompileCpuSeconds=static_cast<double>(std::clock()-RegionCompileCpuStart)
/CLOCKS_PER_SEC;
const std::size_t ParallelWorkers=RegionCompileStats.regionsBuilt>1
?std::min<std::size_t>(8,RegionCompileStats.regionsBuilt):0;
std::printf("[MovingRegionCompile] distance_m=%.3f entering_tiles=%zu covered_tiles=%zu regions=%zu lattice_nodes=%llu wall_s=%.6f cpu_s=%.6f worker_threads=%zu mode=analytic-shape-pages\n",
std::sqrt(static_cast<double>(CameraStepVoxels.X)*CameraStepVoxels.X
+static_cast<double>(CameraStepVoxels.Y)*CameraStepVoxels.Y
+static_cast<double>(CameraStepVoxels.Z)*CameraStepVoxels.Z)
*static_cast<double>(World.CoreVoxelSizeMicrons)/1000000.0,
RegionCompileRequested,RegionCompileCovered,RegionCompileStats.regionsBuilt,
static_cast<unsigned long long>(RegionCompileStats.latticeNodes),
RegionCompileWallSeconds,RegionCompileCpuSeconds,ParallelWorkers);
}
const auto MovingTileStart=Clock::now();
const auto MovingUpdate=Streamer.UpdateDesiredWithSet(MovedDesired,Desired.Tiles,1); const auto MovingUpdate=Streamer.UpdateDesiredWithSet(MovedDesired,Desired.Tiles,1);
std::array<std::uint64_t,9> MovingTilesByLOD{}; std::array<std::uint64_t,9> MovingTilesByLOD{};
std::array<double,9> MovingWorkerSecondsByLOD{}; std::array<double,9> MovingWorkerSecondsByLOD{};
std::vector<FHashedTile> MovingDensityRows,MovingMeshRows,
MovingPayloadRows,MovingTopologyRows;
int32 MovingFinished=0; int32 MovingFinished=0;
while(MovingFinished<MovingUpdate.NewJobs) while(MovingFinished<MovingUpdate.NewJobs)
{ {
@@ -490,8 +653,17 @@ namespace StandaloneStreamBenchmark
++MovingTilesByLOD[LOD]; ++MovingTilesByLOD[LOD];
MovingWorkerSecondsByLOD[LOD]+=Tile.WorkerSeconds; MovingWorkerSecondsByLOD[LOD]+=Tile.WorkerSeconds;
} }
if(bHashDensity)
{
if(Tile.bHasDensityHash)MovingDensityRows.push_back({Tile.Key,Tile.DensityHash});
if(Tile.bHasMeshHash)MovingMeshRows.push_back({Tile.Key,Tile.MeshHash});
if(Tile.bHasPayloadHash)MovingPayloadRows.push_back({Tile.Key,Tile.PayloadHash});
if(Tile.bHasTopologyHash)MovingTopologyRows.push_back({Tile.Key,Tile.TopologyHash});
} }
} }
}
const double MovingTileWallSeconds=std::chrono::duration<double>(
Clock::now()-MovingTileStart).count();
const double MovingWallSeconds=std::chrono::duration<double>(Clock::now()-MovingStart).count(); const double MovingWallSeconds=std::chrono::duration<double>(Clock::now()-MovingStart).count();
const double StepDistanceMeters=std::sqrt( const double StepDistanceMeters=std::sqrt(
static_cast<double>(CameraStepVoxels.X)*CameraStepVoxels.X static_cast<double>(CameraStepVoxels.X)*CameraStepVoxels.X
@@ -502,6 +674,23 @@ namespace StandaloneStreamBenchmark
CameraStepVoxels.X,CameraStepVoxels.Y,CameraStepVoxels.Z,StepDistanceMeters, CameraStepVoxels.X,CameraStepVoxels.Y,CameraStepVoxels.Z,StepDistanceMeters,
MovedDesired.Tiles.size(),MovingUpdate.NewJobs,MovingFinished,MovingWallSeconds, MovedDesired.Tiles.size(),MovingUpdate.NewJobs,MovingFinished,MovingWallSeconds,
bUseSharedColumnCache?"on":"off"); bUseSharedColumnCache?"on":"off");
std::printf("[MovingTileWork] tiles=%d wall_s=%.6f worker_sum_s=%.6f region_compile_wall_s=%.6f crossing_total_s=%.6f\n",
MovingFinished,MovingTileWallSeconds,
std::accumulate(MovingWorkerSecondsByLOD.begin(),MovingWorkerSecondsByLOD.end(),0.0),
RegionCompileWallSeconds,MovingWallSeconds);
if(bHashDensity)
std::printf("[MovingTileHash] tiles=%d rows=(%zu,%zu,%zu,%zu) density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx status=%s\n",
MovingFinished,MovingDensityRows.size(),MovingMeshRows.size(),
MovingPayloadRows.size(),MovingTopologyRows.size(),
static_cast<unsigned long long>(HashTileRows(MovingDensityRows)),
static_cast<unsigned long long>(HashTileRows(MovingMeshRows)),
static_cast<unsigned long long>(HashTileRows(MovingPayloadRows)),
static_cast<unsigned long long>(HashTileRows(MovingTopologyRows)),
MovingDensityRows.size()==static_cast<std::size_t>(MovingFinished)
&&MovingMeshRows.size()==static_cast<std::size_t>(MovingFinished)
&&MovingPayloadRows.size()==static_cast<std::size_t>(MovingFinished)
&&MovingTopologyRows.size()==static_cast<std::size_t>(MovingFinished)
?"PASS":"FAIL");
for(std::size_t LOD=0;LOD<MovingTilesByLOD.size();++LOD) for(std::size_t LOD=0;LOD<MovingTilesByLOD.size();++LOD)
if(MovingTilesByLOD[LOD]!=0) if(MovingTilesByLOD[LOD]!=0)
std::printf("[MovingCameraStepLOD] lod=%zu tiles=%llu worker_sum_s=%.6f\n", std::printf("[MovingCameraStepLOD] lod=%zu tiles=%llu worker_sum_s=%.6f\n",
@@ -623,12 +812,14 @@ namespace StandaloneStreamBenchmark
} }
} }
const double NeighborhoodSeconds=std::chrono::duration<double>(NeighborhoodReady-ColdStart).count(); const double NeighborhoodSeconds=std::chrono::duration<double>(NeighborhoodReady-ColdStart).count();
const double NeighborhoodMeshSeconds=std::chrono::duration<double>(NeighborhoodReady-ShapeReady).count(); const double NeighborhoodMeshSeconds=std::max(0.0,
std::chrono::duration<double>(NeighborhoodReady-ShapeReady).count());
const double FullDetailSeconds=std::chrono::duration<double>(FullDetailReady-ColdStart).count(); const double FullDetailSeconds=std::chrono::duration<double>(FullDetailReady-ColdStart).count();
const double WholeViewSeconds=std::chrono::duration<double>(WholeViewReady-ColdStart).count(); const double WholeViewSeconds=std::chrono::duration<double>(WholeViewReady-ColdStart).count();
const double StreamSeconds=std::chrono::duration<double>(WholeViewReady-StreamStart).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", const double FirstTileSeconds=std::chrono::duration<double>(FirstTileReady-ColdStart).count();
WorkerCount,ShapeSeconds,NeighborhoodMeshSeconds,NeighborhoodSeconds, std::printf("[StreamPipeline] workers=%d shape=%.3fs first_tile_ready=%.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,FirstTileSeconds,NeighborhoodMeshSeconds,NeighborhoodSeconds,
FullDetailSeconds,WholeViewSeconds,NeighborhoodFinished, FullDetailSeconds,WholeViewSeconds,NeighborhoodFinished,
FullDetailFinished,FullDetailExpected,Finished,Desired.Tiles.size(),StreamSeconds); FullDetailFinished,FullDetailExpected,Finished,Desired.Tiles.size(),StreamSeconds);
if(bReportMeshCoverage) if(bReportMeshCoverage)
+2
View File
@@ -60,6 +60,7 @@ struct FStandaloneCpuTile
bool bHasPayloadHash=false; bool bHasPayloadHash=false;
uint64 TopologyHash=14695981039346656037ull; uint64 TopologyHash=14695981039346656037ull;
bool bHasTopologyHash=false; bool bHasTopologyHash=false;
bool bGenerationFailed=false;
uint64 MeshPayloadAllocatedBytes=0; uint64 MeshPayloadAllocatedBytes=0;
VoxelForgeCore::TileGenerationStats CoreDiagnostics{}; VoxelForgeCore::TileGenerationStats CoreDiagnostics{};
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{}; std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
@@ -834,6 +835,7 @@ private:
if(!Result) if(!Result)
{ {
std::fprintf(stderr,"[VoxelForgeCore] tile generation failed: %s\n",Error.c_str()); std::fprintf(stderr,"[VoxelForgeCore] tile generation failed: %s\n",Error.c_str());
Out.bGenerationFailed=true;
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count(); Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
Out.CompletedAt=FClock::now();return Out; Out.CompletedAt=FClock::now();return Out;
} }