standalone: owner review fixes -- paged streaming in Core, no LOD fins, LOD0 stair patches fixed

From the owner's fly-through ("very impressive, in some cases slightly better than the original"):
- Streaming: Core compiles world-aligned fine-detail pages for tiles entering the view and releases
  them when they leave; the viewer keeps resident tiles. One chunk crossing: 144 new tiles, 1,822
  kept (was a full rebuild). A tile built as a late page hashes identically (density, mesh, payload,
  topology) to the same tile from an initial build.
- LOD>=1 fins: off-surface planar connector panels are now checked against the source surface and
  projected onto it; coarse audit 0 off-surface panels; far survival passes, bad-shell still fails.
- LOD0 stair patches: 2,630 sign-changing edges had one endpoint at the fixed +-4.125 fallback
  (carve surfaces were excluded from distance storage); distances now completed analytically, 462
  packed signs corrected; the audit now fails on any sign edge missing either endpoint (0 of 952,997).
- W1/W8 hashes equal; routes 5642/5642; look, passage and far audits pass.
Verified by me, interleaved (8 workers, cold): 1.014 s vs 1.008 s for aeeedaa, no regression.
Open: a chunk crossing still costs ~1.2 s of page compilation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-05 18:17:25 +02:00
1 parent aeeedaa73d
commit 3b37d5702e
6 files changed
+939 -25

No files matched your search

