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

+159 -7
View File
@@ -487,7 +487,13 @@ public:
const auto Stage=Clock::now();
BuildLayerBounds();BuildOwnerPrimitives();
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.PrimitiveCount=Primitives.size();
if(Primitives.empty())
@@ -497,6 +503,59 @@ public:
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; }
bool IsBuilt() const { return Built; }
bool HasFarWorld() const { return FarBuilt; }
@@ -951,6 +1010,15 @@ public:
// edge interpolation and normals to the existing mesher.
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;
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
{
@@ -975,6 +1043,17 @@ public:
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;
if(NativeFloatingIslandDensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z),IslandDensity))
@@ -1003,6 +1082,15 @@ public:
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;
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
{
@@ -1023,6 +1111,8 @@ public:
VoxelForgeCore::DetailFieldSample DetailSampleAt(float X,float Y,float Z) const
{
VoxelForgeCore::DetailFieldSample Sample;
if(bNativeOnly)
{RecordNativeOnlyViolation();return Sample;}
AnalyticDensityAt(X,Y,Z,false,&Sample.displacement,
&Sample.caveInfluence,&Sample.floorInfluence);
return Sample;
@@ -1040,6 +1130,12 @@ public:
{
Count=std::clamp(Count,0,8);
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 bHasNativeIslandSample=false;
for(int32 Lane=0;Lane<Count;++Lane)
@@ -1202,10 +1298,17 @@ public:
{
if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
||!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))
!=ECaveGeneratorType::SurfaceWorld)return false;
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);
if(!Plan)return false;
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
@@ -1250,6 +1353,17 @@ public:
!=ECaveGeneratorType::SurfaceWorld)return false;
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;
}
@@ -1281,7 +1395,7 @@ public:
{
bSurfaceOverlap=true;
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);
if(!Candidate||(SurfacePlan&&SurfacePlan!=Candidate))
bBadPlan=true;
@@ -1306,7 +1420,7 @@ public:
{
bIslandOverlap=true;
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);
if(!Candidate||(IslandPlan&&IslandPlan!=Candidate))bBadPlan=true;
IslandPlan=Candidate;
@@ -1327,6 +1441,19 @@ public:
else ++Counts.MixedBoxes;
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.
// Une boite qui traverse plusieurs recettes reste Mixed, car l'intervalle d'un seul champ ne suffit pas.
if(bBadPlan||(bSurfaceOverlap&&(bIslandOverlap||bOtherStrateOverlap))
@@ -1341,7 +1468,7 @@ public:
// for SurfaceWorld and FloatingIslands. Legacy analytic primitives/layer bounds cannot
// 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.
if(!bUseNativeLatticeProof||(!bSurfaceOverlap&&!bIslandOverlap))
if(!bNativeOnly&&(!bUseNativeLatticeProof||(!bSurfaceOverlap&&!bIslandOverlap)))
{
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)
@@ -1880,6 +2007,21 @@ public:
}
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 thread_local FClassificationThreadStats Stats;
@@ -1914,7 +2056,7 @@ private:
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
!=ECaveGeneratorType::SurfaceWorld)return false;
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);
if(!Plan)return false;
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
@@ -1942,7 +2084,7 @@ private:
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
!=ECaveGeneratorType::FloatingIslands)return false;
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);
if(!Plan)return false;
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
@@ -4675,6 +4817,12 @@ public:
int32 Count,float* OutDensity) const
{
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;
for(int32 Lane=0;Lane<Count;++Lane)
bHasLegacySurfaceSample|=UsesLegacySurfaceField(static_cast<float>(X),
@@ -5211,6 +5359,10 @@ private:
size_t WordsPerZ=0;
EDetailVariant DetailVariant=EDetailVariant::Coherent;
bool Built=false;
bool bNativeOnly=false;
FIntVector NativeOnlyMinimum=FIntVector::ZeroValue;
FIntVector NativeOnlyMaximum=FIntVector::ZeroValue;
mutable std::atomic<uint32> NativeOnlyViolations{0};
bool FarBuilt=false;
std::vector<FInterval> LayerRanges;
std::vector<FLayerField> LayerFields;
+291 -27
View File
@@ -13,6 +13,8 @@
#include <set>
#include <functional>
#include <thread>
#include <new>
#include <stdexcept>
namespace vfcore::detail
{
@@ -943,6 +945,7 @@ namespace VoxelForgeCore
std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
WorldConfiguration configuration;
std::unique_ptr<FStandaloneShapeWorld> shape;
std::atomic<bool> shapeReady{false};
std::unique_ptr<world_assembly_detail::FSharedHeightfieldColumnCache>
sharedHeightfieldColumns;
std::unique_ptr<floating_island_detail::FSharedIslandColumnCache>
@@ -996,9 +999,136 @@ namespace VoxelForgeCore
World::World(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)
{
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())
{
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);
impl_->tileRegions.clear();
impl_->tileRegionCount.store(0,std::memory_order_release);
impl_->shapeReady.store(true,std::memory_order_release);
}
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
{
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)
return false;
if(CoveredByBaseShape(impl_->buildRequest,impl_->configuration,key))return false;
@@ -1039,10 +1230,15 @@ namespace VoxelForgeCore
{
if(outCompiledTiles)*outCompiledTiles=0;
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;}
if(tiles.empty())return true;
constexpr std::int64_t RegionPadding=10;
struct FPendingRegion
{
TileKey pageKey;
@@ -1061,12 +1257,27 @@ namespace VoxelForgeCore
const TileKey& key=tiles[inputIndex];
if(key.lod<0||key.lod>8)
{error="tile region contains an invalid LOD";return false;}
if(key.lod>=impl_->configuration.fullResolutionLodCount
||CoveredByBaseShape(impl_->buildRequest,impl_->configuration,key))continue;
if(key.lod>=impl_->configuration.fullResolutionLodCount)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;
// Analytic regions evaluate only requested fine tiles and can safely coalesce a
// whole entering LOD slab. Packed pages retain their fixed absolute parent cells.
const TileKey pageKey=analyticOnly?TileKey{{0,0,0},key.lod}:ShapePageKey(key);
// Keep every region's bounds spatially finite. A large camera step can bring in
// disjoint tiles across the whole view; coalescing an entire LOD slab makes the
// 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());
if(inserted)pendingRegions.push_back({pageKey,{}});
pendingRegions[found->second].tiles.push_back(key);
@@ -1173,18 +1384,37 @@ 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(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);
}
stats.wallSeconds=std::chrono::duration<double>(Clock::now()-compileStart).count();
if(outCompiledTiles)*outCompiledTiles=compiledTileCount;
if(outStats)*outStats=stats;
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)
@@ -1201,14 +1431,17 @@ namespace VoxelForgeCore
}
void World::buildFarWorld()
{ if(impl_->shape)impl_->shape->BuildFarWorld(); }
bool World::isCompiled() const noexcept { return impl_&&impl_->shape&&impl_->shape->IsBuilt(); }
bool World::hasFarWorld() const noexcept { return impl_&&impl_->shape&&impl_->shape->HasFarWorld(); }
{ if(impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape)impl_->shape->BuildFarWorld(); }
bool World::isCompiled() const noexcept
{ 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 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();
out.shapeSeconds=stats.ShapeSeconds;out.intervalSeconds=stats.IntervalSeconds;
out.surfaceSeconds=stats.SurfaceSeconds;out.farShapeSeconds=stats.FarShapeSeconds;
@@ -1218,18 +1451,19 @@ namespace VoxelForgeCore
}
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
{ 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
{ 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
{ 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::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 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)));
@@ -1243,7 +1477,7 @@ namespace VoxelForgeCore
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);
return result==EVoxelTileClass::AllSolid?TileClass::allSolid:
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.
Heightfield2D::setThreadColumnCacheLegacyDirectMapped(
options.legacyDirectMappedColumnCache||key.lod<=2);
if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt())
{ error="world must be compiled before generating tiles";return std::nullopt; }
if(!impl_||!impl_->owner)
{ 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
?impl_->sharedHeightfieldColumns.get():nullptr,
options.useSharedColumnCache?impl_->sharedIslandColumns.get():nullptr);
const auto& world=*impl_->owner;
FStandaloneShapeWorld nativeOnlyShape;
FIntVector nativeOnlyMinimum,nativeOnlyMaximum;
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);
if(const auto found=impl_->tileRegions.find(key);found!=impl_->tileRegions.end())
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;
mesher.SetGenerator(world.Generator.get());
mesher.SetGeometryVoxelSizeCm(static_cast<float>(world.CoreVoxelSizeMicrons)/10000.0f);
@@ -1435,6 +1687,12 @@ namespace VoxelForgeCore
}
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
// 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
@@ -1493,7 +1751,7 @@ namespace VoxelForgeCore
out.diagnostics.featureRefinedResolution=bFeatureRefinedResolution;
const bool bHasWorldAssembly=impl_->worldAssembly!=nullptr;
const bool bNeedsFarShell=bHasWorldAssembly&&key.lod>=7
&&(tileShape.HasFarWorld()||impl_->shape->HasFarWorld());
&&(tileShape.HasFarWorld()||(bShapeReady&&impl_->shape->HasFarWorld()));
bool bOverlapsPlayerEdit=false;
if(tileShape.HasEditTransaction())
{
@@ -1767,6 +2025,11 @@ namespace VoxelForgeCore
Heightfield2D::setThreadColumnOctaveDrop(PreviousOctaveDrop);
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>(
std::chrono::steady_clock::now()-MarchingCubesStart).count();
const double ColumnMillisecondsAfterMeshing=
@@ -1801,7 +2064,8 @@ namespace VoxelForgeCore
std::chrono::steady_clock::now()-MeshingStart).count();
out.diagnostics.farHorizonVersion=key.lod>=4?options.farHorizonVersion:0;
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;
if(farShape.HasFarWorld()&&!bSkipFarShellRepair)
{