#pragma once #include "VoxelForgeCore/VoxelForgeCore.h" // 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 VoxelForgeCore::World& InShape, const std::vector& 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(Params.BoundarySealThickness)+8.0); AddBoundary(static_cast(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth); AddBoundary(static_cast(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth); } std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B) {return A.Z(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.HalfWidthOrigin.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 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(Counts.BoundaryColumns), static_cast(Counts.BoundaryReferenceSurfaces), static_cast(Counts.BoundaryMissing), static_cast(Counts.BoundaryExtra), static_cast(Counts.BoundaryHoles), static_cast(Counts.ConnectorsInView), static_cast(Counts.ConnectorSamples), static_cast(Counts.ConnectorVisibleSamples), static_cast(Counts.ConnectorNoSurfaceSamples), static_cast(Counts.ConnectorSampleMissing), static_cast(Counts.ConnectorGoodSamples), static_cast(Counts.ConnectorRays), static_cast(Counts.ConnectorReferenceSurfaces), static_cast(Counts.ConnectorRayMissing), static_cast(Counts.ConnectorMissing), static_cast(Counts.ConnectorExtra), static_cast(Counts.ConnectorHoles), static_cast(Counts.ConnectorUnsampled), static_cast(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(InCells)*InCells) { const size_t TriangleCount=Tile.Indices.size()/3; for(size_t Triangle=0;Triangle=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(std::floor((MinX-Origin.X)/Step)),0,Cells-1); const int32 X1=std::clamp(static_cast(std::floor((MaxX-Origin.X)/Step)),0,Cells-1); const int32 Y0=std::clamp(static_cast(std::floor((MinY-Origin.Y)/Step)),0,Cells-1); const int32 Y1=std::clamp(static_cast(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(Y)*Cells+X].push_back(static_cast(Triangle)); } } std::vector VerticalHits(double X,double Y,double ZMin,double ZMax) const { const int32 CX=std::clamp(static_cast(std::floor((X-Origin.X)/Step)),0,Cells-1); const int32 CY=std::clamp(static_cast(std::floor((Y-Origin.Y)/Step)),0,Cells-1); const auto& Candidates=ByColumn[static_cast(CY)*Cells+CX]; std::vector Hits;Hits.reserve(8); for(const uint32 Triangle:Candidates) { const uint32 IA=Tile.Indices[static_cast(Triangle)*3]; const uint32 IB=Tile.Indices[static_cast(Triangle)*3+1]; const uint32 IC=Tile.Indices[static_cast(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 SegmentHits(V3 Start,V3 Direction,double Length) const { std::vector Hits;Hits.reserve(8); const size_t TriangleCount=Tile.Indices.size()/3; for(size_t Triangle=0;Triangle=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(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(Tile.Vertices[Index].Position[Axis])*4.0;} static void SortUnique(std::vector& 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> 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& 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(P.X),static_cast(P.Y),static_cast(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& DesiredTiles) { const auto Connectors=Shape.farConnectorSegments(); ConnectorInView.assign(Connectors.size(),0); ConnectorHasSamples.assign(Connectors.size(),0); ConnectorHasGoodSample.assign(Connectors.size(),0); std::vector FarTiles; for(const FVoxelTileKey& Key:DesiredTiles) if(Key.Level>=AuditMinLevel)FarTiles.push_back(Key); for(size_t I=0;I(Connector.start.x),static_cast(Connector.start.y),static_cast(Connector.start.z)}; const V3 B{static_cast(Connector.end.x),static_cast(Connector.end.y),static_cast(Connector.end.z)}; 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(std::ceil(Length/128.0f))); for(int32 S=0;S(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(RayLength))))) Mask|=static_cast(1u< FindVerticalCrossings(double X,double Y,double ZMin,double ZMax, double Stride) const { std::vector Crossings; if(ZMax<=ZMin)return Crossings; double PreviousZ=ZMin; bool PreviousSolid=Shape.densityAt({static_cast(X),static_cast(Y), static_cast(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(X),static_cast(Y), static_cast(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(X),static_cast(Y), static_cast(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& Source,double Step, uint64& Missing,uint64& Extra,uint64& Holes) { std::vector Mesh=MeshIndex.VerticalHits(X,Y,ZMin,ZMax); const double Tolerance=std::max(4.0,Step*1.5); std::vector Used(Mesh.size(),0); uint64 ColumnMissing=0,ColumnExtra=0; for(const double Crossing:Source) { size_t Best=Mesh.size();double BestDistance=std::numeric_limits::infinity(); for(size_t I=0;ITolerance)++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(ColumnMissing), static_cast(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(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< 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)SourceRadiusBand)continue; ++ReferenceDirections; ReferenceMask|=static_cast(1u< Mesh=MeshIndex.SegmentHits(Sample.Center,Direction,RayLength); const double Tolerance=std::max(8.0,static_cast(Sample.Radius)*0.35); double BestDistance=std::numeric_limits::infinity(); for(const double Hit:Mesh) { const double Distance=std::abs(Hit-*SourceHit); if(Distance(1u<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 FindRadialCrossings(V3 Start,V3 Direction,double Length) const { std::vector 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(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(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.HalfWidthOrigin.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 Kept;Kept.reserve(Tile.Indices.size()); for(size_t T=0;T+2=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(Counts.CorruptedTriangles)); std::fflush(stdout); return; } } } FStandaloneOwnerWorld& World; const VoxelForgeCore::World& Shape; static constexpr int32 AuditMinLevel=6; bool bDropOneShell=false,bCorruptionApplied=false; std::vector Boundaries; std::unordered_map,FStandaloneTileKeyHash> ConnectorSamples; FCounts Counts; std::vector ConnectorInView,ConnectorHasSamples,ConnectorHasGoodSample; uint32 DebugColumns=0; uint32 DebugConnectorMismatches=0; };