+187 -2
View File
@@ -255,6 +255,7 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
std::vector<FVector2f> NewUVs;
std::vector<FColor> NewColors;
std::vector<int32> NewTriangles;
std::vector<std::array<int32,4>> NewQuads;
const int32 OldVertexCount=Mesh.Vertices.Num();
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount;
@@ -579,7 +580,7 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
const int32 C=RingVertexIndex[Ring+1][FaceSide];
const int32 D=RingVertexIndex[Ring+1][Next];
// Shared panels are emitted once even when both adjoining radial rays are missing.
NewTriangles.insert(NewTriangles.end(),{A,C,B,B,C,D});
NewQuads.push_back({A,B,C,D});
}
}
}
@@ -587,7 +588,191 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
RunStart=RunEnd+1;
}
}
if(NewVertices.empty()||NewTriangles.empty())return;
if(NewVertices.empty()||NewQuads.empty())return;
// The survival repair normally emits long connected strips. A few isolated
// two-triangle panels can be assembled from a radius borrowed at another axial
// sample, though, leaving a free-edged fin away from the sampled density surface.
// Match the visible coarse-panel audit's topology and six density samples here.
std::vector<uint8> QuadUseCount(NewVertices.size(),0);
for(const auto& Quad:NewQuads)
for(const int32 Vertex:Quad)
if(Vertex>=0&&static_cast<size_t>(Vertex)<QuadUseCount.size()
&&QuadUseCount[static_cast<size_t>(Vertex)]<255)
++QuadUseCount[static_cast<size_t>(Vertex)];
std::vector<uint8> KeepQuad(NewQuads.size(),1);
std::vector<std::array<int32,4>> RefinedQuads;
const double ResidualLimit=std::max(2.0,Step*0.10);
const double TileMin[3]={static_cast<double>(TileOrigin.X),static_cast<double>(TileOrigin.Y),
static_cast<double>(TileOrigin.Z)};
const double TileMax[3]={TileMin[0]+TileExtent,TileMin[1]+TileExtent,TileMin[2]+TileExtent};
const auto ToVoxels=[&](int32 Vertex)
{
return FVector(NewVertices[Vertex].X*0.04,NewVertices[Vertex].Y*0.04,
NewVertices[Vertex].Z*0.04);
};
const auto IsTileBoundaryEdge=[&](const FVector& A,const FVector& B)
{
for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex)
{
const double PA=AxisIndex==0?A.X:(AxisIndex==1?A.Y:A.Z);
const double PB=AxisIndex==0?B.X:(AxisIndex==1?B.Y:B.Z);
if((std::abs(PA-TileMin[AxisIndex])<0.75&&std::abs(PB-TileMin[AxisIndex])<0.75)
||(std::abs(PA-TileMax[AxisIndex])<0.75&&std::abs(PB-TileMax[AxisIndex])<0.75))
return true;
}
return false;
};
for(size_t QuadIndex=0;QuadIndex<NewQuads.size();++QuadIndex)
{
const auto& Quad=NewQuads[QuadIndex];
if(std::any_of(Quad.begin(),Quad.end(),[&](int32 Vertex)
{return Vertex<0||static_cast<size_t>(Vertex)>=QuadUseCount.size()
||QuadUseCount[static_cast<size_t>(Vertex)]!=1;}))continue;
const FVector A=ToVoxels(Quad[0]),B=ToVoxels(Quad[1]);
const FVector C=ToVoxels(Quad[2]),D=ToVoxels(Quad[3]);
const FVector N0=FVector::CrossProduct(C-A,B-A);
const FVector N1=FVector::CrossProduct(C-B,D-B);
const double Area0=N0.Size(),Area1=N1.Size();
const double AreaSum=Area0+Area1;
if(AreaSum<=1.0e-12)continue;
const double Coherence=(N0+N1).Size()/AreaSum;
if(Coherence<0.995)continue;
if(IsTileBoundaryEdge(A,B)&&IsTileBoundaryEdge(B,D)
&&IsTileBoundaryEdge(D,C)&&IsTileBoundaryEdge(C,A))continue;
const FVector Samples[6]={A,B,C,D,(A+C+B)/3.0,(B+C+D)/3.0};
int32 OffSourceSamples=0;
for(const FVector& P:Samples)
if(std::abs(Shape.DensityAt(static_cast<float>(P.X),static_cast<float>(P.Y),
static_cast<float>(P.Z)))>ResidualLimit)++OffSourceSamples;
if(OffSourceSamples*4<6)continue;
constexpr int32 Subdivisions=8;
const FVector PreferredNormal=(NewNormals[Quad[0]]+NewNormals[Quad[1]]
+NewNormals[Quad[2]]+NewNormals[Quad[3]]).GetSafeNormal();
const auto SourceGradient=[&](const FVector& P)
{
constexpr float H=2.0f;
const float GX=(Shape.DensityAt(static_cast<float>(P.X+H),static_cast<float>(P.Y),
static_cast<float>(P.Z))-Shape.DensityAt(static_cast<float>(P.X-H),
static_cast<float>(P.Y),static_cast<float>(P.Z)))*0.25f;
const float GY=(Shape.DensityAt(static_cast<float>(P.X),static_cast<float>(P.Y+H),
static_cast<float>(P.Z))-Shape.DensityAt(static_cast<float>(P.X),
static_cast<float>(P.Y-H),static_cast<float>(P.Z)))*0.25f;
const float GZ=(Shape.DensityAt(static_cast<float>(P.X),static_cast<float>(P.Y),
static_cast<float>(P.Z+H))-Shape.DensityAt(static_cast<float>(P.X),
static_cast<float>(P.Y),static_cast<float>(P.Z-H)))*0.25f;
return FVector(GX,GY,GZ);
};
std::vector<FVector> PatchVertices;
std::vector<FVector> PatchNormals;
std::vector<FVector2f> PatchUVs;
std::vector<FColor> PatchColors;
const auto ProjectToSurface=[&](FVector& P,FVector& Normal)
{
for(int32 Iteration=0;Iteration<12;++Iteration)
{
const double Density=Shape.DensityAt(static_cast<float>(P.X),
static_cast<float>(P.Y),static_cast<float>(P.Z));
if(std::abs(Density)<=0.25)break;
FVector Gradient=SourceGradient(P);
const double GradientSquared=Gradient.SizeSquared();
if(GradientSquared<1.0e-8)return false;
double Distance=std::clamp(Density/GradientSquared,-64.0,64.0);
P-=Gradient*Distance;
}
if(std::abs(Shape.DensityAt(static_cast<float>(P.X),static_cast<float>(P.Y),
static_cast<float>(P.Z)))>2.0)return false;
Normal=SourceGradient(P);
if(Normal.SizeSquared()<1.0e-8)return false;
Normal.Normalize();
if(PreferredNormal.SizeSquared()>1.0e-8&&FVector::DotProduct(Normal,PreferredNormal)<0.0)
Normal*= -1.0;
return true;
};
std::vector<int32> Grid(static_cast<size_t>(Subdivisions+1)*(Subdivisions+1),INDEX_NONE);
bool bProjectionSucceeded=true;
for(int32 Y=0;Y<=Subdivisions&&bProjectionSucceeded;++Y)
{
const double V=static_cast<double>(Y)/Subdivisions;
for(int32 X=0;X<=Subdivisions;++X)
{
const double U=static_cast<double>(X)/Subdivisions;
FVector P=A*((1.0-U)*(1.0-V))+B*(U*(1.0-V))+C*((1.0-U)*V)+D*(U*V);
FVector Normal;
if(!ProjectToSurface(P,Normal)){bProjectionSucceeded=false;break;}
const int32 LocalIndex=static_cast<int32>(PatchVertices.size());
Grid[static_cast<size_t>(Y)*(Subdivisions+1)+X]=LocalIndex;
PatchVertices.emplace_back(P.X*25.0,P.Y*25.0,P.Z*25.0);
PatchNormals.push_back(Normal);
if(bHasUVs)
{
const FVector2f UA=NewUVs[Quad[0]],UB=NewUVs[Quad[1]];
const FVector2f UC=NewUVs[Quad[2]],UD=NewUVs[Quad[3]];
PatchUVs.emplace_back(static_cast<float>(UA.X*((1-U)*(1-V))+UB.X*(U*(1-V))
+UC.X*((1-U)*V)+UD.X*(U*V)),
static_cast<float>(UA.Y*((1-U)*(1-V))+UB.Y*(U*(1-V))
+UC.Y*((1-U)*V)+UD.Y*(U*V)));
}
if(bHasColors)PatchColors.push_back(NewColors[Quad[0]]);
}
}
if(!bProjectionSucceeded)continue;
KeepQuad[QuadIndex]=0;
const int32 PatchBase=static_cast<int32>(NewVertices.size());
NewVertices.insert(NewVertices.end(),PatchVertices.begin(),PatchVertices.end());
NewNormals.insert(NewNormals.end(),PatchNormals.begin(),PatchNormals.end());
if(bHasUVs)NewUVs.insert(NewUVs.end(),PatchUVs.begin(),PatchUVs.end());
if(bHasColors)NewColors.insert(NewColors.end(),PatchColors.begin(),PatchColors.end());
for(int32 Y=0;Y<Subdivisions;++Y)
for(int32 X=0;X<Subdivisions;++X)
{
const int32 A0=PatchBase+Grid[static_cast<size_t>(Y)*(Subdivisions+1)+X];
const int32 B0=PatchBase+Grid[static_cast<size_t>(Y)*(Subdivisions+1)+X+1];
const int32 C0=PatchBase+Grid[static_cast<size_t>(Y+1)*(Subdivisions+1)+X];
const int32 D0=PatchBase+Grid[static_cast<size_t>(Y+1)*(Subdivisions+1)+X+1];
RefinedQuads.push_back({A0,B0,C0,D0});
}
}
std::vector<std::array<int32,4>> FinalQuads;
const size_t KeptQuadCount=static_cast<size_t>(std::count(KeepQuad.begin(),KeepQuad.end(),uint8{1}));
FinalQuads.reserve(KeptQuadCount+RefinedQuads.size());
for(size_t QuadIndex=0;QuadIndex<NewQuads.size();++QuadIndex)
if(KeepQuad[QuadIndex])FinalQuads.push_back(NewQuads[QuadIndex]);
FinalQuads.insert(FinalQuads.end(),RefinedQuads.begin(),RefinedQuads.end());
if(FinalQuads.empty())return;
std::vector<int32> Remap(NewVertices.size(),INDEX_NONE);
std::vector<FVector> KeptVertices,KeptNormals;
std::vector<FVector2f> KeptUVs;
std::vector<FColor> KeptColors;
KeptVertices.reserve(FinalQuads.size()*2);KeptNormals.reserve(FinalQuads.size()*2);
if(bHasUVs)KeptUVs.reserve(FinalQuads.size()*2);
if(bHasColors)KeptColors.reserve(FinalQuads.size()*2);
const auto RemapVertex=[&](int32 Vertex)
{
int32& Mapped=Remap[static_cast<size_t>(Vertex)];
if(Mapped==INDEX_NONE)
{
Mapped=static_cast<int32>(KeptVertices.size());
KeptVertices.push_back(NewVertices[static_cast<size_t>(Vertex)]);
KeptNormals.push_back(NewNormals[static_cast<size_t>(Vertex)]);
if(bHasUVs)KeptUVs.push_back(NewUVs[static_cast<size_t>(Vertex)]);
if(bHasColors)KeptColors.push_back(NewColors[static_cast<size_t>(Vertex)]);
}
return Mapped;
};
NewTriangles.reserve(KeptQuadCount*6);
for(const auto& Quad:FinalQuads)
{
const int32 A=RemapVertex(Quad[0]),B=RemapVertex(Quad[1]);
const int32 C=RemapVertex(Quad[2]),D=RemapVertex(Quad[3]);
NewTriangles.insert(NewTriangles.end(),{A,C,B,B,C,D});
}
NewVertices=std::move(KeptVertices);NewNormals=std::move(KeptNormals);
if(bHasUVs)NewUVs=std::move(KeptUVs);
if(bHasColors)NewColors=std::move(KeptColors);
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
auto InsertVertices=[&](auto& Existing,const auto& Added)
+185
View File
@@ -319,6 +319,7 @@ public:
Stage=Clock::now();
ApplyDetailBand(Stats.DetailBandLines);
Stats.SurfaceProxyNodes+=CompleteSignEdgeDistances();
Stats.SurfaceSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
ValidatePassageRoutes();
Built=true;
@@ -782,6 +783,89 @@ public:
return bPassageOnly?bPassageDetail:(Tested>0&&FlatShare<20.0&&bPass&&bPassageDetail);
}
bool AuditGridStairIntegrity() const
{
uint64 CheckedEdges=0,MissingAny=0,MissingBoth=0;
std::array<std::array<int32,4>,8> Examples{};
size_t ExampleCount=0;
const auto Record=[&](uint64 EdgeMask,uint64 CandidateA,uint64 CandidateB,
int32 X,int32 Y,int32 Z,int32 Axis)
{
CheckedEdges+=std::popcount(EdgeMask);
const uint64 Missing=EdgeMask&~(CandidateA&CandidateB);
uint64 AnyMissing=Missing;
MissingAny+=std::popcount(Missing);
MissingBoth+=std::popcount(EdgeMask&~(CandidateA|CandidateB));
while(AnyMissing&&ExampleCount<Examples.size())
{
const uint32 Bit=std::countr_zero(AnyMissing);AnyMissing&=AnyMissing-1;
Examples[ExampleCount++]={X,Y,Z+static_cast<int32>(Bit),Axis};
}
};
const size_t Columns=static_cast<size_t>(Size[0])*Size[1];
for(size_t Line=0;Line<Columns;++Line)
{
const int32 X=Min[0]+static_cast<int32>(Line%Size[0]);
const int32 Y=Min[1]+static_cast<int32>(Line/Size[0]);
const size_t Base=Line*WordsPerZ;
for(size_t W=0;W<WordsPerZ;++W)
{
const size_t Word=Base+W;
const uint64 Rock=RockWords[Word],Candidates=CandidateWords[Word];
const uint64 PreviousRock=W?RockWords[Word-1]>>63:0;
const uint64 PreviousCandidates=W?CandidateWords[Word-1]>>63:0;
uint64 Valid=(Size[2]-W*64)>=64?~uint64{0}
:((uint64{1}<<(Size[2]-W*64))-1);
if(W==0)Valid&=~uint64{1};
const uint64 ShiftedRock=(Rock<<1)|PreviousRock;
const uint64 ShiftedCandidates=(Candidates<<1)|PreviousCandidates;
Record((Rock^ShiftedRock)&Valid,Candidates,ShiftedCandidates,
X,Y,Min[2]+static_cast<int32>(W*64),2);
if(X+1<Max[0]+1)
{
const size_t Other=Word+WordsPerZ;
Record(Rock^RockWords[Other],Candidates,CandidateWords[Other],
X,Y,Min[2]+static_cast<int32>(W*64),0);
}
if(Y+1<Max[1]+1)
{
const size_t Other=Word+static_cast<size_t>(Size[0])*WordsPerZ;
Record(Rock^RockWords[Other],Candidates,CandidateWords[Other],
X,Y,Min[2]+static_cast<int32>(W*64),1);
}
}
}
std::printf("[GridStairDistanceAudit] region=(%d,%d,%d)..(%d,%d,%d) sign_edges=%llu candidate_fallback_edges=%llu both_fallback_edges=%llu status=%s\n",
Min[0],Min[1],Min[2],Max[0],Max[1],Max[2],
static_cast<unsigned long long>(CheckedEdges),
static_cast<unsigned long long>(MissingAny),
static_cast<unsigned long long>(MissingBoth),MissingAny==0?"PASS":"FAIL");
const auto CandidateAt=[&](int32 X,int32 Y,int32 Z)
{
if(!Contains(X,Y,Z))return false;
const size_t Column=static_cast<size_t>(Y-Min[1])*Size[0]+static_cast<size_t>(X-Min[0]);
const size_t Zi=static_cast<size_t>(Z-Min[2]);
return (CandidateWords[Column*WordsPerZ+(Zi>>6)]&(uint64{1}<<(Zi&63u)))!=0;
};
for(size_t I=0;I<ExampleCount;++I)
{
int32 AX=Examples[I][0],AY=Examples[I][1],AZ=Examples[I][2];
int32 BX=AX,BY=AY,BZ=AZ;
if(Examples[I][3]==0)++BX;else if(Examples[I][3]==1)++BY;else --BZ;
const float AD=DensityAtLattice(AX,AY,AZ),BD=DensityAtLattice(BX,BY,BZ);
const float AExact=AnalyticDensityAt(static_cast<float>(AX),static_cast<float>(AY),static_cast<float>(AZ));
const float BExact=AnalyticDensityAt(static_cast<float>(BX),static_cast<float>(BY),static_cast<float>(BZ));
std::printf("[GridStairDistanceCandidate] a=(%d,%d,%d) b=(%d,%d,%d) axis=%d candidate=(%d,%d) solid=(%d,%d) density=(%.4f,%.4f) exact=(%.4f,%.4f) critical_air=(%d,%d)\n",
AX,AY,AZ,BX,BY,BZ,Examples[I][3],CandidateAt(AX,AY,AZ),CandidateAt(BX,BY,BZ),
IsRock(AX,AY,AZ),IsRock(BX,BY,BZ),
AD,BD,AExact,BExact,
IsInsideCriticalAir(AX,AY,AZ),IsInsideCriticalAir(BX,BY,BZ));
}
return MissingAny==0;
}
bool AuditViewSurface(double CameraX,double CameraY,double CameraZ,
float Yaw,float Pitch) const
{
@@ -2812,6 +2896,107 @@ private:
}
}
uint64 CompleteSignEdgeDistances()
{
uint64 AddedTotal=0,SignCorrections=0;
const size_t Columns=static_cast<size_t>(Size[0])*Size[1];
for(int32 Pass=0;Pass<4;++Pass)
{
uint64 AddedThisPass=0;
bool bSignChanged=false;
const auto EnsureDistance=[&](int32 X,int32 Y,int32 Z)
{
const size_t Column=static_cast<size_t>(Y-Min[1])*Size[0]
+static_cast<size_t>(X-Min[0]);
const size_t Zi=static_cast<size_t>(Z-Min[2]);
const size_t Word=Column*WordsPerZ+(Zi>>6);
const uint64 Mask=uint64{1}<<(Zi&63u);
if(CandidateWords[Word]&Mask)return;
const float Density=AnalyticDensityAt(static_cast<float>(X),
static_cast<float>(Y),static_cast<float>(Z));
const size_t Node=(Zi*static_cast<size_t>(Size[1])
+static_cast<size_t>(Y-Min[1]))*Size[0]
+static_cast<size_t>(X-Min[0]);
StoreSurfaceDistance(Node,Density);
CandidateWords[Word]|=Mask;
++AddedThisPass;
const bool bExactRock=Density<0.0f;
const bool bPackedRock=(RockWords[Word]&Mask)!=0;
if(bExactRock!=bPackedRock)
{
if(bExactRock)RockWords[Word]|=Mask;
else RockWords[Word]&=~Mask;
++SignCorrections;
bSignChanged=true;
}
};
for(size_t Line=0;Line<Columns;++Line)
{
const int32 X=Min[0]+static_cast<int32>(Line%Size[0]);
const int32 Y=Min[1]+static_cast<int32>(Line/Size[0]);
const size_t Base=Line*WordsPerZ;
for(size_t W=0;W<WordsPerZ;++W)
{
const size_t Word=Base+W;
const uint64 Rock=RockWords[Word],Candidates=CandidateWords[Word];
const uint64 PreviousRock=W?RockWords[Word-1]>>63:0;
const uint64 PreviousCandidates=W?CandidateWords[Word-1]>>63:0;
uint64 Valid=(Size[2]-W*64)>=64?~uint64{0}
:((uint64{1}<<(Size[2]-W*64))-1);
if(W==0)Valid&=~uint64{1};
const uint64 ShiftedRock=(Rock<<1)|PreviousRock;
const uint64 ShiftedCandidates=(Candidates<<1)|PreviousCandidates;
uint64 Missing=((Rock^ShiftedRock)&Valid)
&~(Candidates&ShiftedCandidates);
while(Missing)
{
const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1;
const int32 Z=Min[2]+static_cast<int32>(W*64+Bit);
EnsureDistance(X,Y,Z);
EnsureDistance(X,Y,Z-1);
}
if(X<Max[0])
{
const size_t Other=Word+WordsPerZ;
Missing=(Rock^RockWords[Other])&Valid
&~(Candidates&CandidateWords[Other]);
while(Missing)
{
const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1;
const int32 Z=Min[2]+static_cast<int32>(W*64+Bit);
EnsureDistance(X,Y,Z);
EnsureDistance(X+1,Y,Z);
}
}
if(Y<Max[1])
{
const size_t Other=Word+static_cast<size_t>(Size[0])*WordsPerZ;
Missing=(Rock^RockWords[Other])&Valid
&~(Candidates&CandidateWords[Other]);
while(Missing)
{
const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1;
const int32 Z=Min[2]+static_cast<int32>(W*64+Bit);
EnsureDistance(X,Y,Z);
EnsureDistance(X,Y+1,Z);
}
}
}
}
AddedTotal+=AddedThisPass;
if(!bSignChanged)break;
}
std::printf("[GridStairDistanceRepair] exact_endpoint_proxies=%llu occupancy_corrections=%llu\n",
static_cast<unsigned long long>(AddedTotal),
static_cast<unsigned long long>(SignCorrections));
return AddedTotal;
}
void StoreSurfaceDistance(size_t Node,float Density)
{
int32 Q=static_cast<int32>(std::lround(Density*16.0f));
+143 -5
View File
@@ -2,6 +2,12 @@
#include "StandaloneSnapshotAdapter.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include <shared_mutex>
#include <mutex>
#include <atomic>
#include <unordered_map>
#include <unordered_set>
namespace vfcore::detail
{
#include "FarShellsInternal.h"
@@ -29,6 +35,40 @@ namespace VoxelForgeCore
TileKey FromNative(const FVoxelTileKey& value)
{ return {{value.Coord.X,value.Coord.Y,value.Coord.Z},value.Level}; }
std::int32_t FloorDiv(std::int32_t value,std::int32_t divisor)
{
const std::int32_t quotient=value/divisor,remainder=value%divisor;
return quotient-((remainder!=0&&remainder<0)?1:0);
}
std::int32_t TilesPerShapePage(std::int32_t lod)
{
constexpr std::int32_t PageWidthVoxels=128;
return std::max(1,(PageWidthVoxels/CHUNK_SIZE)>>std::clamp(lod,0,30));
}
TileKey ShapePageKey(const TileKey& tile)
{
TileKey page=tile;
const std::int32_t span=TilesPerShapePage(tile.lod);
page.coordinate={FloorDiv(tile.coordinate.x,span),
FloorDiv(tile.coordinate.y,span),FloorDiv(tile.coordinate.z,span)};
return page;
}
bool CoveredByBaseShape(const ShapeBuildRequest& request,
const WorldConfiguration& configuration,const TileKey& tile)
{
if(tile.lod<0||tile.lod>=configuration.fullResolutionLodCount)return false;
const FVoxelTileKey native=ToNative(tile);
const FIntVector origin=native.OriginVoxels();
const std::int32_t extent=native.ExtentVoxels();
for(std::int32_t axis=0;axis<3;++axis)
if((&origin.X)[axis]<(&request.minVoxel.x)[axis]
||(&origin.X)[axis]+extent>(&request.maxVoxel.x)[axis])return false;
return true;
}
WorldConfiguration MakeConfiguration(const FStandaloneOwnerWorld& owner)
{
WorldConfiguration out;
@@ -233,6 +273,10 @@ namespace VoxelForgeCore
std::shared_ptr<FStandaloneOwnerWorld> owner;
WorldConfiguration configuration;
std::unique_ptr<FStandaloneShapeWorld> shape;
ShapeBuildRequest buildRequest;
mutable std::shared_mutex tileRegionMutex;
std::unordered_map<TileKey,std::shared_ptr<FStandaloneShapeWorld>,TileKeyHash> tileRegions;
std::atomic<std::size_t> tileRegionCount{0};
std::vector<FStandaloneShapeWorld::FEditBrush> editLog;
std::map<std::uint64_t,EditResult> transactions;
bool hasLastSequence=false;
@@ -259,10 +303,88 @@ namespace VoxelForgeCore
ToNative(request.clipMinVoxel),ToNative(request.clipMaxVoxel),
static_cast<int32>(std::clamp<std::uint32_t>(request.workerLimit,1,8)),ToNative(request.detail),
request.includeFarWorld,error,&scheduler,request.roundTunnels,request.profileDetailBand);
if(ok)impl_->shape=std::move(shape);
if(ok)
{
impl_->shape=std::move(shape);
impl_->buildRequest=request;
std::unique_lock lock(impl_->tileRegionMutex);
impl_->tileRegions.clear();
impl_->tileRegionCount.store(0,std::memory_order_release);
}
return ok;
}
bool World::compileTileRegion(std::span<const TileKey> tiles,std::uint32_t workerLimit,
const JobScheduler& scheduler,std::string& error,
std::size_t* outCompiledTiles)
{
if(outCompiledTiles)*outCompiledTiles=0;
if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt())
{error="the base world must be compiled before adding tile regions";return false;}
if(tiles.empty())return true;
std::unordered_map<TileKey,std::vector<TileKey>,TileKeyHash> pendingPages;
pendingPages.reserve(tiles.size());
{
std::shared_lock lock(impl_->tileRegionMutex);
for(const TileKey& key:tiles)
{
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;
const TileKey pageKey=ShapePageKey(key);
if(!impl_->tileRegions.contains(pageKey))pendingPages[pageKey].push_back(key);
}
}
if(pendingPages.empty())return true;
std::size_t compiledTileCount=0;
for(const auto& [pageKey,requestedTiles]:pendingPages)
{
const std::int32_t tileCount=TilesPerShapePage(pageKey.lod);
const std::int32_t tileExtent=CHUNK_SIZE<<pageKey.lod;
const FIntVector pageMin(pageKey.coordinate.x*tileCount*tileExtent,
pageKey.coordinate.y*tileCount*tileExtent,
pageKey.coordinate.z*tileCount*tileExtent);
const FIntVector pageMax(pageMin.X+tileCount*tileExtent,
pageMin.Y+tileCount*tileExtent,pageMin.Z+tileCount*tileExtent);
constexpr std::int32_t PagePad=8;
const FIntVector fineMin(pageMin.X-PagePad,pageMin.Y-PagePad,pageMin.Z-PagePad);
const FIntVector fineMax(pageMax.X+PagePad,pageMax.Y+PagePad,pageMax.Z+PagePad);
auto page=std::make_shared<FStandaloneShapeWorld>();
const ShapeBuildRequest& base=impl_->buildRequest;
if(!page->Build(*impl_->owner,fineMin,fineMax,pageMin,pageMax,
static_cast<int32>(std::clamp<std::uint32_t>(workerLimit,1,8)),
ToNative(base.detail),false,error,&scheduler,
base.roundTunnels,base.profileDetailBand))return false;
if(!impl_->editLog.empty())page->SetEditTransaction(impl_->editLog);
{
std::unique_lock lock(impl_->tileRegionMutex);
impl_->tileRegions.try_emplace(pageKey,std::move(page));
impl_->tileRegionCount.store(impl_->tileRegions.size(),std::memory_order_release);
}
compiledTileCount+=requestedTiles.size();
}
if(outCompiledTiles)*outCompiledTiles=compiledTileCount;
return true;
}
void World::retainTileRegions(std::span<const TileKey> desiredTiles)
{
if(!impl_)return;
std::unordered_set<TileKey,TileKeyHash> desired;
desired.reserve(desiredTiles.size()*2+1);
for(const TileKey& key:desiredTiles)
if(key.lod>=0&&key.lod<impl_->configuration.fullResolutionLodCount)
desired.insert(ShapePageKey(key));
std::unique_lock lock(impl_->tileRegionMutex);
std::erase_if(impl_->tileRegions,[&](const auto& entry){return !desired.contains(entry.first);});
impl_->tileRegionCount.store(impl_->tileRegions.size(),std::memory_order_release);
}
void World::buildFarWorld()
{ if(impl_->shape)impl_->shape->BuildFarWorld(); }
bool World::isCompiled() const noexcept { return impl_&&impl_->shape&&impl_->shape->IsBuilt(); }
@@ -357,9 +479,17 @@ namespace VoxelForgeCore
if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt())
{ error="world must be compiled before generating tiles";return std::nullopt; }
const auto& world=*impl_->owner;
std::shared_ptr<FStandaloneShapeWorld> page;
if(impl_->tileRegionCount.load(std::memory_order_acquire)!=0)
{
std::shared_lock lock(impl_->tileRegionMutex);
if(const auto found=impl_->tileRegions.find(ShapePageKey(key));found!=impl_->tileRegions.end())
page=found->second;
}
const FStandaloneShapeWorld& tileShape=page?*page:*impl_->shape;
UVoxelMarchingCubesMesher mesher;
mesher.SetGenerator(world.Generator.get());
mesher.SetStandaloneShapeWorld(impl_->shape.get());
mesher.SetStandaloneShapeWorld(&tileShape);
mesher.bStandaloneSkipUvAndColors=options.skipUvAndColors;
mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors;
mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups;
@@ -374,10 +504,11 @@ namespace VoxelForgeCore
FVoxelMeshData mesh=mesher.GenerateMesh(origin,step,cells,nullptr,
options.bandMinZVoxel,options.bandMaxZVoxel,&sampleCount,nullptr,nullptr,
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr);
if(impl_->shape->HasFarWorld())
const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:*impl_->shape;
if(farShape.HasFarWorld())
{
AppendFarStratumShells(world,*impl_->shape,nativeKey,step,mesh);
AppendFarConnectorShells(*impl_->shape,nativeKey,step,world.CoarseTileCells,mesh);
AppendFarStratumShells(world,farShape,nativeKey,step,mesh);
AppendFarConnectorShells(farShape,nativeKey,step,world.CoarseTileCells,mesh);
}
TileMesh out;out.key=key;out.densitySampleCount=static_cast<std::uint64_t>(std::max<int64>(0,sampleCount));
@@ -451,6 +582,11 @@ namespace VoxelForgeCore
}
impl_->editLog.insert(impl_->editLog.end(),native.begin(),native.end());
impl_->shape->SetEditTransaction(impl_->editLog);
{
std::shared_lock lock(impl_->tileRegionMutex);
for(const auto& [key,page]:impl_->tileRegions)
if(page)page->SetEditTransaction(impl_->editLog);
}
const FIntVector min=impl_->shape->GetEditBoundsMin(),max=impl_->shape->GetEditBoundsMax();
EditResult result;result.digest=digest;
result.affectedByLod.resize(static_cast<std::size_t>(std::max(impl_->configuration.maxLod,0)+1));
@@ -487,6 +623,8 @@ namespace VoxelForgeCore
}
bool World::auditDetailPassing() const { return impl_&&impl_->shape&&impl_->shape->IsDetailAuditPassing(); }
bool World::auditGridStairIntegrity() const
{ return impl_&&impl_->shape&&impl_->shape->AuditGridStairIntegrity(); }
bool World::auditSurfaceFamilies(bool passageOnly) const
{ return impl_&&impl_->shape&&impl_->shape->AuditSurfaceBandFamilies(passageOnly); }
bool World::auditViewSurface(Vec3d camera,double yaw,double pitch) const