standalone: far-survival audit + LOD8 default with explicit far shells; neighbourhood-first viewer

- FarSurvivalAudit: checks every stratum top/bottom boundary (4x4 columns per far tile) and every
  in-view far connector (sections <=128 voxels, 4 radial rays) against the emitted mesh. Falsified:
  removing one tile's shell triangles -> 16 missing, 16 holes, exit 14. Passes at LOD6/7/8.
- Far connectors follow source radial crossings; strata patches emitted at LOD7/8. Default LOD8.
- Viewer: separate near ShapeWorld for the 27 player tiles, full world built on its own thread;
  27/27 tiles hash-identical to the direct build. W1/W8 60 hash fields equal; routes 5642/5642.
Cost, measured by me head to head (8 workers, cold, BelowNormal 0xFF00): LOD8 without the shells
1.17-1.25 s, LOD8 with the shells 1.80 s; payload 92.6 -> 276.7 MiB. The shells are far too
expensive, and the pipelined benchmark whole view is 2.92 s (shape compiled twice): both to fix.

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-04 22:51:44 +02:00
1 parent 5602a281b9
commit a40b0454b3
7 files changed
+1350 -17

No files matched your search

+523
View File
@@ -0,0 +1,523 @@
#pragma once
// Standalone-only proof that far meshes retain the owner's strata-boundary shells and
// generated far-parent connectors. This deliberately compares the dense source field with
// vertical/radial intersections against the triangles that were actually emitted.
struct FFarSurvivalAudit
{
struct FBoundary
{
double Z=0.0;
double HalfWidth=16.0;
};
struct FConnectorSample
{
size_t ConnectorIndex=0;
V3 Center{},U{},V{};
float Radius=0.0f;
uint8 DirectionMask=0;
};
struct FCounts
{
uint64 BoundaryColumns=0,BoundaryReferenceSurfaces=0;
uint64 BoundaryMissing=0,BoundaryExtra=0,BoundaryHoles=0;
uint64 ConnectorsInView=0,ConnectorSamples=0,ConnectorRays=0;
uint64 ConnectorReferenceSurfaces=0,ConnectorRayMissing=0,ConnectorGoodSamples=0,ConnectorMissing=0;
uint64 ConnectorVisibleSamples=0,ConnectorNoSurfaceSamples=0,ConnectorSampleMissing=0;
uint64 ConnectorExtra=0,ConnectorHoles=0,ConnectorUnsampled=0;
uint64 CorruptedTriangles=0;
};
FFarSurvivalAudit(FStandaloneOwnerWorld& InWorld,const FStandaloneShapeWorld& InShape,
const std::vector<FVoxelTileKey>& DesiredTiles,bool bInDropOneShell)
:World(InWorld),Shape(InShape),bDropOneShell(bInDropOneShell)
{
const auto& Layout=World.Manager->GetLayout();
for(const FStrateSlot& Slot:Layout)
{
const FIntVector Mid(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2);
const FStrateGenerationParams Params=World.Manager->GetGenerationParams(Mid);
const double HalfWidth=std::max(8.0,static_cast<double>(Params.BoundarySealThickness)+8.0);
AddBoundary(static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth);
AddBoundary(static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth);
}
std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B)
{return A.Z<B.Z;});
BuildConnectorSamples(DesiredTiles);
}
bool AuditTile(FStandaloneCpuTile& Tile)
{
if(Tile.Key.Level<AuditMinLevel)return true;
const FIntVector Origin=Tile.Key.OriginVoxels();
const int32 Extent=Tile.Key.ExtentVoxels();
const int32 Cells=std::max(1,World.CoarseTileCells);
const double Step=static_cast<double>(std::max(1,Extent/Cells));
if(bDropOneShell&&!bCorruptionApplied)
TryDropShellInTile(Tile,Origin,Extent,Step);
FMeshColumnIndex MeshIndex(Tile,Origin,Extent,Cells,Step);
static constexpr int32 ColumnOffsets[4]={1,5,9,13};
for(const FBoundary& Boundary:Boundaries)
{
if(Boundary.Z+Boundary.HalfWidth<Origin.Z
||Boundary.Z-Boundary.HalfWidth>Origin.Z+Extent)continue;
for(const int32 CY:ColumnOffsets)for(const int32 CX:ColumnOffsets)
{
const double X=Origin.X+(CX+0.371)*Step;
const double Y=Origin.Y+(CY+0.619)*Step;
const std::vector<double> Source=FindVerticalCrossings(X,Y,
Boundary.Z-Boundary.HalfWidth,Boundary.Z+Boundary.HalfWidth,1.0);
if(Source.empty())continue;
++Counts.BoundaryColumns;
Counts.BoundaryReferenceSurfaces+=Source.size();
CompareVerticalColumn(MeshIndex,X,Y,Boundary.Z-Boundary.HalfWidth,
Boundary.Z+Boundary.HalfWidth,Source,Step,
Counts.BoundaryMissing,Counts.BoundaryExtra,Counts.BoundaryHoles);
}
}
const auto Samples=ConnectorSamples.find(Tile.Key);
if(Samples!=ConnectorSamples.end())
{
for(const FConnectorSample& Sample:Samples->second)
AuditConnectorSample(Tile,Sample,Step,MeshIndex);
}
return true;
}
bool Passing() const
{
return bCorruptionApplied==bDropOneShell
&&Counts.BoundaryReferenceSurfaces>0
&&Counts.ConnectorsInView>0&&Counts.ConnectorSamples>0
&&Counts.BoundaryMissing==0&&Counts.BoundaryHoles==0
&&Counts.ConnectorMissing==0&&Counts.ConnectorSampleMissing==0
&&Counts.ConnectorHoles==0
&&Counts.ConnectorUnsampled==0;
}
void Print() const
{
std::printf("[FarSurvivalAudit] boundaries=%zu boundary_columns=%llu boundary_surfaces=%llu boundary_missing=%llu boundary_extra=%llu boundary_holes=%llu far_connectors=%llu connector_samples=%llu connector_visible_samples=%llu connector_no_surface_samples=%llu connector_sample_missing=%llu connector_good_connectors=%llu connector_rays=%llu connector_surfaces=%llu connector_ray_missing=%llu connector_missing=%llu connector_extra=%llu connector_holes=%llu connector_unsampled=%llu bad_shell_removed_triangles=%llu status=%s\n",
Boundaries.size(),static_cast<unsigned long long>(Counts.BoundaryColumns),
static_cast<unsigned long long>(Counts.BoundaryReferenceSurfaces),
static_cast<unsigned long long>(Counts.BoundaryMissing),
static_cast<unsigned long long>(Counts.BoundaryExtra),
static_cast<unsigned long long>(Counts.BoundaryHoles),
static_cast<unsigned long long>(Counts.ConnectorsInView),
static_cast<unsigned long long>(Counts.ConnectorSamples),
static_cast<unsigned long long>(Counts.ConnectorVisibleSamples),
static_cast<unsigned long long>(Counts.ConnectorNoSurfaceSamples),
static_cast<unsigned long long>(Counts.ConnectorSampleMissing),
static_cast<unsigned long long>(Counts.ConnectorGoodSamples),
static_cast<unsigned long long>(Counts.ConnectorRays),
static_cast<unsigned long long>(Counts.ConnectorReferenceSurfaces),
static_cast<unsigned long long>(Counts.ConnectorRayMissing),
static_cast<unsigned long long>(Counts.ConnectorMissing),
static_cast<unsigned long long>(Counts.ConnectorExtra),
static_cast<unsigned long long>(Counts.ConnectorHoles),
static_cast<unsigned long long>(Counts.ConnectorUnsampled),
static_cast<unsigned long long>(Counts.CorruptedTriangles),Passing()?"PASS":"FAIL");
std::fflush(stdout);
}
private:
struct FMeshColumnIndex
{
FMeshColumnIndex(const FStandaloneCpuTile& InTile,const FIntVector& InOrigin,
int32 InExtent,int32 InCells,double InStep)
:Tile(InTile),Origin(InOrigin),Extent(InExtent),Cells(InCells),Step(InStep),
ByColumn(static_cast<size_t>(InCells)*InCells)
{
const size_t TriangleCount=Tile.Indices.size()/3;
for(size_t Triangle=0;Triangle<TriangleCount;++Triangle)
{
uint32 A=Tile.Indices[Triangle*3],B=Tile.Indices[Triangle*3+1],C=Tile.Indices[Triangle*3+2];
if(A>=Tile.Vertices.size()||B>=Tile.Vertices.size()||C>=Tile.Vertices.size())continue;
const double MinX=std::min({PositionAxis(A,0),PositionAxis(B,0),PositionAxis(C,0)});
const double MaxX=std::max({PositionAxis(A,0),PositionAxis(B,0),PositionAxis(C,0)});
const double MinY=std::min({PositionAxis(A,1),PositionAxis(B,1),PositionAxis(C,1)});
const double MaxY=std::max({PositionAxis(A,1),PositionAxis(B,1),PositionAxis(C,1)});
const int32 X0=std::clamp(static_cast<int32>(std::floor((MinX-Origin.X)/Step)),0,Cells-1);
const int32 X1=std::clamp(static_cast<int32>(std::floor((MaxX-Origin.X)/Step)),0,Cells-1);
const int32 Y0=std::clamp(static_cast<int32>(std::floor((MinY-Origin.Y)/Step)),0,Cells-1);
const int32 Y1=std::clamp(static_cast<int32>(std::floor((MaxY-Origin.Y)/Step)),0,Cells-1);
for(int32 Y=Y0;Y<=Y1;++Y)for(int32 X=X0;X<=X1;++X)
ByColumn[static_cast<size_t>(Y)*Cells+X].push_back(static_cast<uint32>(Triangle));
}
}
std::vector<double> VerticalHits(double X,double Y,double ZMin,double ZMax) const
{
const int32 CX=std::clamp(static_cast<int32>(std::floor((X-Origin.X)/Step)),0,Cells-1);
const int32 CY=std::clamp(static_cast<int32>(std::floor((Y-Origin.Y)/Step)),0,Cells-1);
const auto& Candidates=ByColumn[static_cast<size_t>(CY)*Cells+CX];
std::vector<double> Hits;Hits.reserve(8);
for(const uint32 Triangle:Candidates)
{
const uint32 IA=Tile.Indices[static_cast<size_t>(Triangle)*3];
const uint32 IB=Tile.Indices[static_cast<size_t>(Triangle)*3+1];
const uint32 IC=Tile.Indices[static_cast<size_t>(Triangle)*3+2];
const double AX=PositionAxis(IA,0),AY=PositionAxis(IA,1),AZ=PositionAxis(IA,2);
const double BX=PositionAxis(IB,0),BY=PositionAxis(IB,1),BZ=PositionAxis(IB,2);
const double CXv=PositionAxis(IC,0),CYv=PositionAxis(IC,1),CZ=PositionAxis(IC,2);
const double Den=(BX-AX)*(CYv-AY)-(BY-AY)*(CXv-AX);
if(std::abs(Den)<1.0e-10)continue;
const double U=((X-AX)*(CYv-AY)-(Y-AY)*(CXv-AX))/Den;
const double V=((BX-AX)*(Y-AY)-(BY-AY)*(X-AX))/Den;
const double W=1.0-U-V;
if(U<-1.0e-7||V<-1.0e-7||W<-1.0e-7)continue;
const double Z=AZ+U*(BZ-AZ)+V*(CZ-AZ);
if(Z>=ZMin-1.0e-5&&Z<=ZMax+1.0e-5)Hits.push_back(Z);
}
SortUnique(Hits,0.25);
return Hits;
}
std::vector<double> SegmentHits(V3 Start,V3 Direction,double Length) const
{
std::vector<double> Hits;Hits.reserve(8);
const size_t TriangleCount=Tile.Indices.size()/3;
for(size_t Triangle=0;Triangle<TriangleCount;++Triangle)
{
const uint32 IA=Tile.Indices[Triangle*3],IB=Tile.Indices[Triangle*3+1],IC=Tile.Indices[Triangle*3+2];
if(IA>=Tile.Vertices.size()||IB>=Tile.Vertices.size()||IC>=Tile.Vertices.size())continue;
const V3 A=PositionVector(IA),B=PositionVector(IB),C=PositionVector(IC);
const V3 E1=Sub(B,A),E2=Sub(C,A),P=Cross(Direction,E2);
const float Determinant=Dot(E1,P);
if(std::abs(Determinant)<1.0e-8f)continue;
const float Inv=1.0f/Determinant;
const V3 T=Sub(Start,A);
const float U=Dot(T,P)*Inv;
if(U<-1.0e-5f||U>1.00001f)continue;
const V3 Q=Cross(T,E1);
const float V=Dot(Direction,Q)*Inv;
if(V<-1.0e-5f||U+V>1.00001f)continue;
const float Distance=Dot(E2,Q)*Inv;
if(Distance>=-1.0e-4f&&Distance<=Length+1.0e-4f)
Hits.push_back(std::clamp(static_cast<double>(Distance),0.0,Length));
}
SortUnique(Hits,0.25);
return Hits;
}
private:
V3 PositionVector(uint32 Index) const
{return {Tile.Vertices[Index].Position[0]*4.0f,Tile.Vertices[Index].Position[1]*4.0f,
Tile.Vertices[Index].Position[2]*4.0f};}
double PositionAxis(uint32 Index,int32 Axis) const
{return static_cast<double>(Tile.Vertices[Index].Position[Axis])*4.0;}
static void SortUnique(std::vector<double>& Values,double Epsilon)
{
std::sort(Values.begin(),Values.end());
Values.erase(std::unique(Values.begin(),Values.end(),[&](double A,double B)
{return std::abs(A-B)<=Epsilon;}),Values.end());
}
const FStandaloneCpuTile& Tile;
FIntVector Origin;
int32 Extent=0,Cells=0;
double Step=1.0;
std::vector<std::vector<uint32>> ByColumn;
};
static V3 AddV(V3 A,V3 B){return {A.X+B.X,A.Y+B.Y,A.Z+B.Z};}
static V3 MulV(V3 A,float S){return {A.X*S,A.Y*S,A.Z*S};}
static float LengthV(V3 A){return std::sqrt(std::max(0.0f,Dot(A,A)));}
static void SortUnique(std::vector<double>& Values,double Epsilon)
{
std::sort(Values.begin(),Values.end());
Values.erase(std::unique(Values.begin(),Values.end(),[&](double A,double B)
{return std::abs(A-B)<=Epsilon;}),Values.end());
}
void AddBoundary(double Z,double HalfWidth)
{
for(FBoundary& Existing:Boundaries)
if(std::abs(Existing.Z-Z)<0.01)
{Existing.HalfWidth=std::max(Existing.HalfWidth,HalfWidth);return;}
Boundaries.push_back({Z,HalfWidth});
}
static V3 ToV3(const FVector& P)
{return {static_cast<float>(P.X),static_cast<float>(P.Y),static_cast<float>(P.Z)};}
static bool Contains(const FVoxelTileKey& Key,V3 P,double Margin=0.0)
{
const FIntVector Origin=Key.OriginVoxels();
const int32 Extent=Key.ExtentVoxels();
return P.X>=Origin.X+Margin&&P.X<=Origin.X+Extent-Margin
&&P.Y>=Origin.Y+Margin&&P.Y<=Origin.Y+Extent-Margin
&&P.Z>=Origin.Z+Margin&&P.Z<=Origin.Z+Extent-Margin;
}
void BuildConnectorSamples(const std::vector<FVoxelTileKey>& DesiredTiles)
{
const auto& Connectors=Shape.GetFarConnectorSegments();
ConnectorInView.assign(Connectors.size(),0);
ConnectorHasSamples.assign(Connectors.size(),0);
ConnectorHasGoodSample.assign(Connectors.size(),0);
std::vector<FVoxelTileKey> FarTiles;
for(const FVoxelTileKey& Key:DesiredTiles)
if(Key.Level>=AuditMinLevel)FarTiles.push_back(Key);
for(size_t I=0;I<Connectors.size();++I)
{
const auto& Connector=Connectors[I];
const V3 A=ToV3(Connector.Start),B=ToV3(Connector.End);
const V3 Delta=Sub(B,A);const float Length=LengthV(Delta);
if(Length<=2.0f*Connector.Radius)continue;
const V3 Axis=MulV(Delta,1.0f/Length);
V3 U=Cross(Axis,{0,0,1});
if(LengthV(U)<0.1f)U=Cross(Axis,{0,1,0});
U=Normalize(U);const V3 V=Normalize(Cross(Axis,U));
bool bInsideView=false;
const int32 SampleCount=std::max(1,static_cast<int32>(std::ceil(Length/128.0f)));
for(int32 S=0;S<SampleCount;++S)
{
const float T=(static_cast<float>(S)+0.5f)/SampleCount;
const V3 Center=AddV(A,MulV(Delta,T));
for(const FVoxelTileKey& Key:FarTiles)
{
if(!Contains(Key,Center))continue;
bInsideView=true;
uint8 Mask=0;
const V3 Directions[4]={U,MulV(U,-1.0f),V,MulV(V,-1.0f)};
const double RayLength=Connector.Radius*2.0+32.0;
for(int32 D=0;D<4;++D)
if(Contains(Key,AddV(Center,MulV(Directions[D],static_cast<float>(RayLength)))))
Mask|=static_cast<uint8>(1u<<D);
if(!Mask)continue;
ConnectorSamples[Key].push_back({I,Center,U,V,Connector.Radius,Mask});
ConnectorHasSamples[I]=1;break;
}
}
ConnectorInView[I]=bInsideView?1:0;
}
}
public:
void Finalize()
{
Counts.ConnectorsInView=0;
Counts.ConnectorUnsampled=0;
Counts.ConnectorMissing=0;
Counts.ConnectorGoodSamples=0;
for(size_t I=0;I<ConnectorInView.size();++I)
{
if(!ConnectorInView[I])continue;
++Counts.ConnectorsInView;
if(!ConnectorHasSamples[I])++Counts.ConnectorUnsampled;
if(!ConnectorHasGoodSample[I])++Counts.ConnectorMissing;
else ++Counts.ConnectorGoodSamples;
}
}
private:
std::vector<double> FindVerticalCrossings(double X,double Y,double ZMin,double ZMax,
double Stride) const
{
std::vector<double> Crossings;
if(ZMax<=ZMin)return Crossings;
double PreviousZ=ZMin;
bool PreviousSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(PreviousZ))<0.0f;
for(double Z=std::min(ZMin+Stride,ZMax);Z<=ZMax+1.0e-6;Z=std::min(Z+Stride,ZMax))
{
const bool Solid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z))<0.0f;
if(Solid!=PreviousSolid)
{
double Lo=PreviousZ,Hi=Z;const bool LoSolid=PreviousSolid;
for(int32 I=0;I<6;++I)
{
const double Mid=(Lo+Hi)*0.5;
const bool MidSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Mid))<0.0f;
if(MidSolid==LoSolid)Lo=Mid;else Hi=Mid;
}
Crossings.push_back((Lo+Hi)*0.5);
}
if(Z>=ZMax-1.0e-6)break;
PreviousZ=Z;PreviousSolid=Solid;
}
return Crossings;
}
void CompareVerticalColumn(const FMeshColumnIndex& MeshIndex,double X,double Y,
double ZMin,double ZMax,const std::vector<double>& Source,double Step,
uint64& Missing,uint64& Extra,uint64& Holes)
{
std::vector<double> Mesh=MeshIndex.VerticalHits(X,Y,ZMin,ZMax);
const double Tolerance=std::max(4.0,Step*1.5);
std::vector<uint8> Used(Mesh.size(),0);
uint64 ColumnMissing=0,ColumnExtra=0;
for(const double Crossing:Source)
{
size_t Best=Mesh.size();double BestDistance=std::numeric_limits<double>::infinity();
for(size_t I=0;I<Mesh.size();++I)
if(!Used[I]&&std::abs(Mesh[I]-Crossing)<BestDistance)
{Best=I;BestDistance=std::abs(Mesh[I]-Crossing);}
if(Best==Mesh.size()||BestDistance>Tolerance)++ColumnMissing;
else Used[Best]=1;
}
for(const uint8 WasUsed:Used)if(!WasUsed)++ColumnExtra;
Missing+=ColumnMissing;Extra+=ColumnExtra;
if(ColumnMissing)
{
if((ColumnMissing&1u)!=0)++Holes;
if(DebugColumns<6)
{
std::printf("[FarAuditColumnMismatch] xy=(%.1f,%.1f) z=(%.1f..%.1f) source=%zu mesh=%zu first=(%.1f,%.1f) counts=(%llu,%llu)\n",
X,Y,ZMin,ZMax,Source.size(),Mesh.size(),Source.empty()?0.0:Source.front(),
Mesh.empty()?0.0:Mesh.front(),static_cast<unsigned long long>(ColumnMissing),
static_cast<unsigned long long>(ColumnExtra));
++DebugColumns;
}
}
}
void AuditConnectorSample(const FStandaloneCpuTile& Tile,const FConnectorSample& Sample,
double Step,const FMeshColumnIndex& MeshIndex)
{
++Counts.ConnectorSamples;
const V3 Directions[4]={Sample.U,MulV(Sample.U,-1.0f),Sample.V,MulV(Sample.V,-1.0f)};
const float RayLength=Sample.Radius*2.0f+32.0f;
const double SourceRadiusBand=std::max(16.0,static_cast<double>(Sample.Radius)*0.75);
uint32 MatchedDirections=0,ReferenceDirections=0;
uint8 ReferenceMask=0,MatchedMask=0;
for(int32 D=0;D<4;++D)
{
if((Sample.DirectionMask&(1u<<D))==0)continue;
++Counts.ConnectorRays;
const V3 Direction=Directions[D];
const std::vector<double> Source=FindRadialCrossings(Sample.Center,Direction,RayLength);
if(Source.empty())continue;
const auto SourceHit=std::min_element(Source.begin(),Source.end(),[&](double A,double B)
{return std::abs(A-Sample.Radius)<std::abs(B-Sample.Radius);});
if(SourceHit==Source.end()||std::abs(*SourceHit-Sample.Radius)>SourceRadiusBand)continue;
++ReferenceDirections;
ReferenceMask|=static_cast<uint8>(1u<<D);
++Counts.ConnectorReferenceSurfaces;
const std::vector<double> Mesh=MeshIndex.SegmentHits(Sample.Center,Direction,RayLength);
const double Tolerance=std::max(8.0,static_cast<double>(Sample.Radius)*0.35);
double BestDistance=std::numeric_limits<double>::infinity();
for(const double Hit:Mesh)
{
const double Distance=std::abs(Hit-*SourceHit);
if(Distance<BestDistance)BestDistance=Distance;
}
if(BestDistance<=Tolerance)
{++MatchedDirections;MatchedMask|=static_cast<uint8>(1u<<D);}
else
{
++Counts.ConnectorRayMissing;
if(DebugConnectorMismatches<24)
{
std::printf("[FarConnectorRayMismatch] connector=%zu tile_lod=%d center=(%.1f,%.1f,%.1f) radius=%.1f direction=%d source=%.2f nearest_mesh=%.2f hits=%zu step=%.1f tolerance=%.1f\n",
Sample.ConnectorIndex,Tile.Key.Level,Sample.Center.X,Sample.Center.Y,Sample.Center.Z,
Sample.Radius,D,*SourceHit,BestDistance,Mesh.size(),Step,Tolerance);
++DebugConnectorMismatches;
}
}
if(Mesh.size()>1)Counts.ConnectorExtra+=Mesh.size()-1;
}
// The source field can expose only one or two radial walls where a link
// joins a parent room. Preserve every source wall at every sampled section;
// require both sides whenever the source itself exposes an opposing pair.
const bool bSourcePair=(ReferenceMask&0x03u)==0x03u||(ReferenceMask&0x0Cu)==0x0Cu;
const bool bMatchedPair=(MatchedMask&0x03u)==0x03u||(MatchedMask&0x0Cu)==0x0Cu;
if(ReferenceDirections==0)
{++Counts.ConnectorNoSurfaceSamples;return;}
++Counts.ConnectorVisibleSamples;
const bool bSectionPassing=ReferenceDirections>0
&&MatchedDirections==ReferenceDirections
&&(!bSourcePair||bMatchedPair);
if(bSectionPassing&&!ConnectorHasGoodSample[Sample.ConnectorIndex])
ConnectorHasGoodSample[Sample.ConnectorIndex]=1;
if(!bSectionPassing)
{
++Counts.ConnectorSampleMissing;
if(MatchedDirections<ReferenceDirections||(bSourcePair&&!bMatchedPair))
++Counts.ConnectorHoles;
}
}
std::vector<double> FindRadialCrossings(V3 Start,V3 Direction,double Length) const
{
std::vector<double> Crossings;
double PreviousDistance=0.0;
bool PreviousSolid=Shape.DensityAt(Start.X,Start.Y,Start.Z)<0.0f;
constexpr double Stride=2.0;
for(double Distance=Stride;Distance<=Length+1.0e-6;Distance=std::min(Distance+Stride,Length))
{
const V3 P=AddV(Start,MulV(Direction,static_cast<float>(Distance)));
const bool Solid=Shape.DensityAt(P.X,P.Y,P.Z)<0.0f;
if(Solid!=PreviousSolid)
{
double Lo=PreviousDistance,Hi=Distance;
for(int32 I=0;I<6;++I)
{
const double Mid=(Lo+Hi)*0.5;
const V3 M=AddV(Start,MulV(Direction,static_cast<float>(Mid)));
const bool MidSolid=Shape.DensityAt(M.X,M.Y,M.Z)<0.0f;
if(MidSolid==PreviousSolid)Lo=Mid;else Hi=Mid;
}
Crossings.push_back((Lo+Hi)*0.5);
}
if(Distance>=Length-1.0e-6)break;
PreviousDistance=Distance;PreviousSolid=Solid;
}
return Crossings;
}
void TryDropShellInTile(FStandaloneCpuTile& Tile,const FIntVector& Origin,int32 Extent,double Step)
{
static constexpr int32 Offsets[4]={1,5,9,13};
for(const FBoundary& Boundary:Boundaries)
{
if(Boundary.Z+Boundary.HalfWidth<Origin.Z
||Boundary.Z-Boundary.HalfWidth>Origin.Z+Extent)continue;
for(const int32 CY:Offsets)for(const int32 CX:Offsets)
{
const double X=Origin.X+(CX+0.371)*Step;
const double Y=Origin.Y+(CY+0.619)*Step;
if(FindVerticalCrossings(X,Y,Boundary.Z-Boundary.HalfWidth,
Boundary.Z+Boundary.HalfWidth,1.0).empty())continue;
const double DropHalfWidth=std::max(Boundary.HalfWidth+4.0,Step*0.75);
std::vector<uint32> Kept;Kept.reserve(Tile.Indices.size());
for(size_t T=0;T+2<Tile.Indices.size();T+=3)
{
const uint32 A=Tile.Indices[T],B=Tile.Indices[T+1],C=Tile.Indices[T+2];
if(A>=Tile.Vertices.size()||B>=Tile.Vertices.size()||C>=Tile.Vertices.size())continue;
const double CenterZ=(Tile.Vertices[A].Position[2]+Tile.Vertices[B].Position[2]
+Tile.Vertices[C].Position[2])*4.0/3.0;
if(std::abs(CenterZ-Boundary.Z)<=DropHalfWidth)
{++Counts.CorruptedTriangles;continue;}
Kept.push_back(A);Kept.push_back(B);Kept.push_back(C);
}
Tile.Indices.swap(Kept);
bCorruptionApplied=Counts.CorruptedTriangles>0;
std::printf("[FarAuditBadSetting] removed stratum shell triangles at z=%.1f vox tile_lod=%d removed=%llu\n",
Boundary.Z,Tile.Key.Level,static_cast<unsigned long long>(Counts.CorruptedTriangles));
std::fflush(stdout);
return;
}
}
}
FStandaloneOwnerWorld& World;
const FStandaloneShapeWorld& Shape;
static constexpr int32 AuditMinLevel=6;
bool bDropOneShell=false,bCorruptionApplied=false;
std::vector<FBoundary> Boundaries;
std::unordered_map<FVoxelTileKey,std::vector<FConnectorSample>,FStandaloneTileKeyHash> ConnectorSamples;
FCounts Counts;
std::vector<uint8> ConnectorInView,ConnectorHasSamples,ConnectorHasGoodSample;
uint32 DebugColumns=0;
uint32 DebugConnectorMismatches=0;
};
+2 -2
View File
@@ -30,7 +30,7 @@ struct FStandaloneOwnerWorld
bool ViewerStartFound = false;
int32 Seed = 0;
int32 ClipRadius = 6;
int32 MaxClipLevel = 6;
int32 MaxClipLevel = 8;
int32 FullResClipLevels = 2;
int32 CoarseTileCells = 16;
int32 RenderDistanceChunks = 1536;
@@ -253,7 +253,7 @@ namespace StandaloneOwnerLoader
}
Out.Seed = static_cast<int32>(Root.Get("seed").Integer());
Out.ClipRadius = static_cast<int32>(Root.Get("clip_radius").Integer(6));
Out.MaxClipLevel = static_cast<int32>(Root.Get("max_clip_level").Integer(6));
Out.MaxClipLevel = static_cast<int32>(Root.Get("max_clip_level").Integer(8));
Out.FullResClipLevels = static_cast<int32>(Root.Get("full_res_clip_levels").Integer(2));
Out.CoarseTileCells = static_cast<int32>(Root.Get("coarse_tile_cells").Integer(16));
Out.RenderDistanceChunks = static_cast<int32>(Root.Get("render_distance_chunks").Integer(1536));
+83
View File
@@ -21,6 +21,7 @@
#include <thread>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <vector>
// Standalone-only world compiler. It compiles the runtime room/tunnel/passages into
@@ -30,6 +31,12 @@ class FStandaloneShapeWorld
{
public:
enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel };
struct FFarConnectorSegment
{
FVector Start=FVector::ZeroVector;
FVector End=FVector::ZeroVector;
float Radius=0.0f;
};
struct FEditBrush
{
enum class EShape : uint8 { Sphere, Capsule, RoundedBox };
@@ -393,6 +400,64 @@ public:
const FBuildStats& GetStats() const { return Stats; }
bool IsBuilt() const { return Built; }
bool HasFarWorld() const { return FarBuilt; }
const std::vector<FFarConnectorSegment>& GetFarConnectorSegments() const
{ return FarConnectorSegments; }
size_t BuildFarConnectorAirSamples(const FIntVector& TileOrigin,int32 Step,int32 Cells,
std::unordered_set<uint64>& OutSamples) const
{
OutSamples.clear();
if(FarConnectorSegments.empty()||Step<=0||Cells<=0)return 0;
const double Pad=Step;
const double TileMin[3]={TileOrigin.X-Pad,TileOrigin.Y-Pad,TileOrigin.Z-Pad};
const double TileMax[3]={TileOrigin.X+(Cells+1)*Step,TileOrigin.Y+(Cells+1)*Step,
TileOrigin.Z+(Cells+1)*Step};
for(const FFarConnectorSegment& Connector:FarConnectorSegments)
{
const double Start[3]={Connector.Start.X,Connector.Start.Y,Connector.Start.Z};
const double End[3]={Connector.End.X,Connector.End.Y,Connector.End.Z};
bool bOverlaps=true;
for(int32 Axis=0;Axis<3;++Axis)
bOverlaps &= std::min(Start[Axis],End[Axis])-Connector.Radius<=TileMax[Axis]
&&std::max(Start[Axis],End[Axis])+Connector.Radius>=TileMin[Axis];
if(!bOverlaps)continue;
const double Delta[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]};
const double Length=std::sqrt(Delta[0]*Delta[0]+Delta[1]*Delta[1]+Delta[2]*Delta[2]);
if(Length<=1.0e-6)continue;
const int32 Samples=std::max(1,static_cast<int32>(std::ceil(Length/(Step*0.25))));
for(int32 I=0;I<=Samples;++I)
{
const double T=static_cast<double>(I)/Samples;
const double P[3]={Start[0]+Delta[0]*T,Start[1]+Delta[1]*T,Start[2]+Delta[2]*T};
if(P[0]<TileMin[0]||P[0]>TileMax[0]||P[1]<TileMin[1]||P[1]>TileMax[1]
||P[2]<TileMin[2]||P[2]>TileMax[2])continue;
int32 Base[3];
for(int32 Axis=0;Axis<3;++Axis)
Base[Axis]=static_cast<int32>(std::floor((P[Axis]-(&TileOrigin.X)[Axis])/Step));
for(int32 DZ=0;DZ<=1;++DZ)for(int32 DY=0;DY<=1;++DY)for(int32 DX=0;DX<=1;++DX)
{
const int32 IX=Base[0]+DX,IY=Base[1]+DY,IZ=Base[2]+DZ;
if(IX<0||IY<0||IZ<0||IX>Cells||IY>Cells||IZ>Cells)continue;
OutSamples.insert(PackLatticeKey(TileOrigin.X+IX*Step,
TileOrigin.Y+IY*Step,TileOrigin.Z+IZ*Step));
}
}
}
return OutSamples.size();
}
class FScopedForcedFarAirSamples
{
public:
explicit FScopedForcedFarAirSamples(const std::unordered_set<uint64>* AirSamples)
:PreviousAir(ForcedFarAirSamples)
{ ForcedFarAirSamples=AirSamples; }
~FScopedForcedFarAirSamples(){ForcedFarAirSamples=PreviousAir;}
FScopedForcedFarAirSamples(const FScopedForcedFarAirSamples&)=delete;
FScopedForcedFarAirSamples& operator=(const FScopedForcedFarAirSamples&)=delete;
private:
const std::unordered_set<uint64>* PreviousAir=nullptr;
};
bool DebugCriticalAirAt(double X,double Y,double Z) const { return IsInsideCriticalAir(X,Y,Z); }
bool DebugSolidOverlayAt(double X,double Y,double Z) const { return IsInsideSolidOverlay(X,Y,Z); }
@@ -451,6 +516,7 @@ public:
{
int32 IX=0,IY=0,IZ=0;
const bool bInteger=NearInteger(X,IX)&&NearInteger(Y,IY)&&NearInteger(Z,IZ);
if(bInteger&&IsForcedFarAirSample(IX,IY,IZ))return 0.05f;
const bool bPacked=bInteger&&Contains(IX,IY,IZ);
if(bPacked)
return PackedDensityAt(IX,IY,IZ,IsRock(IX,IY,IZ),X,Y,Z);
@@ -459,6 +525,7 @@ public:
float DensityAtLattice(int32 X,int32 Y,int32 Z) const
{
if(IsForcedFarAirSample(X,Y,Z))return 0.05f;
const bool bPacked=Contains(X,Y,Z);
if(bPacked)
return PackedDensityAt(X,Y,Z,IsRock(X,Y,Z),static_cast<float>(X),
@@ -468,6 +535,18 @@ public:
}
private:
static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z)
{
constexpr uint64 Mask=0x1FFFFFull;
return (static_cast<uint64>(static_cast<uint32>(X)&Mask)<<42)
|(static_cast<uint64>(static_cast<uint32>(Y)&Mask)<<21)
|(static_cast<uint64>(static_cast<uint32>(Z)&Mask));
}
static bool IsForcedFarAirSample(int32 X,int32 Y,int32 Z)
{
const auto* Samples=ForcedFarAirSamples;
return Samples&&Samples->contains(PackLatticeKey(X,Y,Z));
}
float PackedDensityAt(int32 X,int32 Y,int32 Z,bool bRock,float FX,float FY,float FZ) const
{
const size_t Column=static_cast<size_t>(Y-Min[1])*Size[0]+static_cast<size_t>(X-Min[0]);
@@ -1970,6 +2049,7 @@ private:
void BuildFarParentShapes()
{
constexpr int32 ParentPitch=2048;
FarConnectorSegments.clear();
struct FParent { int32 GX=0,GY=0,Strate=0; FVector Center; double Rx=0,Ry=0,Rz=0; };
const int32 GX0=FloorDiv(ClipMin[0],ParentPitch)-1,GY0=FloorDiv(ClipMin[1],ParentPitch)-1;
const int32 GX1=FloorDiv(ClipMax[0],ParentPitch)+1,GY1=FloorDiv(ClipMax[1],ParentPitch)+1;
@@ -2024,6 +2104,7 @@ private:
if(MaxX>=Min[0]&&MinX<=Max[0]&&MaxY>=Min[1]&&MinY<=Max[1])continue;
AddTaperedCapsule(A,B,64.0f,64.0f,true);
Primitives.back().BlendRadius=0.0;
FarConnectorSegments.push_back({A,B,64.0f});
}
}
if(!Parents.empty())
@@ -3154,6 +3235,7 @@ private:
std::vector<FInterval> LayerRanges;
std::vector<FLayerField> LayerFields;
std::vector<FPrimitive> Primitives;
std::vector<FFarConnectorSegment> FarConnectorSegments;
std::vector<FEditBrush> EditBrushes;
std::array<FAxisBins,3> AxisBins;
std::unordered_map<FBinKey,FBinItems,FBinHash> SpatialBins;
@@ -3164,6 +3246,7 @@ private:
std::array<std::vector<uint64>,3> CandidateAxisWords;
std::vector<uint64> CandidateWords;
FBuildStats Stats;
inline static thread_local const std::unordered_set<uint64>* ForcedFarAirSamples=nullptr;
};
namespace FStandaloneShapeWorldAccess
+414 -14
View File
@@ -49,6 +49,8 @@ namespace
V3 Cross(V3 A,V3 B){return {A.Y*B.Z-A.Z*B.Y,A.Z*B.X-A.X*B.Z,A.X*B.Y-A.Y*B.X};}
V3 Normalize(V3 A){return Mul(A,1.0f/std::sqrt(std::max(1.0e-20f,Dot(A,A))));}
#include "FarSurvivalAudit.h"
struct FMeshSheetAudit
{
uint64 Components=0;
@@ -337,7 +339,13 @@ namespace
ID3D11RasterizerState* WireRasterizer=nullptr;
std::unique_ptr<FStandaloneOwnerWorld> World;
std::unique_ptr<FStandaloneShapeWorld> ShapeWorld;
std::unique_ptr<FStandaloneShapeWorld> PendingFullShapeWorld;
std::unique_ptr<FStandaloneStreamingGenerator> Streamer;
std::thread FullShapeBuildThread;
std::atomic<bool> bFullShapeBuildFinished{false};
bool bFullShapeBuildSucceeded=false;
std::string FullShapeBuildError;
std::chrono::steady_clock::time_point FullShapeBuildStart{};
FStandaloneMeshCache MeshCache;
std::string DataPath;
bool bHasCameraOverride=false;
@@ -349,6 +357,7 @@ namespace
std::unordered_set<FStandaloneBatchKey,FStandaloneBatchKeyHash> DirtyBatchSet;
std::unordered_map<uint64,FCrossingWork> CrossingWork;
std::vector<FVoxelTileKey> DesiredTiles;
std::vector<FVoxelTileKey> InitialNeighborhoodTiles;
int32 DesiredByLevel[MaxLevels+1]{};
uint64 GeneratedByLevel[MaxLevels+1]{};
uint64 SamplesByLevel[MaxLevels+1]{};
@@ -362,6 +371,8 @@ namespace
bool bRoundTunnels=false;
bool bRoundTunnelEnhanced=true;
bool bShapeWorld=false;
bool bShapePipelineActive=false;
bool bNeighborhoodReadyReported=false;
bool bShapeAuditOnly=false;
bool bShapeCompare=false;
bool bFarShapeStagePending=false;
@@ -386,6 +397,7 @@ namespace
bool bForceGenerate=false;
std::string CapturePath;
std::string MeshCachePath;
std::chrono::steady_clock::time_point ColdProcessStart{};
std::chrono::steady_clock::time_point GenerationStart{};
bool bShowLevel[MaxLevels+1]={true,true,true,true,true,true,true,true,true};
bool Keys[256]{};
@@ -435,6 +447,15 @@ namespace
(void)bCancel;
if(Streamer)Streamer->Stop();
Streamer.reset();
if(FullShapeBuildThread.joinable())FullShapeBuildThread.join();
PendingFullShapeWorld.reset();
bFullShapeBuildFinished.store(false,std::memory_order_relaxed);
bFullShapeBuildSucceeded=false;
FullShapeBuildError.clear();
InitialNeighborhoodTiles.clear();
bShapePipelineActive=false;
bNeighborhoodReadyReported=false;
bFarShapeStagePending=false;
bGenerationRunning=false;
}
@@ -653,42 +674,95 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
std::printf("Standalone max clip level override applied after world reload: max_level=%d.\n",
MaxClipLevelOverride);
}
if(bShapeWorld&&!BuildShapeWorldForCamera(Error))return false;
if(bShapeWorld&&!BuildShapeWorldForCamera(Error,false,true))return false;
LoadMeshCache();
return true;
}
bool BuildShapeWorldForCamera(std::string& Error,bool bIncludeFarWorld=false)
bool BuildShapeWorldForCamera(std::string& Error,bool bIncludeFarWorld=false,
bool bNeighborhoodOnly=false)
{
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels);
FIntVector Min(INT32_MAX,INT32_MAX,INT32_MAX),Max(INT32_MIN,INT32_MIN,INT32_MIN);
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
uint64 FineTiles=0;
InitialNeighborhoodTiles.clear();
const FIntVector CenterChunk=Desired.CenterChunk;
for(const FVoxelTileKey& Tile:Desired.Tiles)
{
const FIntVector Origin=Tile.OriginVoxels();
const int32 Extent=Tile.ExtentVoxels();
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
if(Tile.Level>=World->FullResClipLevels)continue;
if(bNeighborhoodOnly)
{
if(Tile.Level!=0||std::abs(Tile.Coord.X-CenterChunk.X)>1
||std::abs(Tile.Coord.Y-CenterChunk.Y)>1
||std::abs(Tile.Coord.Z-CenterChunk.Z)>1)continue;
InitialNeighborhoodTiles.push_back(Tile);
}
else if(Tile.Level>=World->FullResClipLevels)continue;
Min.X=std::min(Min.X,Origin.X);Min.Y=std::min(Min.Y,Origin.Y);Min.Z=std::min(Min.Z,Origin.Z);
Max.X=std::max(Max.X,Origin.X+Extent);Max.Y=std::max(Max.Y,Origin.Y+Extent);Max.Z=std::max(Max.Z,Origin.Z+Extent);
++FineTiles;
}
if(!FineTiles){Error="no full-detail tiles are available for shape compilation";return false;}
if(bNeighborhoodOnly)
{
if(InitialNeighborhoodTiles.size()!=27)
{Error="the 3x3x3 player neighborhood does not contain 27 LOD0 tiles";return false;}
constexpr int32 NearBuildPad=64;
Min.X-=NearBuildPad;Min.Y-=NearBuildPad;Min.Z-=NearBuildPad;
Max.X+=NearBuildPad;Max.Y+=NearBuildPad;Max.Z+=NearBuildPad;
}
ShapeWorld=std::make_unique<FStandaloneShapeWorld>();
if(!ShapeWorld->Build(*World,Min,Max,ClipMin,ClipMax,WorkerCount,
const int32 ShapeWorkers=bNeighborhoodOnly?(WorkerCount>=8?2:1):WorkerCount;
if(!ShapeWorld->Build(*World,Min,Max,ClipMin,ClipMax,ShapeWorkers,
ShapeDetailVariant,bIncludeFarWorld,Error))
{ShapeWorld.reset();return false;}
FStandaloneShapeWorldAccess::SetActive(ShapeWorld.get());
bFarShapeStagePending=bShapeWorld&&!ShapeWorld->HasFarWorld();
bShapePipelineActive=bNeighborhoodOnly;
bNeighborhoodReadyReported=false;
bFarShapeStagePending=bShapeWorld&&!bNeighborhoodOnly&&!ShapeWorld->HasFarWorld();
std::printf("[ShapeRegion] fine_tiles=%llu bounds=(%d,%d,%d)..(%d,%d,%d) clip=(%d,%d,%d)..(%d,%d,%d)\n",
static_cast<unsigned long long>(FineTiles),Min.X,Min.Y,Min.Z,Max.X,Max.Y,Max.Z,
ClipMin.X,ClipMin.Y,ClipMin.Z,ClipMax.X,ClipMax.Y,ClipMax.Z);
return true;
}
bool StartFullShapeBuild(const std::vector<FVoxelTileKey>& FullDesired,std::string& Error)
{
FIntVector FineMin(INT32_MAX,INT32_MAX,INT32_MAX),FineMax(INT32_MIN,INT32_MIN,INT32_MIN);
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
for(const FVoxelTileKey& Tile:FullDesired)
{
const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
if(Tile.Level>=World->FullResClipLevels)continue;
FineMin.X=std::min(FineMin.X,Origin.X);FineMin.Y=std::min(FineMin.Y,Origin.Y);FineMin.Z=std::min(FineMin.Z,Origin.Z);
FineMax.X=std::max(FineMax.X,Origin.X+Extent);FineMax.Y=std::max(FineMax.Y,Origin.Y+Extent);FineMax.Z=std::max(FineMax.Z,Origin.Z+Extent);
}
if(FineMin.X==INT32_MAX||ClipMin.X==INT32_MAX)
{Error="the full shape build has no desired detail or clipmap tiles";return false;}
PendingFullShapeWorld=std::make_unique<FStandaloneShapeWorld>();
FullShapeBuildError.clear();bFullShapeBuildSucceeded=false;
bFullShapeBuildFinished.store(false,std::memory_order_relaxed);
const int32 FullWorkers=WorkerCount>=8?6:WorkerCount;
FullShapeBuildStart=std::chrono::steady_clock::now();
FullShapeBuildThread=std::thread([this,FineMin,FineMax,ClipMin,ClipMax,FullWorkers]()
{
bFullShapeBuildSucceeded=PendingFullShapeWorld->Build(*World,FineMin,FineMax,
ClipMin,ClipMax,FullWorkers,ShapeDetailVariant,true,FullShapeBuildError);
bFullShapeBuildFinished.store(true,std::memory_order_release);
});
std::printf("[ShapePipelineBuildStart] neighborhood_tiles=%zu near_mesh_workers=%d full_shape_workers=%d\n",
InitialNeighborhoodTiles.size(),WorkerCount>=8?2:1,FullWorkers);
std::fflush(stdout);
return true;
}
FStandaloneMeshCacheMetadata MakeMeshCacheMetadata()const
{
FStandaloneMeshCacheMetadata Metadata;
@@ -752,10 +826,19 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
DesiredTiles=Desired.Tiles;
for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0;
for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level];
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,WorkerCount,false,
const int32 StreamWorkers=bShapePipelineActive?(WorkerCount>=8?2:1):WorkerCount;
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,StreamWorkers,false,
(!bShapeWorld&&MeshCache.IsLoaded())?&MeshCache:nullptr,
bFarShapeStagePending?World->FullResClipLevels-1:-1);
if(bShapePipelineActive)Streamer->SetDesiredTileFilter(InitialNeighborhoodTiles);
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex);
if(bShapePipelineActive&&Update.DesiredTiles!=static_cast<int32>(InitialNeighborhoodTiles.size()))
{
StopGeneration();
Error="the initial neighborhood streamer did not accept all 27 tiles";
std::fprintf(stderr,"World load failed: %s\n",Error.c_str());
return;
}
FCrossingWork& InitialWork=CrossingWork[CrossingIndex];
InitialWork.SubmittedJobs=Update.NewJobs;InitialWork.PendingJobs=Update.NewJobs;
InitialWork.StartTime=GenerationStart;
@@ -765,6 +848,16 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
std::printf("Streaming seed %d%s%s: %d desired tiles, %d full-detail tiles, %d workers; upload budget %d batches / %.1f ms per frame.\n",
Seed,TunnelMode,bShapeWorld?" [shape intervals]":"",Update.DesiredTiles,
Update.FullResTiles,Streamer->GetWorkerCount(),MaxBatchUploadsPerFrame,BatchUploadBudgetMilliseconds);
if(bShapePipelineActive)
{
std::string ShapeError;
if(!StartFullShapeBuild(DesiredTiles,ShapeError))
{
StopGeneration();
std::fprintf(stderr,"World load failed: %s\n",ShapeError.c_str());
return;
}
}
std::fflush(stdout);
}
@@ -797,6 +890,67 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
std::fflush(stdout);
}
void TryCompleteShapePipeline()
{
if(!bShapePipelineActive||InitialNeighborhoodTiles.empty())return;
bool bNeighborhoodUploaded=true;
for(const FVoxelTileKey& Key:InitialNeighborhoodTiles)
if(!ResidentTiles.contains(Key)){bNeighborhoodUploaded=false;break;}
bNeighborhoodUploaded &= DirtyBatchSet.empty();
if(bNeighborhoodUploaded&&!bNeighborhoodReadyReported)
{
bNeighborhoodReadyReported=true;
const double Elapsed=std::chrono::duration<double>(
std::chrono::steady_clock::now()-ColdProcessStart).count();
std::printf("[NeighborhoodReady] %.3fs after cold process start; tiles=%zu/%zu vertices=%llu indices=%llu\n",
Elapsed,InitialNeighborhoodTiles.size(),InitialNeighborhoodTiles.size(),
static_cast<unsigned long long>(TotalVertices),
static_cast<unsigned long long>(TotalIndices));
std::fflush(stdout);
}
if(!bNeighborhoodReadyReported
||!bFullShapeBuildFinished.load(std::memory_order_acquire))return;
if(FullShapeBuildThread.joinable())FullShapeBuildThread.join();
if(!bFullShapeBuildSucceeded||!PendingFullShapeWorld)
{
bShapePipelineActive=false;bGenerationRunning=false;
std::fprintf(stderr,"[ShapePipeline] full shape build failed: %s\n",
FullShapeBuildError.c_str());
PostMessageA(Window,WM_CLOSE,0,0);
return;
}
const double FullShapeSeconds=std::chrono::duration<double>(
std::chrono::steady_clock::now()-FullShapeBuildStart).count();
std::vector<FVoxelTileKey> ResidentKeys;ResidentKeys.reserve(ResidentTiles.size());
for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first);
if(Streamer)Streamer->Stop();
Streamer.reset();
ShapeWorld=std::move(PendingFullShapeWorld);
FStandaloneShapeWorldAccess::SetActive(ShapeWorld.get());
bFarShapeStagePending=false;bShapePipelineActive=false;
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,WorkerCount,false,nullptr,-1);
++CrossingIndex;
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,ResidentKeys,CrossingIndex);
if(Update.DesiredTiles!=static_cast<int32>(DesiredTiles.size()))
{
Streamer->Stop();Streamer.reset();bGenerationRunning=false;
std::fprintf(stderr,"[ShapePipeline] full streamer accepted %d of %zu desired tiles.\n",
Update.DesiredTiles,DesiredTiles.size());
PostMessageA(Window,WM_CLOSE,0,0);
return;
}
FCrossingWork& Work=CrossingWork[CrossingIndex];
Work.SubmittedJobs=Update.NewJobs;Work.PendingJobs=Update.NewJobs;
Work.StartTime=std::chrono::steady_clock::now();
bGenerationRunning=true;
std::printf("[ShapePipelineFullReady] full_shape=%.3fs retained=%d new_jobs=%d workers=%d; filling outward\n",
FullShapeSeconds,Update.RetainedResidentTiles,Update.NewJobs,WorkerCount);
std::fflush(stdout);
}
void MarkBatchDirty(const FStandaloneBatchKey& Key)
{DirtyBatchSet.insert(Key);}
@@ -1050,6 +1204,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
}
ReportCrossingWork();
UploadDirtyBatches();
if(bShapePipelineActive)TryCompleteShapePipeline();
if(bTestOneChunkCrossing&&bCrossingMoveFinished&&!bCrossingClosePosted
&&DirtyBatchSet.empty())
{
@@ -1062,11 +1217,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
PostMessageA(Window,WM_CLOSE,0,0);
}
}
if(!bFullDetailReported&&IsFullDetailUploaded())
if(!bShapePipelineActive&&!bFullDetailReported&&IsFullDetailUploaded())
{
bFullDetailReported=true;
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-GenerationStart).count();
std::printf("[FullDetailReady] %.3f s after spawn; resident=%zu/%d tiles through LOD%d.\n",
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-ColdProcessStart).count();
std::printf("[FullDetailReady] %.3f s after cold process start; resident=%zu/%d tiles through LOD%d.\n",
Elapsed,ResidentTiles.size(),Streamer->FullResDesiredCount(),World->FullResClipLevels-1);
ReportTileTimings();std::fflush(stdout);
StartFarShapeGeneration();
@@ -1074,8 +1229,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
if(!bInitialClipmapReported&&ResidentTiles.size()==DesiredTiles.size()&&DirtyBatchSet.empty())
{
bInitialClipmapReported=true;
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-GenerationStart).count();
std::printf("[InitialClipmapReady] %.3f s after spawn; %zu tiles, %llu vertices, %llu indices, %zu GPU batches.\n",
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-ColdProcessStart).count();
std::printf("[InitialClipmapReady] %.3f s after cold process start; %zu tiles, %llu vertices, %llu indices, %zu GPU batches.\n",
Elapsed,ResidentTiles.size(),static_cast<unsigned long long>(TotalVertices),
static_cast<unsigned long long>(TotalIndices),StreamingBatches.size());
ReportTileTimings();std::fflush(stdout);
@@ -1709,12 +1864,225 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
return Hash;
}
int RunPipelinedStreamingBenchmark(FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels,
int32 WorkerCount,int32 MaxLevel,FStandaloneShapeWorld::EDetailVariant DetailVariant,
bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,bool bSkipUV,bool bSkipColors,
std::chrono::steady_clock::time_point ColdStart)
{
using Clock=std::chrono::steady_clock;
FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels);
if(MaxLevel>=0)
Desired.Tiles.erase(std::remove_if(Desired.Tiles.begin(),Desired.Tiles.end(),
[&](const FVoxelTileKey& Tile){return Tile.Level>MaxLevel;}),Desired.Tiles.end());
FIntVector FineMin(INT32_MAX,INT32_MAX,INT32_MAX),FineMax(INT32_MIN,INT32_MIN,INT32_MIN);
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
for(const FVoxelTileKey& Tile:Desired.Tiles)
{
const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
if(Tile.Level<World.FullResClipLevels)
{
FineMin.X=std::min(FineMin.X,Origin.X);FineMin.Y=std::min(FineMin.Y,Origin.Y);FineMin.Z=std::min(FineMin.Z,Origin.Z);
FineMax.X=std::max(FineMax.X,Origin.X+Extent);FineMax.Y=std::max(FineMax.Y,Origin.Y+Extent);FineMax.Z=std::max(FineMax.Z,Origin.Z+Extent);
}
}
if(FineMin.X==INT32_MAX||ClipMin.X==INT32_MAX)
{std::fprintf(stderr,"[StreamPipeline] no desired detail or clipmap tiles.\n");return 7;}
const FIntVector CenterChunk=Desired.CenterChunk;
std::vector<FVoxelTileKey> Neighborhood;
FIntVector NearMin(INT32_MAX,INT32_MAX,INT32_MAX),NearMax(INT32_MIN,INT32_MIN,INT32_MIN);
for(const FVoxelTileKey& Tile:Desired.Tiles)
{
if(Tile.Level!=0||std::abs(Tile.Coord.X-CenterChunk.X)>1
||std::abs(Tile.Coord.Y-CenterChunk.Y)>1||std::abs(Tile.Coord.Z-CenterChunk.Z)>1)continue;
Neighborhood.push_back(Tile);const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
NearMin.X=std::min(NearMin.X,Origin.X);NearMin.Y=std::min(NearMin.Y,Origin.Y);NearMin.Z=std::min(NearMin.Z,Origin.Z);
NearMax.X=std::max(NearMax.X,Origin.X+Extent);NearMax.Y=std::max(NearMax.Y,Origin.Y+Extent);NearMax.Z=std::max(NearMax.Z,Origin.Z+Extent);
}
if(Neighborhood.empty())
{std::fprintf(stderr,"[StreamPipeline] the player neighborhood is absent from the desired set.\n");return 7;}
constexpr int32 NearBuildPad=64;
NearMin.X-=NearBuildPad;NearMin.Y-=NearBuildPad;NearMin.Z-=NearBuildPad;
NearMax.X+=NearBuildPad;NearMax.Y+=NearBuildPad;NearMax.Z+=NearBuildPad;
const int32 NearWorkers=WorkerCount>=8?2:1;
const int32 FullBuildWorkers=WorkerCount>=8?6:1;
auto NearShape=std::make_unique<FStandaloneShapeWorld>();
std::string BuildError;
const auto NearBuildStart=Clock::now();
if(!NearShape->Build(World,NearMin,NearMax,ClipMin,ClipMax,NearWorkers,
DetailVariant,false,BuildError))
{std::fprintf(stderr,"[StreamPipeline] near shape build failed: %s\n",BuildError.c_str());return 2;}
const double NearShapeSeconds=std::chrono::duration<double>(Clock::now()-NearBuildStart).count();
FStandaloneShapeWorldAccess::SetActive(NearShape.get());
FStandaloneStreamingGenerator NearStreamer(World,NearWorkers,bHashDensity,nullptr,MaxLevel,
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors);
NearStreamer.SetDesiredTileFilter(Neighborhood);
const auto NearStreamStart=Clock::now();
const FStandaloneStreamingUpdate NearUpdate=NearStreamer.Start(CameraPositionVoxels);
if(NearUpdate.DesiredTiles!=static_cast<int32>(Neighborhood.size()))
{NearStreamer.Stop();FStandaloneShapeWorldAccess::SetActive(nullptr);
std::fprintf(stderr,"[StreamPipeline] expected %zu player tiles but queued %d.\n",Neighborhood.size(),NearUpdate.DesiredTiles);return 7;}
FStandaloneShapeWorld FullShape;
bool bFullBuildOk=false;std::string FullBuildError;
const auto FullBuildStart=Clock::now();
std::thread FullBuildThread([&]
{bFullBuildOk=FullShape.Build(World,FineMin,FineMax,ClipMin,ClipMax,FullBuildWorkers,
DetailVariant,true,FullBuildError);});
struct FPipelineTileHashes {uint64 Density=0,Mesh=0,Payload=0,Topology=0;};
std::unordered_map<FVoxelTileKey,FPipelineTileHashes,FStandaloneTileKeyHash> NearHashByKey;
NearHashByKey.reserve(Neighborhood.size()*2+1);
std::vector<FHashedTile> DensityRows,MeshRows,PayloadRows,TopologyRows;
std::vector<FStandaloneCpuTile> NearTiles;NearTiles.reserve(Neighborhood.size());
std::chrono::steady_clock::time_point NeighborhoodReady{};
int32 NearFinished=0;
while(NearFinished<NearUpdate.DesiredTiles)
{
std::vector<FStandaloneCpuTile> Ready=NearStreamer.TakeReady();
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
for(FStandaloneCpuTile& Tile:Ready)
{
++NearFinished;
if(bHashDensity)
{
if(Tile.bHasDensityHash)DensityRows.push_back({Tile.Key,Tile.DensityHash});
if(Tile.bHasMeshHash)MeshRows.push_back({Tile.Key,Tile.MeshHash});
if(Tile.bHasPayloadHash)PayloadRows.push_back({Tile.Key,Tile.PayloadHash});
if(Tile.bHasTopologyHash)TopologyRows.push_back({Tile.Key,Tile.TopologyHash});
NearHashByKey[Tile.Key]={Tile.DensityHash,Tile.MeshHash,Tile.PayloadHash,Tile.TopologyHash};
}
NeighborhoodReady=std::max(NeighborhoodReady,Tile.CompletedAt);
NearTiles.push_back(std::move(Tile));
}
}
NearStreamer.Stop();
const double NearStreamSeconds=std::chrono::duration<double>(NeighborhoodReady-NearStreamStart).count();
FStandaloneShapeWorldAccess::SetActive(nullptr);
// Release the small shape before the large build finishes allocating its packed field.
NearTiles.clear();NearTiles.shrink_to_fit();NearShape.reset();
FullBuildThread.join();
const double FullShapeSeconds=std::chrono::duration<double>(Clock::now()-FullBuildStart).count();
if(!bFullBuildOk)
{std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",FullBuildError.c_str());return 2;}
FStandaloneShapeWorldAccess::SetActive(&FullShape);
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> NearSet;
NearSet.reserve(Neighborhood.size()*2+1);for(const FVoxelTileKey& Key:Neighborhood)NearSet.insert(Key);
std::vector<FVoxelTileKey> Remaining;Remaining.reserve(Desired.Tiles.size()-Neighborhood.size());
int32 FullDetailExpected=0;
for(const FVoxelTileKey& Tile:Desired.Tiles)
{
if(Tile.Level<World.FullResClipLevels)++FullDetailExpected;
if(!NearSet.contains(Tile))Remaining.push_back(Tile);
}
FStandaloneStreamingGenerator FullStreamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors);
FullStreamer.SetDesiredTileFilter(Remaining);
const auto FillStart=Clock::now();
const FStandaloneStreamingUpdate FillUpdate=FullStreamer.Start(CameraPositionVoxels);
int32 Finished=0,FullDetailFinished=static_cast<int32>(Neighborhood.size());
const int32 FullDetailRemaining=std::max(0,FullDetailExpected-FullDetailFinished);
std::chrono::steady_clock::time_point FullDetailReady=NeighborhoodReady,WholeViewReady{};
bool bFullDetailReported=false;
while(Finished<FillUpdate.DesiredTiles)
{
std::vector<FStandaloneCpuTile> Ready=FullStreamer.TakeReady();
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
for(const FStandaloneCpuTile& Tile:Ready)
{
++Finished;WholeViewReady=std::max(WholeViewReady,Tile.CompletedAt);
if(Tile.Key.Level<World.FullResClipLevels)
{++FullDetailFinished;FullDetailReady=std::max(FullDetailReady,Tile.CompletedAt);}
if(bHashDensity)
{
if(Tile.bHasDensityHash)DensityRows.push_back({Tile.Key,Tile.DensityHash});
if(Tile.bHasMeshHash)MeshRows.push_back({Tile.Key,Tile.MeshHash});
if(Tile.bHasPayloadHash)PayloadRows.push_back({Tile.Key,Tile.PayloadHash});
if(Tile.bHasTopologyHash)TopologyRows.push_back({Tile.Key,Tile.TopologyHash});
}
}
if(!bFullDetailReported&&FullDetailRemaining>0&&FullDetailFinished>=FullDetailExpected)
{
std::printf("[PipelineMilestone] full_detail_ready=%.3fs generated=%d/%d\n",
std::chrono::duration<double>(FullDetailReady-ColdStart).count(),
FullDetailFinished,FullDetailExpected);
bFullDetailReported=true;
}
}
FullStreamer.Stop();
const double WholeViewSeconds=std::chrono::duration<double>(WholeViewReady-ColdStart).count();
const double NeighborhoodSeconds=std::chrono::duration<double>(NeighborhoodReady-ColdStart).count();
const double FullDetailSeconds=std::chrono::duration<double>(FullDetailReady-ColdStart).count();
bool bNearParityPassing=true;
if(bHashDensity)
{
FStandaloneStreamingGenerator NearVerifier(World,NearWorkers,true,nullptr,MaxLevel,
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors);
NearVerifier.SetDesiredTileFilter(Neighborhood);
const FStandaloneStreamingUpdate VerifyUpdate=NearVerifier.Start(CameraPositionVoxels);
int32 VerifiedTiles=0,NearHashMismatches=0;
while(VerifiedTiles<VerifyUpdate.DesiredTiles)
{
std::vector<FStandaloneCpuTile> Ready=NearVerifier.TakeReady();
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
for(const FStandaloneCpuTile& Tile:Ready)
{
++VerifiedTiles;
const auto It=NearHashByKey.find(Tile.Key);
if(It==NearHashByKey.end()||!Tile.bHasDensityHash||!Tile.bHasMeshHash
||!Tile.bHasPayloadHash||!Tile.bHasTopologyHash
||It->second.Density!=Tile.DensityHash||It->second.Mesh!=Tile.MeshHash
||It->second.Payload!=Tile.PayloadHash||It->second.Topology!=Tile.TopologyHash)
{
++NearHashMismatches;
if(NearHashMismatches<=4)
std::fprintf(stderr,"[PipelineNeighborhoodHashMismatch] key=(%d,%d,%d,lod%d)\n",
Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,Tile.Key.Level);
}
}
}
NearVerifier.Stop();
bNearParityPassing=VerifiedTiles==static_cast<int32>(Neighborhood.size())
&&NearHashMismatches==0;
std::printf("[PipelineNeighborhoodParity] tiles=%d/%zu hash_fields=%d mismatched_tiles=%d status=%s\n",
VerifiedTiles,Neighborhood.size(),VerifiedTiles*4,NearHashMismatches,
bNearParityPassing?"PASS":"FAIL");
}
std::printf("[StreamPipeline] workers=%d near_mesh_workers=%d full_shape_workers=%d near_shape=%.3fs near_mesh=%.3fs full_shape=%.3fs neighborhood_ready=%.3fs full_detail_ready=%.3fs whole_view_ready=%.3fs neighborhood_tiles=%zu/%zu full_detail_tiles=%d/%d whole_tiles=%d/%zu cache_hits=0 full_payload=1\n",
WorkerCount,NearWorkers,FullBuildWorkers,NearShapeSeconds,NearStreamSeconds,FullShapeSeconds,
NeighborhoodSeconds,FullDetailSeconds,WholeViewSeconds,Neighborhood.size(),Neighborhood.size(),
FullDetailFinished,FullDetailExpected,static_cast<int32>(Desired.Tiles.size()),Desired.Tiles.size());
if(bHashDensity)
{
std::vector<FHashedTile> FullDensity,FullMesh,FullPayload,FullTopology;
for(const FHashedTile& Row:DensityRows)if(Row.Key.Level<World.FullResClipLevels)FullDensity.push_back(Row);
for(const FHashedTile& Row:MeshRows)if(Row.Key.Level<World.FullResClipLevels)FullMesh.push_back(Row);
for(const FHashedTile& Row:PayloadRows)if(Row.Key.Level<World.FullResClipLevels)FullPayload.push_back(Row);
for(const FHashedTile& Row:TopologyRows)if(Row.Key.Level<World.FullResClipLevels)FullTopology.push_back(Row);
std::printf("[PipelineFullDetailHash] tiles=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
FullDetailExpected,static_cast<unsigned long long>(HashTileRows(FullDensity)),
static_cast<unsigned long long>(HashTileRows(FullMesh)),
static_cast<unsigned long long>(HashTileRows(FullPayload)),
static_cast<unsigned long long>(HashTileRows(FullTopology)));
std::printf("[PipelineWholeViewHash] tiles=%zu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
Desired.Tiles.size(),static_cast<unsigned long long>(HashTileRows(DensityRows)),
static_cast<unsigned long long>(HashTileRows(MeshRows)),
static_cast<unsigned long long>(HashTileRows(PayloadRows)),
static_cast<unsigned long long>(HashTileRows(TopologyRows)));
}
FStandaloneShapeWorldAccess::SetActive(nullptr);
return FullDetailFinished==FullDetailExpected&&Finished==FillUpdate.DesiredTiles
&&bNearParityPassing?0:6;
}
int RunStreamingBenchmark(FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels,
int32 WorkerCount,int32 MaxLevel,bool bRoundTunnels,
bool bEnhancedWalls,const FStandaloneMeshCache* Cache,
bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,
bool bSkipUV,bool bSkipColors,
bool bShardAudit,float CameraYaw,float CameraPitch,
bool bShardAudit,bool bFarSurvivalAudit,bool bFarAuditBadShell,
float CameraYaw,float CameraPitch,
const std::string& BakeCachePath,
const FStandaloneMeshCacheMetadata& CacheMetadata)
{
@@ -1731,6 +2099,14 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
const int32 FullDetailExpected=static_cast<int32>(std::count_if(
Desired.Tiles.begin(),Desired.Tiles.end(),[&](const FVoxelTileKey& Tile)
{return Tile.Level<World.FullResClipLevels;}));
std::unique_ptr<FFarSurvivalAudit> FarAudit;
if(bFarSurvivalAudit)
{
const FStandaloneShapeWorld* Shape=FStandaloneShapeWorldAccess::GetActive();
if(!Shape||!Shape->HasFarWorld())
{std::fprintf(stderr,"Far survival audit requires an active shape world with far-parent data.\n");return 15;}
FarAudit=std::make_unique<FFarSurvivalAudit>(World,*Shape,Desired.Tiles,bFarAuditBadShell);
}
std::array<std::vector<FHashedTile>,MaxLevels+1> HashRows;
std::array<std::vector<FHashedTile>,MaxLevels+1> MeshHashRows;
std::array<std::vector<FHashedTile>,MaxLevels+1> PayloadHashRows;
@@ -1787,6 +2163,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
}
for(FStandaloneCpuTile& Tile:Ready)
{
if(FarAudit)FarAudit->AuditTile(Tile);
++Finished;
const int32 Level=Tile.Key.Level;
if(PlayerNeighborhoodTiles.contains(Tile.Key))
@@ -1960,6 +2337,12 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
}
if(Finished!=DesiredTotal||FullDetailFinished!=FullDetailExpected)return 6;
if(bShardAudit&&(MeshSheetStats.ThinSheets!=0||MeshSheetStats.SmallComponentsInView!=0))return 13;
if(FarAudit)
{
FarAudit->Finalize();
FarAudit->Print();
if(!FarAudit->Passing())return 14;
}
return 0;
}
@@ -2038,7 +2421,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
int main(int Argc,char** Argv)
{
FApp App;App.DataPath=DefaultDataPath().string();
const auto ColdProcessStart=std::chrono::steady_clock::now();
FApp App;App.DataPath=DefaultDataPath().string();App.ColdProcessStart=ColdProcessStart;
std::string JoinedCommandLine;
for(int32 I=1;I<Argc;++I)
{if(!JoinedCommandLine.empty())JoinedCommandLine.push_back(' ');JoinedCommandLine+=Argv[I];}
@@ -2047,8 +2431,11 @@ int main(int Argc,char** Argv)
int32 MaxClipLevelOverride=-1;
int32 ShapeCompareLevel=-1,ShapeCompareTileLimit=0;
bool bStreamBenchmark=false;
bool bStreamPipeline=false;
bool bProfileDetailBand=false;
bool bShardAudit=false;
bool bFarSurvivalAudit=false;
bool bFarAuditBadShell=false;
bool bStreamHash=false;
bool bSkipUvAndColors=false;
bool bSkipUV=false;
@@ -2085,6 +2472,8 @@ int main(int Argc,char** Argv)
else if(Arg=="--round-tunnels-legacy")
{App.bRoundTunnels=true;App.bRoundTunnelEnhanced=false;}
else if(Arg=="--stream-benchmark")bStreamBenchmark=true;
else if(Arg=="--stream-pipeline")
{bStreamPipeline=true;bStreamBenchmark=true;App.bShapeWorld=true;App.bForceGenerate=true;}
else if(Arg=="--edit-benchmark")
{bEditBenchmark=true;bStreamBenchmark=true;App.bShapeWorld=true;}
else if(Arg=="--stream-hash")bStreamHash=true;
@@ -2093,6 +2482,9 @@ int main(int Argc,char** Argv)
else if(Arg=="--skip-colors")bSkipColors=true;
else if(Arg=="--skip-material-groups")bSkipMaterialGroups=true;
else if(Arg=="--shard-audit")bShardAudit=true;
else if(Arg=="--far-survival-audit")bFarSurvivalAudit=true,App.bShapeWorld=true;
else if(Arg=="--far-audit-bad-shell")
{bFarAuditBadShell=true;bFarSurvivalAudit=true;App.bShapeWorld=true;}
else if(Arg=="--compare-round-walls")
{bRoundWallCompare=true;App.bRoundTunnels=true;}
else if(Arg.starts_with("--round-compare-tiles="))
@@ -2166,6 +2558,7 @@ int main(int Argc,char** Argv)
:(App.bShapeCompare?ShapeCompareLevel>=App.World->FullResClipLevels
:(BenchmarkLevel>=App.World->FullResClipLevels));
if(App.bShapeWorld&&(App.bShapeAuditOnly||App.bShapeCompare||bStreamBenchmark||BenchmarkLevel>=0)
&&!bStreamPipeline
&&!App.BuildShapeWorldForCamera(Error,bNeedFarShapeWorld))
{std::fprintf(stderr,"Shape interval build failed: %s\n",Error.c_str());return 2;}
if(App.bShapeAuditOnly)
@@ -2181,6 +2574,12 @@ int main(int Argc,char** Argv)
return RunRoundWallComparison(*App.World,App.Position,RoundCompareTileCount);
if(bStreamBenchmark)
{
if(bStreamPipeline)
return RunPipelinedStreamingBenchmark(*App.World,
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),
App.WorkerCount,StreamBenchmarkMaxLevel,App.ShapeDetailVariant,
bStreamHash||!BakeMeshCachePath.empty(),bSkipUvAndColors,bSkipMaterialGroups,
bSkipUV,bSkipColors,ColdProcessStart);
if(bEditBenchmark)
return RunEditTransactionBenchmark(*App.World,*App.ShapeWorld,
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),App.WorkerCount);
@@ -2194,7 +2593,8 @@ int main(int Argc,char** Argv)
App.WorkerCount,StreamBenchmarkMaxLevel,App.bRoundTunnels,
App.bRoundTunnelEnhanced,(!App.bShapeWorld&&App.MeshCache.IsLoaded())?&App.MeshCache:nullptr,
bStreamHash,bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,bShardAudit,
App.Yaw,App.Pitch,BakeMeshCachePath,App.MakeMeshCacheMetadata());
bFarSurvivalAudit,bFarAuditBadShell,App.Yaw,App.Pitch,
BakeMeshCachePath,App.MakeMeshCacheMetadata());
}
if(BenchmarkLevel>=0)
return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount,
+283
View File
@@ -1,6 +1,7 @@
#pragma once
#include "WorldMesher.h"
#include "StandaloneShapeWorld.h"
#include <condition_variable>
#include <algorithm>
@@ -152,6 +153,277 @@ inline uint64 HashStandaloneCpuTileMesh(const FStandaloneCpuTile& Tile)
return Hash;
}
inline double FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,const FVector& Center,
const FVector& Direction,double PreferredRadius)
{
const double SearchLength=PreferredRadius*2.0+32.0;
const double AcceptBand=std::max(16.0,PreferredRadius*0.75);
constexpr double Stride=8.0;
double PreviousDistance=0.0;
bool PreviousSolid=Shape.DensityAt(static_cast<float>(Center.X),static_cast<float>(Center.Y),
static_cast<float>(Center.Z))<0.0f;
double BestRadius=PreferredRadius,BestDifference=std::numeric_limits<double>::infinity();
for(double Distance=Stride;Distance<=SearchLength+1.0e-6;Distance=std::min(Distance+Stride,SearchLength))
{
const FVector P=Center+Direction*Distance;
const bool Solid=Shape.DensityAt(static_cast<float>(P.X),static_cast<float>(P.Y),
static_cast<float>(P.Z))<0.0f;
if(Solid!=PreviousSolid)
{
double Lo=PreviousDistance,Hi=Distance;
for(int32 I=0;I<6;++I)
{
const double Mid=(Lo+Hi)*0.5;
const FVector M=Center+Direction*Mid;
const bool MidSolid=Shape.DensityAt(static_cast<float>(M.X),static_cast<float>(M.Y),
static_cast<float>(M.Z))<0.0f;
if(MidSolid==PreviousSolid)Lo=Mid;else Hi=Mid;
}
const double Crossing=(Lo+Hi)*0.5;
const double Difference=std::abs(Crossing-PreferredRadius);
if(Difference<=AcceptBand&&Difference<BestDifference)
{BestRadius=Crossing;BestDifference=Difference;}
}
if(Distance>=SearchLength-1.0e-6)break;
PreviousDistance=Distance;PreviousSolid=Solid;
}
return BestRadius;
}
inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,
int32 Step,FVoxelMeshData& Mesh)
{
if(Key.Level<6||Step<=0)return;
constexpr int32 SideCount=8;
const FIntVector TileOrigin=Key.OriginVoxels();
const int32 TileExtent=Key.ExtentVoxels();
std::vector<FVector> NewVertices;
std::vector<FVector> NewNormals;
std::vector<FVector2f> NewUVs;
std::vector<FColor> NewColors;
std::vector<int32> NewTriangles;
const int32 OldVertexCount=Mesh.Vertices.Num();
const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount;
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
for(const auto& Connector:Shape.GetFarConnectorSegments())
{
const FVector Delta=Connector.End-Connector.Start;
const double Length=std::sqrt(Delta.X*Delta.X+Delta.Y*Delta.Y+Delta.Z*Delta.Z);
if(Length<=1.0e-6)continue;
const double Starts[3]={Connector.Start.X,Connector.Start.Y,Connector.Start.Z};
const double Deltas[3]={Delta.X,Delta.Y,Delta.Z};
const double Low[3]={TileOrigin.X-Connector.Radius,TileOrigin.Y-Connector.Radius,
TileOrigin.Z-Connector.Radius};
const double High[3]={TileOrigin.X+TileExtent+Connector.Radius,
TileOrigin.Y+TileExtent+Connector.Radius,TileOrigin.Z+TileExtent+Connector.Radius};
double TMin=0.0,TMax=1.0;bool bIntersects=true;
for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex)
{
if(std::abs(Deltas[AxisIndex])<1.0e-9)
{if(Starts[AxisIndex]<Low[AxisIndex]||Starts[AxisIndex]>High[AxisIndex])bIntersects=false;continue;}
double A=(Low[AxisIndex]-Starts[AxisIndex])/Deltas[AxisIndex];
double B=(High[AxisIndex]-Starts[AxisIndex])/Deltas[AxisIndex];
if(A>B)std::swap(A,B);
TMin=std::max(TMin,A);TMax=std::min(TMax,B);
if(TMin>TMax){bIntersects=false;break;}
}
if(!bIntersects||TMax-TMin<1.0e-6)continue;
const FVector Axis=Delta/Length;
FVector U=FVector::CrossProduct(Axis,FVector::UpVector);
if(U.SizeSquared()<0.01)U=FVector::CrossProduct(Axis,FVector::RightVector);
U.Normalize();
FVector V=FVector::CrossProduct(Axis,U);V.Normalize();
const int32 AxialSampleCount=std::max(1,static_cast<int32>(std::ceil(Length/128.0)));
std::vector<double> RingParameters{TMin};
for(int32 Sample=0;Sample<AxialSampleCount;++Sample)
{
const double T=(static_cast<double>(Sample)+0.5)/AxialSampleCount;
if(T>TMin+1.0e-6&&T<TMax-1.0e-6)RingParameters.push_back(T);
}
RingParameters.push_back(TMax);
const int32 RingCount=static_cast<int32>(RingParameters.size())-1;
const int32 VertexBase=static_cast<int32>(NewVertices.size());
for(int32 Ring=0;Ring<=RingCount;++Ring)
{
const double T=RingParameters[static_cast<size_t>(Ring)];
const FVector Center=Connector.Start+Delta*T;
for(int32 Side=0;Side<SideCount;++Side)
{
const double Angle=2.0*PI*Side/SideCount;
FVector Radial=U*std::cos(Angle)+V*std::sin(Angle);Radial.Normalize();
const double SourceRadius=FindFarConnectorSurfaceRadius(Shape,Center,Radial,
Connector.Radius);
const FVector P=Center+Radial*SourceRadius;
NewVertices.emplace_back(P.X*25.0,P.Y*25.0,P.Z*25.0);
NewNormals.emplace_back(-Radial.X,-Radial.Y,-Radial.Z);
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(T*Length/256.0),
static_cast<float>(Side)/SideCount);
if(bHasColors)NewColors.push_back(Color);
}
}
for(int32 Ring=0;Ring<RingCount;++Ring)
for(int32 Side=0;Side<SideCount;++Side)
{
const int32 Next=(Side+1)%SideCount;
const int32 A=VertexBase+Ring*SideCount+Side;
const int32 B=VertexBase+Ring*SideCount+Next;
const int32 C=VertexBase+(Ring+1)*SideCount+Side;
const int32 D=VertexBase+(Ring+1)*SideCount+Next;
// The far links are carved passages, so their visible normals face inward.
NewTriangles.insert(NewTriangles.end(),{A,C,B,B,C,D});
}
}
if(NewVertices.empty()||NewTriangles.empty())return;
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
auto InsertVertices=[&](auto& Existing,const auto& Added)
{
using TArrayType=std::remove_reference_t<decltype(Existing)>;
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
for(const auto& Value:Added)Combined.Add(Value);
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
Existing=std::move(Combined);
};
InsertVertices(Mesh.Vertices,NewVertices);
InsertVertices(Mesh.Normals,NewNormals);
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
Mesh.Triangles=std::move(CombinedTriangles);
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
}
inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FStandaloneShapeWorld& Shape,
const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
{
if(Key.Level<7||Step<4||!World.Manager)return;
FStandaloneShapeWorld::FScopedForcedFarAirSamples SourceFieldScope(nullptr);
struct FBoundary { double Z=0.0,HalfWidth=8.0; };
const auto& Layout=World.Manager->GetLayout();
std::vector<FBoundary> Boundaries;
for(const FStrateSlot& Slot:Layout)
{
const FStrateGenerationParams Params=World.Manager->GetGenerationParams(
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
const double HalfWidth=std::max(8.0,static_cast<double>(Params.BoundarySealThickness)+8.0);
Boundaries.push_back({static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth});
Boundaries.push_back({static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth});
}
std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B){return A.Z<B.Z;});
Boundaries.erase(std::unique(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B)
{return std::abs(A.Z-B.Z)<0.01;}),Boundaries.end());
const FIntVector Origin=Key.OriginVoxels();const int32 Extent=Key.ExtentVoxels();
const int32 FineStep=std::max(1,Step/2);const int32 GridCells=Extent/FineStep;
if(GridCells<=0||GridCells>256)return;
struct FNode { double Z=0.0;FVector Normal{};int32 Index=INDEX_NONE;bool bValid=false,bBelowSolid=false; };
std::vector<FVector> NewVertices,NewNormals;
std::vector<FVector2f> NewUVs;std::vector<FColor> NewColors;
std::vector<int32> NewTriangles;
const int32 OldVertexCount=Mesh.Vertices.Num();
const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount;
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
const auto Density=[&](double X,double Y,double Z)
{return Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z));};
for(const FBoundary& Boundary:Boundaries)
{
if(Boundary.Z+Boundary.HalfWidth<Origin.Z||Boundary.Z-Boundary.HalfWidth>Origin.Z+Extent)continue;
const int32 ZMin=std::max(Origin.Z,static_cast<int32>(std::floor(Boundary.Z-Boundary.HalfWidth)));
const int32 ZMax=std::min(Origin.Z+Extent,static_cast<int32>(std::ceil(Boundary.Z+Boundary.HalfWidth)));
std::vector<FNode> Nodes(static_cast<size_t>(GridCells+1)*(GridCells+1));
for(int32 YI=0;YI<=GridCells;++YI)for(int32 XI=0;XI<=GridCells;++XI)
{
const double X=Origin.X+XI*FineStep,Y=Origin.Y+YI*FineStep;
int32 PreviousZ=ZMin;bool PreviousSolid=Density(X,Y,PreviousZ)<0.0f;
FNode& Node=Nodes[static_cast<size_t>(YI)*(GridCells+1)+XI];
for(int32 Z=ZMin+1;Z<=ZMax;++Z)
{
const bool Solid=Density(X,Y,Z)<0.0f;
if(Solid==PreviousSolid){PreviousZ=Z;PreviousSolid=Solid;continue;}
double Lo=PreviousZ,Hi=Z;
for(int32 I=0;I<8;++I)
{
const double Mid=(Lo+Hi)*0.5;
if((Density(X,Y,Mid)<0.0f)==PreviousSolid)Lo=Mid;else Hi=Mid;
}
Node.Z=(Lo+Hi)*0.5;Node.bBelowSolid=PreviousSolid;Node.bValid=true;
const float GX=Density(X+2,Y,Node.Z)-Density(X-2,Y,Node.Z);
const float GY=Density(X,Y+2,Node.Z)-Density(X,Y-2,Node.Z);
const float GZ=Density(X,Y,Node.Z+2)-Density(X,Y,Node.Z-2);
Node.Normal=FVector(GX,GY,GZ);
if(Node.Normal.SizeSquared()<1.0e-8)Node.Normal=FVector(0,0,PreviousSolid?1:-1);
Node.Normal.Normalize();break;
}
}
const auto EnsureVertex=[&](FNode& Node,int32 XI,int32 YI)
{
if(Node.Index!=INDEX_NONE)return Node.Index;
Node.Index=static_cast<int32>(NewVertices.size());
NewVertices.emplace_back((Origin.X+XI*FineStep)*25.0,(Origin.Y+YI*FineStep)*25.0,Node.Z*25.0);
NewNormals.push_back(Node.Normal);
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(XI)*FineStep/256.0f,
static_cast<float>(YI)*FineStep/256.0f);
if(bHasColors)NewColors.push_back(Color);
return Node.Index;
};
for(int32 YI=0;YI<GridCells;++YI)for(int32 XI=0;XI<GridCells;++XI)
{
FNode* Corners[4]={&Nodes[static_cast<size_t>(YI)*(GridCells+1)+XI],
&Nodes[static_cast<size_t>(YI)*(GridCells+1)+XI+1],
&Nodes[static_cast<size_t>(YI+1)*(GridCells+1)+XI+1],
&Nodes[static_cast<size_t>(YI+1)*(GridCells+1)+XI]};
int32 Valid[4],Count=0;
for(int32 I=0;I<4;++I)if(Corners[I]->bValid)Valid[Count++]=I;
if(Count<3)continue;
int32 Indices[4];
const int32 CornerXI[4]={XI,XI+1,XI+1,XI};
const int32 CornerYI[4]={YI,YI,YI+1,YI+1};
for(int32 I=0;I<Count;++I)
{
const int32 C=Valid[I];Indices[I]=EnsureVertex(*Corners[C],CornerXI[C],CornerYI[C]);
}
const bool bUp=Corners[Valid[0]]->bBelowSolid;
if(Count==4)
{
if(bUp)NewTriangles.insert(NewTriangles.end(),{Indices[0],Indices[1],Indices[3],Indices[1],Indices[2],Indices[3]});
else NewTriangles.insert(NewTriangles.end(),{Indices[0],Indices[3],Indices[1],Indices[1],Indices[3],Indices[2]});
}
else if(bUp)NewTriangles.insert(NewTriangles.end(),{Indices[0],Indices[1],Indices[2]});
else NewTriangles.insert(NewTriangles.end(),{Indices[0],Indices[2],Indices[1]});
}
}
if(NewVertices.empty()||NewTriangles.empty())return;
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
auto InsertVertices=[&](auto& Existing,const auto& Added)
{
using TArrayType=std::remove_reference_t<decltype(Existing)>;
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
for(const auto& Value:Added)Combined.Add(Value);
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
Existing=std::move(Combined);
};
InsertVertices(Mesh.Vertices,NewVertices);InsertVertices(Mesh.Normals,NewNormals);
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
Mesh.Triangles=std::move(CombinedTriangles);Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
}
struct FStandaloneMeshCacheMetadata
{
uint64 SourceDataHash=0;
@@ -556,6 +828,12 @@ private:
const int32 Extent=Job.Key.ExtentVoxels();
const int32 Cells=Level<World->FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World->CoarseTileCells);
const int32 Step=FMath::Max(1,Extent/Cells);
std::unordered_set<uint64> ForcedFarAirSamples;
const FStandaloneShapeWorld* Shape=FStandaloneShapeWorldAccess::GetActive();
if(Level>=6&&Shape&&Shape->HasFarWorld())
Shape->BuildFarConnectorAirSamples(Job.Key.OriginVoxels(),Step,Cells,ForcedFarAirSamples);
FStandaloneShapeWorld::FScopedForcedFarAirSamples ForcedSamplesScope(
ForcedFarAirSamples.empty()?nullptr:&ForcedFarAirSamples);
int64 SampleCount=0;
const bool bProfile=VoxelDensityProfile::GetMode()!=VoxelDensityProfile::EMode::Disabled;
const VoxelDensityProfile::FSnapshot Before=bProfile
@@ -564,6 +842,11 @@ private:
FVoxelMeshData Mesh=Mesher.GenerateMesh(Job.Key.OriginVoxels(),Step,Cells,nullptr,
Job.BandZMin,Job.BandZMax,&SampleCount,nullptr,nullptr,
bHashDensity?&DensityValues:nullptr);
if(Shape&&Shape->HasFarWorld())
{
AppendFarConnectorShells(*Shape,Job.Key,Step,Mesh);
AppendFarStratumShells(*World,*Shape,Job.Key,Step,Mesh);
}
if(bHashDensity)
{
Out.bHasDensityHash=true;