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:
1 parent
b370908174
commit
b1cfaf53b8
8 files changed
+808
-59
No files matched your search
@@ -1082,6 +1082,11 @@ namespace VoxelForgeCore
|
||||
std::uint32_t diagnosticGlobalOctaveDrop = 0;
|
||||
std::uint32_t diagnosticCellStride = 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
|
||||
@@ -1099,11 +1104,20 @@ namespace VoxelForgeCore
|
||||
// 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.
|
||||
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
|
||||
// shared world-coordinate regions. The default analyticOnly path coalesces each entering
|
||||
// LOD slab, builds its primitives/bins, and evaluates density on demand without
|
||||
// rasterizing unused lattice volumes. Set false to use fixed absolute packed pages. The
|
||||
// optional count is newly covered tile keys; regions stay resident until retainTileRegions.
|
||||
// bounded, world-aligned pages. The default analyticOnly path builds each page's
|
||||
// primitives/bins and evaluates density on demand without rasterizing unused lattice
|
||||
// volumes. Set false to use fixed absolute packed pages. The optional count is newly
|
||||
// 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,
|
||||
const JobScheduler& scheduler, std::string& error,
|
||||
std::size_t* outCompiledTiles = nullptr,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user