- Capture exports now cover a 90-degree player frustum (radius 3, LOD 0-8, overlap patches); a 17x9 ray coverage probe aborts before rendering if tiles are missing (truncated-mesh control: 480 uncovered samples, rejected). Clear colour is sky blue, so un-meshed space can't pass as dark rock. 16 views of the owner's four-strate plan. - Explore commandlet: finite XYZ spawn override for traces (Source/, Linux-compiled). - In-engine four-strate trace NOT obtained: the engine-generated plan fails passage validation at a solid centerline voxel (~(-43,-34.5,-448.5) m) while the headless owner_world.json plan passes. Engine and headless plans diverge. - R36 (read-only): order TN -> VS -> FI for native migration; native FI is reachable from the adapter but not selected in the owner run, cause unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
5754 lines
360 KiB
C++
5754 lines
360 KiB
C++
#pragma once
|
||
|
||
#include "OwnerWorldLoader.h"
|
||
#include "StreamingWorld.h"
|
||
#include "LinuxScreenSpaceGate.h"
|
||
|
||
#include <algorithm>
|
||
#include <array>
|
||
#include <bit>
|
||
#include <chrono>
|
||
#include <cmath>
|
||
#include <cstdint>
|
||
#include <ctime>
|
||
#include <cstdio>
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
#include <future>
|
||
#include <limits>
|
||
#include <numeric>
|
||
#include <queue>
|
||
#include <string_view>
|
||
#include <thread>
|
||
#include <unordered_map>
|
||
#include <unordered_set>
|
||
#include <vector>
|
||
|
||
namespace StandaloneStreamBenchmark
|
||
{
|
||
struct FRouteDensityComparison
|
||
{
|
||
std::uint64_t samples=0;
|
||
std::uint64_t signMismatches=0;
|
||
std::uint64_t crossingEdgeMismatches=0;
|
||
std::uint64_t sharedCrossingEdges=0;
|
||
std::uint64_t exactQuerySamples=0;
|
||
std::uint64_t exactQueryMismatches=0;
|
||
double maxAbsoluteDelta=0.0;
|
||
double sumAbsoluteDelta=0.0;
|
||
double maxSurfaceDisplacementVoxels=0.0;
|
||
double sumSurfaceDisplacementVoxels=0.0;
|
||
bool valid=false;
|
||
};
|
||
|
||
inline FRouteDensityComparison CompareRouteDensities(
|
||
const VoxelForgeCore::TileMesh& A,const VoxelForgeCore::TileMesh& B,
|
||
const VoxelForgeCore::World& World)
|
||
{
|
||
FRouteDensityComparison Result;
|
||
if(A.debugDensitySamples.empty()
|
||
||A.debugDensitySamples.size()!=B.debugDensitySamples.size())return Result;
|
||
const double Root=std::cbrt(static_cast<double>(A.debugDensitySamples.size()));
|
||
const std::int32_t Dim=static_cast<std::int32_t>(std::llround(Root));
|
||
if(Dim<4||static_cast<std::size_t>(Dim)*Dim*Dim!=A.debugDensitySamples.size())return Result;
|
||
const std::int32_t GridDim=Dim-3;
|
||
const std::int32_t Extent=CHUNK_SIZE<<A.key.lod;
|
||
if(GridDim<=0||Extent%GridDim!=0)return Result;
|
||
const std::int32_t Step=Extent/GridDim;
|
||
const FVoxelTileKey NativeKey(
|
||
FIntVector(A.key.coordinate.x,A.key.coordinate.y,A.key.coordinate.z),A.key.lod);
|
||
const FIntVector Origin=NativeKey.OriginVoxels();
|
||
const auto Index=[Dim](std::int32_t X,std::int32_t Y,std::int32_t Z)
|
||
{return (static_cast<std::size_t>(Z)*Dim+Y)*Dim+X;};
|
||
const auto IsSolid=[](float Density){return Density<0.0f;};
|
||
const auto EdgeCrosses=[&](const std::vector<float>& Values,
|
||
std::int32_t X0,std::int32_t Y0,std::int32_t Z0,
|
||
std::int32_t X1,std::int32_t Y1,std::int32_t Z1)
|
||
{
|
||
return IsSolid(Values[Index(X0,Y0,Z0)])!=IsSolid(Values[Index(X1,Y1,Z1)]);
|
||
};
|
||
for(std::int32_t Z=0;Z<Dim;++Z)
|
||
for(std::int32_t Y=0;Y<Dim;++Y)
|
||
for(std::int32_t X=0;X<Dim;++X)
|
||
{
|
||
const std::size_t I=Index(X,Y,Z);
|
||
const float DA=A.debugDensitySamples[I],DB=B.debugDensitySamples[I];
|
||
if(!std::isfinite(DA)||!std::isfinite(DB))return Result;
|
||
const double Delta=std::abs(static_cast<double>(DA)-DB);
|
||
Result.maxAbsoluteDelta=std::max(Result.maxAbsoluteDelta,Delta);
|
||
Result.sumAbsoluteDelta+=Delta;
|
||
Result.signMismatches+=IsSolid(DA)!=IsSolid(DB)?1u:0u;
|
||
++Result.samples;
|
||
|
||
const std::int32_t WorldX=Origin.X+(X-1)*Step;
|
||
const std::int32_t WorldY=Origin.Y+(Y-1)*Step;
|
||
const std::int32_t WorldZ=Origin.Z+(Z-1)*Step;
|
||
const VoxelForgeCore::Vec3i Lattice{WorldX,WorldY,WorldZ};
|
||
const float ExactLattice=World.densityAtLattice(Lattice);
|
||
const float ExactPoint=World.densityAt({static_cast<double>(WorldX),
|
||
static_cast<double>(WorldY),static_cast<double>(WorldZ)});
|
||
++Result.exactQuerySamples;
|
||
if(std::bit_cast<std::uint32_t>(DA)!=std::bit_cast<std::uint32_t>(ExactLattice)
|
||
||std::bit_cast<std::uint32_t>(DA)!=std::bit_cast<std::uint32_t>(ExactPoint))
|
||
++Result.exactQueryMismatches;
|
||
}
|
||
|
||
const auto CompareEdge=[&](std::int32_t X0,std::int32_t Y0,std::int32_t Z0,
|
||
std::int32_t X1,std::int32_t Y1,std::int32_t Z1)
|
||
{
|
||
const bool CrossA=EdgeCrosses(A.debugDensitySamples,X0,Y0,Z0,X1,Y1,Z1);
|
||
const bool CrossB=EdgeCrosses(B.debugDensitySamples,X0,Y0,Z0,X1,Y1,Z1);
|
||
if(CrossA!=CrossB)++Result.crossingEdgeMismatches;
|
||
if(!CrossA||!CrossB)return;
|
||
const float A0=A.debugDensitySamples[Index(X0,Y0,Z0)];
|
||
const float A1=A.debugDensitySamples[Index(X1,Y1,Z1)];
|
||
const float B0=B.debugDensitySamples[Index(X0,Y0,Z0)];
|
||
const float B1=B.debugDensitySamples[Index(X1,Y1,Z1)];
|
||
const double TA=static_cast<double>(A0)/(static_cast<double>(A0)-A1);
|
||
const double TB=static_cast<double>(B0)/(static_cast<double>(B0)-B1);
|
||
const double Displacement=std::abs(TA-TB)*Step;
|
||
Result.maxSurfaceDisplacementVoxels=std::max(
|
||
Result.maxSurfaceDisplacementVoxels,Displacement);
|
||
Result.sumSurfaceDisplacementVoxels+=Displacement;
|
||
++Result.sharedCrossingEdges;
|
||
};
|
||
for(std::int32_t Z=0;Z<Dim;++Z)
|
||
for(std::int32_t Y=0;Y<Dim;++Y)
|
||
for(std::int32_t X=0;X<Dim;++X)
|
||
{
|
||
if(X+1<Dim)CompareEdge(X,Y,Z,X+1,Y,Z);
|
||
if(Y+1<Dim)CompareEdge(X,Y,Z,X,Y+1,Z);
|
||
if(Z+1<Dim)CompareEdge(X,Y,Z,X,Y,Z+1);
|
||
}
|
||
Result.valid=true;
|
||
return Result;
|
||
}
|
||
|
||
struct FLandmarkReachabilityResult
|
||
{
|
||
std::uint64_t pathSamples=0,capsuleSamples=0,blockedCapsuleSamples=0;
|
||
std::uint64_t supportSamples=0,unsupportedSamples=0,gradeFailures=0;
|
||
double maximumGradeDegrees=0.0,maximumSupportDepthMetres=0.0;
|
||
double minimumClearanceMetres=std::numeric_limits<double>::infinity();
|
||
double minimumHeadroomMetres=std::numeric_limits<double>::infinity();
|
||
double latestPointMinimumClearanceMetres=std::numeric_limits<double>::infinity();
|
||
std::array<double,3> latestPointMinimumClearancePosition{};
|
||
bool passed=false;
|
||
};
|
||
|
||
inline FLandmarkReachabilityResult AuditLandmarkWalkPath(
|
||
const VoxelForgeCore::World& shape,
|
||
const VoxelForgeCore::WorldAssemblyManifest* assembly,
|
||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
||
std::uint32_t coreVoxelSizeMicrometres,std::string_view label)
|
||
{
|
||
FLandmarkReachabilityResult result;
|
||
const auto surface=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||
[](const auto& value){return value.generatorKind=="SurfaceWorld";});
|
||
if(surface==layout.strates.end())return result;
|
||
const auto passage=std::find_if(layout.passages.begin(),layout.passages.end(),
|
||
[&](const auto& value){return value.lowerStrateIndex==surface->strateIndex;});
|
||
if(passage==layout.passages.end()||assembly==nullptr)return result;
|
||
const auto surfacePlan=std::find_if(assembly->plan.strates.begin(),assembly->plan.strates.end(),
|
||
[&](const auto& value){return value.strateIndex==surface->strateIndex;});
|
||
if(surfacePlan==assembly->plan.strates.end())return result;
|
||
const VoxelForgeCore::Heightfield2D surfaceField(assembly->recipe,*surfacePlan);
|
||
if(!surfaceField.isValid())return result;
|
||
const auto tunnel=std::find_if(layout.cavernEdges.begin(),layout.cavernEdges.end(),
|
||
[](const auto& edge)
|
||
{return edge.stableId=="landmark/sw-arrival-chamber-tunnel-v1";});
|
||
std::vector<const VoxelForgeCore::ResolvedLayoutStampV1*> legacyRamp;
|
||
if(tunnel==layout.cavernEdges.end())
|
||
{
|
||
for(const auto& stamp:layout.stamps)
|
||
if(stamp.stableId.starts_with("landmark/sw-arrival-chamber-ramp-"))
|
||
legacyRamp.push_back(&stamp);
|
||
std::sort(legacyRamp.begin(),legacyRamp.end(),[](const auto* a,const auto* b)
|
||
{return a->stableId<b->stableId;});
|
||
if(legacyRamp.size()<2)return result;
|
||
}
|
||
|
||
const double voxelMetres=static_cast<double>(std::max<std::uint32_t>(1,
|
||
coreVoxelSizeMicrometres))/1000000.0;
|
||
const double metresPerSubvoxel=voxelMetres
|
||
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||
struct FPathPoint { double x=0.0,y=0.0,z=0.0;bool tunnel=false; };
|
||
std::vector<FPathPoint> path;
|
||
const auto& arrival=passage->lowerMouthSubvoxels;
|
||
const double arrivalX=arrival[0]*metresPerSubvoxel;
|
||
const double arrivalY=arrival[1]*metresPerSubvoxel;
|
||
const double arrivalCenterZ=arrival[2]*metresPerSubvoxel+0.9;
|
||
|
||
const auto densityMetres=[&](double x,double y,double z)
|
||
{
|
||
return static_cast<double>(shape.densityAt({x/voxelMetres,
|
||
y/voxelMetres,z/voxelMetres}))*voxelMetres;
|
||
};
|
||
const auto samplePoint=[&](double x,double y,double centerZ,bool inTunnel,
|
||
bool measureHeadroom=false)
|
||
{
|
||
constexpr double FloorStep=0.01;
|
||
double floorZ=centerZ-5.0;
|
||
bool foundFloor=false;
|
||
if(inTunnel)
|
||
{
|
||
if(densityMetres(x,y,centerZ)>0.025)
|
||
for(double offset=-0.10;offset>=-10.0;offset-=FloorStep)
|
||
if(densityMetres(x,y,centerZ+offset)<0.0)
|
||
{floorZ=centerZ+offset+FloorStep;foundFloor=true;break;}
|
||
}
|
||
else
|
||
{
|
||
const auto xSubvoxels=static_cast<std::int64_t>(std::llround(x/metresPerSubvoxel));
|
||
const auto ySubvoxels=static_cast<std::int64_t>(std::llround(y/metresPerSubvoxel));
|
||
floorZ=surfaceField.point(xSubvoxels,ySubvoxels,0).groundHeightSubvoxels
|
||
*metresPerSubvoxel;
|
||
foundFloor=std::isfinite(floorZ);
|
||
}
|
||
++result.pathSamples;
|
||
if(!foundFloor)
|
||
{
|
||
++result.unsupportedSamples;
|
||
return std::pair<double,bool>{floorZ,false};
|
||
}
|
||
|
||
// Reserve a footfall margin above the sampled ground for the vertical
|
||
// capsule; it prevents the lower hemisphere from grazing adjacent floor
|
||
// samples on a gently sloped ramp.
|
||
floorZ+=0.15;
|
||
|
||
constexpr double CapsuleRadius=0.34;
|
||
constexpr double CapsuleHeight=1.76;
|
||
constexpr double RequiredClearance=0.025;
|
||
constexpr double VerticalStep=0.18;
|
||
constexpr double HorizontalStep=0.17;
|
||
result.latestPointMinimumClearanceMetres=std::numeric_limits<double>::infinity();
|
||
for(double height=0.08;height<=CapsuleHeight;height+=VerticalStep)
|
||
{
|
||
double capOffset=0.0;
|
||
if(height<CapsuleRadius)capOffset=CapsuleRadius-height;
|
||
else if(height>CapsuleHeight-CapsuleRadius)
|
||
capOffset=height-(CapsuleHeight-CapsuleRadius);
|
||
const double radial=std::sqrt(std::max(0.0,
|
||
CapsuleRadius*CapsuleRadius-capOffset*capOffset));
|
||
for(double oy=-radial;oy<=radial+1.0e-9;oy+=HorizontalStep)
|
||
for(double ox=-radial;ox<=radial+1.0e-9;ox+=HorizontalStep)
|
||
{
|
||
if(ox*ox+oy*oy>radial*radial+1.0e-9)continue;
|
||
const double clearance=densityMetres(x+ox,y+oy,floorZ+height);
|
||
++result.capsuleSamples;
|
||
result.minimumClearanceMetres=std::min(
|
||
result.minimumClearanceMetres,clearance);
|
||
if(clearance<result.latestPointMinimumClearanceMetres)
|
||
{
|
||
result.latestPointMinimumClearanceMetres=clearance;
|
||
result.latestPointMinimumClearancePosition={x+ox,y+oy,floorZ+height};
|
||
}
|
||
if(clearance<RequiredClearance)++result.blockedCapsuleSamples;
|
||
}
|
||
}
|
||
|
||
if(measureHeadroom)
|
||
{
|
||
constexpr std::array<std::array<double,2>,5> headOffsets={{{0.0,0.0},
|
||
{0.24,0.0},{-0.24,0.0},{0.0,0.24},{0.0,-0.24}}};
|
||
constexpr double HeadStep=0.05,HeadLimit=4.0;
|
||
for(const auto& offset:headOffsets)
|
||
{
|
||
double headroom=HeadLimit;
|
||
for(double rise=0.0;rise<=HeadLimit;rise+=HeadStep)
|
||
if(densityMetres(x+offset[0],y+offset[1],
|
||
floorZ+CapsuleHeight+rise)<0.0)
|
||
{headroom=std::max(0.0,rise-HeadStep);break;}
|
||
result.minimumHeadroomMetres=std::min(
|
||
result.minimumHeadroomMetres,headroom);
|
||
}
|
||
}
|
||
|
||
const std::array<std::array<double,2>,5> supportOffsets={{{0.0,0.0},
|
||
{0.28,0.0},{-0.28,0.0},{0.0,0.28},{0.0,-0.28}}};
|
||
std::uint32_t supported=0;
|
||
for(const auto& offset:supportOffsets)
|
||
{
|
||
++result.supportSamples;
|
||
bool hit=false;
|
||
for(double depth=0.10;depth<=0.55;depth+=0.08)
|
||
if(densityMetres(x+offset[0],y+offset[1],floorZ-depth)<0.0)
|
||
{hit=true;result.maximumSupportDepthMetres=std::max(
|
||
result.maximumSupportDepthMetres,depth);break;}
|
||
if(hit)++supported;
|
||
}
|
||
const bool supportOk=supported>=3;
|
||
if(!supportOk)++result.unsupportedSamples;
|
||
return std::pair<double,bool>{floorZ,supportOk};
|
||
};
|
||
|
||
// Find a walkable surface route around the passage's support rock. The nearby grid is
|
||
// evaluated against the same composed density field as the ramp and both L43/L45 runs.
|
||
constexpr int GridRadius=52;
|
||
constexpr int GridWidth=GridRadius*2+1;
|
||
constexpr double GridStep=0.50;
|
||
constexpr double MaximumGradeRadians=15.0*3.14159265358979323846/180.0;
|
||
struct FSurfaceNode
|
||
{
|
||
bool evaluated=false,walkable=false,closed=false;
|
||
double floorZ=0.0,cost=std::numeric_limits<double>::infinity();
|
||
int parent=-1;
|
||
};
|
||
const double edgeStartX=tunnel!=layout.cavernEdges.end()
|
||
?tunnel->controlPointsMicrometres[0][0]/1000000.0
|
||
:legacyRamp.front()->positionMicrometres[0]/1000000.0;
|
||
const double edgeStartY=tunnel!=layout.cavernEdges.end()
|
||
?tunnel->controlPointsMicrometres[0][1]/1000000.0
|
||
:legacyRamp.front()->positionMicrometres[1]/1000000.0;
|
||
const int goalX=GridRadius+static_cast<int>(std::llround(
|
||
(edgeStartX-arrivalX)/GridStep));
|
||
const int goalY=GridRadius+static_cast<int>(std::llround(
|
||
(edgeStartY-arrivalY)/GridStep));
|
||
const auto nodeIndex=[](int x,int y){return y*GridWidth+x;};
|
||
std::vector<FSurfaceNode> surfaceNodes(static_cast<std::size_t>(GridWidth*GridWidth));
|
||
std::uint64_t exploredNodes=0;
|
||
const auto evaluateNode=[&](int ix,int iy)->FSurfaceNode*
|
||
{
|
||
if(ix<0||iy<0||ix>=GridWidth||iy>=GridWidth)return nullptr;
|
||
auto& node=surfaceNodes[static_cast<std::size_t>(nodeIndex(ix,iy))];
|
||
if(node.evaluated)return &node;
|
||
node.evaluated=true;++exploredNodes;
|
||
const double x=arrivalX+(ix-GridRadius)*GridStep;
|
||
const double y=arrivalY+(iy-GridRadius)*GridStep;
|
||
const auto blockedBefore=result.blockedCapsuleSamples;
|
||
const auto unsupportedBefore=result.unsupportedSamples;
|
||
const auto pathBefore=result.pathSamples;
|
||
const double scanCenter=arrivalCenterZ;
|
||
const auto [floorZ,supported]=samplePoint(x,y,scanCenter,false);
|
||
node.floorZ=floorZ;
|
||
node.walkable=supported&&result.pathSamples>pathBefore
|
||
&&result.blockedCapsuleSamples==blockedBefore
|
||
&&result.unsupportedSamples==unsupportedBefore;
|
||
return &node;
|
||
};
|
||
struct FOpenNode { double score=0.0;int index=0; };
|
||
const auto farther=[](const FOpenNode& a,const FOpenNode& b)
|
||
{return a.score>b.score;};
|
||
std::priority_queue<FOpenNode,std::vector<FOpenNode>,decltype(farther)> open(farther);
|
||
const int goalIndex=goalX>=0&&goalY>=0&&goalX<GridWidth&&goalY<GridWidth
|
||
?nodeIndex(goalX,goalY):-1;
|
||
if(goalIndex>=0)
|
||
{
|
||
constexpr double GroundStartRadius=18.0;
|
||
const int sourceRadius=static_cast<int>(std::ceil(GroundStartRadius/GridStep));
|
||
for(int iy=GridRadius-sourceRadius;iy<=GridRadius+sourceRadius;++iy)
|
||
for(int ix=GridRadius-sourceRadius;ix<=GridRadius+sourceRadius;++ix)
|
||
{
|
||
const double dx=(ix-GridRadius)*GridStep,dy=(iy-GridRadius)*GridStep;
|
||
const double distance=std::hypot(dx,dy);
|
||
if(distance>GroundStartRadius)continue;
|
||
auto* start=evaluateNode(ix,iy);
|
||
if(!start||!start->walkable)continue;
|
||
start->cost=distance;start->parent=-1;
|
||
open.push({distance+GridStep*std::hypot(goalX-ix,goalY-iy),
|
||
nodeIndex(ix,iy)});
|
||
}
|
||
constexpr std::array<std::array<int,2>,8> neighbours={{{1,0},{-1,0},{0,1},{0,-1},
|
||
{1,1},{1,-1},{-1,1},{-1,-1}}};
|
||
while(!open.empty())
|
||
{
|
||
const int currentIndex=open.top().index;open.pop();
|
||
auto& current=surfaceNodes[static_cast<std::size_t>(currentIndex)];
|
||
if(current.closed)continue;
|
||
current.closed=true;
|
||
if(currentIndex==goalIndex)break;
|
||
const int currentX=currentIndex%GridWidth,currentY=currentIndex/GridWidth;
|
||
for(const auto& direction:neighbours)
|
||
{
|
||
const int nextX=currentX+direction[0],nextY=currentY+direction[1];
|
||
auto* next=evaluateNode(nextX,nextY);
|
||
if(!next||!next->walkable||next->closed)continue;
|
||
const double run=GridStep*std::hypot(direction[0],direction[1]);
|
||
const double rise=std::abs(next->floorZ-current.floorZ);
|
||
if(std::atan2(rise,run)>MaximumGradeRadians)continue;
|
||
if(direction[0]!=0&&direction[1]!=0)
|
||
{
|
||
const auto* sideA=evaluateNode(currentX+direction[0],currentY);
|
||
const auto* sideB=evaluateNode(currentX,currentY+direction[1]);
|
||
if(!sideA||!sideA->walkable||!sideB||!sideB->walkable)continue;
|
||
}
|
||
const double cost=current.cost+run+rise*0.25;
|
||
if(cost>=next->cost)continue;
|
||
next->cost=cost;next->parent=currentIndex;
|
||
const double heuristic=GridStep*std::hypot(goalX-nextX,goalY-nextY);
|
||
open.push({cost+heuristic,nodeIndex(nextX,nextY)});
|
||
}
|
||
}
|
||
}
|
||
std::vector<int> surfaceRoute;
|
||
if(goalIndex>=0&&surfaceNodes[static_cast<std::size_t>(goalIndex)].closed)
|
||
{
|
||
for(int current=goalIndex;current>=0;current=surfaceNodes[static_cast<std::size_t>(current)].parent)
|
||
surfaceRoute.push_back(current);
|
||
std::reverse(surfaceRoute.begin(),surfaceRoute.end());
|
||
}
|
||
const bool surfaceGoalWalkable=goalIndex>=0
|
||
&&evaluateNode(goalX,goalY)->walkable;
|
||
if(surfaceRoute.size()<2)surfaceRoute.clear();
|
||
const auto surfaceRouteSamples=surfaceRoute.size();
|
||
double surfaceStartDistance=std::numeric_limits<double>::infinity();
|
||
if(!surfaceRoute.empty())
|
||
{
|
||
path.reserve(surfaceRoute.size()+128);
|
||
const int startX=surfaceRoute.front()%GridWidth;
|
||
const int startY=surfaceRoute.front()/GridWidth;
|
||
surfaceStartDistance=GridStep*std::hypot(startX-GridRadius,startY-GridRadius);
|
||
for(const int index:surfaceRoute)
|
||
{
|
||
const int ix=index%GridWidth,iy=index/GridWidth;
|
||
const auto& node=surfaceNodes[static_cast<std::size_t>(index)];
|
||
path.push_back({arrivalX+(ix-GridRadius)*GridStep,
|
||
arrivalY+(iy-GridRadius)*GridStep,node.floorZ+0.9,false});
|
||
}
|
||
if(tunnel!=layout.cavernEdges.end())
|
||
{
|
||
const auto curveValue=[&](double t,std::size_t axis)
|
||
{
|
||
const double one=1.0-t;
|
||
return (one*one*one*tunnel->controlPointsMicrometres[0][axis]
|
||
+3.0*one*one*t*tunnel->controlPointsMicrometres[1][axis]
|
||
+3.0*one*t*t*tunnel->controlPointsMicrometres[2][axis]
|
||
+t*t*t*tunnel->controlPointsMicrometres[3][axis])/1000000.0;
|
||
};
|
||
double curveLength=0.0,previousCurveX=curveValue(0.0,0),previousCurveY=curveValue(0.0,1);
|
||
for(int sample=1;sample<=128;++sample)
|
||
{
|
||
const double t=static_cast<double>(sample)/128.0;
|
||
const double x=curveValue(t,0),y=curveValue(t,1);
|
||
curveLength+=std::hypot(x-previousCurveX,y-previousCurveY);
|
||
previousCurveX=x;previousCurveY=y;
|
||
}
|
||
const int curveSteps=std::max(2,static_cast<int>(std::ceil(curveLength/0.60)));
|
||
for(int sample=0;sample<=curveSteps;++sample)
|
||
{
|
||
const double t=static_cast<double>(sample)/curveSteps;
|
||
path.push_back({curveValue(t,0),curveValue(t,1),
|
||
curveValue(t,2)+0.9,true});
|
||
}
|
||
}
|
||
else
|
||
{
|
||
for(const auto* stamp:legacyRamp)
|
||
path.push_back({stamp->positionMicrometres[0]/1000000.0,
|
||
stamp->positionMicrometres[1]/1000000.0,
|
||
stamp->positionMicrometres[2]/1000000.0,true});
|
||
}
|
||
}
|
||
std::printf("[LandmarkSurfaceRoute] reachable=%s goal_walkable=%s grid_step_m=%.2f start_offset_m=%.2f explored=%llu surface_points=%zu grade_limit_deg=15.000\n",
|
||
surfaceRoute.empty()?"false":"true",surfaceGoalWalkable?"true":"false",GridStep,surfaceStartDistance,
|
||
static_cast<unsigned long long>(exploredNodes),surfaceRouteSamples);
|
||
result={};
|
||
if(path.size()<2)return result;
|
||
|
||
bool allSupport=true,allGrade=true;
|
||
bool havePrior=false;double priorX=0.0,priorY=0.0,priorFloor=0.0;
|
||
std::size_t diagnosticCount=0;
|
||
for(std::size_t segment=1;segment<path.size();++segment)
|
||
{
|
||
const auto& from=path[segment-1];const auto& to=path[segment];
|
||
const double horizontal=std::hypot(to.x-from.x,to.y-from.y);
|
||
const std::size_t steps=std::max<std::size_t>(1,
|
||
static_cast<std::size_t>(std::ceil(horizontal/0.8)));
|
||
for(std::size_t step=segment==1?0:1;step<=steps;++step)
|
||
{
|
||
const double t=static_cast<double>(step)/steps;
|
||
const double x=from.x+(to.x-from.x)*t;
|
||
const double y=from.y+(to.y-from.y)*t;
|
||
const double centerZ=from.z+(to.z-from.z)*t;
|
||
const auto blockedBefore=result.blockedCapsuleSamples;
|
||
const auto capsuleBefore=result.capsuleSamples;
|
||
const auto supportBefore=result.supportSamples;
|
||
const bool tunnel=from.tunnel&&to.tunnel;
|
||
const auto [floorZ,supportOk]=samplePoint(x,y,centerZ,tunnel,true);
|
||
allSupport&=supportOk;
|
||
double grade=0.0;
|
||
if(havePrior)
|
||
{
|
||
const double run=std::hypot(x-priorX,y-priorY);
|
||
grade=run>1.0e-6
|
||
?std::atan2(std::abs(floorZ-priorFloor),run)*180.0/3.14159265358979323846
|
||
:std::numeric_limits<double>::infinity();
|
||
result.maximumGradeDegrees=std::max(result.maximumGradeDegrees,grade);
|
||
if(grade>15.0) {++result.gradeFailures;allGrade=false;}
|
||
}
|
||
if(diagnosticCount<16&&(result.blockedCapsuleSamples>blockedBefore
|
||
||!supportOk||grade>15.0))
|
||
{
|
||
std::printf("[LandmarkWalkPoint] field=%.*s mode=%s x=%.3f y=%.3f center_z=%.3f floor_z=%.3f previous_x=%.3f previous_y=%.3f previous_floor_z=%.3f horizontal_step_m=%.3f blocked=%llu/%llu support_probes=%llu grade_deg=%.3f density_center_m=%.3f capsule_min_m=%.3f capsule_min_at_m=(%.3f,%.3f,%.3f)\n",
|
||
static_cast<int>(label.size()),label.data(),tunnel?"tunnel":"surface",x,y,centerZ,floorZ,
|
||
priorX,priorY,priorFloor,std::hypot(x-priorX,y-priorY),
|
||
static_cast<unsigned long long>(result.blockedCapsuleSamples-blockedBefore),
|
||
static_cast<unsigned long long>(result.capsuleSamples-capsuleBefore),
|
||
static_cast<unsigned long long>(result.supportSamples-supportBefore),
|
||
grade,densityMetres(x,y,centerZ),result.latestPointMinimumClearanceMetres,
|
||
result.latestPointMinimumClearancePosition[0],
|
||
result.latestPointMinimumClearancePosition[1],
|
||
result.latestPointMinimumClearancePosition[2]);
|
||
++diagnosticCount;
|
||
}
|
||
havePrior=true;priorX=x;priorY=y;priorFloor=floorZ;
|
||
}
|
||
}
|
||
result.passed=result.pathSamples>1&&result.capsuleSamples>0
|
||
&&result.blockedCapsuleSamples==0&&allSupport&&allGrade
|
||
&&result.unsupportedSamples==0&&result.gradeFailures==0
|
||
&&result.minimumHeadroomMetres>=0.300;
|
||
return result;
|
||
}
|
||
|
||
struct FLandmarkTunnelRippleResult
|
||
{
|
||
std::size_t floorSamples=0,entranceFloorSamples=0,wallSamples=0;
|
||
double floorPeakPeriodMetres=0.0,floorPeakAmplitudeMetres=0.0;
|
||
double entranceFloorMaxStepMetres=0.0;
|
||
double entranceFloorWorstDistanceMetres=0.0;
|
||
double entranceFloorPriorResidualMetres=0.0,entranceFloorCurrentResidualMetres=0.0;
|
||
std::size_t entranceFloorWorstTrack=0;
|
||
double wallPeakPeriodMetres=0.0,wallPeakAmplitudeMetres=0.0;
|
||
bool valid=false,passed=false;
|
||
};
|
||
|
||
inline FLandmarkTunnelRippleResult AuditLandmarkTunnelRipples(
|
||
const VoxelForgeCore::World& shape,
|
||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
||
std::uint32_t coreVoxelSizeMicrometres)
|
||
{
|
||
FLandmarkTunnelRippleResult result;
|
||
const double voxelMetres=static_cast<double>(std::max<std::uint32_t>(1,
|
||
coreVoxelSizeMicrometres))/1000000.0;
|
||
const auto densityMetres=[&](double x,double y,double z)
|
||
{return static_cast<double>(shape.densityAt({x/voxelMetres,y/voxelMetres,
|
||
z/voxelMetres}))*voxelMetres;};
|
||
struct FRoutePoint { double x=0.0,y=0.0,floor=0.0; };
|
||
std::vector<FRoutePoint> route;
|
||
const auto edge=std::find_if(layout.cavernEdges.begin(),layout.cavernEdges.end(),
|
||
[](const auto& value)
|
||
{return value.stableId=="landmark/sw-arrival-chamber-tunnel-v1";});
|
||
if(edge!=layout.cavernEdges.end())
|
||
{
|
||
constexpr int CurveSamples=512;
|
||
route.reserve(CurveSamples+1);
|
||
for(int index=0;index<=CurveSamples;++index)
|
||
{
|
||
const double t=static_cast<double>(index)/CurveSamples,one=1.0-t;
|
||
const std::array<double,4> weights={one*one*one,3.0*one*one*t,
|
||
3.0*one*t*t,t*t*t};
|
||
FRoutePoint point;
|
||
point.x=point.y=point.floor=0.0;
|
||
for(std::size_t control=0;control<4;++control)
|
||
{
|
||
point.x+=weights[control]*edge->controlPointsMicrometres[control][0]/1e6;
|
||
point.y+=weights[control]*edge->controlPointsMicrometres[control][1]/1e6;
|
||
point.floor+=weights[control]*edge->controlPointsMicrometres[control][2]/1e6;
|
||
}
|
||
route.push_back(point);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
std::vector<const VoxelForgeCore::ResolvedLayoutStampV1*> ramp;
|
||
for(const auto& stamp:layout.stamps)
|
||
if(stamp.stableId.starts_with("landmark/sw-arrival-chamber-ramp-"))
|
||
ramp.push_back(&stamp);
|
||
std::sort(ramp.begin(),ramp.end(),[](const auto* a,const auto* b)
|
||
{return a->stableId<b->stableId;});
|
||
for(const auto* stamp:ramp)
|
||
route.push_back({stamp->positionMicrometres[0]/1e6,
|
||
stamp->positionMicrometres[1]/1e6,
|
||
stamp->positionMicrometres[2]/1e6-1.6});
|
||
}
|
||
if(route.size()<2)return result;
|
||
std::vector<double> arc(route.size(),0.0);
|
||
for(std::size_t index=1;index<route.size();++index)
|
||
arc[index]=arc[index-1]+std::hypot(route[index].x-route[index-1].x,
|
||
route[index].y-route[index-1].y);
|
||
const double length=arc.back();
|
||
if(length<4.0)return result;
|
||
const auto atDistance=[&](double distance)
|
||
{
|
||
distance=std::clamp(distance,0.0,length);
|
||
const auto upper=std::lower_bound(arc.begin(),arc.end(),distance);
|
||
const std::size_t hi=std::min<std::size_t>(route.size()-1,
|
||
static_cast<std::size_t>(upper-arc.begin()));
|
||
const std::size_t lo=hi?hi-1:0;
|
||
const double span=arc[hi]-arc[lo];
|
||
const double t=span>0.0?(distance-arc[lo])/span:0.0;
|
||
return FRoutePoint{std::lerp(route[lo].x,route[hi].x,t),
|
||
std::lerp(route[lo].y,route[hi].y,t),
|
||
std::lerp(route[lo].floor,route[hi].floor,t)};
|
||
};
|
||
const auto floorRoot=[&](double x,double y,double expected)->std::optional<double>
|
||
{
|
||
constexpr double Step=0.025,Below=0.65,Above=0.35;
|
||
double priorZ=expected-Below;
|
||
double priorDensity=densityMetres(x,y,priorZ);
|
||
std::optional<double> closest;
|
||
double closestError=std::numeric_limits<double>::infinity();
|
||
for(double z=priorZ+Step;z<=expected+Above+1.0e-9;z+=Step)
|
||
{
|
||
const double density=densityMetres(x,y,z);
|
||
if(priorDensity<0.0&&density>=0.0)
|
||
{
|
||
const double span=density-priorDensity;
|
||
const double root=span>0.0?priorZ+Step*(-priorDensity)/span:z;
|
||
const double error=std::abs(root-expected);
|
||
if(error<closestError){closest=root;closestError=error;}
|
||
}
|
||
priorZ=z;priorDensity=density;
|
||
}
|
||
return closest;
|
||
};
|
||
std::vector<std::pair<double,double>> floorResiduals;
|
||
std::array<std::vector<std::pair<double,double>>,3> floorTrackResiduals;
|
||
std::array<std::optional<double>,3> previousFloorResiduals;
|
||
std::array<double,3> previousFloorDistances{};
|
||
constexpr std::array<double,3> FloorTrackOffsets={-1.5,0.0,1.5};
|
||
for(double distance=0.0;distance<=length-0.75;distance+=0.125)
|
||
{
|
||
const auto point=atDistance(distance);
|
||
const auto prior=atDistance(std::max(0.0,distance-0.1));
|
||
const auto next=atDistance(std::min(length,distance+0.1));
|
||
const double tx=next.x-prior.x,ty=next.y-prior.y;
|
||
const double tangentLength=std::hypot(tx,ty);
|
||
if(tangentLength<1.0e-6)continue;
|
||
const double normalX=-ty/tangentLength,normalY=tx/tangentLength;
|
||
double offsetSum=0.0;std::size_t offsetCount=0;
|
||
for(std::size_t track=0;track<FloorTrackOffsets.size();++track)
|
||
{
|
||
const double lateral=FloorTrackOffsets[track];
|
||
const auto root=floorRoot(point.x+normalX*lateral,
|
||
point.y+normalY*lateral,point.floor);
|
||
if(!root)continue;
|
||
const double residual=*root-point.floor;
|
||
floorTrackResiduals[track].push_back({distance,residual});
|
||
offsetSum+=residual;++offsetCount;
|
||
if(distance<=10.0+1.0e-9)
|
||
{
|
||
++result.entranceFloorSamples;
|
||
if(previousFloorResiduals[track]
|
||
&&distance-previousFloorDistances[track]<=0.2)
|
||
{
|
||
const double step=std::abs(residual-*previousFloorResiduals[track]);
|
||
if(step>result.entranceFloorMaxStepMetres)
|
||
{
|
||
result.entranceFloorMaxStepMetres=step;
|
||
result.entranceFloorWorstDistanceMetres=distance;
|
||
result.entranceFloorPriorResidualMetres=*previousFloorResiduals[track];
|
||
result.entranceFloorCurrentResidualMetres=residual;
|
||
result.entranceFloorWorstTrack=track;
|
||
}
|
||
}
|
||
}
|
||
previousFloorResiduals[track]=residual;
|
||
previousFloorDistances[track]=distance;
|
||
}
|
||
if(offsetCount)
|
||
{floorResiduals.push_back({distance,offsetSum/offsetCount});}
|
||
}
|
||
result.floorSamples=floorTrackResiduals[0].size()
|
||
+floorTrackResiduals[1].size()+floorTrackResiduals[2].size();
|
||
const auto peakPeriod=[&](const std::vector<std::pair<double,double>>& samples,
|
||
double minimumPeriod,double maximumPeriod,
|
||
double& outPeriod)
|
||
{
|
||
double amplitude=0.0;outPeriod=0.0;
|
||
if(samples.size()<16)return amplitude;
|
||
for(double period=minimumPeriod;period<=maximumPeriod+1.0e-9;period+=0.125)
|
||
{
|
||
double cosine=0.0,sine=0.0;
|
||
for(const auto& [distance,residual]:samples)
|
||
{
|
||
const double phase=2.0*3.14159265358979323846*distance/period;
|
||
cosine+=residual*std::cos(phase);sine+=residual*std::sin(phase);
|
||
}
|
||
const double candidate=2.0*std::hypot(cosine,sine)/samples.size();
|
||
if(candidate>amplitude){amplitude=candidate;outPeriod=period;}
|
||
}
|
||
return amplitude;
|
||
};
|
||
result.floorPeakAmplitudeMetres=peakPeriod(floorResiduals,0.375,
|
||
std::min(8.0,length/2.0),result.floorPeakPeriodMetres);
|
||
for(const auto& track:floorTrackResiduals)
|
||
{
|
||
double trackPeriod=0.0;
|
||
const double trackAmplitude=peakPeriod(track,0.375,
|
||
std::min(8.0,length/2.0),trackPeriod);
|
||
if(trackAmplitude>result.floorPeakAmplitudeMetres)
|
||
{
|
||
result.floorPeakAmplitudeMetres=trackAmplitude;
|
||
result.floorPeakPeriodMetres=trackPeriod;
|
||
}
|
||
}
|
||
|
||
std::vector<std::pair<double,double>> wallResiduals;
|
||
const double expectedWallRadius=edge!=layout.cavernEdges.end()
|
||
?edge->boreRadiusMicrometres/1e6*std::sqrt(1.0
|
||
-std::pow(2.0/(edge->boreHeightMicrometres/1e6),2.0))
|
||
:2.6*std::sqrt(1.0-std::pow(0.4/2.0,2.0));
|
||
// Keep the edge audit away from the ground portal and the room union;
|
||
// those are transitions between features, not tunnel-wall ripple.
|
||
const double wallAuditStart=std::max(4.0,length*0.38);
|
||
const double wallAuditEnd=std::min(length*0.55,length-4.0);
|
||
for(double distance=wallAuditStart;distance<=wallAuditEnd;distance+=0.0625)
|
||
{
|
||
const auto point=atDistance(distance);
|
||
const auto prior=atDistance(std::max(0.0,distance-0.1));
|
||
const auto next=atDistance(std::min(length,distance+0.1));
|
||
const double tx=next.x-prior.x,ty=next.y-prior.y;
|
||
const double tangentLength=std::hypot(tx,ty);
|
||
if(tangentLength<1.0e-6)continue;
|
||
const double normalX=-ty/tangentLength,normalY=tx/tangentLength;
|
||
const double height=point.floor+2.0;
|
||
for(const double sign:{-1.0,1.0})
|
||
{
|
||
double priorRadius=0.0;
|
||
double priorDensity=densityMetres(point.x,point.y,height);
|
||
bool found=false;
|
||
for(double radius=0.025;radius<=8.0;radius+=0.025)
|
||
{
|
||
const double density=densityMetres(point.x+normalX*sign*radius,
|
||
point.y+normalY*sign*radius,height);
|
||
if(priorDensity>=0.0&&density<0.0)
|
||
{
|
||
const double span=priorDensity-density;
|
||
const double root=span>0.0?priorRadius+0.025*priorDensity/span:radius;
|
||
wallResiduals.push_back({distance,root-expectedWallRadius});
|
||
found=true;break;
|
||
}
|
||
priorRadius=radius;priorDensity=density;
|
||
}
|
||
if(!found)continue;
|
||
}
|
||
}
|
||
result.wallSamples=wallResiduals.size();
|
||
// Remove the slow wall envelope from the amplitude profile before testing
|
||
// for segment-scale ribs. The moving baseline covers 2 m, while the audit
|
||
// band covers the shorter repeated seams seen in the L35/L36 controls.
|
||
std::vector<std::pair<double,double>> wallRippleResiduals;
|
||
wallRippleResiduals.reserve(wallResiduals.size());
|
||
for(const auto& sample:wallResiduals)
|
||
{
|
||
double baseline=0.0;std::size_t neighbours=0;
|
||
for(const auto& other:wallResiduals)
|
||
if(std::abs(other.first-sample.first)<=1.0)
|
||
{baseline+=other.second;++neighbours;}
|
||
if(neighbours>0&&sample.first>=wallAuditStart+1.0
|
||
&&sample.first<=wallAuditEnd-1.0)
|
||
wallRippleResiduals.push_back({sample.first,
|
||
sample.second-baseline/static_cast<double>(neighbours)});
|
||
}
|
||
result.wallPeakAmplitudeMetres=peakPeriod(wallRippleResiduals,0.25,
|
||
std::min(1.0,length/2.0),result.wallPeakPeriodMetres);
|
||
result.valid=result.floorSamples>=16&&result.entranceFloorSamples>=150
|
||
&&result.wallSamples>=16;
|
||
result.passed=result.valid&&result.floorPeakAmplitudeMetres<=0.015
|
||
&&result.entranceFloorMaxStepMetres<=0.05
|
||
&&result.wallPeakAmplitudeMetres<=0.035;
|
||
return result;
|
||
}
|
||
|
||
inline bool MakeL43BuriedLandmarkControl(
|
||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& source,
|
||
VoxelForgeCore::ResolvedLayoutSnapshotV3& control,std::string& error)
|
||
{
|
||
using namespace VoxelForgeCore;
|
||
control=source;control.stamps.clear();control.stampVocabularyVersion=1;
|
||
control.cavernGraphVocabularyVersion=0;
|
||
control.cavernRooms.clear();control.cavernEdges.clear();
|
||
const auto add=[&](std::string id,ResolvedSdfStampKindV1 kind,
|
||
std::array<std::int64_t,3> position,
|
||
std::array<std::int64_t,3> half,std::int64_t smooth,
|
||
std::int64_t rough,std::int64_t wavelength,std::uint64_t seed,
|
||
std::uint8_t yaw)
|
||
{
|
||
ResolvedLayoutStampV1 stamp;stamp.stableId=std::move(id);
|
||
stamp.strateIndex=1;stamp.kind=kind;stamp.positionMicrometres=position;
|
||
stamp.halfExtentsMicrometres=half;stamp.smoothWidthMicrometres=smooth;
|
||
stamp.irregularityAmplitudeMicrometres=rough;
|
||
stamp.irregularityWavelengthMicrometres=wavelength;
|
||
stamp.irregularitySeed=seed;stamp.yawQuarterTurns=yaw;
|
||
const std::int64_t pad=smooth+rough;
|
||
const std::array<std::int64_t,3> bounds={
|
||
half[yaw%2?1:0]+pad,half[yaw%2?0:1]+pad,half[2]+pad};
|
||
for(std::size_t axis=0;axis<3;++axis)
|
||
{stamp.boundsMinimumMicrometres[axis]=position[axis]-bounds[axis];
|
||
stamp.boundsMaximumMicrometres[axis]=position[axis]+bounds[axis];}
|
||
control.stamps.push_back(std::move(stamp));
|
||
};
|
||
add("landmark/sw-arrival-chamber-v1",ResolvedSdfStampKindV1::carveEllipsoid,
|
||
{-8917798,31949858,-513083425},{8500000,7500000,6000000},
|
||
800000,700000,4800000,4704841454711558ull,0);
|
||
add("landmark/sw-arrival-chamber-entrance-v1",ResolvedSdfStampKindV1::carveArch,
|
||
{-12289139,37026886,-513310578},{2600000,4200000,3200000},
|
||
350000,250000,2600000,2371181934872559ull,0);
|
||
add("landmark/sw-arrival-chamber-dais-v1",ResolvedSdfStampKindV1::fillDais,
|
||
{-7489021,30688194,-515838454},{3200000,2800000,900000},
|
||
300000,180000,2200000,7240637434535735ull,0);
|
||
if(!control.finalize(error))return false;
|
||
std::vector<ResolvedLayoutIssueV2> issues;
|
||
return control.validate(ResolvedLayoutValidationModeV3::strict,issues,error);
|
||
}
|
||
|
||
inline bool SameTileOutput(const VoxelForgeCore::TileMesh& A,
|
||
const VoxelForgeCore::TileMesh& B)
|
||
{
|
||
return A.hasDensityHash&&B.hasDensityHash&&A.hasMeshHash&&B.hasMeshHash
|
||
&&A.hasPayloadHash&&B.hasPayloadHash&&A.hasTopologyHash&&B.hasTopologyHash
|
||
&&A.densityHash==B.densityHash&&A.meshHash==B.meshHash
|
||
&&A.payloadHash==B.payloadHash&&A.topologyHash==B.topologyHash;
|
||
}
|
||
|
||
struct FSceneExportWriter
|
||
{
|
||
std::ofstream File;
|
||
bool bGood=true;
|
||
bool bIncludeMaterialAttributes=false;
|
||
std::uint32_t TileCount=0;
|
||
|
||
template<typename T> void Write(const T& Value)
|
||
{
|
||
File.write(reinterpret_cast<const char*>(&Value),sizeof(Value));
|
||
bGood=bGood&&static_cast<bool>(File);
|
||
}
|
||
|
||
bool Open(const std::filesystem::path& Path,std::uint32_t VoxelSizeMicrons,
|
||
const FVector& CameraVoxels,float YawDegrees,float PitchDegrees,
|
||
std::uint32_t ExpectedTiles,bool IncludeMaterialAttributes)
|
||
{
|
||
std::error_code Error;
|
||
if(!Path.parent_path().empty())
|
||
std::filesystem::create_directories(Path.parent_path(),Error);
|
||
if(Error)return false;
|
||
File.open(Path,std::ios::binary|std::ios::trunc);
|
||
if(!File)return false;
|
||
bIncludeMaterialAttributes=IncludeMaterialAttributes;
|
||
const char Magic[8]={'V','F','M','S','H','0','0',
|
||
static_cast<char>(bIncludeMaterialAttributes?'2':'1')};
|
||
File.write(Magic,sizeof(Magic));
|
||
const std::uint32_t Version=bIncludeMaterialAttributes?2u:1u;
|
||
Write(Version);Write(VoxelSizeMicrons);Write(ExpectedTiles);
|
||
const double MicronsToMeters=static_cast<double>(VoxelSizeMicrons)/1000000.0;
|
||
const double Camera[3]={CameraVoxels.X*MicronsToMeters,
|
||
CameraVoxels.Y*MicronsToMeters,CameraVoxels.Z*MicronsToMeters};
|
||
for(const double Value:Camera)Write(Value);
|
||
const double Yaw=YawDegrees,Pitch=PitchDegrees;
|
||
Write(Yaw);Write(Pitch);
|
||
return bGood;
|
||
}
|
||
|
||
void Add(const FStandaloneCpuTile& Tile)
|
||
{
|
||
const std::int32_t Key[4]={Tile.Key.Coord.X,Tile.Key.Coord.Y,
|
||
Tile.Key.Coord.Z,Tile.Key.Level};
|
||
for(const auto Value:Key)Write(Value);
|
||
const std::uint32_t SurfaceVertexCount=Tile.SurfaceVertexCount;
|
||
const std::uint32_t SkirtTriangleCount=Tile.SkirtTriangleCount;
|
||
const std::uint32_t VertexCount=static_cast<std::uint32_t>(Tile.Vertices.size());
|
||
const std::uint32_t IndexCount=static_cast<std::uint32_t>(Tile.Indices.size());
|
||
const std::uint32_t MaskCount=static_cast<std::uint32_t>(Tile.TriangleSkirtFaceMasks.size());
|
||
Write(SurfaceVertexCount);Write(SkirtTriangleCount);
|
||
Write(VertexCount);Write(IndexCount);Write(MaskCount);
|
||
for(const FStandaloneCpuVertex& Vertex:Tile.Vertices)
|
||
for(const float Value:Vertex.Position)Write(Value);
|
||
for(const FStandaloneCpuVertex& Vertex:Tile.Vertices)
|
||
for(const float Value:Vertex.Normal)Write(Value);
|
||
if(bIncludeMaterialAttributes)
|
||
for(const FStandaloneCpuVertex& Vertex:Tile.Vertices)
|
||
{
|
||
Write(Vertex.HostMaterialId);
|
||
for(const std::uint16_t id:Vertex.ResourceMaterialIds)Write(id);
|
||
for(const std::uint16_t weight:Vertex.ResourceWeightsQ16)Write(weight);
|
||
}
|
||
if(IndexCount)
|
||
File.write(reinterpret_cast<const char*>(Tile.Indices.data()),
|
||
static_cast<std::streamsize>(sizeof(std::uint32_t)*Tile.Indices.size()));
|
||
if(MaskCount)
|
||
File.write(reinterpret_cast<const char*>(Tile.TriangleSkirtFaceMasks.data()),
|
||
static_cast<std::streamsize>(Tile.TriangleSkirtFaceMasks.size()));
|
||
bGood=bGood&&static_cast<bool>(File);
|
||
++TileCount;
|
||
}
|
||
|
||
bool Close(std::uint32_t ExpectedTiles)
|
||
{
|
||
File.flush();
|
||
const bool bComplete=bGood&&static_cast<bool>(File)&&TileCount==ExpectedTiles;
|
||
File.close();
|
||
return bComplete;
|
||
}
|
||
};
|
||
|
||
inline void HashWord(uint64& Hash,uint64 Word,int32 Bytes=8)
|
||
{
|
||
for(int32 I=0;I<Bytes;++I)
|
||
{
|
||
Hash^=static_cast<uint8>(Word>>(I*8));
|
||
Hash*=1099511628211ull;
|
||
}
|
||
}
|
||
|
||
struct FHashedTile
|
||
{
|
||
FVoxelTileKey Key;
|
||
uint64 Hash=14695981039346656037ull;
|
||
};
|
||
|
||
inline bool TileKeyLessForHash(const FVoxelTileKey& A,const FVoxelTileKey& B)
|
||
{
|
||
if(A.Level!=B.Level)return A.Level<B.Level;
|
||
if(A.Coord.Z!=B.Coord.Z)return A.Coord.Z<B.Coord.Z;
|
||
if(A.Coord.Y!=B.Coord.Y)return A.Coord.Y<B.Coord.Y;
|
||
return A.Coord.X<B.Coord.X;
|
||
}
|
||
|
||
inline uint64 HashTileRows(std::vector<FHashedTile>& Rows)
|
||
{
|
||
std::sort(Rows.begin(),Rows.end(),[](const FHashedTile& A,const FHashedTile& B)
|
||
{return TileKeyLessForHash(A.Key,B.Key);});
|
||
uint64 Hash=14695981039346656037ull;
|
||
for(const FHashedTile& Row:Rows)
|
||
{
|
||
HashWord(Hash,static_cast<uint32>(Row.Key.Level),4);
|
||
HashWord(Hash,static_cast<uint32>(Row.Key.Coord.X),4);
|
||
HashWord(Hash,static_cast<uint32>(Row.Key.Coord.Y),4);
|
||
HashWord(Hash,static_cast<uint32>(Row.Key.Coord.Z),4);
|
||
HashWord(Hash,Row.Hash);
|
||
}
|
||
return Hash;
|
||
}
|
||
|
||
inline bool AuditResolvedStrateTransitions(const VoxelForgeCore::World& Field,
|
||
const FStandaloneOwnerWorld& Owner,
|
||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& Layout)
|
||
{
|
||
constexpr std::int64_t XYExtentMicrometres=256000000;
|
||
constexpr std::int64_t XYStepMicrometres=8000000;
|
||
const std::int64_t coreMicrometres=std::max<std::uint32_t>(1,Owner.CoreVoxelSizeMicrons);
|
||
const std::int64_t zStepMicrometres=std::max<std::int64_t>(1,coreMicrometres/2);
|
||
const std::int64_t minimumBandMicrometres=2*coreMicrometres;
|
||
std::uint64_t interfacesPassed=0,totalColumns=0,passageColumnsSkipped=0;
|
||
std::uint64_t holeColumns=0,airSamples=0,solidSamples=0;
|
||
bool negativeWidthRejected=false,negativeThinBandRejected=false;
|
||
|
||
for(std::size_t interfaceIndex=0;interfaceIndex+1<Layout.strates.size();++interfaceIndex)
|
||
{
|
||
const auto& upper=Layout.strates[interfaceIndex];
|
||
const auto& lower=Layout.strates[interfaceIndex+1];
|
||
std::int64_t minimumOffset=std::numeric_limits<std::int64_t>::max();
|
||
std::int64_t maximumOffset=std::numeric_limits<std::int64_t>::min();
|
||
std::int64_t maximumCrossingDepth=0;
|
||
std::int64_t minimumUpperThickness=std::numeric_limits<std::int64_t>::max();
|
||
std::int64_t minimumLowerThickness=std::numeric_limits<std::int64_t>::max();
|
||
std::uint64_t columns=0,skipped=0,holes=0,air=0,solid=0;
|
||
bool firstHoleReported=false;
|
||
const std::int64_t seam=upper.bedrock.bottomMicrometres;
|
||
const auto passageCoversColumn=[&](std::int64_t xUm,std::int64_t yUm,
|
||
std::int64_t zMinimumUm,std::int64_t zMaximumUm)
|
||
{
|
||
constexpr double Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||
for(const auto& passage:Layout.passages)
|
||
{
|
||
if(passage.upperStrateIndex!=upper.strateIndex
|
||
||passage.lowerStrateIndex!=lower.strateIndex)continue;
|
||
for(std::size_t pointIndex=1;pointIndex<passage.controlPoints.size();++pointIndex)
|
||
{
|
||
const auto& a=passage.controlPoints[pointIndex-1];
|
||
const auto& b=passage.controlPoints[pointIndex];
|
||
const double ax=a.positionSubvoxels[0]*coreMicrometres/Q;
|
||
const double ay=a.positionSubvoxels[1]*coreMicrometres/Q;
|
||
const double az=a.positionSubvoxels[2]*coreMicrometres/Q;
|
||
const double bx=b.positionSubvoxels[0]*coreMicrometres/Q;
|
||
const double by=b.positionSubvoxels[1]*coreMicrometres/Q;
|
||
const double bz=b.positionSubvoxels[2]*coreMicrometres/Q;
|
||
const double radius=std::max(a.radiusSubvoxels,b.radiusSubvoxels)
|
||
*coreMicrometres/Q+750000.0;
|
||
if(std::max(az,bz)+radius<zMinimumUm
|
||
||std::min(az,bz)-radius>zMaximumUm)continue;
|
||
const double dx=bx-ax,dy=by-ay;
|
||
const double lengthSquared=dx*dx+dy*dy;
|
||
const double t=lengthSquared>0.0
|
||
?std::clamp(((xUm-ax)*dx+(yUm-ay)*dy)/lengthSquared,0.0,1.0):0.0;
|
||
const double ex=xUm-(ax+t*dx),ey=yUm-(ay+t*dy);
|
||
if(ex*ex+ey*ey<=radius*radius)return true;
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
|
||
for(std::int64_t iy=-XYExtentMicrometres/XYStepMicrometres;
|
||
iy<=XYExtentMicrometres/XYStepMicrometres;++iy)
|
||
for(std::int64_t ix=-XYExtentMicrometres/XYStepMicrometres;
|
||
ix<=XYExtentMicrometres/XYStepMicrometres;++ix)
|
||
{
|
||
const std::int64_t xUm=ix*XYStepMicrometres;
|
||
const std::int64_t yUm=iy*XYStepMicrometres;
|
||
const auto bands=VoxelForgeCore::EvaluateResolvedLayoutTransitionBandsV1(
|
||
upper,lower,Owner.Seed,Layout.strateLayoutChunkSizeMicrometres,xUm,yUm);
|
||
const std::int64_t offset=bands.upperBedrockBottomMicrometres-seam;
|
||
minimumOffset=std::min(minimumOffset,offset);
|
||
maximumOffset=std::max(maximumOffset,offset);
|
||
maximumCrossingDepth=std::max(maximumCrossingDepth,
|
||
bands.crossingDepthMicrometres);
|
||
minimumUpperThickness=std::min(minimumUpperThickness,
|
||
bands.upperBedrockTopMicrometres-bands.upperBedrockBottomMicrometres);
|
||
minimumLowerThickness=std::min(minimumLowerThickness,
|
||
bands.lowerCeilingTopMicrometres-bands.lowerCeilingBottomMicrometres);
|
||
const std::int64_t zMinimum=std::min(bands.upperBedrockBottomMicrometres,
|
||
bands.lowerCeilingBottomMicrometres);
|
||
const std::int64_t zMaximum=std::max(bands.upperBedrockTopMicrometres,
|
||
bands.lowerCeilingTopMicrometres);
|
||
if(passageCoversColumn(xUm,yUm,zMinimum,zMaximum)){++skipped;continue;}
|
||
|
||
++columns;
|
||
bool hasHole=false;
|
||
for(std::int64_t zUm=zMinimum+zStepMicrometres/2;
|
||
zUm<zMaximum-zStepMicrometres/2;zUm+=zStepMicrometres)
|
||
{
|
||
const VoxelForgeCore::Vec3d point{
|
||
static_cast<double>(xUm)/coreMicrometres,
|
||
static_cast<double>(yUm)/coreMicrometres,
|
||
static_cast<double>(zUm)/coreMicrometres};
|
||
const float density=Field.densityAt(point);
|
||
if(!std::isfinite(density)||density>=0.0f)
|
||
{
|
||
if(!firstHoleReported)
|
||
{
|
||
std::printf("[StrateTransitionFirstHole] x_um=%lld y_um=%lld z_um=%lld z_min_um=%lld z_max_um=%lld density=%.9g dispatch=%s\n",
|
||
static_cast<long long>(xUm),static_cast<long long>(yUm),
|
||
static_cast<long long>(zUm),static_cast<long long>(zMinimum),
|
||
static_cast<long long>(zMaximum),density,
|
||
Field.densityDispatchPathAt(point).c_str());
|
||
firstHoleReported=true;
|
||
}
|
||
hasHole=true;++air;
|
||
}
|
||
else ++solid;
|
||
}
|
||
if(hasHole)++holes;
|
||
}
|
||
|
||
const std::int64_t boundaryPeakToPeak=maximumOffset-minimumOffset;
|
||
const std::int64_t allowedWidth=upper.transitionMode=="Hard"
|
||
||upper.transitionFallsBackToHard?0:upper.transitionWidthMicrometres;
|
||
if(interfaceIndex==0)
|
||
{
|
||
auto wide=Layout;
|
||
wide.strates[0].transitionMode="Interleaved";
|
||
wide.strates[0].transitionFallsBackToHard=false;
|
||
wide.strates[0].transitionWidthMicrometres=8000000;
|
||
wide.strates[0].transitionInterpenetrationMicrometres=8000001;
|
||
std::vector<VoxelForgeCore::ResolvedLayoutIssueV2> issues;
|
||
std::string error;
|
||
negativeWidthRejected=!wide.validate(
|
||
VoxelForgeCore::ResolvedLayoutValidationModeV3::strict,issues,error);
|
||
|
||
auto thin=Layout;
|
||
auto& bedrock=thin.strates[0].bedrock;
|
||
bedrock.topMicrometres=bedrock.bottomMicrometres+minimumBandMicrometres-1;
|
||
thin.strates[0].content.bottomMicrometres=bedrock.topMicrometres;
|
||
negativeThinBandRejected=!thin.validate(
|
||
VoxelForgeCore::ResolvedLayoutValidationModeV3::strict,issues,error);
|
||
}
|
||
const bool passed=columns>0&&holes==0&&maximumCrossingDepth<=allowedWidth
|
||
&&minimumUpperThickness>=minimumBandMicrometres
|
||
&&minimumLowerThickness>=minimumBandMicrometres;
|
||
interfacesPassed+=passed?1u:0u;
|
||
totalColumns+=columns;passageColumnsSkipped+=skipped;
|
||
holeColumns+=holes;airSamples+=air;solidSamples+=solid;
|
||
std::printf("[StrateTransitionInterface] upper=%d lower=%d mode=%s width_um=%lld max_crossing_depth_um=%lld boundary_peak_to_peak_um=%lld allowed_um=%lld upper_min_band_um=%lld lower_min_band_um=%lld columns=%llu passage_columns=%llu hole_columns=%llu air_samples=%llu status=%s\n",
|
||
upper.strateIndex,lower.strateIndex,upper.transitionMode.c_str(),
|
||
static_cast<long long>(upper.transitionWidthMicrometres),
|
||
static_cast<long long>(maximumCrossingDepth),
|
||
static_cast<long long>(boundaryPeakToPeak),static_cast<long long>(allowedWidth),
|
||
static_cast<long long>(minimumUpperThickness),static_cast<long long>(minimumLowerThickness),
|
||
static_cast<unsigned long long>(columns),static_cast<unsigned long long>(skipped),
|
||
static_cast<unsigned long long>(holes),static_cast<unsigned long long>(air),
|
||
passed?"PASS":"FAIL");
|
||
}
|
||
const bool controlsPassed=negativeWidthRejected&&negativeThinBandRejected;
|
||
std::printf("[StrateTransitionNegativeControls] wider_than_declared=%s thinner_than_minimum=%s minimum_band_um=%lld status=%s\n",
|
||
negativeWidthRejected?"REJECTED":"ACCEPTED",
|
||
negativeThinBandRejected?"REJECTED":"ACCEPTED",
|
||
static_cast<long long>(minimumBandMicrometres),controlsPassed?"PASS":"FAIL");
|
||
const bool passed=!Layout.strates.empty()&&interfacesPassed+1==Layout.strates.size()
|
||
&&holeColumns==0&&airSamples==0&&controlsPassed;
|
||
std::printf("[StrateTransitionAuditSummary] interfaces=%llu passed=%llu columns=%llu skipped_passage_columns=%llu hole_columns=%llu air_samples=%llu solid_samples=%llu status=%s\n",
|
||
static_cast<unsigned long long>(Layout.strates.size()-1),
|
||
static_cast<unsigned long long>(interfacesPassed),
|
||
static_cast<unsigned long long>(totalColumns),
|
||
static_cast<unsigned long long>(passageColumnsSkipped),
|
||
static_cast<unsigned long long>(holeColumns),
|
||
static_cast<unsigned long long>(airSamples),
|
||
static_cast<unsigned long long>(solidSamples),passed?"PASS":"FAIL");
|
||
return passed;
|
||
}
|
||
|
||
inline bool AuditNativeCavesField(const VoxelForgeCore::World& shape,
|
||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,std::uint32_t voxelMicrometres)
|
||
{
|
||
using namespace VoxelForgeCore;
|
||
const double vm=std::max<std::uint32_t>(1,voxelMicrometres)/1e6;
|
||
const auto metres=[](const std::array<std::int64_t,3>& p)
|
||
{return std::array<double,3>{p[0]/1e6,p[1]/1e6,p[2]/1e6};};
|
||
const auto density=[&](double x,double y,double z)
|
||
{return static_cast<double>(shape.densityAt({x/vm,y/vm,z/vm}))*vm;};
|
||
const auto cave=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||
[](const auto& s){return s.generatorKind=="Caves"
|
||
&&s.recipeId=="Core.Caves.PrimitiveField"&&s.recipeVersion==1
|
||
&&s.passageCapabilities.hasNativeCaveField;});
|
||
if(cave==layout.strates.end()||layout.cavernRooms.size()!=1||layout.cavernEdges.size()!=2)
|
||
{std::printf("[CavesAudit] recipe=%s rooms=%zu edges=%zu status=FAIL\n",
|
||
cave==layout.strates.end()?"missing":"present",layout.cavernRooms.size(),layout.cavernEdges.size());return false;}
|
||
std::vector<const ResolvedLayoutPassageV1*> mouths;
|
||
for(const auto& p:layout.passages)if(p.lowerStrateIndex==cave->strateIndex)mouths.push_back(&p);
|
||
std::sort(mouths.begin(),mouths.end(),[](auto* a,auto* b){return a->stableId<b->stableId;});
|
||
if(mouths.size()!=2)return false;
|
||
std::array<const ResolvedCavernEdgeV1*,2> edges{};
|
||
for(std::size_t i=0;i<2;++i)
|
||
{
|
||
const auto it=std::find_if(layout.cavernEdges.begin(),layout.cavernEdges.end(),
|
||
[&](const auto& e){return e.startAnchorStableId==mouths[i]->stableId+"/lower-mouth";});
|
||
if(it==layout.cavernEdges.end()||it->polylineMicrometres.size()<2
|
||
||it->strateIndex!=cave->strateIndex)return false;
|
||
edges[i]=&*it;
|
||
}
|
||
const auto& room=layout.cavernRooms.front();
|
||
const auto rc=metres(room.centerMicrometres);
|
||
const double floor=room.floorMicrometres/1e6;
|
||
struct Segment {std::array<double,3> a,b;};
|
||
std::vector<Segment> segments;
|
||
std::array<double,4> bounds={1e30,-1e30,1e30,-1e30};
|
||
double minFloor=floor,maxFloor=floor;
|
||
for(const auto* edge:edges)
|
||
{
|
||
for(std::size_t i=0;i<edge->polylineMicrometres.size();++i)
|
||
{
|
||
const auto p=metres(edge->polylineMicrometres[i]);
|
||
bounds[0]=std::min(bounds[0],p[0]);bounds[1]=std::max(bounds[1],p[0]);
|
||
bounds[2]=std::min(bounds[2],p[1]);bounds[3]=std::max(bounds[3],p[1]);
|
||
minFloor=std::min(minFloor,p[2]);maxFloor=std::max(maxFloor,p[2]);
|
||
if(i)segments.push_back({metres(edge->polylineMicrometres[i-1]),p});
|
||
}
|
||
}
|
||
const double roomX=room.halfExtentsMicrometres[0]/1e6;
|
||
const double roomY=room.halfExtentsMicrometres[1]/1e6;
|
||
bounds[0]=std::min(bounds[0],rc[0]-roomX);bounds[1]=std::max(bounds[1],rc[0]+roomX);
|
||
bounds[2]=std::min(bounds[2],rc[1]-roomY);bounds[3]=std::max(bounds[3],rc[1]+roomY);
|
||
constexpr double H=0.50,GradeLimit=0.2679491924311227;
|
||
bounds[0]-=4.0;bounds[1]+=4.0;bounds[2]-=4.0;bounds[3]+=4.0;
|
||
const int minX=static_cast<int>(std::floor(bounds[0]/H));
|
||
const int maxX=static_cast<int>(std::ceil(bounds[1]/H));
|
||
const int minY=static_cast<int>(std::floor(bounds[2]/H));
|
||
const int maxY=static_cast<int>(std::ceil(bounds[3]/H));
|
||
const int nx=maxX-minX+1,ny=maxY-minY+1;
|
||
if(nx<=0||ny<=0||static_cast<std::uint64_t>(nx)*ny>4000000)return false;
|
||
struct Node {int gx=0,gy=0;double x=0.0,y=0.0,floor=0.0;bool walkable=false;};
|
||
std::vector<Node> nodes;
|
||
std::vector<std::vector<int>> columns(static_cast<std::size_t>(nx)*ny);
|
||
const auto columnIndex=[&](int x,int y){return static_cast<std::size_t>(y-minY)*nx+(x-minX);};
|
||
const double bore=edges[0]->boreRadiusMicrometres/1e6;
|
||
std::uint64_t capsules=0,supports=0,floorQueries=0;
|
||
const auto testCapsule=[&](double x,double y,double surface)
|
||
{
|
||
bool clear=true;constexpr double radius=0.34,height=1.76;
|
||
for(double h=0.08;h<=height+1e-9;h+=0.18)
|
||
{
|
||
double cap=0;if(h<radius)cap=radius-h;else if(h>height-radius)cap=h-(height-radius);
|
||
const double radial=std::sqrt(std::max(0.0,radius*radius-cap*cap));
|
||
for(double oy=-radial;oy<=radial+1e-9;oy+=0.17)
|
||
for(double ox=-radial;ox<=radial+1e-9;ox+=0.17)
|
||
{if(ox*ox+oy*oy>radial*radial+1e-9)continue;++capsules;
|
||
if(density(x+ox,y+oy,surface+0.12+h)<0.025)clear=false;}
|
||
}
|
||
constexpr std::array<std::array<double,2>,5> head={{{0,0},{0.24,0},{-0.24,0},{0,0.24},{0,-0.24}}};
|
||
for(auto o:head)for(double h=0;h<=0.30+1e-9;h+=0.05)
|
||
if(density(x+o[0],y+o[1],surface+0.12+height+h)<0)clear=false;
|
||
constexpr std::array<std::array<double,2>,5> foot={{{0,0},{0.28,0},{-0.28,0},{0,0.28},{0,-0.28}}};
|
||
unsigned support=0;
|
||
for(auto o:foot){++supports;bool hit=false;for(double d=0.1;d<=0.55;d+=0.08)
|
||
if(density(x+o[0],y+o[1],surface-d)<0){hit=true;break;}if(hit)++support;}
|
||
return clear&&support>=3;
|
||
};
|
||
for(int gy=minY;gy<=maxY;++gy)for(int gx=minX;gx<=maxX;++gx)
|
||
{
|
||
const double x=gx*H,y=gy*H;double nearest=1e30;
|
||
for(const auto& s:segments)
|
||
{
|
||
const double dx=s.b[0]-s.a[0],dy=s.b[1]-s.a[1],l2=dx*dx+dy*dy;
|
||
const double t=l2>0?std::clamp(((x-s.a[0])*dx+(y-s.a[1])*dy)/l2,0.0,1.0):0.0;
|
||
nearest=std::min(nearest,std::hypot(x-(s.a[0]+t*dx),y-(s.a[1]+t*dy)));
|
||
}
|
||
const bool roomXY=std::abs(x-rc[0])<=roomX+0.5&&std::abs(y-rc[1])<=roomY+0.5;
|
||
if(nearest>bore+0.9&&!roomXY)continue;
|
||
double priorZ=minFloor-0.75,priorDensity=density(x,y,priorZ);
|
||
for(double z=priorZ+0.05;z<=maxFloor+0.75+1e-9;z+=0.05)
|
||
{
|
||
const double value=density(x,y,z);++floorQueries;
|
||
if(priorDensity<0.0&&value>=0.0)
|
||
{
|
||
double lo=z-0.05,hi=z;
|
||
for(int q=0;q<12;++q){const double mid=(lo+hi)*0.5;if(density(x,y,mid)<0.0)lo=mid;else hi=mid;}
|
||
const double surface=(lo+hi)*0.5;
|
||
Node node;node.gx=gx;node.gy=gy;node.x=x;node.y=y;node.floor=surface;
|
||
node.walkable=testCapsule(x,y,surface);
|
||
const int id=static_cast<int>(nodes.size());nodes.push_back(node);
|
||
columns[columnIndex(gx,gy)].push_back(id);
|
||
}
|
||
priorZ=z;priorDensity=value;
|
||
}
|
||
}
|
||
const auto mouth=[&](const ResolvedLayoutPassageV1& p)
|
||
{return std::array<double,3>{p.lowerMouthSubvoxels[0]*vm/WorldAssemblySubvoxelsPerVoxel,
|
||
p.lowerMouthSubvoxels[1]*vm/WorldAssemblySubvoxelsPerVoxel,
|
||
p.lowerMouthSubvoxels[2]*vm/WorldAssemblySubvoxelsPerVoxel};};
|
||
const auto start=mouth(*mouths[0]),end=mouth(*mouths[1]);
|
||
std::vector<int> starts,goals;
|
||
double nearestStart=std::numeric_limits<double>::infinity(),nearestEnd=nearestStart;
|
||
int nearestStartNode=-1,nearestEndNode=-1;
|
||
for(std::size_t id=0;id<nodes.size();++id)
|
||
{
|
||
const auto& n=nodes[id];if(!n.walkable)continue;
|
||
const double ds=std::hypot(std::hypot(n.x-start[0],n.y-start[1]),n.floor-start[2]);
|
||
const double de=std::hypot(std::hypot(n.x-end[0],n.y-end[1]),n.floor-end[2]);
|
||
if(ds<nearestStart){nearestStart=ds;nearestStartNode=static_cast<int>(id);}
|
||
if(de<nearestEnd){nearestEnd=de;nearestEndNode=static_cast<int>(id);}
|
||
if(ds<=2.0)starts.push_back(static_cast<int>(id));
|
||
if(de<=2.0)goals.push_back(static_cast<int>(id));
|
||
}
|
||
constexpr std::array<std::array<int,2>,8> dirs={{{1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}}};
|
||
const auto flood=[&](bool enforceGrade,std::uint64_t* rejected)
|
||
{
|
||
std::vector<int> parent(nodes.size(),-1);std::queue<int> open;
|
||
for(int id:starts){parent[static_cast<std::size_t>(id)]=-2;open.push(id);}
|
||
while(!open.empty())
|
||
{
|
||
const int current=open.front();open.pop();const auto& a=nodes[static_cast<std::size_t>(current)];
|
||
for(const auto& d:dirs)
|
||
{
|
||
const int gx=a.gx+d[0],gy=a.gy+d[1];if(gx<minX||gx>maxX||gy<minY||gy>maxY)continue;
|
||
const double run=H*std::hypot(d[0],d[1]);
|
||
for(const int next:columns[columnIndex(gx,gy)])
|
||
{
|
||
if(parent[static_cast<std::size_t>(next)]!=-1)continue;
|
||
const auto& b=nodes[static_cast<std::size_t>(next)];if(!b.walkable)continue;
|
||
if(enforceGrade&&std::abs(b.floor-a.floor)/run>GradeLimit)
|
||
{if(rejected)++*rejected;continue;}
|
||
parent[static_cast<std::size_t>(next)]=current;open.push(next);
|
||
}
|
||
}
|
||
}
|
||
return parent;
|
||
};
|
||
auto rawParent=flood(false,nullptr);std::uint64_t gradeRejected=0;
|
||
auto parent=flood(true,&gradeRejected);
|
||
const auto reachableGoal=[&](const std::vector<int>& parents)
|
||
{
|
||
int selected=-1;double distance=std::numeric_limits<double>::infinity();
|
||
for(int candidate:goals)if(parents[static_cast<std::size_t>(candidate)]!=-1)
|
||
{
|
||
const auto& n=nodes[static_cast<std::size_t>(candidate)];
|
||
const double d=std::hypot(std::hypot(n.x-end[0],n.y-end[1]),n.floor-end[2]);
|
||
if(d<distance){distance=d;selected=candidate;}
|
||
}
|
||
return std::pair<int,double>{selected,distance};
|
||
};
|
||
const auto [rawGoal,rawGoalD]=reachableGoal(rawParent);
|
||
const auto [goal,goalD]=reachableGoal(parent);
|
||
const bool rawReached=rawGoal>=0;
|
||
std::size_t gradedReachable=0,roomWalkable=0,roomReachable=0;
|
||
double reachableFloorMin=std::numeric_limits<double>::infinity();
|
||
double reachableFloorMax=-std::numeric_limits<double>::infinity();
|
||
for(std::size_t id=0;id<nodes.size();++id)
|
||
{
|
||
const auto& n=nodes[id];if(!n.walkable)continue;
|
||
const bool insideRoom=std::abs(n.x-rc[0])<=roomX+0.5
|
||
&&std::abs(n.y-rc[1])<=roomY+0.5;
|
||
if(insideRoom)++roomWalkable;
|
||
if(parent[id]!=-1)
|
||
{
|
||
++gradedReachable;reachableFloorMin=std::min(reachableFloorMin,n.floor);
|
||
reachableFloorMax=std::max(reachableFloorMax,n.floor);
|
||
if(insideRoom)++roomReachable;
|
||
}
|
||
}
|
||
std::printf("[CavesFloodFillGraph] graded_reachable=%zu room_walkable=%zu room_reachable=%zu reachable_floor_m=%.3f..%.3f arrival_floor_m=%.3f departure_floor_m=%.3f\n",
|
||
gradedReachable,roomWalkable,roomReachable,reachableFloorMin,reachableFloorMax,
|
||
nearestStartNode>=0?nodes[static_cast<std::size_t>(nearestStartNode)].floor:0.0,
|
||
nearestEndNode>=0?nodes[static_cast<std::size_t>(nearestEndNode)].floor:0.0);
|
||
for(std::size_t edgeIndex=0;edgeIndex<edges.size();++edgeIndex)
|
||
{
|
||
const auto& polyline=edges[edgeIndex]->polylineMicrometres;
|
||
std::vector<std::array<double,3>> linePoints;linePoints.reserve(polyline.size());
|
||
std::vector<double> cumulativeLength(polyline.size(),0.0);
|
||
for(const auto& p:polyline)linePoints.push_back(metres(p));
|
||
for(std::size_t i=1;i<linePoints.size();++i)
|
||
cumulativeLength[i]=cumulativeLength[i-1]+std::hypot(
|
||
std::hypot(linePoints[i][0]-linePoints[i-1][0],
|
||
linePoints[i][1]-linePoints[i-1][1]),
|
||
linePoints[i][2]-linePoints[i-1][2]);
|
||
std::printf("[CavesFloodFillProgress] edge=%s",
|
||
edges[edgeIndex]->stableId.c_str());
|
||
for(int sample=0;sample<=10;++sample)
|
||
{
|
||
const double distance=cumulativeLength.back()*sample/10.0;
|
||
const auto upper=std::lower_bound(cumulativeLength.begin(),
|
||
cumulativeLength.end(),distance);
|
||
const std::size_t upperIndex=std::clamp<std::size_t>(
|
||
static_cast<std::size_t>(upper-cumulativeLength.begin()),1,
|
||
cumulativeLength.size()-1);
|
||
const std::size_t lowerIndex=upperIndex-1;
|
||
const double span=cumulativeLength[upperIndex]-cumulativeLength[lowerIndex];
|
||
const double t=span>0.0?(distance-cumulativeLength[lowerIndex])/span:0.0;
|
||
const std::array<double,3> target={
|
||
linePoints[lowerIndex][0]+(linePoints[upperIndex][0]-linePoints[lowerIndex][0])*t,
|
||
linePoints[lowerIndex][1]+(linePoints[upperIndex][1]-linePoints[lowerIndex][1])*t,
|
||
linePoints[lowerIndex][2]+(linePoints[upperIndex][2]-linePoints[lowerIndex][2])*t};
|
||
double nearestWalkable=std::numeric_limits<double>::infinity();
|
||
double nearestReachable=nearestWalkable;
|
||
int nearestWalkableNode=-1,nearestReachableNode=-1;
|
||
for(std::size_t id=0;id<nodes.size();++id)
|
||
{
|
||
const auto& n=nodes[id];if(!n.walkable)continue;
|
||
const double d=std::hypot(std::hypot(n.x-target[0],n.y-target[1]),
|
||
n.floor-target[2]);
|
||
if(d<nearestWalkable){nearestWalkable=d;nearestWalkableNode=static_cast<int>(id);}
|
||
if(parent[id]!=-1&&d<nearestReachable)
|
||
{nearestReachable=d;nearestReachableNode=static_cast<int>(id);}
|
||
}
|
||
const double fieldFloor=density(target[0],target[1],target[2]);
|
||
const double fieldAbove=density(target[0],target[1],target[2]+0.3);
|
||
const double walkFloor=nearestWalkableNode>=0
|
||
?nodes[static_cast<std::size_t>(nearestWalkableNode)].floor:0.0;
|
||
const double reachFloor=nearestReachableNode>=0
|
||
?nodes[static_cast<std::size_t>(nearestReachableNode)].floor:0.0;
|
||
std::printf(" t%d=%.2f/%.2f(f%.2f/r%.2f):%.2f/%.2f",sample,
|
||
nearestWalkable,nearestReachable,walkFloor,reachFloor,fieldFloor,fieldAbove);
|
||
}
|
||
std::printf("\n");
|
||
}
|
||
double rawMaximumGrade=0.0;std::size_t rawPathCells=0;
|
||
if(rawReached)for(int p=rawGoal;p>=0;){++rawPathCells;const int prior=rawParent[static_cast<std::size_t>(p)];if(prior==-2)break;
|
||
const auto& a=nodes[static_cast<std::size_t>(p)];const auto& b=nodes[static_cast<std::size_t>(prior)];
|
||
const double run=H*std::hypot(a.gx-b.gx,a.gy-b.gy);rawMaximumGrade=std::max(rawMaximumGrade,
|
||
std::atan2(std::abs(a.floor-b.floor),run)*180.0/3.14159265358979323846);p=prior;}
|
||
const bool reached=goal>=0;std::vector<int> path;
|
||
if(reached){for(int p=goal;p>=0;){path.push_back(p);const int prior=parent[static_cast<std::size_t>(p)];if(prior==-2)break;p=prior;}std::reverse(path.begin(),path.end());}
|
||
double headroom=std::numeric_limits<double>::infinity();
|
||
for(int id:path){const auto& n=nodes[static_cast<std::size_t>(id)];double h=0.30;
|
||
for(double rise=0;rise<=0.30+1e-9;rise+=0.05)if(density(n.x,n.y,n.floor+0.12+1.76+rise)<0){h=std::max(0.0,rise-0.05);break;}
|
||
headroom=std::min(headroom,h);}
|
||
std::printf("[CavesFloodFillReachability] ungraded_connected=%s ungraded_path_cells=%zu ungraded_max_grade_deg=%.3f grade_rejected_edges=%llu\n",
|
||
rawReached?"yes":"no",rawPathCells,rawMaximumGrade,static_cast<unsigned long long>(gradeRejected));
|
||
std::printf("[CavesFloodFillEndpoints] start_seeds=%zu departure_candidates=%zu raw_goal=%s graded_goal=%s nearest_arrival_m=%.3f nearest_departure_m=%.3f selected_departure_m=%.3f raw_selected_departure_m=%.3f\n",
|
||
starts.size(),goals.size(),rawGoal>=0?"yes":"no",goal>=0?"yes":"no",nearestStart,nearestEnd,goalD,rawGoalD);
|
||
|
||
// Fit the actual zero contour in the wall-foot cross section; the local quadratic
|
||
// curvature is the fillet radius, W/(2 sqrt(2)) for this cubic smooth-max.
|
||
const auto& edge=*edges[0];const auto p0=metres(edge.polylineMicrometres[edge.polylineMicrometres.size()/2-1]);
|
||
const auto p1=metres(edge.polylineMicrometres[edge.polylineMicrometres.size()/2]);
|
||
const double tx=p1[0]-p0[0],ty=p1[1]-p0[1],tl=std::hypot(tx,ty),width=edge.smoothWidthMicrometres/1e6;
|
||
double measured=0;
|
||
const double horizontal=std::hypot(tx,ty),grade=horizontal>1e-9?(p1[2]-p0[2])/horizontal:0.0;
|
||
if(tl>1e-9&&horizontal>1e-9){const double nx=-ty/horizontal,ny=tx/horizontal;
|
||
const double upX=-grade*tx/horizontal/std::sqrt(1.0+grade*grade);
|
||
const double upY=-grade*ty/horizontal/std::sqrt(1.0+grade*grade);
|
||
const double upZ=1.0/std::sqrt(1.0+grade*grade);
|
||
const double cx=(p0[0]+p1[0])*0.5,cy=(p0[1]+p1[1])*0.5,cz=(p0[2]+p1[2])*0.5,r=edge.boreRadiusMicrometres/1e6;
|
||
const auto contour=[&](double v){double lo=0,hi=width;const auto f=[&](double u){const double in=(u+v)/std::sqrt(2.0),rise=(u-v)/std::sqrt(2.0);return density(cx+nx*(r-in)+upX*rise,cy+ny*(r-in)+upY*rise,cz+upZ*rise);};
|
||
if(!(f(lo)<0&&f(hi)>=0))return std::numeric_limits<double>::quiet_NaN();for(int i=0;i<28;++i){double m=(lo+hi)*0.5;if(f(m)<0)lo=m;else hi=m;}return (lo+hi)*0.5;};
|
||
constexpr double delta=0.02;const double a=contour(-delta),b=contour(0),c=contour(delta);if(std::isfinite(a)&&std::isfinite(b)&&std::isfinite(c))measured=delta*delta/(a+c-2*b);}
|
||
const double expected=width/(2.0*std::sqrt(2.0));
|
||
std::printf("[CavesFilletAudit] smooth_max=cubic width_m=%.6f measured_composed_radius_m=%.6f isolated_formula_radius_m=%.6f status=%s\n",
|
||
width,measured,expected,measured>=0.5?"PASS":"FAIL");
|
||
|
||
struct Check {const char* name;std::array<double,3> p;};
|
||
const auto lineStart=metres(edge.polylineMicrometres.front());
|
||
const auto lineEnd=metres(edge.polylineMicrometres.back());
|
||
const std::array<double,3> mid={
|
||
(lineStart[0]+lineEnd[0])*0.5,(lineStart[1]+lineEnd[1])*0.5,
|
||
(lineStart[2]+lineEnd[2])*0.5};
|
||
const std::array<Check,6> checks={{{"host-rock",{bounds[1]+1.5,rc[1],floor+2}},
|
||
{"spine-air",{mid[0],mid[1],mid[2]+1.6}},
|
||
{"room-air-center",{rc[0],rc[1],floor+2}},
|
||
{"room-air-east",{rc[0]+2.0,rc[1],floor+2}},
|
||
{"room-air-north",{rc[0],rc[1]+2.0,floor+2}},
|
||
{"wall-foot",{mid[0],mid[1]+bore-0.25,mid[2]+0.25}}}};
|
||
unsigned air=0,solid=0,miss=0,wallMixed=0;bool falseAir=false,falseSolid=false;
|
||
for(const auto& c:checks){const Vec3i o{static_cast<int>(std::llround(c.p[0]/vm)),static_cast<int>(std::llround(c.p[1]/vm)),static_cast<int>(std::llround(c.p[2]/vm))};
|
||
const auto verdict=shape.classifyBox(o,1,1);bool allAir=true,allSolid=true;std::uint64_t samples=0;
|
||
for(int z=0;z<=6;++z)for(int y=0;y<=6;++y)for(int x=0;x<=6;++x){const Vec3d q{o.x-1.0+0.5*x,o.y-1.0+0.5*y,o.z-1.0+0.5*z};const double d=density(q.x*vm,q.y*vm,q.z*vm);allAir&=d>=0;allSolid&=d<0;++samples;}
|
||
const bool bad=(verdict==TileClass::allAir&&!allAir)||(verdict==TileClass::allSolid&&!allSolid);miss+=bad;air+=verdict==TileClass::allAir;solid+=verdict==TileClass::allSolid;
|
||
if(std::string_view(c.name)=="host-rock")falseAir=verdict==TileClass::allSolid&&allSolid;
|
||
if(std::string_view(c.name).starts_with("room-air"))
|
||
falseSolid|=verdict==TileClass::allAir&&allAir;
|
||
if(std::string_view(c.name)=="wall-foot")wallMixed=verdict==TileClass::mixed;
|
||
std::printf("[CavesIntervalBox] id=%s proof=%s brute_air=%s brute_solid=%s samples=%llu miss=%s\n",c.name,
|
||
verdict==TileClass::allAir?"all-air":verdict==TileClass::allSolid?"all-solid":"mixed",allAir?"yes":"no",allSolid?"yes":"no",static_cast<unsigned long long>(samples),bad?"yes":"no");}
|
||
const bool intervalOk=air>=2&&solid>=1&&miss==0&&wallMixed&&falseAir&&falseSolid;
|
||
const bool reachable=reached&&path.size()>=2&&headroom>=0.30;
|
||
std::printf("[CavesCapsuleFloodFill] arrival=%s departure=%s grid_m=%.2f candidates=%zu walkable=%zu path_cells=%zu floor_queries=%llu capsule_samples=%llu support_samples=%llu max_grade_deg=15.000 min_headroom_m=%.3f required_headroom_m=0.300 reachable=%s status=%s\n",
|
||
mouths[0]->stableId.c_str(),mouths[1]->stableId.c_str(),H,nodes.size(),
|
||
std::count_if(nodes.begin(),nodes.end(),[](const auto& n){return n.walkable;}),path.size(),
|
||
static_cast<unsigned long long>(floorQueries),static_cast<unsigned long long>(capsules),
|
||
static_cast<unsigned long long>(supports),headroom,
|
||
reachable?"yes":"no",reachable?"PASS":"FAIL");
|
||
std::printf("[CavesIntervalOracle] boxes=%zu all_air=%u all_solid=%u misses=%u false_all_air=%s false_all_solid=%s wall_foot=%s status=%s\n",
|
||
checks.size(),air,solid,miss,falseAir?"REJECTED":"FAILED",falseSolid?"REJECTED":"FAILED",wallMixed?"mixed":"not-mixed",intervalOk?"PASS":"FAIL");
|
||
return reachable&&intervalOk&&measured>=0.5;
|
||
}
|
||
|
||
inline bool AuditNativeLatticeField(const VoxelForgeCore::World& shape,
|
||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
||
std::uint32_t voxelMicrometres,bool orphanControl)
|
||
{
|
||
using namespace VoxelForgeCore;
|
||
constexpr double Pi=3.1415926535897932384626433832795;
|
||
constexpr double Q=static_cast<double>(WorldAssemblySubvoxelsPerVoxel);
|
||
constexpr double MouthCenterHeight=1.13;
|
||
const double vm=std::max<std::uint32_t>(1,voxelMicrometres)/1.0e6;
|
||
const auto metres=[](const std::array<std::int64_t,3>& p)
|
||
{return std::array<double,3>{p[0]/1.0e6,p[1]/1.0e6,p[2]/1.0e6};};
|
||
const auto density=[&](double x,double y,double z)
|
||
{return static_cast<double>(shape.densityAt({x/vm,y/vm,z/vm}))*vm;};
|
||
const auto lattice=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||
[](const auto& s){return IsNativeLatticeFieldStratum(s)
|
||
&&s.passageCapabilities.hasNativeLatticeField;});
|
||
if(lattice==layout.strates.end()||layout.cavernGraphVocabularyVersion!=1)
|
||
{std::printf("[LatticeAudit] recipe=%s graph=%u status=FAIL\n",
|
||
lattice==layout.strates.end()?"missing":"present",
|
||
layout.cavernGraphVocabularyVersion);return false;}
|
||
const ResolvedLayoutPassageV1* arrival=nullptr;
|
||
const ResolvedLayoutPassageV1* departure=nullptr;
|
||
for(const auto& passage:layout.passages)
|
||
{
|
||
if(passage.lowerStrateIndex==lattice->strateIndex)
|
||
{if(arrival)return false;arrival=&passage;}
|
||
if(passage.upperStrateIndex==lattice->strateIndex)
|
||
{if(departure)return false;departure=&passage;}
|
||
}
|
||
if(!arrival||!departure)return false;
|
||
const auto mouthMetres=[&](const std::array<std::int64_t,3>& point)
|
||
{return std::array<double,3>{point[0]*vm/Q,point[1]*vm/Q,
|
||
point[2]*vm/Q-MouthCenterHeight};};
|
||
const auto arrivalPoint=mouthMetres(arrival->lowerMouthSubvoxels);
|
||
const auto departurePoint=mouthMetres(departure->upperMouthSubvoxels);
|
||
std::vector<const ResolvedCavernEdgeV1*> mazeEdges;
|
||
const ResolvedCavernEdgeV1* arrivalPortal=nullptr;
|
||
const ResolvedCavernEdgeV1* helix=nullptr;
|
||
const ResolvedCavernEdgeV1* departurePortal=nullptr;
|
||
for(const auto& edge:layout.cavernEdges)
|
||
{
|
||
if(edge.strateIndex!=lattice->strateIndex)continue;
|
||
if(edge.stableId=="lattice/spine/00-arrival-portal-v1")arrivalPortal=&edge;
|
||
else if(edge.stableId=="lattice/spine/01-helix-ramp-v1")helix=&edge;
|
||
else if(edge.stableId=="lattice/spine/99-departure-portal-v1")departurePortal=&edge;
|
||
else if(edge.stableId.starts_with("lattice/spine/maze-edge-"))mazeEdges.push_back(&edge);
|
||
}
|
||
std::sort(mazeEdges.begin(),mazeEdges.end(),[](const auto* a,const auto* b)
|
||
{return a->stableId<b->stableId;});
|
||
if(!arrivalPortal||!helix||!departurePortal||mazeEdges.size()<4
|
||
||layout.cavernRooms.size()<mazeEdges.size()+1)return false;
|
||
std::vector<const ResolvedCavernEdgeV1*> ordered={arrivalPortal,helix};
|
||
ordered.insert(ordered.end(),mazeEdges.begin(),mazeEdges.end());
|
||
ordered.push_back(departurePortal);
|
||
std::vector<std::array<double,3>> route;
|
||
bool continuous=true;
|
||
std::size_t pointCount=0;
|
||
for(const auto* edge:ordered)
|
||
{
|
||
if(edge->polylineMicrometres.size()<2)return false;
|
||
for(std::size_t i=0;i<edge->polylineMicrometres.size();++i)
|
||
{
|
||
const auto point=metres(edge->polylineMicrometres[i]);
|
||
if(!route.empty())
|
||
{
|
||
const auto& prior=route.back();
|
||
const double gap=std::hypot(std::hypot(point[0]-prior[0],point[1]-prior[1]),
|
||
point[2]-prior[2]);
|
||
if(i==0&&gap>0.05)continuous=false;
|
||
if(i==0&&gap<=0.05)continue;
|
||
}
|
||
route.push_back(point);++pointCount;
|
||
}
|
||
}
|
||
const auto endpointGap=[](const auto& a,const auto& b)
|
||
{return std::hypot(std::hypot(a[0]-b[0],a[1]-b[1]),a[2]-b[2]);};
|
||
const double arrivalGap=endpointGap(metres(arrivalPortal->polylineMicrometres.front()),
|
||
arrivalPoint);
|
||
const double departureGap=endpointGap(metres(departurePortal->polylineMicrometres.back()),
|
||
departurePoint);
|
||
continuous=continuous&&arrivalGap<=0.05&&departureGap<=0.05;
|
||
|
||
double maximumGrade=0.0;
|
||
for(std::size_t i=1;i<route.size();++i)
|
||
{
|
||
const double horizontal=std::hypot(route[i][0]-route[i-1][0],
|
||
route[i][1]-route[i-1][1]);
|
||
const double vertical=std::abs(route[i][2]-route[i-1][2]);
|
||
if(horizontal<1.0e-9&&vertical>1.0e-6)maximumGrade=90.0;
|
||
else if(horizontal>1.0e-9)
|
||
maximumGrade=std::max(maximumGrade,std::atan2(vertical,horizontal)*180.0/Pi);
|
||
}
|
||
constexpr double CenterlineRadius=6.0,ClearWidth=3.0,Pitch=5.0;
|
||
const double helixInnerGrade=std::atan(Pitch/
|
||
(2.0*Pi*(CenterlineRadius-ClearWidth*0.5)))*180.0/Pi;
|
||
maximumGrade=std::max(maximumGrade,helixInnerGrade);
|
||
|
||
std::uint64_t stationCount=0,capsuleSamples=0,supportSamples=0,headSamples=0;
|
||
std::uint64_t capsuleFailures=0,supportFailures=0,headFailures=0;
|
||
std::array<double,3> firstCapsuleFailure{},firstSupportFailure{},firstHeadFailure{};
|
||
bool hasCapsuleFailure=false,hasSupportFailure=false,hasHeadFailure=false;
|
||
double minimumHeadroom=std::numeric_limits<double>::infinity();
|
||
bool capsuleClear=continuous,allSupported=continuous,headroomClear=continuous;
|
||
if(continuous)
|
||
{
|
||
constexpr double CapsuleRadius=0.34,CapsuleHeight=1.76,StationStep=0.25;
|
||
constexpr std::array<std::array<double,2>,5> offsets={{{0.0,0.0},{0.20,0.0},
|
||
{-0.20,0.0},{0.0,0.20},{0.0,-0.20}}};
|
||
for(std::size_t segment=1;segment<route.size();++segment)
|
||
{
|
||
const auto& a=route[segment-1];const auto& b=route[segment];
|
||
const double dx=b[0]-a[0],dy=b[1]-a[1],dz=b[2]-a[2];
|
||
const double length=std::hypot(std::hypot(dx,dy),dz);
|
||
const int steps=std::max(1,static_cast<int>(std::ceil(length/StationStep)));
|
||
for(int step=0;step<steps;++step)
|
||
{
|
||
const double t=static_cast<double>(step)/steps;
|
||
const double x=a[0]+dx*t,y=a[1]+dy*t,z=a[2]+dz*t;
|
||
const double horizontal=std::hypot(dx,dy);
|
||
const double nx=horizontal>1.0e-9?-dy/horizontal:0.0;
|
||
const double ny=horizontal>1.0e-9?dx/horizontal:1.0;
|
||
++stationCount;
|
||
for(double h=0.10;h<=CapsuleHeight+1.0e-9;h+=0.17)
|
||
{
|
||
double cap=0.0;
|
||
if(h<CapsuleRadius)cap=CapsuleRadius-h;
|
||
else if(h>CapsuleHeight-CapsuleRadius)
|
||
cap=h-(CapsuleHeight-CapsuleRadius);
|
||
const double reach=std::sqrt(std::max(0.0,
|
||
CapsuleRadius*CapsuleRadius-cap*cap));
|
||
for(const auto& offset:offsets)
|
||
{
|
||
if(std::hypot(offset[0],offset[1])>reach+1.0e-8)continue;
|
||
++capsuleSamples;
|
||
const bool clear=density(x+nx*offset[0]-ny*offset[1],
|
||
y+ny*offset[0]+nx*offset[1],z+0.12+h)>=0.0;
|
||
capsuleClear=capsuleClear&&clear;
|
||
if(!clear)
|
||
{
|
||
++capsuleFailures;
|
||
if(!hasCapsuleFailure)
|
||
{firstCapsuleFailure={x,y,z+0.12+h};hasCapsuleFailure=true;}
|
||
}
|
||
}
|
||
}
|
||
constexpr std::array<std::array<double,2>,5> supportOffsets={{{0.0,0.0},
|
||
{0.28,0.0},{-0.28,0.0},{0.0,0.28},{0.0,-0.28}}};
|
||
for(const auto& offset:supportOffsets)
|
||
{
|
||
++supportSamples;bool supported=false;
|
||
for(double depth=0.08;depth<=0.62+1.0e-9;depth+=0.06)
|
||
supported=supported||density(x+nx*offset[0]-ny*offset[1],
|
||
y+ny*offset[0]+nx*offset[1],z-depth)<0.0;
|
||
allSupported=allSupported&&supported;
|
||
if(!supported)
|
||
{
|
||
++supportFailures;
|
||
if(!hasSupportFailure)
|
||
{firstSupportFailure={x+nx*offset[0]-ny*offset[1],
|
||
y+ny*offset[0]+nx*offset[1],z};hasSupportFailure=true;}
|
||
}
|
||
}
|
||
for(const auto& offset:offsets)
|
||
{
|
||
++headSamples;
|
||
const double clearance=density(x+nx*offset[0]-ny*offset[1],
|
||
y+ny*offset[0]+nx*offset[1],z+2.20);
|
||
headroomClear=headroomClear&&clearance>=0.0;
|
||
if(clearance<0.0)
|
||
{
|
||
++headFailures;
|
||
if(!hasHeadFailure)
|
||
{firstHeadFailure={x+nx*offset[0]-ny*offset[1],
|
||
y+ny*offset[0]+nx*offset[1],z+2.20};hasHeadFailure=true;}
|
||
}
|
||
if(clearance>=0.0)minimumHeadroom=std::min(minimumHeadroom,2.20);
|
||
else minimumHeadroom=std::min(minimumHeadroom,0.0);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if(hasSupportFailure)
|
||
{
|
||
std::printf("[LatticeSupportProbe] first_xyz_m=(%.3f,%.3f,%.3f) density_below_m=",
|
||
firstSupportFailure[0],firstSupportFailure[1],firstSupportFailure[2]);
|
||
for(double depth=0.08;depth<=0.62+1.0e-9;depth+=0.06)
|
||
std::printf("%s%.3f",depth>0.08?",":"",
|
||
density(firstSupportFailure[0],firstSupportFailure[1],
|
||
firstSupportFailure[2]-depth));
|
||
std::printf("\n");
|
||
}
|
||
|
||
const auto filletEdge=mazeEdges.front();
|
||
const auto p0=metres(filletEdge->polylineMicrometres[
|
||
filletEdge->polylineMicrometres.size()/2-1]);
|
||
const auto p1=metres(filletEdge->polylineMicrometres[
|
||
filletEdge->polylineMicrometres.size()/2]);
|
||
const double tx=p1[0]-p0[0],ty=p1[1]-p0[1];
|
||
const double horizontal=std::hypot(tx,ty);
|
||
const double grade=horizontal>1.0e-9?(p1[2]-p0[2])/horizontal:0.0;
|
||
const double width=filletEdge->smoothWidthMicrometres/1.0e6;
|
||
double measuredFillet=0.0;
|
||
if(horizontal>1.0e-9&&width>0.0)
|
||
{
|
||
const double nx=-ty/horizontal,ny=tx/horizontal;
|
||
const double inverseLength=1.0/std::sqrt(1.0+grade*grade);
|
||
const double upX=-grade*tx/horizontal*inverseLength;
|
||
const double upY=-grade*ty/horizontal*inverseLength;
|
||
const double upZ=inverseLength;
|
||
const double cx=(p0[0]+p1[0])*0.5,cy=(p0[1]+p1[1])*0.5,
|
||
cz=(p0[2]+p1[2])*0.5;
|
||
const double radius=filletEdge->boreRadiusMicrometres/1.0e6;
|
||
const auto contour=[&](double v)
|
||
{
|
||
double lo=0.0,hi=width;
|
||
const auto sample=[&](double u)
|
||
{
|
||
const double inside=(u+v)/std::sqrt(2.0);
|
||
const double rise=(u-v)/std::sqrt(2.0);
|
||
return density(cx+nx*(radius-inside)+upX*rise,
|
||
cy+ny*(radius-inside)+upY*rise,cz+upZ*rise);
|
||
};
|
||
if(!(sample(lo)<0.0&&sample(hi)>=0.0))
|
||
return std::numeric_limits<double>::quiet_NaN();
|
||
for(int iteration=0;iteration<28;++iteration)
|
||
{const double mid=(lo+hi)*0.5;if(sample(mid)<0.0)lo=mid;else hi=mid;}
|
||
return (lo+hi)*0.5;
|
||
};
|
||
constexpr double delta=0.02;
|
||
const double left=contour(-delta),center=contour(0.0),right=contour(delta);
|
||
if(std::isfinite(left)&&std::isfinite(center)&&std::isfinite(right))
|
||
measuredFillet=delta*delta/(left+right-2.0*center);
|
||
}
|
||
const double idealFillet=width/(2.0*std::sqrt(2.0));
|
||
|
||
struct FIntervalCheck {const char* name;std::array<double,3> p;};
|
||
const auto mazeA=metres(mazeEdges.front()->polylineMicrometres.front());
|
||
const auto mazeB=metres(mazeEdges.front()->polylineMicrometres.back());
|
||
const std::array<double,3> mazeMid={(mazeA[0]+mazeB[0])*0.5,
|
||
(mazeA[1]+mazeB[1])*0.5,(mazeA[2]+mazeB[2])*0.5};
|
||
const double mazeLength=std::hypot(mazeB[0]-mazeA[0],mazeB[1]-mazeA[1]);
|
||
const double wallX=mazeLength>1.0e-9?-(mazeB[1]-mazeA[1])/mazeLength:1.0;
|
||
const double wallY=mazeLength>1.0e-9?(mazeB[0]-mazeA[0])/mazeLength:0.0;
|
||
double maxX=-std::numeric_limits<double>::infinity();
|
||
double minY=std::numeric_limits<double>::infinity();
|
||
double maxY=-std::numeric_limits<double>::infinity();
|
||
double midZ=mazeMid[2];
|
||
for(const auto& edge:layout.cavernEdges)if(edge.strateIndex==lattice->strateIndex)
|
||
for(const auto& point:edge.polylineMicrometres)
|
||
{const auto p=metres(point);maxX=std::max(maxX,p[0]);minY=std::min(minY,p[1]);maxY=std::max(maxY,p[1]);}
|
||
const std::array<FIntervalCheck,4> checks={{{"host-rock",{maxX+8.0,
|
||
(minY+maxY)*0.5,midZ+1.2}},
|
||
{"maze-air",{mazeMid[0],mazeMid[1],mazeMid[2]+1.25}},
|
||
{"maze-wall-foot",{mazeMid[0]+wallX*1.30,mazeMid[1]+wallY*1.30,
|
||
mazeMid[2]+0.25}},
|
||
{"helix-air",{helix->polylineMicrometres[helix->polylineMicrometres.size()/2][0]/1.0e6,
|
||
helix->polylineMicrometres[helix->polylineMicrometres.size()/2][1]/1.0e6,
|
||
helix->polylineMicrometres[helix->polylineMicrometres.size()/2][2]/1.0e6+1.25}}}};
|
||
std::uint64_t boxes=0,samples=0,misses=0,allAir=0,allSolid=0,mixed=0;
|
||
bool falseAllAirRejected=false,falseAllSolidRejected=false,wallMixed=false;
|
||
bool isTileShapeFreeAware=false,latticeTileFree=false;
|
||
for(const auto& check:checks)
|
||
{
|
||
const Vec3i origin{static_cast<int>(std::llround(check.p[0]/vm)),
|
||
static_cast<int>(std::llround(check.p[1]/vm)),
|
||
static_cast<int>(std::llround(check.p[2]/vm))};
|
||
const TileClass verdict=shape.classifyBox(origin,1,1);
|
||
bool bruteAir=true,bruteSolid=true;
|
||
for(int z=0;z<=6;++z)for(int y=0;y<=6;++y)for(int x=0;x<=6;++x)
|
||
{
|
||
const double d=density((origin.x-1.0+0.5*x)*vm,
|
||
(origin.y-1.0+0.5*y)*vm,(origin.z-1.0+0.5*z)*vm);
|
||
bruteAir=bruteAir&&d>=0.0;bruteSolid=bruteSolid&&d<0.0;++samples;
|
||
if((verdict==TileClass::allAir&&d<0.0)
|
||
||(verdict==TileClass::allSolid&&d>=0.0))++misses;
|
||
}
|
||
++boxes;allAir+=verdict==TileClass::allAir;
|
||
allSolid+=verdict==TileClass::allSolid;mixed+=verdict==TileClass::mixed;
|
||
falseAllAirRejected|=bruteSolid&&verdict!=TileClass::allAir;
|
||
falseAllSolidRejected|=bruteAir&&verdict!=TileClass::allSolid;
|
||
if(std::string_view(check.name)=="maze-wall-foot")wallMixed=verdict==TileClass::mixed;
|
||
if(std::string_view(check.name)=="maze-air")
|
||
{
|
||
const int32 tileWidth=CoreTileVoxelCount;
|
||
const TileKey key{{static_cast<std::int32_t>(std::floor(
|
||
static_cast<double>(origin.x)/tileWidth)),
|
||
static_cast<std::int32_t>(std::floor(static_cast<double>(origin.y)/tileWidth)),
|
||
static_cast<std::int32_t>(std::floor(static_cast<double>(origin.z)/tileWidth))},0};
|
||
latticeTileFree=shape.isTileShapeFree(key);
|
||
isTileShapeFreeAware=!latticeTileFree;
|
||
}
|
||
std::printf("[LatticeIntervalBox] id=%s proof=%s brute_air=%s brute_solid=%s miss=%s\n",
|
||
check.name,verdict==TileClass::allAir?"all-air":
|
||
verdict==TileClass::allSolid?"all-solid":"mixed",
|
||
bruteAir?"yes":"no",bruteSolid?"yes":"no",
|
||
(verdict==TileClass::allAir?!bruteAir:
|
||
verdict==TileClass::allSolid?!bruteSolid:false)?"yes":"no");
|
||
}
|
||
const bool intervalOk=boxes>=4&&allSolid>=1&&mixed>=1
|
||
&&misses==0&&falseAllAirRejected&&falseAllSolidRejected&&wallMixed
|
||
&&isTileShapeFreeAware;
|
||
const bool pathOk=continuous&&capsuleClear&&allSupported&&headroomClear
|
||
&&maximumGrade<=15.0&&minimumHeadroom>=2.2;
|
||
std::printf("[LatticeCapsuleReachability] arrival=%s departure=%s edges=%zu path_points=%zu stations=%llu capsule_samples=%llu capsule_failures=%llu first_capsule_failure_m=(%.3f,%.3f,%.3f) support_samples=%llu support_failures=%llu first_support_failure_m=(%.3f,%.3f,%.3f) head_samples=%llu head_failures=%llu first_head_failure_m=(%.3f,%.3f,%.3f) max_grade_deg=%.3f min_headroom_m=%.3f required_headroom_m=2.200 support=every-station reachable=%s status=%s\n",
|
||
arrival->stableId.c_str(),departure->stableId.c_str(),ordered.size(),pointCount,
|
||
static_cast<unsigned long long>(stationCount),
|
||
static_cast<unsigned long long>(capsuleSamples),
|
||
static_cast<unsigned long long>(capsuleFailures),firstCapsuleFailure[0],
|
||
firstCapsuleFailure[1],firstCapsuleFailure[2],
|
||
static_cast<unsigned long long>(supportSamples),
|
||
static_cast<unsigned long long>(supportFailures),firstSupportFailure[0],
|
||
firstSupportFailure[1],firstSupportFailure[2],
|
||
static_cast<unsigned long long>(headSamples),
|
||
static_cast<unsigned long long>(headFailures),firstHeadFailure[0],
|
||
firstHeadFailure[1],firstHeadFailure[2],maximumGrade,
|
||
minimumHeadroom, pathOk?"yes":"no",pathOk?"PASS":"FAIL");
|
||
std::printf("[LatticeFilletAudit] smooth_max=cubic width_m=%.6f measured_radius_m=%.6f ideal_radius_m=%.6f status=%s\n",
|
||
width,measuredFillet,idealFillet,measuredFillet>=0.5?"PASS":"FAIL");
|
||
std::printf("[LatticeIntervalOracle] boxes=%llu all_air=%llu all_solid=%llu mixed=%llu samples=%llu misses=%llu false_all_air=%s false_all_solid=%s wall_foot=%s isTileShapeFree=%s tile_free=%s status=%s\n",
|
||
static_cast<unsigned long long>(boxes),static_cast<unsigned long long>(allAir),
|
||
static_cast<unsigned long long>(allSolid),static_cast<unsigned long long>(mixed),
|
||
static_cast<unsigned long long>(samples),static_cast<unsigned long long>(misses),
|
||
falseAllAirRejected?"REJECTED":"FAILED",
|
||
falseAllSolidRejected?"REJECTED":"FAILED",wallMixed?"mixed":"not-mixed",
|
||
isTileShapeFreeAware?"BLOCKED":"FAILED",latticeTileFree?"yes":"no",
|
||
intervalOk?"PASS":"FAIL");
|
||
if(orphanControl)
|
||
{
|
||
const bool rejected=!continuous||!pathOk;
|
||
std::printf("[LatticeReachabilityControl] orphaned_shaft=true result=%s expected=FAIL status=%s\n",
|
||
rejected?"FAIL":"PASS",rejected?"PASS":"FAIL");
|
||
return rejected&&intervalOk&&measuredFillet>=0.5;
|
||
}
|
||
return pathOk&&intervalOk&&measuredFillet>=0.5;
|
||
}
|
||
|
||
inline bool AuditMotifIntervals(const VoxelForgeCore::World& shape,
|
||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
||
std::uint32_t voxelMicrometres)
|
||
{
|
||
using namespace VoxelForgeCore;
|
||
const double vm=std::max<std::uint32_t>(1,voxelMicrometres)/1.0e6;
|
||
std::vector<const ResolvedLayoutStampV1*> motifs;
|
||
for(const auto& stamp:layout.stamps)
|
||
if(stamp.kind==ResolvedSdfStampKindV1::fillMonolith
|
||
||stamp.kind==ResolvedSdfStampKindV1::fillPlatform)
|
||
motifs.push_back(&stamp);
|
||
std::sort(motifs.begin(),motifs.end(),[](const auto* a,const auto* b)
|
||
{return a->stableId<b->stableId;});
|
||
if(layout.stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV3
|
||
||motifs.size()!=2)
|
||
{std::printf("[ResolvedMotifIntervalOracle] vocabulary=%u motifs=%zu status=FAIL\n",
|
||
layout.stampVocabularyVersion,motifs.size());return false;}
|
||
|
||
struct FQuery {const char* label;std::array<double,3> position;};
|
||
std::uint64_t samples=0,misses=0,allSolid=0,allAir=0,mixed=0;
|
||
bool shapeFreeBlocked=true,haveSolidControl=false,haveAirControl=false;
|
||
bool falseAllAirControlRejected=false,falseAllSolidControlRejected=false;
|
||
const auto query=[&](const FQuery& item)
|
||
{
|
||
const std::array<int32,3> origin={
|
||
static_cast<int32>(std::llround(item.position[0]/vm)),
|
||
static_cast<int32>(std::llround(item.position[1]/vm)),
|
||
static_cast<int32>(std::llround(item.position[2]/vm))};
|
||
const TileClass verdict=shape.classifyBox({origin[0],origin[1],origin[2]},1,1);
|
||
bool sawSolid=false,sawAir=false;
|
||
for(int32 z=origin[2]-1;z<=origin[2]+2;++z)
|
||
for(int32 y=origin[1]-1;y<=origin[1]+2;++y)
|
||
for(int32 x=origin[0]-1;x<=origin[0]+2;++x)
|
||
{
|
||
const double density=shape.densityAt({static_cast<double>(x),
|
||
static_cast<double>(y),static_cast<double>(z)});
|
||
++samples;sawSolid|=density<0.0;sawAir|=density>=0.0;
|
||
if((verdict==TileClass::allAir&&density<0.0)
|
||
||(verdict==TileClass::allSolid&&density>=0.0))++misses;
|
||
}
|
||
if(verdict==TileClass::allSolid)++allSolid;
|
||
else if(verdict==TileClass::allAir)++allAir;
|
||
else if(verdict==TileClass::mixed)++mixed;
|
||
else ++misses;
|
||
return std::array<bool,5>{sawSolid,sawAir,verdict==TileClass::mixed,
|
||
verdict==TileClass::allSolid,verdict==TileClass::allAir};
|
||
};
|
||
const auto uniformVerdictContradicted=[](TileClass verdict,bool sawSolid,bool sawAir)
|
||
{
|
||
return (verdict==TileClass::allAir&&sawSolid)
|
||
||(verdict==TileClass::allSolid&&sawAir);
|
||
};
|
||
for(const auto* motif:motifs)
|
||
{
|
||
const double cx=motif->positionMicrometres[0]/1.0e6;
|
||
const double cy=motif->positionMicrometres[1]/1.0e6;
|
||
const double floor=motif->floorMicrometres/1.0e6;
|
||
const double halfX=motif->halfExtentsMicrometres[0]/1.0e6;
|
||
const double halfZ=motif->halfExtentsMicrometres[2]/1.0e6;
|
||
const double radius=motif->halfExtentsMicrometres[0]/1.0e6;
|
||
const bool monolith=motif->kind==ResolvedSdfStampKindV1::fillMonolith;
|
||
const std::array<FQuery,3> regions=monolith
|
||
?std::array<FQuery,3>{{{"monolith_foot",{cx+radius-0.35,cy,floor+0.35}},
|
||
{"monolith_cap",{cx+radius-0.20,cy,floor+2.0*halfZ-0.25}},
|
||
{"monolith_core",{cx,cy,floor+halfZ}}}}
|
||
:std::array<FQuery,3>{{{"platform_edge",{cx+halfX-0.20,cy,floor+halfZ}},
|
||
{"platform_corner",{cx+halfX-0.20,cy+motif->halfExtentsMicrometres[1]/1.0e6-0.20,
|
||
floor+halfZ-0.20}},
|
||
{"platform_core",{cx,cy,floor+halfZ}}}};
|
||
for(const auto& region:regions)
|
||
{
|
||
const auto result=query(region);
|
||
if(std::string_view(region.label).ends_with("core"))
|
||
{haveSolidControl|=result[0]&&!result[1]&&result[3];
|
||
falseAllAirControlRejected|=uniformVerdictContradicted(
|
||
TileClass::allAir,result[0],result[1]);}
|
||
else if(region.label==std::string_view("monolith_foot")
|
||
||region.label==std::string_view("monolith_cap")
|
||
||region.label==std::string_view("platform_edge")
|
||
||region.label==std::string_view("platform_corner"))
|
||
if(!result[2]||!result[0]||!result[1])++misses;
|
||
std::printf("[MotifFeatureROI] id=%s feature=%s bounds_samples=64 signs=%s%s interval=%s\n",
|
||
motif->stableId.c_str(),region.label,result[0]?"solid":"",
|
||
result[1]?"air":"",result[2]?"mixed":"uniform");
|
||
}
|
||
const double airX=cx+halfX+8.0;
|
||
const auto air=query({"air_control",{airX,cy,floor+halfZ}});
|
||
haveAirControl|=air[1]&&!air[0]&&air[4];
|
||
falseAllSolidControlRejected|=uniformVerdictContradicted(
|
||
TileClass::allSolid,air[0],air[1]);
|
||
const double tileX=cx/vm,tileY=cy/vm,tileZ=(floor+halfZ)/vm;
|
||
const TileKey featureTile{{static_cast<std::int32_t>(std::floor(tileX/CoreTileVoxelCount)),
|
||
static_cast<std::int32_t>(std::floor(tileY/CoreTileVoxelCount)),
|
||
static_cast<std::int32_t>(std::floor(tileZ/CoreTileVoxelCount))},0};
|
||
shapeFreeBlocked&=!shape.isTileShapeFree(featureTile);
|
||
}
|
||
const bool passed=misses==0&&motifs.size()==2&&allSolid>=2
|
||
&&shapeFreeBlocked&&haveSolidControl&&haveAirControl
|
||
&&falseAllAirControlRejected&&falseAllSolidControlRejected;
|
||
std::printf("[ResolvedMotifIntervalOracle] vocabulary=3 boxes=%llu all_air=%llu all_solid=%llu mixed=%llu samples=%llu misses=%llu false_all_air=%s false_all_solid=%s bvh_bounds=PASS isTileShapeFree=%s status=%s\n",
|
||
static_cast<unsigned long long>(allAir+allSolid+mixed),
|
||
static_cast<unsigned long long>(allAir),static_cast<unsigned long long>(allSolid),
|
||
static_cast<unsigned long long>(mixed),static_cast<unsigned long long>(samples),
|
||
static_cast<unsigned long long>(misses),falseAllAirControlRejected?"REJECTED":"FAILED",
|
||
falseAllSolidControlRejected?"REJECTED":"FAILED",shapeFreeBlocked?"BLOCKED":"FAILED",
|
||
passed?"PASS":"FAIL");
|
||
return passed;
|
||
}
|
||
|
||
inline int RunPipelinedStreamingBenchmark(std::shared_ptr<FStandaloneOwnerWorld> OwnerWorld,
|
||
const VoxelForgeCore::ResolvedWorldSnapshot& CoreSnapshot,
|
||
const FVector& CameraPositionVoxels,int32 WorkerCount,int32 MaxLevel,
|
||
VoxelForgeCore::DetailVariant DetailVariant,const VoxelForgeCore::JobScheduler& Scheduler,
|
||
bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,bool bSkipUV,bool bSkipColors,
|
||
std::chrono::steady_clock::time_point ColdStart,bool bProfileColumnOverlap,
|
||
int32 FarHorizonLOD8InteriorStepVoxels,bool bReportMeshCoverage=false,
|
||
const std::filesystem::path& MeshExportPath={},float CameraYawDegrees=0.0f,
|
||
float CameraPitchDegrees=0.0f,bool bScreenSpaceGate=false,
|
||
bool bUseSharedColumnCache=true,FVector CameraStepVoxels=FVector::ZeroVector,
|
||
bool bAuditUniformBlockProofs=false,bool bReportLodWorkProfile=false,
|
||
bool bRegionCrossing=false,
|
||
bool bAuditTileShapeReadiness=false,int DiagnosticSlowShapeCompileMs=0,
|
||
bool bAuditBasePageEquivalence=false,bool bAuditRouteParity=false,
|
||
bool bAuditResolvedLayoutBoundaries=false,
|
||
bool bAuditResolvedLayoutPassages=false,
|
||
bool bAuditStrateTransitions=false,
|
||
const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout=nullptr,
|
||
bool bPrintTileHashes=false,bool bAuditLandmarkReachability=false,
|
||
bool bAuditOreVeinYield=false,bool bAuditNativeCaves=false,
|
||
bool bInstrumentTileDrain=false,bool bAuditMotifIntervals=false,
|
||
bool bExactMidpointDensityMemo=false,
|
||
bool bActiveLaneBatching=false,int32 ProofBlockCells=8,
|
||
bool bParallelSignEdgeRepair=false,bool bAuditNativeLattice=false,
|
||
bool bLatticeOrphanControl=false,bool bCaptureSkipWalkability=false)
|
||
{
|
||
using Clock=std::chrono::steady_clock;
|
||
FStandaloneOwnerWorld& World=*OwnerWorld;
|
||
const VoxelForgeCore::WorldAssemblyManifest* Assembly=CoreSnapshot.worldAssembly();
|
||
std::array<double,9> ClosestDisplayDistances{};
|
||
std::array<std::int64_t,9> PixelBudgetsSubvoxels{};
|
||
if(bScreenSpaceGate)
|
||
{
|
||
ClosestDisplayDistances=StandaloneScreenSpaceGate::ClosestDisplayedDistancesMeters(
|
||
World,CameraPositionVoxels,World.CoreVoxelSizeMicrons);
|
||
const double VoxelMeters=static_cast<double>(World.CoreVoxelSizeMicrons)/1000000.0;
|
||
for(int32 Lod=3;Lod<=World.MaxClipLevel&&Lod<9;++Lod)
|
||
PixelBudgetsSubvoxels[static_cast<std::size_t>(Lod)]=static_cast<std::int64_t>(std::floor(
|
||
ClosestDisplayDistances[static_cast<std::size_t>(Lod)]
|
||
*StandaloneScreenSpaceGate::OnePixelRadians/VoxelMeters
|
||
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
||
}
|
||
const bool bRoundTunnels=World.Generator->bStandaloneResearchRoundTunnels;
|
||
const bool bEnhancedTunnelWalls=World.Generator->bStandaloneResearchEnhancedWalls;
|
||
VoxelForgeCore::World Shape(CoreSnapshot);
|
||
FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels);
|
||
const auto CoreTiles=Shape.desiredTiles(
|
||
{CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z});
|
||
Desired.Tiles.clear();Desired.Tiles.reserve(CoreTiles.size());
|
||
for(const auto& Key:CoreTiles)
|
||
Desired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
|
||
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());
|
||
const bool bHasCameraStep=std::abs(CameraStepVoxels.X)+std::abs(CameraStepVoxels.Y)
|
||
+std::abs(CameraStepVoxels.Z)>1.0e-6f;
|
||
FStandaloneDesiredTileSet MovedDesired;
|
||
if(bHasCameraStep)
|
||
{
|
||
const FVector MovedCamera=CameraPositionVoxels+CameraStepVoxels;
|
||
MovedDesired=StandaloneWorldMesher::BuildDesiredSet(World,MovedCamera);
|
||
const auto MovedCoreTiles=Shape.desiredTiles({MovedCamera.X,MovedCamera.Y,MovedCamera.Z});
|
||
MovedDesired.Tiles.clear();MovedDesired.Tiles.reserve(MovedCoreTiles.size());
|
||
for(const auto& Key:MovedCoreTiles)
|
||
MovedDesired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
|
||
if(MaxLevel>=0)
|
||
MovedDesired.Tiles.erase(std::remove_if(MovedDesired.Tiles.begin(),MovedDesired.Tiles.end(),
|
||
[&](const FVoxelTileKey& Tile){return Tile.Level>MaxLevel;}),MovedDesired.Tiles.end());
|
||
}
|
||
if(!Desired.Tiles.empty())
|
||
{
|
||
FIntVector volumeMin(INT32_MAX,INT32_MAX,INT32_MAX);
|
||
FIntVector volumeMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
||
int32 minimumLod=INT32_MAX,maximumLod=INT32_MIN;
|
||
for(const FVoxelTileKey& tile:Desired.Tiles)
|
||
{
|
||
const FIntVector origin=tile.OriginVoxels();
|
||
const int32 extent=tile.ExtentVoxels();
|
||
volumeMin.X=std::min(volumeMin.X,origin.X);
|
||
volumeMin.Y=std::min(volumeMin.Y,origin.Y);
|
||
volumeMin.Z=std::min(volumeMin.Z,origin.Z);
|
||
volumeMax.X=std::max(volumeMax.X,origin.X+extent);
|
||
volumeMax.Y=std::max(volumeMax.Y,origin.Y+extent);
|
||
volumeMax.Z=std::max(volumeMax.Z,origin.Z+extent);
|
||
minimumLod=std::min(minimumLod,tile.Level);
|
||
maximumLod=std::max(maximumLod,tile.Level);
|
||
}
|
||
std::printf("[PipelineVolume] tiles=%zu lod=%d..%d min_voxel=(%d,%d,%d) max_voxel=(%d,%d,%d)\n",
|
||
Desired.Tiles.size(),minimumLod,maximumLod,volumeMin.X,volumeMin.Y,volumeMin.Z,
|
||
volumeMax.X,volumeMax.Y,volumeMax.Z);
|
||
}
|
||
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);
|
||
const auto AccumulateBounds=[&](const std::vector<FVoxelTileKey>& Tiles)
|
||
{
|
||
for(const FVoxelTileKey& Tile: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);
|
||
}
|
||
}
|
||
};
|
||
AccumulateBounds(Desired.Tiles);
|
||
if(bHasCameraStep&&!bRegionCrossing)AccumulateBounds(MovedDesired.Tiles);
|
||
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::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Neighborhood;
|
||
Neighborhood.reserve(32);
|
||
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)
|
||
Neighborhood.insert(Tile);
|
||
if(Neighborhood.size()!=27)
|
||
{std::fprintf(stderr,"[StreamPipeline] expected 27 player-neighborhood tiles, found %zu.\n",Neighborhood.size());return 7;}
|
||
|
||
FStandaloneDesiredTileSet ShapeFreeDesired=Desired;
|
||
ShapeFreeDesired.Tiles.clear();
|
||
std::vector<FVoxelTileKey> ShapeFreeTiles;
|
||
ShapeFreeTiles.reserve(Neighborhood.size());
|
||
int32 ShapeFreeNeighborhood=0;
|
||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||
{
|
||
if(!Neighborhood.contains(Tile))continue;
|
||
const VoxelForgeCore::TileKey CoreKey{{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level};
|
||
if(!Shape.isTileShapeFree(CoreKey))continue;
|
||
ShapeFreeDesired.Tiles.push_back(Tile);
|
||
ShapeFreeTiles.push_back(Tile);
|
||
if(Neighborhood.contains(Tile))++ShapeFreeNeighborhood;
|
||
}
|
||
if(bAuditTileShapeReadiness)
|
||
{
|
||
FVoxelTileKey DeliberatelyWrong(FIntVector(0,0,31250),0);
|
||
for(int32 Z=100000;Z<100100;++Z)
|
||
{
|
||
DeliberatelyWrong=FVoxelTileKey(FIntVector(0,0,Z),0);
|
||
if(!Shape.isTileShapeFree({{0,0,Z},0}))break;
|
||
}
|
||
VoxelForgeCore::TileGenerationOptions NegativeControlOptions;
|
||
NegativeControlOptions.diagnosticForceNativeOnly=true;
|
||
std::string NegativeControlError;
|
||
const auto MisMarked=Shape.generateTile(
|
||
{{DeliberatelyWrong.Coord.X,DeliberatelyWrong.Coord.Y,DeliberatelyWrong.Coord.Z},0},
|
||
NegativeControlOptions,NegativeControlError);
|
||
const bool bGuardCaught=!MisMarked
|
||
&&NegativeControlError.find("shape-free guard")!=std::string::npos;
|
||
std::printf("[ShapeReadinessNegativeControl] deliberately_mismarked=(%d,%d,%d,lod0) result=%s guard_caught=%d error=%s\n",
|
||
DeliberatelyWrong.Coord.X,DeliberatelyWrong.Coord.Y,DeliberatelyWrong.Coord.Z,
|
||
MisMarked?"generated":"rejected",bGuardCaught?1:0,
|
||
NegativeControlError.c_str());
|
||
if(!bGuardCaught)
|
||
{std::fprintf(stderr,"[StreamPipeline] shape-free guard did not reject the deliberately mis-marked tile.\n");return 9;}
|
||
}
|
||
std::printf("[ShapeReadiness] shape_free_desired=%zu/%zu near_ring=%d/27 proof=single_SurfaceWorld_plan_no_features_routes_passages_edits_or_owner_primitive_overlap\n",
|
||
ShapeFreeTiles.size(),Desired.Tiles.size(),ShapeFreeNeighborhood);
|
||
|
||
// The initial desired set now owns all clipmap metadata needed by this Core-only run.
|
||
// Release the separate compatibility-world generation graph before compiling Core.
|
||
World.Generator.reset();
|
||
World.Manager.reset();
|
||
World.DefinitionStorage.clear();
|
||
World.BiomeStorage.clear();
|
||
World.OperationStorage.clear();
|
||
World.DensitySamples.clear();
|
||
const bool bIncludeFarWorld=MaxLevel<0||MaxLevel>=World.FullResClipLevels;
|
||
VoxelForgeCore::ShapeBuildRequest ShapeRequest;
|
||
ShapeRequest.minVoxel={FineMin.X,FineMin.Y,FineMin.Z};
|
||
ShapeRequest.maxVoxel={FineMax.X,FineMax.Y,FineMax.Z};
|
||
ShapeRequest.clipMinVoxel={ClipMin.X,ClipMin.Y,ClipMin.Z};
|
||
ShapeRequest.clipMaxVoxel={ClipMax.X,ClipMax.Y,ClipMax.Z};
|
||
ShapeRequest.workerLimit=static_cast<uint32>(std::clamp(WorkerCount,1,16));
|
||
ShapeRequest.parallelSignEdgeRepair=bParallelSignEdgeRepair;
|
||
ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld;
|
||
ShapeRequest.roundTunnels=bRoundTunnels;
|
||
ShapeRequest.enhancedTunnelWalls=bEnhancedTunnelWalls;
|
||
ShapeRequest.validatePassageRoutes=!bCaptureSkipWalkability;
|
||
if(bCaptureSkipWalkability)
|
||
std::printf("[CaptureWalkabilityGate] validation=deferred geometry_input=unchanged mode=mesh-export\n");
|
||
FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
|
||
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap,
|
||
FarHorizonLOD8InteriorStepVoxels,bUseSharedColumnCache);
|
||
Streamer.SetUniformBlockProofAuditEnabled(bAuditUniformBlockProofs);
|
||
Streamer.SetTileDrainInstrumentationEnabled(bInstrumentTileDrain);
|
||
Streamer.SetExactMidpointDensityMemoEnabled(bExactMidpointDensityMemo);
|
||
Streamer.SetActiveLaneBatchingEnabled(bActiveLaneBatching);
|
||
Streamer.SetProofBlockCells(ProofBlockCells);
|
||
if(bScreenSpaceGate||bReportLodWorkProfile||bInstrumentTileDrain)
|
||
Streamer.SetMesherStageProfilingEnabled(true);
|
||
if(bInstrumentTileDrain)
|
||
{
|
||
Streamer.EnableColdTimelineTrace(ColdStart,nullptr);
|
||
std::printf("[CpuTileInstrumentation] enabled=true labeled=true flush=after_run proof_block_cells=%d active_lane_batching=%s midpoint_density_memo=%s parallel_sign_edge_repair=%s\n",
|
||
ProofBlockCells,bActiveLaneBatching?"on":"off",
|
||
bExactMidpointDensityMemo?"on":"off",
|
||
bParallelSignEdgeRepair?"on":"off");
|
||
}
|
||
const auto StreamStart=Clock::now();
|
||
const FStandaloneStreamingUpdate EarlyUpdate=Streamer.StartWithDesired(ShapeFreeDesired);
|
||
if(EarlyUpdate.DesiredTiles!=static_cast<int32>(ShapeFreeDesired.Tiles.size()))
|
||
{
|
||
Streamer.Stop();
|
||
std::fprintf(stderr,"[StreamPipeline] expected %zu shape-free tiles but queued %d.\n",
|
||
ShapeFreeDesired.Tiles.size(),EarlyUpdate.DesiredTiles);return 7;
|
||
}
|
||
const auto ShapeBuildStart=Clock::now();
|
||
auto ShapeBuildFuture=std::async(std::launch::async,
|
||
[&Shape,&Scheduler,ShapeRequest,DiagnosticSlowShapeCompileMs]() mutable
|
||
{
|
||
if(DiagnosticSlowShapeCompileMs>0)
|
||
std::this_thread::sleep_for(std::chrono::milliseconds(DiagnosticSlowShapeCompileMs));
|
||
std::string Error;
|
||
const bool bCompiled=Shape.compile(ShapeRequest,Scheduler,Error);
|
||
return std::pair<bool,std::string>{bCompiled,std::move(Error)};
|
||
});
|
||
const auto [bShapeCompiled,BuildError]=ShapeBuildFuture.get();
|
||
if(!bShapeCompiled)
|
||
{
|
||
Streamer.Stop();
|
||
std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",BuildError.c_str());return 2;
|
||
}
|
||
if(bAuditNativeCaves)
|
||
{
|
||
Streamer.Stop();
|
||
if(!ResolvedLayout||!CoreSnapshot.resolvedLayout())
|
||
{std::fprintf(stderr,"[CavesAudit] missing resolved layout\n");return 2;}
|
||
return AuditNativeCavesField(Shape,*ResolvedLayout,World.CoreVoxelSizeMicrons)?0:14;
|
||
}
|
||
if(bAuditNativeLattice)
|
||
{
|
||
Streamer.Stop();
|
||
if(!ResolvedLayout||!CoreSnapshot.resolvedLayout())
|
||
{std::fprintf(stderr,"[LatticeAudit] missing resolved layout\n");return 2;}
|
||
return AuditNativeLatticeField(Shape,*ResolvedLayout,
|
||
World.CoreVoxelSizeMicrons,bLatticeOrphanControl)?0:14;
|
||
}
|
||
if(bAuditMotifIntervals)
|
||
{
|
||
Streamer.Stop();
|
||
if(!ResolvedLayout||!CoreSnapshot.resolvedLayout())
|
||
{std::fprintf(stderr,"[ResolvedMotifIntervalOracle] missing resolved layout\n");return 2;}
|
||
return AuditMotifIntervals(Shape,*ResolvedLayout,World.CoreVoxelSizeMicrons)?0:14;
|
||
}
|
||
if(bAuditOreVeinYield)
|
||
{
|
||
if(!ResolvedLayout||ResolvedLayout->oreVeins.empty())
|
||
{std::fprintf(stderr,"[OreMiningYield] missing resolved ore-vein records\n");return 2;}
|
||
const auto vein=std::find_if(ResolvedLayout->oreVeins.begin(),
|
||
ResolvedLayout->oreVeins.end(),[](const auto& value)
|
||
{return value.stableId.find("arrival-route-copper")!=std::string::npos;});
|
||
const auto& selected=vein==ResolvedLayout->oreVeins.end()
|
||
?ResolvedLayout->oreVeins.front():*vein;
|
||
const auto micrometresToVoxel=[&](const std::array<std::int64_t,3>& point)
|
||
{
|
||
const double size=static_cast<double>(World.CoreVoxelSizeMicrons);
|
||
return VoxelForgeCore::Vec3d{point[0]/size,point[1]/size,point[2]/size};
|
||
};
|
||
const std::array<std::int64_t,3> editCenterMicrometres={
|
||
(selected.endpointsMicrometres[0][0]+selected.endpointsMicrometres[1][0])/2,
|
||
(selected.endpointsMicrometres[0][1]+selected.endpointsMicrometres[1][1])/2,
|
||
(selected.endpointsMicrometres[0][2]+selected.endpointsMicrometres[1][2])/2};
|
||
const VoxelForgeCore::Vec3d editCenter=micrometresToVoxel(editCenterMicrometres);
|
||
const auto mined=Shape.measureOreMiningYieldForSphere(editCenter,1.25,0.25);
|
||
std::uint64_t weightedSamples=0;double resourceVolume=0.0;
|
||
for(const auto& resource:mined.resources)
|
||
{weightedSamples+=resource.weightedSamplesQ16;resourceVolume+=resource.volumeCubicMetres;}
|
||
const bool yieldPassed=weightedSamples>0&&resourceVolume>0.0;
|
||
std::printf("[OreMiningYield] edit=sphere record=%s center_voxel=(%.3f,%.3f,%.3f) radius_m=1.250 resolution_m=%.3f removed_solid_samples=%llu resource_volume_m3=%.6f status=%s\n",
|
||
selected.stableId.c_str(),editCenter.x,editCenter.y,editCenter.z,
|
||
mined.sampleSpacingMetres,static_cast<unsigned long long>(mined.solidSamplesRemoved),
|
||
resourceVolume,yieldPassed?"PASS":"FAIL");
|
||
for(const auto& resource:mined.resources)
|
||
std::printf("[OreMiningYieldResource] id=%u volume_m3=%.6f weighted_samples_q16=%llu\n",
|
||
resource.resourceMaterialId,resource.volumeCubicMetres,
|
||
static_cast<unsigned long long>(resource.weightedSamplesQ16));
|
||
VoxelForgeCore::Vec3d airCenter{};
|
||
if(!ResolvedLayout->cavernEdges.empty())
|
||
{
|
||
const auto& edge=ResolvedLayout->cavernEdges.front();
|
||
airCenter=micrometresToVoxel(edge.controlPointsMicrometres[0]);
|
||
airCenter.z+=edge.boreHeightMicrometres*0.5
|
||
/static_cast<double>(World.CoreVoxelSizeMicrons);
|
||
}
|
||
else if(!ResolvedLayout->passages.empty())
|
||
{
|
||
const auto& mouth=ResolvedLayout->passages.front().lowerMouthSubvoxels;
|
||
const double divisor=static_cast<double>(
|
||
VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||
airCenter={mouth[0]/divisor,mouth[1]/divisor,mouth[2]/divisor};
|
||
}
|
||
const auto air=Shape.measureOreMiningYieldForSphere(airCenter,1.0,0.25);
|
||
std::uint64_t airWeightedSamples=0;
|
||
double airResourceVolume=0.0;
|
||
for(const auto& resource:air.resources)
|
||
{airWeightedSamples+=resource.weightedSamplesQ16;airResourceVolume+=resource.volumeCubicMetres;}
|
||
const bool airPassed=airWeightedSamples==0&&airResourceVolume==0.0;
|
||
std::printf("[OreMiningYieldControl] case=already-air center_voxel=(%.3f,%.3f,%.3f) radius_m=1.000 resolution_m=0.250 removed_solid_samples=%llu resource_volume_m3=%.6f status=%s\n",
|
||
airCenter.x,airCenter.y,airCenter.z,
|
||
static_cast<unsigned long long>(air.solidSamplesRemoved),
|
||
airResourceVolume,airPassed?"PASS":"FAIL");
|
||
if(!yieldPassed||!airPassed)return 14;
|
||
}
|
||
if(bAuditLandmarkReachability)
|
||
{
|
||
Streamer.Stop();
|
||
if(ResolvedLayout==nullptr||!CoreSnapshot.resolvedLayout())
|
||
{std::fprintf(stderr,"[LandmarkReachabilityAudit] missing resolved layout\n");return 2;}
|
||
if(ResolvedLayout->cavernEdges.empty())
|
||
{
|
||
const auto control=AuditLandmarkTunnelRipples(Shape,*ResolvedLayout,
|
||
World.CoreVoxelSizeMicrons);
|
||
const bool expectedFail=control.valid
|
||
&&control.floorPeakAmplitudeMetres>0.015;
|
||
std::printf("[CavernTunnelRippleControl] layout=L45-ramp floor_samples=%zu floor_peak_period_m=%.3f floor_peak_amplitude_m=%.4f floor_limit_m=0.015 wall_samples=%zu wall_peak_period_m=%.3f wall_peak_amplitude_m=%.4f wall_limit_m=0.035 observed=%s expected=FAIL status=%s\n",
|
||
control.floorSamples,control.floorPeakPeriodMetres,
|
||
control.floorPeakAmplitudeMetres,control.wallSamples,
|
||
control.wallPeakPeriodMetres,control.wallPeakAmplitudeMetres,
|
||
expectedFail?"FAIL":"PASS",expectedFail?"PASS":"FAIL");
|
||
return expectedFail?0:14;
|
||
}
|
||
if(ResolvedLayout->cavernRooms.empty())
|
||
{std::fprintf(stderr,"[LandmarkReachabilityAudit] missing cavern room node\n");return 2;}
|
||
const auto ripples=AuditLandmarkTunnelRipples(Shape,*ResolvedLayout,
|
||
World.CoreVoxelSizeMicrons);
|
||
const bool entranceFloorPassed=ripples.entranceFloorSamples>=150
|
||
&&ripples.entranceFloorMaxStepMetres<=0.05
|
||
&&ripples.floorPeakAmplitudeMetres<=0.015;
|
||
std::printf("[CavernEntranceFloorAudit] layout=L46 samples=%zu distance_m=0.000..10.000 rails=3 max_step_m=%.4f step_limit_m=0.050 ripple_peak_m=%.4f ripple_limit_m=0.015 status=%s\n",
|
||
ripples.entranceFloorSamples,ripples.entranceFloorMaxStepMetres,
|
||
ripples.floorPeakAmplitudeMetres,entranceFloorPassed?"PASS":"FAIL");
|
||
std::printf("[CavernEntranceFloorStepDetail] rail=%zu distance_m=%.3f residual_m=%.4f->%.4f\n",
|
||
ripples.entranceFloorWorstTrack,ripples.entranceFloorWorstDistanceMetres,
|
||
ripples.entranceFloorPriorResidualMetres,
|
||
ripples.entranceFloorCurrentResidualMetres);
|
||
std::printf("[CavernTunnelRipple] layout=L46 floor_samples=%zu floor_peak_period_m=%.3f floor_peak_amplitude_m=%.4f floor_limit_m=0.015 wall_samples=%zu wall_peak_period_m=%.3f wall_peak_amplitude_m=%.4f wall_period_band_m=0.250..1.000 wall_detrend_window_m=2.000 wall_limit_m=0.035 status=%s\n",
|
||
ripples.floorSamples,ripples.floorPeakPeriodMetres,
|
||
ripples.floorPeakAmplitudeMetres,ripples.wallSamples,
|
||
ripples.wallPeakPeriodMetres,ripples.wallPeakAmplitudeMetres,
|
||
ripples.passed?"PASS":"FAIL");
|
||
const auto candidate=AuditLandmarkWalkPath(Shape,Assembly,*ResolvedLayout,
|
||
World.CoreVoxelSizeMicrons,"L46");
|
||
const auto daisStamp=std::find_if(ResolvedLayout->stamps.begin(),
|
||
ResolvedLayout->stamps.end(),[](const auto& stamp)
|
||
{return stamp.stableId=="landmark/sw-arrival-chamber-v1-dais";});
|
||
if(daisStamp!=ResolvedLayout->stamps.end())
|
||
{
|
||
const double voxelMetres=std::max<std::uint32_t>(1,
|
||
World.CoreVoxelSizeMicrons)/1000000.0;
|
||
for(const double offset:{-0.5,0.0,0.5,0.9,1.2})
|
||
{
|
||
const double x=daisStamp->positionMicrometres[0]/1000000.0;
|
||
const double y=daisStamp->positionMicrometres[1]/1000000.0;
|
||
const double z=daisStamp->positionMicrometres[2]/1000000.0+offset;
|
||
const double density=Shape.densityAt({x/voxelMetres,y/voxelMetres,
|
||
z/voxelMetres})*voxelMetres;
|
||
std::printf("[LandmarkDaisProfile] offset_m=%.2f sample_z_m=%.3f density_m=%.4f\n",
|
||
offset,z,density);
|
||
}
|
||
}
|
||
VoxelForgeCore::ResolvedLayoutSnapshotV3 oldLayout;
|
||
std::string oldError;
|
||
if(!MakeL43BuriedLandmarkControl(*ResolvedLayout,oldLayout,oldError))
|
||
{std::fprintf(stderr,"[LandmarkReachabilityAudit] L43 control layout failed: %s\n",
|
||
oldError.c_str());return 2;}
|
||
auto oldSnapshot=CoreSnapshot.withResolvedLayout(oldLayout);
|
||
if(!oldSnapshot)
|
||
{std::fprintf(stderr,"[LandmarkReachabilityAudit] could not construct L43 control snapshot\n");return 2;}
|
||
VoxelForgeCore::World oldShape(oldSnapshot);
|
||
std::string oldBuildError;
|
||
if(!oldShape.compile(ShapeRequest,Scheduler,oldBuildError))
|
||
{std::fprintf(stderr,"[LandmarkReachabilityAudit] L43 control field build failed: %s\n",
|
||
oldBuildError.c_str());return 2;}
|
||
const auto buried=AuditLandmarkWalkPath(oldShape,Assembly,*ResolvedLayout,
|
||
World.CoreVoxelSizeMicrons,"L43");
|
||
std::printf("[LandmarkWalkability] layout=L46 path_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu max_grade_deg=%.3f limit_deg=15.000 min_clearance_m=%.3f max_support_depth_m=%.3f min_headroom_m=%.3f headroom_required_m=0.300 status=%s\n",
|
||
static_cast<unsigned long long>(candidate.pathSamples),
|
||
static_cast<unsigned long long>(candidate.capsuleSamples),
|
||
static_cast<unsigned long long>(candidate.blockedCapsuleSamples),
|
||
static_cast<unsigned long long>(candidate.supportSamples),
|
||
static_cast<unsigned long long>(candidate.unsupportedSamples),
|
||
static_cast<unsigned long long>(candidate.gradeFailures),
|
||
candidate.maximumGradeDegrees,candidate.minimumClearanceMetres,
|
||
candidate.maximumSupportDepthMetres,candidate.minimumHeadroomMetres,
|
||
candidate.passed?"PASS":"FAIL");
|
||
std::printf("[LandmarkHeadroom] minimum_m=%.3f required_m=0.300 status=%s\n",
|
||
candidate.minimumHeadroomMetres,
|
||
candidate.minimumHeadroomMetres>=0.300?"PASS":"FAIL");
|
||
std::printf("[LandmarkReachabilityControl] layout=L43-buried path_samples=%llu blocked=%llu unsupported=%llu grade_failures=%llu result=%s expected=FAIL status=%s\n",
|
||
static_cast<unsigned long long>(buried.pathSamples),
|
||
static_cast<unsigned long long>(buried.blockedCapsuleSamples),
|
||
static_cast<unsigned long long>(buried.unsupportedSamples),
|
||
static_cast<unsigned long long>(buried.gradeFailures),
|
||
buried.passed?"PASS":"FAIL",buried.passed?"FAIL":"PASS");
|
||
const bool passed=ripples.passed&&entranceFloorPassed
|
||
&&candidate.passed&&!buried.passed;
|
||
std::printf("[LandmarkReachabilityAudit] positive=PASS control=%s status=%s\n",
|
||
buried.passed?"FAILED":"EXPECTED_FAIL",passed?"PASS":"FAIL");
|
||
return passed?0:13;
|
||
}
|
||
if(bAuditStrateTransitions)
|
||
{
|
||
if(ResolvedLayout==nullptr||!CoreSnapshot.resolvedLayout())
|
||
{Streamer.Stop();std::fprintf(stderr,"[StrateTransitionAudit] missing resolved-layout snapshot\n");return 2;}
|
||
const bool passed=AuditResolvedStrateTransitions(Shape,World,*ResolvedLayout);
|
||
Streamer.Stop();
|
||
return passed?0:11;
|
||
}
|
||
if(bAuditResolvedLayoutPassages)
|
||
{
|
||
if(ResolvedLayout==nullptr||!CoreSnapshot.resolvedLayout())
|
||
{Streamer.Stop();std::fprintf(stderr,"[ResolvedPassageAudit] missing resolved-layout snapshot\n");return 2;}
|
||
struct FWalkAudit
|
||
{
|
||
std::uint64_t segments=0,centers=0,capsuleSamples=0,blocked=0;
|
||
std::uint64_t supportSamples=0,unsupported=0,gradeFailures=0;
|
||
std::array<std::array<double,4>,8> firstBlocked{};
|
||
std::array<std::array<double,4>,8> firstUnsupported{};
|
||
std::array<std::array<bool,5>,8> firstSupportMasks{};
|
||
std::size_t firstBlockedCount=0,firstUnsupportedCount=0;
|
||
std::array<double,4> firstDeepUnsupported{};
|
||
std::array<bool,5> firstDeepSupportMask{};
|
||
bool hasDeepUnsupported=false;
|
||
bool passed=false;
|
||
};
|
||
struct FPassageSegmentSupportAudit
|
||
{
|
||
std::uint64_t centers=0,unsupported=0,nativeUnsupported=0;
|
||
std::array<double,3> firstUnsupported{};
|
||
std::array<double,4> firstNativeDensities{};
|
||
std::uint32_t firstNativeSupportedProbes=0;
|
||
bool hasUnsupported=false;
|
||
};
|
||
const double coreMetres=static_cast<double>(World.CoreVoxelSizeMicrons)/1000000.0;
|
||
const auto auditPlayerPath=[&](const VoxelForgeCore::World& Field,
|
||
const std::vector<std::array<double,3>>& path,
|
||
double supportStartMetres,double supportStepMetres,
|
||
double supportMaximumMetres,std::uint32_t minimumSupportProbes,
|
||
double supportProbeRadiusMetres,
|
||
const VoxelForgeCore::World* nativeField=nullptr,
|
||
std::vector<FPassageSegmentSupportAudit>* segmentSupport=nullptr)
|
||
{
|
||
FWalkAudit audit;
|
||
if(path.size()<2)return audit;
|
||
if(segmentSupport)segmentSupport->assign(path.size()-1,{});
|
||
constexpr std::array<double,7> heights{{-0.84,-0.60,-0.30,0.0,0.30,0.60,0.84}};
|
||
constexpr std::array<std::array<double,2>,8> directions{{
|
||
{{1.0,0.0}},{{0.7071067811865475,0.7071067811865475}},{{0.0,1.0}},
|
||
{{-0.7071067811865475,0.7071067811865475}},{{-1.0,0.0}},
|
||
{{-0.7071067811865475,-0.7071067811865475}},{{0.0,-1.0}},
|
||
{{0.7071067811865475,-0.7071067811865475}}}};
|
||
const auto solidAt=[&](double xMetres,double yMetres,double zMetres)
|
||
{
|
||
++audit.capsuleSamples;
|
||
const VoxelForgeCore::Vec3d position{xMetres/coreMetres,yMetres/coreMetres,zMetres/coreMetres};
|
||
const float density=Field.densityAt(position);
|
||
if(density<0.0f)
|
||
{
|
||
if(audit.firstBlockedCount<audit.firstBlocked.size())
|
||
{
|
||
audit.firstBlocked[audit.firstBlockedCount++]={xMetres,yMetres,zMetres,density};
|
||
std::printf("[ResolvedWalkDispatch] kind=blocked xyz_m=(%.3f,%.3f,%.3f) path=%s\n",
|
||
xMetres,yMetres,zMetres,Field.densityDispatchPathAt(position).c_str());
|
||
}
|
||
++audit.blocked;return true;
|
||
}
|
||
return false;
|
||
};
|
||
for(std::size_t segment=1;segment<path.size();++segment)
|
||
{
|
||
++audit.segments;
|
||
const auto& a=path[segment-1];const auto& b=path[segment];
|
||
const double dx=b[0]-a[0],dy=b[1]-a[1],dz=b[2]-a[2];
|
||
const double horizontal=std::hypot(dx,dy)*coreMetres;
|
||
const double vertical=std::abs(dz)*coreMetres;
|
||
if(horizontal<=1.0e-6||vertical>horizontal*0.2679491924311227)
|
||
++audit.gradeFailures;
|
||
const double length=std::sqrt(dx*dx+dy*dy+dz*dz)*coreMetres;
|
||
const std::uint64_t intervals=std::max<std::uint64_t>(1,
|
||
static_cast<std::uint64_t>(std::ceil(length/0.5)));
|
||
for(std::uint64_t step=0;step<=intervals;++step)
|
||
{
|
||
if(segment>1&&step==0)continue;
|
||
const double t=static_cast<double>(step)/intervals;
|
||
const double x=(a[0]+dx*t)*coreMetres;
|
||
const double y=(a[1]+dy*t)*coreMetres;
|
||
const double z=(a[2]+dz*t)*coreMetres;
|
||
++audit.centers;
|
||
FPassageSegmentSupportAudit* segmentAudit=segmentSupport
|
||
?&(*segmentSupport)[segment-1]:nullptr;
|
||
if(segmentAudit)++segmentAudit->centers;
|
||
for(const double h:heights)
|
||
{
|
||
const double capOffset=std::max(0.0,std::abs(h)-0.54);
|
||
const double radial=std::abs(h)<=0.54?0.34
|
||
:std::sqrt(std::max(0.0,0.34*0.34-capOffset*capOffset));
|
||
solidAt(x,y,z+h);
|
||
for(const auto& direction:directions)
|
||
solidAt(x+direction[0]*radial,y+direction[1]*radial,z+h);
|
||
}
|
||
const double footZ=z-0.88;
|
||
const std::array<std::array<double,2>,5> supportOffsets{{
|
||
{{0.0,0.0}},{{supportProbeRadiusMetres,0.0}},
|
||
{{-supportProbeRadiusMetres,0.0}},
|
||
{{0.0,supportProbeRadiusMetres}},
|
||
{{0.0,-supportProbeRadiusMetres}}}};
|
||
std::uint32_t supportedProbes=0;
|
||
std::uint32_t nativeSupportedProbes=0;
|
||
double firstUnsupportedX=x,firstUnsupportedY=y,
|
||
firstUnsupportedZ=footZ,lastUnsupportedDensity=0.0;
|
||
std::array<bool,5> supportMask{};
|
||
for(std::size_t offsetIndex=0;offsetIndex<supportOffsets.size();++offsetIndex)
|
||
{
|
||
const auto& offset=supportOffsets[offsetIndex];
|
||
const double supportX=x+offset[0],supportY=y+offset[1];
|
||
bool supported=false;
|
||
double offsetSampleZ=footZ;
|
||
for(double depth=supportStartMetres;
|
||
depth<=supportMaximumMetres+1.0e-6;depth+=supportStepMetres)
|
||
{
|
||
++audit.supportSamples;
|
||
offsetSampleZ=(footZ-depth)/coreMetres;
|
||
lastUnsupportedDensity=Field.densityAt({supportX/coreMetres,
|
||
supportY/coreMetres,offsetSampleZ});
|
||
if(lastUnsupportedDensity<0.0)
|
||
{
|
||
supported=true;break;
|
||
}
|
||
}
|
||
if(nativeField)
|
||
{
|
||
for(double depth=supportStartMetres;
|
||
depth<=supportMaximumMetres+1.0e-6;depth+=supportStepMetres)
|
||
if(nativeField->densityAt({supportX/coreMetres,
|
||
supportY/coreMetres,(footZ-depth)/coreMetres})<0.0f)
|
||
{++nativeSupportedProbes;break;}
|
||
}
|
||
if(supported){++supportedProbes;supportMask[offsetIndex]=true;}
|
||
else
|
||
{
|
||
firstUnsupportedX=supportX;firstUnsupportedY=supportY;
|
||
firstUnsupportedZ=offsetSampleZ*coreMetres;
|
||
}
|
||
}
|
||
if(nativeField&&segmentAudit
|
||
&&nativeSupportedProbes<minimumSupportProbes)
|
||
++segmentAudit->nativeUnsupported;
|
||
if(supportedProbes<minimumSupportProbes)
|
||
{
|
||
++audit.unsupported;
|
||
if(segmentAudit)
|
||
{
|
||
++segmentAudit->unsupported;
|
||
if(!segmentAudit->hasUnsupported)
|
||
{
|
||
segmentAudit->hasUnsupported=true;
|
||
segmentAudit->firstUnsupported={x,y,footZ};
|
||
segmentAudit->firstNativeSupportedProbes=nativeSupportedProbes;
|
||
if(nativeField)
|
||
{
|
||
constexpr std::array<double,4> depths{{0.125,1.0,2.0,4.0}};
|
||
for(std::size_t depthIndex=0;depthIndex<depths.size();++depthIndex)
|
||
segmentAudit->firstNativeDensities[depthIndex]=nativeField->densityAt({
|
||
x/coreMetres,y/coreMetres,
|
||
(footZ-depths[depthIndex])/coreMetres});
|
||
}
|
||
}
|
||
}
|
||
if(!audit.hasDeepUnsupported&&footZ<-80.0)
|
||
{
|
||
audit.hasDeepUnsupported=true;
|
||
audit.firstDeepUnsupported={firstUnsupportedX,firstUnsupportedY,
|
||
firstUnsupportedZ,lastUnsupportedDensity};
|
||
audit.firstDeepSupportMask=supportMask;
|
||
}
|
||
if(audit.firstUnsupportedCount<audit.firstUnsupported.size())
|
||
{
|
||
const std::size_t sample=audit.firstUnsupportedCount++;
|
||
audit.firstUnsupported[sample]={
|
||
firstUnsupportedX,firstUnsupportedY,firstUnsupportedZ,
|
||
lastUnsupportedDensity};
|
||
audit.firstSupportMasks[sample]=supportMask;
|
||
std::printf("[ResolvedWalkDispatch] kind=unsupported xyz_m=(%.3f,%.3f,%.3f) support_probes=%u/5 mask=%d%d%d%d%d path=%s\n",
|
||
firstUnsupportedX,firstUnsupportedY,firstUnsupportedZ,supportedProbes,
|
||
supportMask[0],supportMask[1],supportMask[2],supportMask[3],supportMask[4],
|
||
Field.densityDispatchPathAt({firstUnsupportedX/coreMetres,
|
||
firstUnsupportedY/coreMetres,footZ/coreMetres}).c_str());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
audit.passed=audit.centers>0&&audit.blocked==0&&audit.unsupported==0
|
||
&&audit.gradeFailures==0;
|
||
return audit;
|
||
};
|
||
const auto qToMetres=[&](std::int64_t q)
|
||
{return static_cast<double>(q)*World.CoreVoxelSizeMicrons
|
||
/(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel*1000000.0);};
|
||
struct FPassageNaturalnessAudit
|
||
{
|
||
double maximumFlatRunMetres=0.0;
|
||
double wallReliefPeakToPeakMetres=0.0;
|
||
std::uint64_t wallSamples=0,missingWallSamples=0;
|
||
bool passed=false;
|
||
};
|
||
struct FPassageMouthProfileAudit
|
||
{
|
||
double floorFlatStepRunMetres=0.0,floorStepMetres=0.0;
|
||
double wallFlatStepRunMetres=0.0,wallStepMetres=0.0;
|
||
std::uint64_t floorSamples=0,wallSamples=0,missingFloorSamples=0,missingWallSamples=0;
|
||
bool passed=false;
|
||
};
|
||
struct FPassageRippleAudit
|
||
{
|
||
double periodMetres=0.0,amplitudeMetres=0.0,localPeakAmplitudeMetres=0.0;
|
||
double coherentPeriodMetres=0.0,coherentAmplitudeMetres=0.0;
|
||
double controlAmplitudeMetres=0.0;
|
||
double floorPeriodMetres=0.0,floorAmplitudeMetres=0.0;
|
||
double floorControlAmplitudeMetres=0.0;
|
||
double floorCoherentPeriodMetres=0.0,floorCoherentAmplitudeMetres=0.0;
|
||
std::vector<std::array<double,6>> floorPeakProfile;
|
||
double floorPeakWindowStartMetres=0.0;
|
||
std::uint64_t samples=0,expectedSamples=0;
|
||
std::uint64_t floorSamples=0,floorExpectedSamples=0;
|
||
bool controlDetected=false,passed=false;
|
||
bool floorControlDetected=false;
|
||
double floorStepControlAmplitudeMetres=0.0;
|
||
bool floorStepControlDetected=false;
|
||
};
|
||
constexpr double MaximumFlatRunMetres=8.0;
|
||
constexpr double MinimumWallReliefMetres=0.20;
|
||
constexpr double PassageProfileSpacingMetres=0.125;
|
||
constexpr double PassageProfilePlateauMinimumMetres=0.50;
|
||
constexpr double PassageProfilePlateauEpsilonMetres=0.020;
|
||
constexpr double PassageProfileStepThresholdMetres=0.100;
|
||
constexpr double PassageMouthProfileReachMetres=12.0;
|
||
const auto auditProfilePlateaus=[&](const std::vector<std::array<double,2>>& profile)
|
||
{
|
||
std::pair<double,double> result{};
|
||
if(profile.size()<2)return result;
|
||
std::size_t runStart=0;
|
||
for(std::size_t i=1;i<profile.size();++i)
|
||
{
|
||
if(profile[i][0]-profile[i-1][0]
|
||
>PassageProfileSpacingMetres*1.5)
|
||
{runStart=i;continue;}
|
||
if(std::abs(profile[i][1]-profile[runStart][1])
|
||
<=PassageProfilePlateauEpsilonMetres)continue;
|
||
const double flatLength=profile[i-1][0]-profile[runStart][0];
|
||
const double step=std::abs(profile[i][1]-profile[i-1][1]);
|
||
if(flatLength>=PassageProfilePlateauMinimumMetres
|
||
&&step>=PassageProfileStepThresholdMetres)
|
||
{
|
||
result.first=std::max(result.first,flatLength);
|
||
result.second=std::max(result.second,step);
|
||
}
|
||
runStart=i;
|
||
}
|
||
return result;
|
||
};
|
||
const auto auditPassageMouthProfiles=[&](const VoxelForgeCore::World& Field,
|
||
const VoxelForgeCore::ResolvedLayoutPassageV1& passage,
|
||
bool profiledLowerSurface)
|
||
{
|
||
FPassageMouthProfileAudit audit;
|
||
if(passage.controlPoints.size()<2)return audit;
|
||
const std::size_t end=std::min(passage.lowerMouthControlIndex,
|
||
passage.controlPoints.size()-1);
|
||
if(end<1)return audit;
|
||
struct FProfilePose
|
||
{
|
||
double distance=0.0,x=0.0,y=0.0,z=0.0,radius=0.0;
|
||
};
|
||
std::vector<FProfilePose> path;
|
||
path.reserve(end*8+1);
|
||
double distance=0.0;
|
||
for(std::size_t segment=0;segment<end;++segment)
|
||
{
|
||
const auto& a=passage.controlPoints[segment];
|
||
const auto& b=passage.controlPoints[segment+1];
|
||
const double ax=qToMetres(a.positionSubvoxels[0]);
|
||
const double ay=qToMetres(a.positionSubvoxels[1]);
|
||
const double az=qToMetres(a.positionSubvoxels[2]);
|
||
const double bx=qToMetres(b.positionSubvoxels[0]);
|
||
const double by=qToMetres(b.positionSubvoxels[1]);
|
||
const double bz=qToMetres(b.positionSubvoxels[2]);
|
||
const double ar=qToMetres(a.radiusSubvoxels);
|
||
const double br=qToMetres(b.radiusSubvoxels);
|
||
const double length=std::sqrt((bx-ax)*(bx-ax)+(by-ay)*(by-ay)+(bz-az)*(bz-az));
|
||
if(!(length>1.0e-8))continue;
|
||
const std::size_t steps=std::max<std::size_t>(1,
|
||
static_cast<std::size_t>(std::ceil(length/PassageProfileSpacingMetres)));
|
||
for(std::size_t step=segment==0?0:1;step<=steps;++step)
|
||
{
|
||
const double t=static_cast<double>(step)/steps;
|
||
path.push_back({distance+length*t,std::lerp(ax,bx,t),
|
||
std::lerp(ay,by,t),std::lerp(az,bz,t),std::lerp(ar,br,t)});
|
||
}
|
||
distance+=length;
|
||
}
|
||
if(path.size()<3)return audit;
|
||
const double lowerMouthDistance=distance;
|
||
constexpr double DensitySearchStepMetres=0.0625;
|
||
const auto densityMetres=[&](double x,double y,double z)
|
||
{
|
||
const float value=Field.densityAt({x/coreMetres,y/coreMetres,z/coreMetres});
|
||
return std::isfinite(value)?static_cast<double>(value)*coreMetres
|
||
:std::numeric_limits<double>::quiet_NaN();
|
||
};
|
||
const auto firstFloorCrossing=[&](const FProfilePose& pose,double x,double y)->double
|
||
{
|
||
const double expectedFloor=pose.z-pose.radius;
|
||
const double maximumRise=std::min(2.0*pose.radius-0.25,
|
||
std::max(0.75,pose.radius-1.20+0.50));
|
||
double previousZ=expectedFloor-1.50;
|
||
double previous=densityMetres(x,y,previousZ);
|
||
if(!std::isfinite(previous))return std::numeric_limits<double>::quiet_NaN();
|
||
double highestFloor=std::numeric_limits<double>::quiet_NaN();
|
||
for(double z=previousZ+DensitySearchStepMetres;
|
||
z<=expectedFloor+maximumRise+DensitySearchStepMetres*0.25;
|
||
z+=DensitySearchStepMetres)
|
||
{
|
||
const double current=densityMetres(x,y,z);
|
||
if(!std::isfinite(current))return std::numeric_limits<double>::quiet_NaN();
|
||
if(previous<=0.0&¤t>=0.0)
|
||
{
|
||
const double denominator=current-previous;
|
||
const double t=std::abs(denominator)>1.0e-12
|
||
?std::clamp(-previous/denominator,0.0,1.0):0.0;
|
||
highestFloor=std::lerp(z-DensitySearchStepMetres,z,t);
|
||
}
|
||
previous=current;
|
||
}
|
||
return highestFloor;
|
||
};
|
||
const auto findWallRadius=[&](const FProfilePose& pose,double nx,double ny)
|
||
{
|
||
const double maximum=pose.radius+2.0;
|
||
double previousRadius=0.0;
|
||
double previous=densityMetres(pose.x,pose.y,pose.z);
|
||
if(!std::isfinite(previous)||previous<0.0)
|
||
return std::numeric_limits<double>::quiet_NaN();
|
||
for(double radius=DensitySearchStepMetres;radius<=maximum;
|
||
radius+=DensitySearchStepMetres)
|
||
{
|
||
const double current=densityMetres(pose.x+nx*radius,
|
||
pose.y+ny*radius,pose.z);
|
||
if(!std::isfinite(current))return std::numeric_limits<double>::quiet_NaN();
|
||
if(previous>=0.0&¤t<=0.0)
|
||
{
|
||
const double denominator=previous-current;
|
||
const double t=std::abs(denominator)>1.0e-12
|
||
?std::clamp(previous/denominator,0.0,1.0):0.0;
|
||
return std::lerp(previousRadius,radius,t);
|
||
}
|
||
previous=current;previousRadius=radius;
|
||
}
|
||
return std::numeric_limits<double>::quiet_NaN();
|
||
};
|
||
std::array<std::array<std::vector<std::array<double,2>>,3>,2> floorProfiles;
|
||
std::array<std::array<std::vector<std::array<double,2>>,2>,2> wallProfiles;
|
||
std::array<double,2> endpointFloorCounts{};
|
||
for(std::size_t i=0;i<path.size();++i)
|
||
{
|
||
const auto& pose=path[i];
|
||
const bool inUpperWindow=pose.distance<=PassageMouthProfileReachMetres;
|
||
const bool inLowerWindow=lowerMouthDistance-pose.distance
|
||
<=PassageMouthProfileReachMetres;
|
||
if(!inUpperWindow&&!inLowerWindow)continue;
|
||
for(std::size_t endpoint=0;endpoint<2;++endpoint)
|
||
{
|
||
if((endpoint==0&&!inUpperWindow)||(endpoint==1&&!inLowerWindow))continue;
|
||
const double localDistance=endpoint==0?pose.distance
|
||
:lowerMouthDistance-pose.distance;
|
||
const double tangentX=i==0?path[i+1].x-path[i].x
|
||
:i+1>=path.size()?path[i].x-path[i-1].x
|
||
:path[i+1].x-path[i-1].x;
|
||
const double tangentY=i==0?path[i+1].y-path[i].y
|
||
:i+1>=path.size()?path[i].y-path[i-1].y
|
||
:path[i+1].y-path[i-1].y;
|
||
const double tangentLength=std::hypot(tangentX,tangentY);
|
||
if(tangentLength<=1.0e-6)
|
||
{audit.missingFloorSamples+=3;audit.missingWallSamples+=2;continue;}
|
||
const double nx=-tangentY/tangentLength,ny=tangentX/tangentLength;
|
||
for(std::size_t lane=0;lane<3;++lane)
|
||
{
|
||
const double lateral=lane==0?0.0:lane==1?0.75:-0.75;
|
||
const double floor=firstFloorCrossing(pose,
|
||
pose.x+nx*lateral,pose.y+ny*lateral);
|
||
if(lane!=0&&localDistance>=4.0&&localDistance<=5.5)
|
||
std::printf("[PassageMouthProfileProbe] id=%s endpoint=%s d_m=%.3f lane_m=%.2f x_m=%.3f y_m=%.3f route_floor_m=%.3f field_floor_m=%.3f\n",
|
||
passage.stableId.c_str(),endpoint==0?"upper":"lower",
|
||
localDistance,lateral,pose.x+nx*lateral,
|
||
pose.y+ny*lateral,pose.z-pose.radius,floor);
|
||
if(std::isfinite(floor))
|
||
{
|
||
floorProfiles[endpoint][lane].push_back({localDistance,floor});
|
||
++endpointFloorCounts[endpoint];++audit.floorSamples;
|
||
}
|
||
else ++audit.missingFloorSamples;
|
||
}
|
||
if(i%4!=0)continue;
|
||
for(std::size_t side=0;side<2;++side)
|
||
{
|
||
const double sign=side==0?-1.0:1.0;
|
||
const double radius=findWallRadius(pose,sign*nx,sign*ny);
|
||
if(std::isfinite(radius))
|
||
{
|
||
wallProfiles[endpoint][side].push_back({localDistance,radius});
|
||
++audit.wallSamples;
|
||
}
|
||
else ++audit.missingWallSamples;
|
||
}
|
||
}
|
||
}
|
||
double worstEndpointFloorCoverage=1.0;
|
||
for(std::size_t endpoint=0;endpoint<2;++endpoint)
|
||
{
|
||
for(std::size_t lane=0;lane<3;++lane)
|
||
std::sort(floorProfiles[endpoint][lane].begin(),
|
||
floorProfiles[endpoint][lane].end(),
|
||
[](const auto& a,const auto& b){return a[0]<b[0];});
|
||
for(std::size_t side=0;side<2;++side)
|
||
std::sort(wallProfiles[endpoint][side].begin(),
|
||
wallProfiles[endpoint][side].end(),
|
||
[](const auto& a,const auto& b){return a[0]<b[0];});
|
||
const double expectedFloor=std::min<double>(path.size(),
|
||
PassageMouthProfileReachMetres/PassageProfileSpacingMetres)+1.0;
|
||
worstEndpointFloorCoverage=std::min(worstEndpointFloorCoverage,
|
||
expectedFloor>0?endpointFloorCounts[endpoint]/expectedFloor:0.0);
|
||
// Lateral probes confirm floor coverage, but do not define a longitudinal
|
||
// floor profile: the normal rotates around tight bends and can jump across
|
||
// unrelated terrain. The centerline is the stable passage-floor trace.
|
||
// The lower surface mouth follows its declared heightfield profile.
|
||
// Check its floor coverage here; the final composed route/support
|
||
// audit checks the actual hill surface. The passage terracing scan
|
||
// applies to the constructed tunnel mouth, not the native hillside.
|
||
if(!(endpoint==1&&profiledLowerSurface))
|
||
{
|
||
const auto floorStep=auditProfilePlateaus(floorProfiles[endpoint][0]);
|
||
audit.floorFlatStepRunMetres=std::max(audit.floorFlatStepRunMetres,
|
||
floorStep.first);
|
||
audit.floorStepMetres=std::max(audit.floorStepMetres,floorStep.second);
|
||
for(std::size_t side=0;side<2;++side)
|
||
{
|
||
const auto wallStep=auditProfilePlateaus(wallProfiles[endpoint][side]);
|
||
audit.wallFlatStepRunMetres=std::max(audit.wallFlatStepRunMetres,
|
||
wallStep.first);
|
||
audit.wallStepMetres=std::max(audit.wallStepMetres,wallStep.second);
|
||
}
|
||
}
|
||
}
|
||
audit.passed=worstEndpointFloorCoverage>=0.75
|
||
&&audit.wallSamples>=8
|
||
&&audit.floorFlatStepRunMetres==0.0&&audit.wallFlatStepRunMetres==0.0;
|
||
return audit;
|
||
};
|
||
std::vector<std::array<double,2>> l23TerracedControl;
|
||
for(std::size_t tread=0;tread<8;++tread)
|
||
for(std::size_t sample=0;sample<=16;++sample)
|
||
l23TerracedControl.push_back({(tread*16+sample)*PassageProfileSpacingMetres,
|
||
static_cast<double>(tread)*0.25});
|
||
const auto l23TerraceControlResult=auditProfilePlateaus(l23TerracedControl);
|
||
const bool l23TerraceControlDetected=l23TerraceControlResult.first
|
||
>=PassageProfilePlateauMinimumMetres
|
||
&&l23TerraceControlResult.second>=PassageProfileStepThresholdMetres;
|
||
const auto auditPassageNaturalness=[&](const VoxelForgeCore::World& Field,
|
||
const VoxelForgeCore::ResolvedLayoutPassageV1& passage)
|
||
{
|
||
FPassageNaturalnessAudit audit;
|
||
const std::size_t end=std::min<std::size_t>(passage.lowerMouthControlIndex,
|
||
passage.controlPoints.size()-1);
|
||
double flatRun=0.0;
|
||
for(std::size_t i=1;i<=end;++i)
|
||
{
|
||
const auto& a=passage.controlPoints[i-1];
|
||
const auto& b=passage.controlPoints[i];
|
||
const double dx=qToMetres(b.positionSubvoxels[0]-a.positionSubvoxels[0]);
|
||
const double dy=qToMetres(b.positionSubvoxels[1]-a.positionSubvoxels[1]);
|
||
const double floorA=qToMetres(a.positionSubvoxels[2]-a.radiusSubvoxels)+1.13;
|
||
const double floorB=qToMetres(b.positionSubvoxels[2]-b.radiusSubvoxels)+1.13;
|
||
const double dz=floorB-floorA;
|
||
const double horizontal=std::hypot(dx,dy);
|
||
if(horizontal>1.0e-6&&std::abs(dz)/horizontal
|
||
<std::tan(0.5*3.14159265358979323846/180.0))
|
||
{flatRun+=horizontal;audit.maximumFlatRunMetres=std::max(
|
||
audit.maximumFlatRunMetres,flatRun);}
|
||
else flatRun=0.0;
|
||
}
|
||
if(end>=6)
|
||
{
|
||
const std::size_t sampleCount=std::min<std::size_t>(64,end-4);
|
||
double minimumRadius=std::numeric_limits<double>::infinity();
|
||
double maximumRadius=-std::numeric_limits<double>::infinity();
|
||
for(std::size_t sample=0;sample<sampleCount;++sample)
|
||
{
|
||
const std::size_t index=2+(sample*(end-4))
|
||
/std::max<std::size_t>(1,sampleCount-1);
|
||
const auto& point=passage.controlPoints[index];
|
||
const auto& before=passage.controlPoints[index-1];
|
||
const auto& after=passage.controlPoints[index+1];
|
||
const double tangentX=qToMetres(after.positionSubvoxels[0]-before.positionSubvoxels[0]);
|
||
const double tangentY=qToMetres(after.positionSubvoxels[1]-before.positionSubvoxels[1]);
|
||
const double tangentLength=std::hypot(tangentX,tangentY);
|
||
if(tangentLength<=1.0e-6){++audit.missingWallSamples;continue;}
|
||
const double radiusMetres=qToMetres(point.radiusSubvoxels);
|
||
const double sampleRadius=std::max(0.20,radiusMetres-0.18);
|
||
const double centerX=qToMetres(point.positionSubvoxels[0]);
|
||
const double centerY=qToMetres(point.positionSubvoxels[1]);
|
||
const double centerZ=qToMetres(point.positionSubvoxels[2]);
|
||
const double nx=-tangentY/tangentLength,ny=tangentX/tangentLength;
|
||
for(const double side:{-1.0,1.0})
|
||
{
|
||
const VoxelForgeCore::Vec3d probe{
|
||
(centerX+side*nx*sampleRadius)/coreMetres,
|
||
(centerY+side*ny*sampleRadius)/coreMetres,
|
||
centerZ/coreMetres};
|
||
const double density=Field.densityAt(probe);
|
||
if(!std::isfinite(density)){++audit.missingWallSamples;continue;}
|
||
const double densityMetres=density*coreMetres;
|
||
if(densityMetres< -0.75||densityMetres>0.75)
|
||
{++audit.missingWallSamples;continue;}
|
||
// For the carved SDF, r + density is the wall radius. Its
|
||
// variation along the passage measures the applied rock relief.
|
||
const double wallRadius=sampleRadius+densityMetres;
|
||
minimumRadius=std::min(minimumRadius,wallRadius);
|
||
maximumRadius=std::max(maximumRadius,wallRadius);
|
||
++audit.wallSamples;
|
||
}
|
||
}
|
||
if(audit.wallSamples>0&&std::isfinite(minimumRadius)
|
||
&&std::isfinite(maximumRadius))
|
||
audit.wallReliefPeakToPeakMetres=maximumRadius-minimumRadius;
|
||
}
|
||
audit.passed=audit.maximumFlatRunMetres<=MaximumFlatRunMetres
|
||
&&audit.wallReliefPeakToPeakMetres>=MinimumWallReliefMetres
|
||
&&audit.wallSamples>=8;
|
||
return audit;
|
||
};
|
||
const auto auditPassageRipples=[&](const VoxelForgeCore::World& Field,
|
||
const VoxelForgeCore::ResolvedLayoutPassageV1& passage)
|
||
{
|
||
FPassageRippleAudit audit;
|
||
constexpr double SampleSpacingMetres=0.125;
|
||
constexpr double WindowLengthMetres=32.0;
|
||
constexpr double MinimumPeriodMetres=0.5;
|
||
constexpr double MaximumPeriodMetres=8.0;
|
||
constexpr double PeriodStepMetres=0.0625;
|
||
constexpr double RippleAmplitudeLimitMetres=0.035;
|
||
constexpr double FloorRippleAmplitudeLimitMetres=0.025;
|
||
struct FPathPoint
|
||
{
|
||
double x=0.0,y=0.0,z=0.0,radius=0.0,distance=0.0;
|
||
};
|
||
const std::size_t end=std::min<std::size_t>(passage.lowerMouthControlIndex,
|
||
passage.controlPoints.size()-1);
|
||
if(end<2)return audit;
|
||
std::vector<FPathPoint> path;
|
||
path.reserve(end+1);
|
||
for(std::size_t i=0;i<=end;++i)
|
||
{
|
||
const auto& point=passage.controlPoints[i];
|
||
FPathPoint value{qToMetres(point.positionSubvoxels[0]),
|
||
qToMetres(point.positionSubvoxels[1]),
|
||
qToMetres(point.positionSubvoxels[2]),
|
||
qToMetres(point.radiusSubvoxels),0.0};
|
||
if(!path.empty())
|
||
{
|
||
const auto& previous=path.back();
|
||
value.distance=previous.distance+std::sqrt(
|
||
(value.x-previous.x)*(value.x-previous.x)
|
||
+(value.y-previous.y)*(value.y-previous.y)
|
||
+(value.z-previous.z)*(value.z-previous.z));
|
||
}
|
||
path.push_back(value);
|
||
}
|
||
const double total=path.back().distance;
|
||
if(total<WindowLengthMetres)return audit;
|
||
const auto interpolate=[&](double distance)
|
||
{
|
||
distance=std::clamp(distance,0.0,total);
|
||
const auto upper=std::lower_bound(path.begin()+1,path.end(),distance,
|
||
[](const FPathPoint& point,double value){return point.distance<value;});
|
||
const auto& b=*upper;
|
||
const auto& a=*(upper-1);
|
||
const double length=b.distance-a.distance;
|
||
const double t=length>1.0e-9?(distance-a.distance)/length:0.0;
|
||
return FPathPoint{std::lerp(a.x,b.x,t),std::lerp(a.y,b.y,t),
|
||
std::lerp(a.z,b.z,t),std::lerp(a.radius,b.radius,t),distance};
|
||
};
|
||
const auto sinusoidAmplitude=[&](const std::vector<std::array<double,2>>& samples,
|
||
double& bestPeriod)
|
||
{
|
||
if(samples.size()<64)return 0.0;
|
||
double best=0.0;
|
||
for(double period=MinimumPeriodMetres;
|
||
period<=MaximumPeriodMetres+1.0e-9;period+=PeriodStepMetres)
|
||
{
|
||
const double angular=6.2831853071795864769/period;
|
||
double cc=0.0,ss=0.0,cs=0.0,yc=0.0,ys=0.0;
|
||
for(const auto& sample:samples)
|
||
{
|
||
const double phase=angular*sample[0];
|
||
const double c=std::cos(phase),s=std::sin(phase);
|
||
cc+=c*c;ss+=s*s;cs+=c*s;
|
||
yc+=sample[1]*c;ys+=sample[1]*s;
|
||
}
|
||
const double determinant=cc*ss-cs*cs;
|
||
if(determinant<=1.0e-9)continue;
|
||
const double cosine=(yc*ss-ys*cs)/determinant;
|
||
const double sine=(ys*cc-yc*cs)/determinant;
|
||
const double amplitude=std::hypot(cosine,sine);
|
||
if(amplitude>best){best=amplitude;bestPeriod=period;}
|
||
}
|
||
return best;
|
||
};
|
||
const auto appendDetrendedProfile=[&](const std::vector<double>& values,
|
||
double startDistance,std::vector<std::array<double,2>>& output)
|
||
{
|
||
if(values.size()<64)return;
|
||
// Remove a smooth mouth taper across each 32 m window while
|
||
// preserving the authored radius, where regular wall ribs can live.
|
||
// A cubic baseline tracks the broad flare without fitting the
|
||
// 0.5–8 m periods that this audit is meant to detect.
|
||
constexpr std::size_t Degree=3;
|
||
std::array<std::array<double,Degree+2>,Degree+1> normal{};
|
||
const double meanIndex=0.5*static_cast<double>(values.size()-1);
|
||
const double halfSpan=std::max(1.0,meanIndex);
|
||
for(std::size_t i=0;i<values.size();++i)
|
||
{
|
||
const double x=(static_cast<double>(i)-meanIndex)/halfSpan;
|
||
std::array<double,2*Degree+1> powers{};
|
||
powers[0]=1.0;
|
||
for(std::size_t power=1;power<powers.size();++power)
|
||
powers[power]=powers[power-1]*x;
|
||
for(std::size_t row=0;row<=Degree;++row)
|
||
{
|
||
for(std::size_t column=0;column<=Degree;++column)
|
||
normal[row][column]+=powers[row+column];
|
||
normal[row][Degree+1]+=values[i]*powers[row];
|
||
}
|
||
}
|
||
for(std::size_t column=0;column<=Degree;++column)
|
||
{
|
||
std::size_t pivot=column;
|
||
for(std::size_t row=column+1;row<=Degree;++row)
|
||
if(std::abs(normal[row][column])>
|
||
std::abs(normal[pivot][column]))pivot=row;
|
||
if(std::abs(normal[pivot][column])<=1.0e-12)return;
|
||
if(pivot!=column)std::swap(normal[pivot],normal[column]);
|
||
const double divisor=normal[column][column];
|
||
for(std::size_t value=column;value<=Degree+1;++value)
|
||
normal[column][value]/=divisor;
|
||
for(std::size_t row=0;row<=Degree;++row)
|
||
{
|
||
if(row==column)continue;
|
||
const double scale=normal[row][column];
|
||
for(std::size_t value=column;value<=Degree+1;++value)
|
||
normal[row][value]-=scale*normal[column][value];
|
||
}
|
||
}
|
||
for(std::size_t i=0;i<values.size();++i)
|
||
{
|
||
const double x=(static_cast<double>(i)-meanIndex)/halfSpan;
|
||
double trend=normal[Degree][Degree+1];
|
||
for(std::size_t coefficient=Degree;coefficient-->0;)
|
||
trend=trend*x+normal[coefficient][Degree+1];
|
||
output.push_back({startDistance+static_cast<double>(i)*SampleSpacingMetres,
|
||
values[i]-trend});
|
||
}
|
||
};
|
||
const auto appendDetrendedPairs=[](
|
||
const std::vector<std::array<double,2>>& values,
|
||
std::vector<std::array<double,2>>& output)
|
||
{
|
||
if(values.size()<64)return;
|
||
double meanX=0.0,meanY=0.0;
|
||
for(const auto& value:values){meanX+=value[0];meanY+=value[1];}
|
||
meanX/=static_cast<double>(values.size());
|
||
meanY/=static_cast<double>(values.size());
|
||
double xy=0.0,xx=0.0;
|
||
for(const auto& value:values)
|
||
{
|
||
const double x=value[0]-meanX;
|
||
xy+=x*(value[1]-meanY);xx+=x*x;
|
||
}
|
||
const double slope=xx>0.0?xy/xx:0.0;
|
||
for(const auto& value:values)
|
||
output.push_back({value[0],value[1]-meanY
|
||
-slope*(value[0]-meanX)});
|
||
};
|
||
std::vector<double> beadControl;
|
||
beadControl.reserve(static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres)+1);
|
||
constexpr double BeadSpacingMetres=1.5;
|
||
constexpr double BeadSphereRadiusMetres=2.0;
|
||
for(std::size_t i=0;i<=static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres);++i)
|
||
{
|
||
const double x=static_cast<double>(i)*SampleSpacingMetres;
|
||
const double nearest=std::abs(std::remainder(x,BeadSpacingMetres));
|
||
beadControl.push_back(std::sqrt(std::max(0.0,
|
||
BeadSphereRadiusMetres*BeadSphereRadiusMetres-nearest*nearest)));
|
||
}
|
||
std::vector<std::array<double,2>> beadSamples;
|
||
appendDetrendedProfile(beadControl,0.0,beadSamples);
|
||
double ignoredPeriod=0.0;
|
||
audit.controlAmplitudeMetres=sinusoidAmplitude(beadSamples,ignoredPeriod);
|
||
audit.controlDetected=audit.controlAmplitudeMetres>=RippleAmplitudeLimitMetres;
|
||
audit.floorControlAmplitudeMetres=audit.controlAmplitudeMetres;
|
||
audit.floorControlDetected=audit.floorControlAmplitudeMetres
|
||
>=FloorRippleAmplitudeLimitMetres;
|
||
// A signed-distance floor with a real 0.25 m ledge must still trip
|
||
// the same floor-ripple gauge after the crossing-refinement change.
|
||
std::vector<std::array<double,2>> floorStepResiduals;
|
||
floorStepResiduals.reserve(static_cast<std::size_t>(WindowLengthMetres/
|
||
SampleSpacingMetres)+1);
|
||
constexpr double FloorStepControlProbeSpacingMetres=0.25;
|
||
for(std::size_t sample=0;
|
||
sample<=static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres);
|
||
++sample)
|
||
{
|
||
const double distance=static_cast<double>(sample)*SampleSpacingMetres;
|
||
const double stepHeight=distance>=16.0?0.25:0.0;
|
||
double previousDensity=0.0,previousZ=0.0;
|
||
bool havePrevious=false,haveFloor=false;
|
||
double bestFloor=0.0,bestDistance=std::numeric_limits<double>::infinity();
|
||
for(int vertical=0;vertical<7;++vertical)
|
||
{
|
||
const double z=-1.0+vertical*FloorStepControlProbeSpacingMetres;
|
||
const double density=z-stepHeight;
|
||
if(havePrevious&&previousDensity<0.0&&density>=0.0)
|
||
{
|
||
double lowZ=previousZ,highZ=z;
|
||
for(int refine=0;refine<5;++refine)
|
||
{
|
||
const double middleZ=(lowZ+highZ)*0.5;
|
||
if(middleZ-stepHeight<0.0)lowZ=middleZ;
|
||
else highZ=middleZ;
|
||
}
|
||
const double crossing=(lowZ+highZ)*0.5;
|
||
const double candidateDistance=std::abs(crossing);
|
||
if(candidateDistance<bestDistance)
|
||
{bestDistance=candidateDistance;bestFloor=crossing;haveFloor=true;}
|
||
}
|
||
previousDensity=density;previousZ=z;havePrevious=true;
|
||
}
|
||
if(haveFloor)floorStepResiduals.push_back({distance,bestFloor});
|
||
}
|
||
std::vector<std::array<double,2>> floorStepSamples;
|
||
appendDetrendedPairs(floorStepResiduals,floorStepSamples);
|
||
double floorStepPeriod=0.0;
|
||
audit.floorStepControlAmplitudeMetres=sinusoidAmplitude(
|
||
floorStepSamples,floorStepPeriod);
|
||
audit.floorStepControlDetected=audit.floorStepControlAmplitudeMetres
|
||
>=FloorRippleAmplitudeLimitMetres;
|
||
const std::array<double,5> windowStarts{{0.03,0.31,0.457,0.61,0.91}};
|
||
std::vector<std::array<double,2>> coherentSamples;
|
||
std::vector<std::array<double,2>> coherentFloorSamples;
|
||
for(const double fraction:windowStarts)
|
||
{
|
||
const double start=(total-WindowLengthMetres)*fraction;
|
||
std::vector<double> radii;
|
||
radii.reserve(static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres)+1);
|
||
std::vector<std::array<double,2>> floorResiduals;
|
||
floorResiduals.reserve(static_cast<std::size_t>(WindowLengthMetres/
|
||
SampleSpacingMetres)+1);
|
||
std::vector<std::array<double,6>> floorDebugSamples;
|
||
floorDebugSamples.reserve(static_cast<std::size_t>(WindowLengthMetres/
|
||
SampleSpacingMetres)+1);
|
||
for(std::size_t sample=0;
|
||
sample<=static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres);
|
||
++sample)
|
||
{
|
||
++audit.expectedSamples;
|
||
++audit.floorExpectedSamples;
|
||
const double distance=start+static_cast<double>(sample)*SampleSpacingMetres;
|
||
const auto pose=interpolate(distance);
|
||
// Recover the composed walk surface from its zero crossing.
|
||
// The support strip is segment-built and is not itself a true
|
||
// signed-distance field, so a fixed centerline radius misses
|
||
// the stepped top that the player actually walks on.
|
||
const double expectedFloor=pose.z-pose.radius+0.20;
|
||
constexpr double FloorProbeStepMetres=0.25;
|
||
constexpr int FloorProbeCount=7;
|
||
double previousDensity=0.0,previousZ=0.0;
|
||
bool havePrevious=false,haveFloor=false;
|
||
double bestFloor=0.0,bestDistance=std::numeric_limits<double>::infinity();
|
||
for(int vertical=0;vertical<FloorProbeCount;++vertical)
|
||
{
|
||
const double z=expectedFloor-1.0+vertical*FloorProbeStepMetres;
|
||
const double density=static_cast<double>(Field.densityAt({
|
||
pose.x/coreMetres,pose.y/coreMetres,z/coreMetres}))*coreMetres;
|
||
if(havePrevious&&previousDensity<0.0&&density>=0.0)
|
||
{
|
||
const double denominator=density-previousDensity;
|
||
double crossing=denominator>1.0e-12
|
||
?previousZ+(z-previousZ)*(-previousDensity)/denominator:z;
|
||
// Density can change sharply at a composed floor. Refine
|
||
// the sign bracket instead of assuming it is linear over
|
||
// an entire Core voxel; linear interpolation can report a
|
||
// one-voxel step even when the zero crossing is nearby.
|
||
double lowZ=previousZ,highZ=z;
|
||
double lowDensity=previousDensity,highDensity=density;
|
||
for(int refine=0;refine<5;++refine)
|
||
{
|
||
const double middleZ=(lowZ+highZ)*0.5;
|
||
const double middleDensity=static_cast<double>(Field.densityAt({
|
||
pose.x/coreMetres,pose.y/coreMetres,middleZ/coreMetres}))
|
||
*coreMetres;
|
||
if(middleDensity<0.0)
|
||
{lowZ=middleZ;lowDensity=middleDensity;}
|
||
else
|
||
{highZ=middleZ;highDensity=middleDensity;}
|
||
}
|
||
(void)lowDensity;(void)highDensity;
|
||
crossing=(lowZ+highZ)*0.5;
|
||
const double candidateDistance=std::abs(crossing-expectedFloor);
|
||
if(candidateDistance<bestDistance)
|
||
{bestDistance=candidateDistance;bestFloor=crossing;haveFloor=true;}
|
||
}
|
||
previousDensity=density;previousZ=z;havePrevious=true;
|
||
}
|
||
if(haveFloor)
|
||
{
|
||
floorResiduals.push_back({distance,bestFloor-expectedFloor});
|
||
floorDebugSamples.push_back({distance,expectedFloor,bestFloor,
|
||
pose.x,pose.y,pose.z});
|
||
++audit.floorSamples;
|
||
}
|
||
const auto before=interpolate(std::max(0.0,distance-0.25));
|
||
const auto after=interpolate(std::min(total,distance+0.25));
|
||
const double tangentX=after.x-before.x,tangentY=after.y-before.y;
|
||
const double tangentLength=std::hypot(tangentX,tangentY);
|
||
if(tangentLength<=1.0e-6)continue;
|
||
const double nx=-tangentY/tangentLength,ny=tangentX/tangentLength;
|
||
const double sampleRadius=std::max(0.20,pose.radius-0.18);
|
||
const double x0=pose.x/coreMetres,y0=pose.y/coreMetres,z0=pose.z/coreMetres;
|
||
const double offset=sampleRadius/coreMetres;
|
||
const double left=static_cast<double>(Field.densityAt(
|
||
{x0+nx*offset,y0+ny*offset,z0}))*coreMetres;
|
||
const double right=static_cast<double>(Field.densityAt(
|
||
{x0-nx*offset,y0-ny*offset,z0}))*coreMetres;
|
||
if(!std::isfinite(left)||!std::isfinite(right)
|
||
||left< -0.75||left>0.75||right< -0.75||right>0.75)continue;
|
||
// The probes sit 0.18 m inside the authored radius. Add that
|
||
// radius back to the signed wall distance so radius-carried ribs
|
||
// remain visible; the cubic detrend removes the smooth mouth flare.
|
||
radii.push_back(sampleRadius+0.5*(left+right));
|
||
++audit.samples;
|
||
}
|
||
if(radii.size()<static_cast<std::size_t>(WindowLengthMetres/
|
||
SampleSpacingMetres)*3/4)continue;
|
||
std::vector<std::array<double,2>> windowSamples;
|
||
appendDetrendedProfile(radii,start,windowSamples);
|
||
double localPeriod=0.0;
|
||
const double localAmplitude=sinusoidAmplitude(windowSamples,localPeriod);
|
||
std::printf("[PassageWallRippleWindow] id=%s start_m=%.3f "
|
||
"period_m=%.3f amplitude_m=%.4f coverage=%zu/%zu threshold_m=0.035\n",
|
||
passage.stableId.c_str(),start,localPeriod,localAmplitude,
|
||
radii.size(),static_cast<std::size_t>(WindowLengthMetres/
|
||
SampleSpacingMetres)+1);
|
||
if(localAmplitude>audit.localPeakAmplitudeMetres)
|
||
{
|
||
audit.localPeakAmplitudeMetres=localAmplitude;
|
||
audit.periodMetres=localPeriod;
|
||
}
|
||
coherentSamples.insert(coherentSamples.end(),windowSamples.begin(),
|
||
windowSamples.end());
|
||
double floorWindowPeriod=0.0,floorWindowAmplitude=0.0;
|
||
if(floorResiduals.size()>=static_cast<std::size_t>(WindowLengthMetres/
|
||
SampleSpacingMetres)*3/4)
|
||
{
|
||
std::vector<std::array<double,2>> floorWindowSamples;
|
||
appendDetrendedPairs(floorResiduals,floorWindowSamples);
|
||
floorWindowAmplitude=sinusoidAmplitude(floorWindowSamples,
|
||
floorWindowPeriod);
|
||
if(floorWindowAmplitude>audit.floorAmplitudeMetres)
|
||
{
|
||
audit.floorAmplitudeMetres=floorWindowAmplitude;
|
||
audit.floorPeriodMetres=floorWindowPeriod;
|
||
audit.floorPeakWindowStartMetres=start;
|
||
audit.floorPeakProfile=std::move(floorDebugSamples);
|
||
}
|
||
coherentFloorSamples.insert(coherentFloorSamples.end(),
|
||
floorWindowSamples.begin(),floorWindowSamples.end());
|
||
}
|
||
const double floorWindowCoverage=static_cast<double>(floorResiduals.size())
|
||
/(WindowLengthMetres/SampleSpacingMetres+1.0);
|
||
double floorResidualMinimum=0.0,floorResidualMaximum=0.0;
|
||
if(!floorResiduals.empty())
|
||
{
|
||
const auto extrema=std::minmax_element(floorResiduals.begin(),
|
||
floorResiduals.end(),[](const auto& a,const auto& b)
|
||
{return a[1]<b[1];});
|
||
floorResidualMinimum=(*extrema.first)[1];
|
||
floorResidualMaximum=(*extrema.second)[1];
|
||
}
|
||
std::printf("[PassageFloorRippleWindow] id=%s start_m=%.3f "
|
||
"period_m=%.3f amplitude_m=%.4f coverage=%.3f "
|
||
"residual_range_m=%.4f..%.4f threshold_m=0.025\n",
|
||
passage.stableId.c_str(),start,floorWindowPeriod,floorWindowAmplitude,
|
||
floorWindowCoverage,floorResidualMinimum,floorResidualMaximum);
|
||
}
|
||
audit.coherentAmplitudeMetres=sinusoidAmplitude(coherentSamples,
|
||
audit.coherentPeriodMetres);
|
||
audit.floorCoherentAmplitudeMetres=sinusoidAmplitude(coherentFloorSamples,
|
||
audit.floorCoherentPeriodMetres);
|
||
audit.amplitudeMetres=audit.localPeakAmplitudeMetres;
|
||
const double coverage=audit.expectedSamples>0
|
||
?static_cast<double>(audit.samples)/audit.expectedSamples:0.0;
|
||
const double floorCoverage=audit.floorExpectedSamples>0
|
||
?static_cast<double>(audit.floorSamples)/audit.floorExpectedSamples:0.0;
|
||
audit.passed=audit.controlDetected&&coverage>=0.75
|
||
&&audit.localPeakAmplitudeMetres<RippleAmplitudeLimitMetres
|
||
&&audit.floorControlDetected&&audit.floorStepControlDetected
|
||
&&floorCoverage>=0.75
|
||
&&audit.floorAmplitudeMetres<FloorRippleAmplitudeLimitMetres;
|
||
return audit;
|
||
};
|
||
const auto looksNatural=[&](double flatRun,double reliefPeakToPeak)
|
||
{return flatRun<=MaximumFlatRunMetres
|
||
&&reliefPeakToPeak>=MinimumWallReliefMetres;};
|
||
const auto findStratum=[&](std::int32_t index)->const VoxelForgeCore::ResolvedLayoutStratumV2*
|
||
{
|
||
const auto it=std::find_if(ResolvedLayout->strates.begin(),ResolvedLayout->strates.end(),
|
||
[index](const auto& s){return s.strateIndex==index;});
|
||
return it==ResolvedLayout->strates.end()?nullptr:&*it;
|
||
};
|
||
const auto controlSnapshot=CoreSnapshot.withoutResolvedLayoutPassages();
|
||
VoxelForgeCore::World blockedWorld(controlSnapshot);
|
||
std::string blockedCompileError;
|
||
const bool blockedCompiled=controlSnapshot
|
||
&&blockedWorld.compile(ShapeRequest,Scheduler,blockedCompileError);
|
||
if(!blockedCompiled)
|
||
{
|
||
Streamer.Stop();
|
||
std::fprintf(stderr,"[ResolvedPassageBlockedControl] compile_failed=%s\n",
|
||
blockedCompileError.c_str());
|
||
return 10;
|
||
}
|
||
struct FPassageOpenAirAudit
|
||
{
|
||
std::uint64_t supportSamples=0,violations=0,legacyViolations=0;
|
||
std::uint64_t columnSamples=0,shortSpanSamples=0;
|
||
std::uint64_t routeSupportSamples=0,routeSupportViolations=0;
|
||
std::array<std::array<double,4>,8> firstViolations{};
|
||
std::size_t firstViolationCount=0;
|
||
};
|
||
constexpr double PassageSupportAuditSpacingMetres=0.25;
|
||
constexpr std::array<double,2> PassageSupportAuditDepthsMetres{{0.125,0.375}};
|
||
constexpr std::array<std::array<double,2>,16> PassageSupportAuditDirections{{
|
||
{{1.0000000000000000,0.0000000000000000}},
|
||
{{0.9238795325112867,0.3826834323650898}},
|
||
{{0.7071067811865476,0.7071067811865476}},
|
||
{{0.3826834323650898,0.9238795325112867}},
|
||
{{0.0000000000000000,1.0000000000000000}},
|
||
{{-0.3826834323650898,0.9238795325112867}},
|
||
{{-0.7071067811865476,0.7071067811865476}},
|
||
{{-0.9238795325112867,0.3826834323650898}},
|
||
{{-1.0000000000000000,0.0000000000000000}},
|
||
{{-0.9238795325112867,-0.3826834323650898}},
|
||
{{-0.7071067811865476,-0.7071067811865476}},
|
||
{{-0.3826834323650898,-0.9238795325112867}},
|
||
{{0.0000000000000000,-1.0000000000000000}},
|
||
{{0.3826834323650898,-0.9238795325112867}},
|
||
{{0.7071067811865476,-0.7071067811865476}},
|
||
{{0.9238795325112867,-0.3826834323650898}}}};
|
||
const auto auditPassageOpenAir=[&](const VoxelForgeCore::World& Field,
|
||
const VoxelForgeCore::World& Native,
|
||
const VoxelForgeCore::ResolvedLayoutPassageV1& passage,
|
||
std::size_t passageIndex)
|
||
{
|
||
FPassageOpenAirAudit audit;
|
||
const auto inspect=[&](double xMetres,double yMetres,double floorMetres,
|
||
double depthMetres,double supportThicknessMetres,bool routeSupport=false,
|
||
std::size_t passageSegment=std::numeric_limits<std::size_t>::max())
|
||
{
|
||
if(depthMetres<=0.0||depthMetres>supportThicknessMetres+1.0e-6)return;
|
||
++audit.supportSamples;
|
||
audit.routeSupportSamples+=routeSupport;
|
||
const VoxelForgeCore::Vec3d sample{xMetres/coreMetres,yMetres/coreMetres,
|
||
(floorMetres-depthMetres)/coreMetres};
|
||
const float nativeDensity=Native.densityAt(sample);
|
||
const float passageDensity=Field.densityAt(sample);
|
||
const float legacySupportDensity=std::min(nativeDensity,
|
||
static_cast<float>(-depthMetres/coreMetres));
|
||
if(nativeDensity>=0.0f&&legacySupportDensity<0.0f)
|
||
++audit.legacyViolations;
|
||
if(nativeDensity>=0.0f&&passageDensity<0.0f)
|
||
{
|
||
const bool connectedColumn=!routeSupport
|
||
&&passageSegment!=std::numeric_limits<std::size_t>::max()
|
||
&&Field.isPassageColumnSample(passageIndex,passageSegment,sample);
|
||
if(connectedColumn){++audit.columnSamples;return;}
|
||
const bool groundedShortSpan=!routeSupport
|
||
&&passageSegment!=std::numeric_limits<std::size_t>::max()
|
||
&&Field.isPassageShortSpanSample(passageIndex,passageSegment,sample);
|
||
if(groundedShortSpan){++audit.shortSpanSamples;return;}
|
||
audit.routeSupportViolations+=routeSupport;
|
||
if(audit.firstViolationCount<audit.firstViolations.size())
|
||
audit.firstViolations[audit.firstViolationCount++]={xMetres,yMetres,
|
||
floorMetres-depthMetres,passageDensity};
|
||
++audit.violations;
|
||
}
|
||
};
|
||
const auto inspectFloorDisc=[&](double xMetres,double yMetres,double floorMetres,
|
||
double supportRadiusMetres,double depthMetres,double supportThicknessMetres,
|
||
bool routeSupport=false,
|
||
std::size_t passageSegment=std::numeric_limits<std::size_t>::max())
|
||
{
|
||
const std::size_t radialSteps=static_cast<std::size_t>(std::ceil(
|
||
supportRadiusMetres/PassageSupportAuditSpacingMetres));
|
||
for(std::size_t radialStep=0;radialStep<=radialSteps;++radialStep)
|
||
{
|
||
const double radial=std::min(supportRadiusMetres,
|
||
radialStep*PassageSupportAuditSpacingMetres);
|
||
for(const auto& direction:PassageSupportAuditDirections)
|
||
inspect(xMetres+direction[0]*radial,yMetres+direction[1]*radial,
|
||
floorMetres,depthMetres,supportThicknessMetres,routeSupport,
|
||
passageSegment);
|
||
}
|
||
};
|
||
for(std::size_t segment=1;segment<passage.controlPoints.size();++segment)
|
||
{
|
||
const auto& a=passage.controlPoints[segment-1];
|
||
const auto& b=passage.controlPoints[segment];
|
||
const double ax=qToMetres(a.positionSubvoxels[0]);
|
||
const double ay=qToMetres(a.positionSubvoxels[1]);
|
||
const double az=qToMetres(a.positionSubvoxels[2]);
|
||
const double bx=qToMetres(b.positionSubvoxels[0]);
|
||
const double by=qToMetres(b.positionSubvoxels[1]);
|
||
const double bz=qToMetres(b.positionSubvoxels[2]);
|
||
const double length=std::sqrt((bx-ax)*(bx-ax)+(by-ay)*(by-ay)+(bz-az)*(bz-az));
|
||
const std::size_t intervals=std::max<std::size_t>(1,static_cast<std::size_t>(
|
||
std::ceil(length/PassageSupportAuditSpacingMetres)));
|
||
for(std::size_t sample=0;sample<=intervals;++sample)
|
||
{
|
||
if(segment>1&&sample==0)continue;
|
||
const double t=static_cast<double>(sample)/intervals;
|
||
const double centerX=ax+(bx-ax)*t,centerY=ay+(by-ay)*t;
|
||
const double centerZ=az+(bz-az)*t;
|
||
const double radius=qToMetres(a.radiusSubvoxels)
|
||
+(qToMetres(b.radiusSubvoxels)-qToMetres(a.radiusSubvoxels))*t;
|
||
const double floorZ=centerZ-radius+0.20;
|
||
for(const double depth:PassageSupportAuditDepthsMetres)
|
||
inspectFloorDisc(centerX,centerY,floorZ,radius,depth,1.0,
|
||
false,segment-1);
|
||
}
|
||
}
|
||
const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(),
|
||
ResolvedLayout->strates.end(),[&](const auto& value)
|
||
{return value.strateIndex==passage.upperStrateIndex;});
|
||
if(upperStratum!=ResolvedLayout->strates.end())
|
||
{
|
||
std::array<const VoxelForgeCore::ResolvedLayoutAnchorV2*,5> route{};
|
||
std::size_t routeCount=0;
|
||
for(const auto& anchor:upperStratum->anchors)
|
||
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||
{
|
||
if(routeCount<route.size())route[routeCount++]=&anchor;
|
||
else
|
||
{
|
||
std::move(route.begin()+1,route.end(),route.begin());
|
||
route.back()=&anchor;
|
||
}
|
||
}
|
||
const double mouthX=qToMetres(passage.upperMouthSubvoxels[0]);
|
||
const double mouthY=qToMetres(passage.upperMouthSubvoxels[1]);
|
||
constexpr double RouteSupportReachMetres=13.0;
|
||
constexpr double RouteFloorOffsetMetres=1.20;
|
||
constexpr double RouteSupportThicknessMetres=1.0;
|
||
constexpr double RouteSupportDepthStepMetres=0.125;
|
||
for(std::size_t segment=1;segment<routeCount;++segment)
|
||
{
|
||
const auto& a=*route[segment-1];
|
||
const auto& b=*route[segment];
|
||
const double ax=qToMetres(a.values[0]),ay=qToMetres(a.values[1]);
|
||
const double az=qToMetres(a.values[2]);
|
||
const double bx=qToMetres(b.values[0]),by=qToMetres(b.values[1]);
|
||
const double bz=qToMetres(b.values[2]);
|
||
const double dx=bx-ax,dy=by-ay,dz=bz-az;
|
||
const double length=std::sqrt(dx*dx+dy*dy+dz*dz);
|
||
const std::size_t intervals=std::max<std::size_t>(1,
|
||
static_cast<std::size_t>(std::ceil(length
|
||
/PassageSupportAuditSpacingMetres)));
|
||
const double routeRadius=a.values.size()>=4
|
||
?std::max(0.75,qToMetres(a.values[3])):0.75;
|
||
for(std::size_t sample=0;sample<=intervals;++sample)
|
||
{
|
||
if(segment>1&&sample==0)continue;
|
||
const double t=static_cast<double>(sample)/intervals;
|
||
const double centerX=ax+dx*t,centerY=ay+dy*t;
|
||
const double centerZ=az+dz*t;
|
||
for(std::size_t radialStep=0;radialStep<=static_cast<std::size_t>(
|
||
std::ceil(routeRadius/PassageSupportAuditSpacingMetres));++radialStep)
|
||
{
|
||
const double radial=std::min(routeRadius,
|
||
radialStep*PassageSupportAuditSpacingMetres);
|
||
for(const auto& direction:PassageSupportAuditDirections)
|
||
{
|
||
const double x=centerX+direction[0]*radial;
|
||
const double y=centerY+direction[1]*radial;
|
||
if(std::hypot(x-mouthX,y-mouthY)>RouteSupportReachMetres)continue;
|
||
const double floorZ=centerZ-RouteFloorOffsetMetres;
|
||
for(double depth=RouteSupportDepthStepMetres;
|
||
depth<=RouteSupportThicknessMetres+1.0e-6;
|
||
depth+=RouteSupportDepthStepMetres)
|
||
inspect(x,y,floorZ,depth,RouteSupportThicknessMetres,true);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return audit;
|
||
};
|
||
struct FPassageColumnAudit
|
||
{
|
||
std::uint64_t columns=0,shortSpans=0,connectedEnds=0,routeSections=0;
|
||
std::uint64_t routeSectionFailures=0,wallSections=0,wallFailures=0;
|
||
double minimumWallMetres=std::numeric_limits<double>::infinity();
|
||
double minimumSurfaceRadiusMetres=std::numeric_limits<double>::infinity();
|
||
double maximumSurfaceRadiusMetres=0.0;
|
||
double surfaceRadiusReliefMetres=0.0;
|
||
double longestShortSpanMetres=0.0;
|
||
bool passed=true;
|
||
bool disconnectedEndControlApplicable=false;
|
||
bool disconnectedEndControlDetected=false;
|
||
};
|
||
const auto auditPassageColumns=[&](const VoxelForgeCore::World& Field,
|
||
const VoxelForgeCore::World& Native,
|
||
const VoxelForgeCore::ResolvedLayoutPassageV1& passage,
|
||
std::size_t passageIndex)
|
||
{
|
||
FPassageColumnAudit audit;
|
||
const double qMetres=coreMetres
|
||
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||
constexpr std::array<std::array<double,2>,5> supportOffsets{{
|
||
{{0.0,0.0}},{{0.45,0.0}},{{-0.45,0.0}},{{0.0,0.45}},{{0.0,-0.45}}}};
|
||
const auto endpointSupportCount=[&](const std::array<double,4>& point)
|
||
{
|
||
const double x=point[0]*qMetres,y=point[1]*qMetres;
|
||
const double floor=point[2]*qMetres-point[3]*qMetres+0.13;
|
||
std::uint32_t supported=0;
|
||
for(const auto& offset:supportOffsets)
|
||
{
|
||
bool solid=false;
|
||
for(const double depth:{0.125,0.25})
|
||
if(Native.densityAt({(x+offset[0])/coreMetres,
|
||
(y+offset[1])/coreMetres,(floor-depth)/coreMetres})<0.0f)
|
||
{solid=true;break;}
|
||
supported+=solid;
|
||
}
|
||
return supported;
|
||
};
|
||
const auto endpointConnected=[&](const std::array<double,4>& point)
|
||
{return endpointSupportCount(point)>=3;};
|
||
struct FColumnCapProbe
|
||
{
|
||
std::uint64_t columnSamples=0,nativeOverlapSamples=0;
|
||
double firstOverlapDepthMetres=std::numeric_limits<double>::infinity();
|
||
double minimumNativeDensity=std::numeric_limits<double>::infinity();
|
||
};
|
||
const auto probeColumnCap=[&](const VoxelForgeCore::PassageVoidCrossingAuditV1& crossing,
|
||
std::size_t segment,double capZMetres,bool upperCap,double depthLimitMetres)
|
||
{
|
||
FColumnCapProbe probe;
|
||
constexpr double DepthStepMetres=0.25;
|
||
constexpr double RadialStepMetres=0.50;
|
||
constexpr std::size_t DirectionCount=16;
|
||
const double axisX=qToMetres(crossing.columnAxisSubvoxels[0]);
|
||
const double axisY=qToMetres(crossing.columnAxisSubvoxels[1]);
|
||
for(double depth=DepthStepMetres;depth<=depthLimitMetres+1.0e-6;
|
||
depth+=DepthStepMetres)
|
||
{
|
||
const double z=capZMetres+(upperCap?-depth:depth);
|
||
const double maxRadius=qToMetres(crossing.columnMaximumRadiusSubvoxels);
|
||
const std::size_t radialSteps=static_cast<std::size_t>(
|
||
std::ceil(maxRadius/RadialStepMetres));
|
||
for(std::size_t radialStep=0;radialStep<=radialSteps;++radialStep)
|
||
{
|
||
const double radius=std::min(maxRadius,
|
||
radialStep*RadialStepMetres);
|
||
for(std::size_t direction=0;direction<DirectionCount;++direction)
|
||
{
|
||
const double angle=2.0*3.14159265358979323846
|
||
*direction/DirectionCount;
|
||
const VoxelForgeCore::Vec3d sample{
|
||
(axisX+radius*std::cos(angle))/coreMetres,
|
||
(axisY+radius*std::sin(angle))/coreMetres,z/coreMetres};
|
||
if(!Field.isPassageColumnSample(passageIndex,segment,sample))continue;
|
||
++probe.columnSamples;
|
||
const double nativeDensity=Native.densityAt(sample);
|
||
probe.minimumNativeDensity=std::min(probe.minimumNativeDensity,
|
||
nativeDensity);
|
||
if(nativeDensity<0.0)
|
||
{
|
||
++probe.nativeOverlapSamples;
|
||
probe.firstOverlapDepthMetres=std::min(
|
||
probe.firstOverlapDepthMetres,depth);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return probe;
|
||
};
|
||
const auto columnSolidAt=[&](std::size_t segment,double x,double y,double z)
|
||
{
|
||
return Field.isPassageColumnSample(passageIndex,segment,
|
||
{x/coreMetres,y/coreMetres,z/coreMetres});
|
||
};
|
||
const auto shortSpanSolidAt=[&](std::size_t segment,double x,double y,double z)
|
||
{
|
||
return Field.isPassageShortSpanSample(passageIndex,segment,
|
||
{x/coreMetres,y/coreMetres,z/coreMetres});
|
||
};
|
||
const auto crossings=Field.passageVoidCrossings();
|
||
double controlLowestRouteZ=std::numeric_limits<double>::infinity();
|
||
bool disconnectedEndControlApplicable=false;
|
||
bool disconnectedEndControlDetected=false;
|
||
for(const auto& crossing:crossings)
|
||
{
|
||
if(crossing.passageIndex!=passageIndex)continue;
|
||
const bool connectedStart=endpointConnected(crossing.startPointSubvoxels);
|
||
const bool connectedEnd=endpointConnected(crossing.endPointSubvoxels);
|
||
if(!crossing.needsColumn)
|
||
{
|
||
++audit.shortSpans;
|
||
audit.longestShortSpanMetres=std::max(audit.longestShortSpanMetres,
|
||
crossing.unsupportedLengthMetres);
|
||
const bool shortSpanPass=connectedStart&&connectedEnd
|
||
&&crossing.unsupportedLengthMetres<=12.5
|
||
&&crossing.lastSegment+1<passage.controlPoints.size();
|
||
audit.passed&=shortSpanPass;
|
||
std::printf("[PassageGroundedShortSpanAudit] id=%s segments=%zu..%zu length_m=%.3f threshold_m=12.500 connected=%d/2 status=%s\n",
|
||
passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment,
|
||
crossing.unsupportedLengthMetres,
|
||
static_cast<int>(connectedStart)+static_cast<int>(connectedEnd),
|
||
shortSpanPass?"PASS":"FAIL");
|
||
const std::size_t segment=std::min(crossing.firstSegment,
|
||
passage.controlPoints.size()-2);
|
||
const auto& a=passage.controlPoints[segment];
|
||
const auto& b=passage.controlPoints[segment+1];
|
||
const double x=(qToMetres(a.positionSubvoxels[0])
|
||
+qToMetres(b.positionSubvoxels[0]))*0.5;
|
||
const double y=(qToMetres(a.positionSubvoxels[1])
|
||
+qToMetres(b.positionSubvoxels[1]))*0.5;
|
||
const double z=(qToMetres(a.positionSubvoxels[2])
|
||
+qToMetres(b.positionSubvoxels[2]))*0.5
|
||
-qToMetres(a.radiusSubvoxels+b.radiusSubvoxels)*0.5+0.13;
|
||
audit.passed&=shortSpanSolidAt(segment,x,y,z-0.25);
|
||
continue;
|
||
}
|
||
if(crossing.lastSegment+1>=passage.controlPoints.size())
|
||
{audit.passed=false;continue;}
|
||
const double capTopMetres=qToMetres(crossing.columnTopZSubvoxels);
|
||
const double capBottomMetres=qToMetres(crossing.columnBottomZSubvoxels);
|
||
const auto topCap=probeColumnCap(crossing,crossing.firstSegment,
|
||
capTopMetres,true,6.0);
|
||
const auto bottomCap=probeColumnCap(crossing,crossing.lastSegment,
|
||
capBottomMetres,false,6.0);
|
||
const bool topCapConnected=topCap.nativeOverlapSamples>0;
|
||
const bool bottomCapConnected=bottomCap.nativeOverlapSamples>0;
|
||
const std::uint32_t routeStartSupport=endpointSupportCount(
|
||
crossing.startPointSubvoxels);
|
||
const std::uint32_t routeEndSupport=endpointSupportCount(
|
||
crossing.endPointSubvoxels);
|
||
std::printf("[PassageColumnRouteEndpointSupport] id=%s start=%u/5 start_z_m=%.3f end=%u/5 end_z_m=%.3f threshold=3/5\n",
|
||
passage.stableId.c_str(),routeStartSupport,
|
||
qToMetres(crossing.startPointSubvoxels[2]),routeEndSupport,
|
||
qToMetres(crossing.endPointSubvoxels[2]));
|
||
const auto printCap=[&](const char* endName,const char* side,double capZ,
|
||
const FColumnCapProbe& probe,bool connected)
|
||
{
|
||
std::printf("[PassageColumnCapAudit] id=%s end=%s side=%s cap_z_m=%.3f column_samples=%llu native_overlap_samples=%llu first_overlap_depth_m=%.3f minimum_native_density=%.6f search_depth_m=6.000 status=%s\n",
|
||
passage.stableId.c_str(),endName,side,capZ,
|
||
static_cast<unsigned long long>(probe.columnSamples),
|
||
static_cast<unsigned long long>(probe.nativeOverlapSamples),
|
||
probe.firstOverlapDepthMetres,probe.minimumNativeDensity,
|
||
connected?"PASS":"FAIL");
|
||
};
|
||
printCap("top","upper",capTopMetres,topCap,topCapConnected);
|
||
printCap("bottom","lower",capBottomMetres,bottomCap,bottomCapConnected);
|
||
const auto upper=std::find_if(ResolvedLayout->strates.begin(),
|
||
ResolvedLayout->strates.end(),[&](const auto& value)
|
||
{return value.strateIndex==passage.upperStrateIndex;});
|
||
const double startRouteZ=qToMetres(crossing.startPointSubvoxels[2]);
|
||
const double endRouteZ=qToMetres(crossing.endPointSubvoxels[2]);
|
||
const double lowerRouteZ=std::min(startRouteZ,endRouteZ);
|
||
if(upper!=ResolvedLayout->strates.end()
|
||
&&upper->generatorKind=="FloatingIslands"
|
||
&&lowerRouteZ<controlLowestRouteZ)
|
||
{
|
||
controlLowestRouteZ=lowerRouteZ;
|
||
// Move the hypothetical lower cap 20 m upward into the FI air gap.
|
||
// Its first 2 m are still inside the original column bounds, but a
|
||
// truncated column there has no native substrate to join.
|
||
const double hypotheticalBottomZ=lowerRouteZ+20.0;
|
||
const auto disconnectedControl=probeColumnCap(crossing,
|
||
crossing.lastSegment,hypotheticalBottomZ,false,2.0);
|
||
disconnectedEndControlApplicable=disconnectedControl.columnSamples>0;
|
||
disconnectedEndControlDetected=disconnectedEndControlApplicable
|
||
&&disconnectedControl.nativeOverlapSamples==0;
|
||
std::printf("[PassageColumnDisconnectedEndControl] source=air_gap_truncated_lower_cap cap_z_m=%.3f column_samples=%llu native_overlap_samples=%llu minimum_native_density=%.6f detected=%s\n",
|
||
hypotheticalBottomZ,
|
||
static_cast<unsigned long long>(disconnectedControl.columnSamples),
|
||
static_cast<unsigned long long>(disconnectedControl.nativeOverlapSamples),
|
||
disconnectedControl.minimumNativeDensity,
|
||
disconnectedEndControlDetected?"true":"false");
|
||
}
|
||
++audit.columns;
|
||
audit.connectedEnds+=topCapConnected+bottomCapConnected;
|
||
const double axisX=qToMetres(crossing.columnAxisSubvoxels[0]);
|
||
const double axisY=qToMetres(crossing.columnAxisSubvoxels[1]);
|
||
const double minimumRadius=qToMetres(crossing.columnMinimumRadiusSubvoxels);
|
||
const double maximumRadius=qToMetres(crossing.columnMaximumRadiusSubvoxels);
|
||
const double minimumWall=minimumRadius-crossing.maximumRouteOffsetMetres
|
||
-crossing.maximumBoreRadiusMetres-0.50;
|
||
audit.minimumWallMetres=std::min(audit.minimumWallMetres,minimumWall);
|
||
const std::size_t segmentCount=crossing.lastSegment-crossing.firstSegment+1;
|
||
const std::size_t probeCount=std::min<std::size_t>(128,segmentCount);
|
||
std::uint64_t crossingRouteFailures=0,crossingWallFailures=0;
|
||
for(std::size_t probe=0;probe<probeCount;++probe)
|
||
{
|
||
const std::size_t segment=crossing.firstSegment
|
||
+std::min(segmentCount-1,(probe*segmentCount)/probeCount);
|
||
const auto& a=passage.controlPoints[segment];
|
||
const auto& b=passage.controlPoints[segment+1];
|
||
const double ax=qToMetres(a.positionSubvoxels[0]);
|
||
const double ay=qToMetres(a.positionSubvoxels[1]);
|
||
const double az=qToMetres(a.positionSubvoxels[2]);
|
||
const double bx=qToMetres(b.positionSubvoxels[0]);
|
||
const double by=qToMetres(b.positionSubvoxels[1]);
|
||
const double bz=qToMetres(b.positionSubvoxels[2]);
|
||
const double x=(ax+bx)*0.5,y=(ay+by)*0.5;
|
||
const double z=(az+bz)*0.5;
|
||
const double radius=(qToMetres(a.radiusSubvoxels)
|
||
+qToMetres(b.radiusSubvoxels))*0.5;
|
||
const double floor=z-radius+0.13;
|
||
const VoxelForgeCore::Vec3d supportPoint{
|
||
x/coreMetres,y/coreMetres,(floor-2.0)/coreMetres};
|
||
const bool solidUnderRoute=columnSolidAt(segment,x,y,floor-2.0)
|
||
||Native.densityAt(supportPoint)<0.0f;
|
||
++audit.routeSections;
|
||
audit.routeSectionFailures+=!solidUnderRoute;
|
||
crossingRouteFailures+=!solidUnderRoute;
|
||
const double radial=std::hypot(x-axisX,y-axisY);
|
||
const double wallRadius=radial+radius+1.5;
|
||
const double outwardX=radial>1.0e-6?(x-axisX)/radial:1.0;
|
||
const double outwardY=radial>1.0e-6?(y-axisY)/radial:0.0;
|
||
const bool wallSolid=columnSolidAt(segment,
|
||
axisX+outwardX*wallRadius,axisY+outwardY*wallRadius,z);
|
||
++audit.wallSections;
|
||
audit.wallFailures+=!wallSolid;
|
||
crossingWallFailures+=!wallSolid;
|
||
if(!wallSolid&&crossingWallFailures<=8)
|
||
std::printf("[PassageRockColumnWallProbe] id=%s segment=%zu xyz_m=(%.3f,%.3f,%.3f) axis_m=(%.3f,%.3f) centerline_radius_m=%.3f bore_radius_m=%.3f wall_probe_radius_m=%.3f\n",
|
||
passage.stableId.c_str(),segment,x,y,z,axisX,axisY,radial,radius,wallRadius);
|
||
}
|
||
double crossingMinimumSurfaceRadius=std::numeric_limits<double>::infinity();
|
||
double crossingMaximumSurfaceRadius=0.0;
|
||
std::array<double,6> meanRadiusByHeight{};
|
||
std::size_t heightSamples=0;
|
||
constexpr double RayStepMetres=0.25;
|
||
constexpr std::array<double,6> HeightFractions{{0.25,0.35,0.45,0.55,0.65,0.75}};
|
||
constexpr std::size_t DirectionCount=16;
|
||
const double topMetres=qToMetres(crossing.columnTopZSubvoxels);
|
||
const double bottomMetres=qToMetres(crossing.columnBottomZSubvoxels);
|
||
const double rayStart=crossing.maximumRouteOffsetMetres
|
||
+crossing.maximumBoreRadiusMetres+0.75;
|
||
for(const double heightFraction:HeightFractions)
|
||
{
|
||
const double z=topMetres+(bottomMetres-topMetres)*heightFraction;
|
||
double heightMeanRadius=0.0;
|
||
std::uint64_t heightRayCount=0;
|
||
for(std::size_t direction=0;direction<DirectionCount;++direction)
|
||
{
|
||
const double angle=2.0*3.14159265358979323846
|
||
*direction/DirectionCount;
|
||
double lastSolidRadius=0.0;
|
||
bool enteredColumn=false;
|
||
const std::size_t raySteps=static_cast<std::size_t>(std::ceil(
|
||
(maximumRadius+2.0-rayStart)/RayStepMetres));
|
||
for(std::size_t step=0;step<=raySteps;++step)
|
||
{
|
||
const double rayRadius=rayStart+step*RayStepMetres;
|
||
const VoxelForgeCore::Vec3d point{
|
||
(axisX+rayRadius*std::cos(angle))/coreMetres,
|
||
(axisY+rayRadius*std::sin(angle))/coreMetres,
|
||
z/coreMetres};
|
||
if(Field.densityAt(point)<0.0f)
|
||
{enteredColumn=true;lastSolidRadius=rayRadius;}
|
||
else if(enteredColumn)break;
|
||
}
|
||
if(!enteredColumn)continue;
|
||
heightMeanRadius+=lastSolidRadius;
|
||
++heightRayCount;
|
||
crossingMinimumSurfaceRadius=std::min(crossingMinimumSurfaceRadius,
|
||
lastSolidRadius);
|
||
crossingMaximumSurfaceRadius=std::max(crossingMaximumSurfaceRadius,
|
||
lastSolidRadius);
|
||
}
|
||
if(heightRayCount==0)continue;
|
||
meanRadiusByHeight[heightSamples++]=heightMeanRadius/heightRayCount;
|
||
}
|
||
const double crossingSurfaceRelief=heightSamples>=2
|
||
?*std::max_element(meanRadiusByHeight.begin(),
|
||
meanRadiusByHeight.begin()+heightSamples)
|
||
-*std::min_element(meanRadiusByHeight.begin(),
|
||
meanRadiusByHeight.begin()+heightSamples):0.0;
|
||
std::printf("[PassageColumnReliefProfile] id=%s heights=%zu radii_m=",
|
||
passage.stableId.c_str(),heightSamples);
|
||
for(std::size_t height=0;height<heightSamples;++height)
|
||
std::printf("%s%.3f",height?",":"",
|
||
meanRadiusByHeight[height]);
|
||
std::printf("\n");
|
||
audit.minimumSurfaceRadiusMetres=std::min(audit.minimumSurfaceRadiusMetres,
|
||
crossingMinimumSurfaceRadius);
|
||
audit.maximumSurfaceRadiusMetres=std::max(audit.maximumSurfaceRadiusMetres,
|
||
crossingMaximumSurfaceRadius);
|
||
audit.surfaceRadiusReliefMetres=std::max(audit.surfaceRadiusReliefMetres,
|
||
crossingSurfaceRelief);
|
||
const double measuredWall=crossingMinimumSurfaceRadius
|
||
-crossing.maximumRouteOffsetMetres-crossing.maximumBoreRadiusMetres;
|
||
const bool columnPass=topCapConnected&&bottomCapConnected
|
||
&&minimumWall>=1.50&&crossingRouteFailures==0
|
||
&&crossingWallFailures==0&&std::isfinite(measuredWall)
|
||
&&measuredWall>=1.50&&crossingSurfaceRelief>=0.20;
|
||
audit.passed&=columnPass;
|
||
std::printf("[PassageRockColumnAudit] id=%s segments=%zu..%zu span_m=%.3f axis_m=(%.3f,%.3f) connected=%d/2 route_support=%llu/%llu wall_failures=%llu wall_margin_m=%.3f threshold_m=1.500 radii_m=%.3f..%.3f relief_p2p_m=%.3f nominal_radius_m=%.3f status=%s\n",
|
||
passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment,
|
||
crossing.unsupportedLengthMetres,axisX,axisY,
|
||
static_cast<int>(topCapConnected)+static_cast<int>(bottomCapConnected),
|
||
static_cast<unsigned long long>(probeCount-crossingRouteFailures),
|
||
static_cast<unsigned long long>(probeCount),
|
||
static_cast<unsigned long long>(crossingWallFailures),measuredWall,
|
||
crossingMinimumSurfaceRadius,crossingMaximumSurfaceRadius,
|
||
crossingSurfaceRelief,qToMetres(crossing.columnNominalRadiusSubvoxels),
|
||
columnPass?"PASS":"FAIL");
|
||
}
|
||
audit.disconnectedEndControlApplicable|=disconnectedEndControlApplicable;
|
||
audit.disconnectedEndControlDetected|=disconnectedEndControlDetected;
|
||
if(disconnectedEndControlApplicable&&!disconnectedEndControlDetected)
|
||
audit.passed=false;
|
||
return audit;
|
||
};
|
||
if(Assembly)
|
||
for(const auto& stratum:ResolvedLayout->strates)
|
||
{
|
||
if(stratum.generatorKind!="FloatingIslands")continue;
|
||
const auto anchor=std::find_if(stratum.anchors.begin(),stratum.anchors.end(),
|
||
[](const auto& value){return value.kind=="passage_anchor"&&value.values.size()>=7;});
|
||
const auto islandPlan=std::find_if(Assembly->floatingIslands.begin(),
|
||
Assembly->floatingIslands.end(),[&](const auto& value)
|
||
{return value.strateIndex==stratum.strateIndex;});
|
||
if(anchor==stratum.anchors.end()||islandPlan==Assembly->floatingIslands.end())continue;
|
||
if(islandPlan->manifest.recipe.geometryVersion>=8)continue;
|
||
const auto ordinal=islandPlan->manifest.recipe.anchorIslandOrdinal;
|
||
if(ordinal>=islandPlan->manifest.plan.islands.size())continue;
|
||
const auto& anchorIsland=islandPlan->manifest.plan.islands[ordinal];
|
||
const double axisX=qToMetres(anchor->values[0]);
|
||
const double axisY=qToMetres(anchor->values[1]);
|
||
const double rootBottom=qToMetres(anchor->values[5]);
|
||
const auto& islandRecipe=islandPlan->manifest.recipe;
|
||
const std::int64_t crustRatioQ16=islandRecipe.thicknessRatioQ16
|
||
+islandRecipe.crustDepthBonusQ16;
|
||
const std::int64_t crustDepthQ=(anchorIsland.radiusSubvoxels*crustRatioQ16
|
||
+32768)/65536;
|
||
const double rootTop=qToMetres(anchorIsland.topZSubvoxels-crustDepthQ);
|
||
const double nominalRootRadius=qToMetres(anchor->values[2]);
|
||
double helixMinimumRadius=std::numeric_limits<double>::infinity();
|
||
double helixMaximumRadius=0.0;
|
||
for(const auto& passage:ResolvedLayout->passages)
|
||
if(passage.upperStrateIndex==stratum.strateIndex)
|
||
for(const auto& point:passage.controlPoints)
|
||
{
|
||
const double z=qToMetres(point.positionSubvoxels[2]);
|
||
if(z<rootBottom||z>rootTop)continue;
|
||
const double radial=std::hypot(
|
||
qToMetres(point.positionSubvoxels[0]-anchor->values[0]),
|
||
qToMetres(point.positionSubvoxels[1]-anchor->values[1]));
|
||
helixMinimumRadius=std::min(helixMinimumRadius,radial);
|
||
helixMaximumRadius=std::max(helixMaximumRadius,radial);
|
||
}
|
||
if(!std::isfinite(helixMinimumRadius))continue;
|
||
double minimumMeasuredRootRadius=std::numeric_limits<double>::infinity();
|
||
std::uint64_t rootRaySamples=0;
|
||
constexpr std::array<double,5> RootHeightFractions{{0.01,0.25,0.5,0.75,0.99}};
|
||
for(const double fraction:RootHeightFractions)
|
||
{
|
||
const double z=rootTop+(rootBottom-rootTop)*fraction;
|
||
double minimumRadius=std::numeric_limits<double>::infinity();
|
||
double maximumRadius=0.0,meanRadius=0.0;
|
||
constexpr std::size_t DirectionCount=32;
|
||
constexpr double RayStepMetres=0.125;
|
||
const std::size_t raySteps=static_cast<std::size_t>(std::ceil(
|
||
(nominalRootRadius+2.0)/RayStepMetres));
|
||
for(std::size_t direction=0;direction<DirectionCount;++direction)
|
||
{
|
||
const double angle=2.0*3.14159265358979323846*direction/DirectionCount;
|
||
double radius=0.0;
|
||
for(std::size_t step=0;step<=raySteps;++step)
|
||
{
|
||
const double probeRadius=step*RayStepMetres;
|
||
const VoxelForgeCore::Vec3d probe{
|
||
(axisX+probeRadius*std::cos(angle))/coreMetres,
|
||
(axisY+probeRadius*std::sin(angle))/coreMetres,z/coreMetres};
|
||
if(blockedWorld.densityAt(probe)>=0.0f)break;
|
||
radius=probeRadius;
|
||
}
|
||
minimumRadius=std::min(minimumRadius,radius);
|
||
maximumRadius=std::max(maximumRadius,radius);
|
||
meanRadius+=radius;
|
||
++rootRaySamples;
|
||
}
|
||
meanRadius/=DirectionCount;
|
||
minimumMeasuredRootRadius=std::min(minimumMeasuredRootRadius,minimumRadius);
|
||
std::printf("[PassageRootRadiusMeasure] strate=%d z_m=%.3f solid_radius_min_m=%.3f mean_m=%.3f max_m=%.3f nominal_root_m=%.3f rays=%zu\n",
|
||
stratum.strateIndex,z,minimumRadius,meanRadius,maximumRadius,
|
||
nominalRootRadius,DirectionCount);
|
||
}
|
||
const auto firstRootPassage=std::find_if(ResolvedLayout->passages.begin(),
|
||
ResolvedLayout->passages.end(),[&](const auto& passage)
|
||
{return passage.upperStrateIndex==stratum.strateIndex;});
|
||
const double mouthBoreRadius=firstRootPassage==ResolvedLayout->passages.end()?0.0
|
||
:qToMetres(firstRootPassage->mouthRadiusSubvoxels);
|
||
const double midBoreRadius=firstRootPassage==ResolvedLayout->passages.end()?0.0
|
||
:qToMetres(firstRootPassage->midRadiusSubvoxels);
|
||
const double maximumBoreRadius=std::max(mouthBoreRadius,midBoreRadius);
|
||
std::printf("[PassageRootMeasure] strate=%d axis_m=(%.3f,%.3f) helix_centerline_radius_min_m=%.3f max_m=%.3f mouth_bore_radius_m=%.3f mid_bore_radius_m=%.3f nominal_root_m=%.3f measured_min_root_m=%.3f minimum_wall_m=%.3f samples=%llu\n",
|
||
stratum.strateIndex,axisX,axisY,helixMinimumRadius,helixMaximumRadius,
|
||
mouthBoreRadius,midBoreRadius,nominalRootRadius,minimumMeasuredRootRadius,
|
||
minimumMeasuredRootRadius-helixMaximumRadius-maximumBoreRadius,
|
||
static_cast<unsigned long long>(rootRaySamples));
|
||
}
|
||
const auto mouthInContent=[&](std::int32_t index,const std::array<std::int64_t,3>& mouth)
|
||
{
|
||
const auto* stratum=findStratum(index);if(!stratum)return false;
|
||
const std::int64_t zUm=static_cast<std::int64_t>(std::llround(
|
||
static_cast<double>(mouth[2])*World.CoreVoxelSizeMicrons
|
||
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
||
return zUm>=stratum->content.bottomMicrometres&&zUm<stratum->content.topMicrometres;
|
||
};
|
||
std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0;
|
||
std::uint64_t mouthProfilePasses=0,ripplePasses=0,rippleControlsDetected=0;
|
||
std::uint64_t floorRippleControlsDetected=0,floorStepRippleControlsDetected=0;
|
||
std::uint64_t openAirViolations=0,openAirLegacyViolations=0,openAirSamples=0;
|
||
std::uint64_t openAirAudits=0,openAirColumnSamples=0,openAirShortSpanSamples=0;
|
||
std::uint64_t columnCrossings=0,shortGroundedSpans=0,columnConnectedEnds=0;
|
||
std::uint64_t columnRouteFailures=0,columnWallFailures=0;
|
||
double longestUncolumnedSpanMetres=0.0,longestShortSpanMetres=0.0;
|
||
double l26LegacyBridgeSpanMetres=0.0;
|
||
bool columnAuditsPassed=true,noLongBridgeAuditPassed=true;
|
||
bool columnDisconnectedEndControlApplicable=false;
|
||
bool columnDisconnectedEndControlDetected=false;
|
||
bool l26NegativeControlApplicable=false;
|
||
const auto plannedVoidCrossings=Shape.passageVoidCrossings();
|
||
for(const auto& crossing:plannedVoidCrossings)
|
||
{
|
||
if(crossing.needsColumn)continue;
|
||
longestUncolumnedSpanMetres=std::max(longestUncolumnedSpanMetres,
|
||
crossing.unsupportedLengthMetres);
|
||
}
|
||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();
|
||
++passageIndex)
|
||
{
|
||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||
const auto* upper=findStratum(passage.upperStrateIndex);
|
||
const auto* lower=findStratum(passage.lowerStrateIndex);
|
||
if(!upper||!lower||lower->generatorKind!="SurfaceWorld"
|
||
||upper->generatorKind=="FloatingIslands")continue;
|
||
l26NegativeControlApplicable=true;
|
||
for(const auto& crossing:plannedVoidCrossings)
|
||
if(crossing.passageIndex==passageIndex)
|
||
l26LegacyBridgeSpanMetres=std::max(l26LegacyBridgeSpanMetres,
|
||
crossing.unsupportedLengthMetres);
|
||
}
|
||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||
{
|
||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||
std::vector<std::array<double,3>> path;path.reserve(passage.controlPoints.size());
|
||
for(const auto& point:passage.controlPoints)
|
||
{
|
||
const double playerZ=static_cast<double>(point.positionSubvoxels[2]
|
||
-point.radiusSubvoxels)+1.13*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel
|
||
*1000000.0/World.CoreVoxelSizeMicrons;
|
||
path.push_back({qToMetres(point.positionSubvoxels[0])/coreMetres,
|
||
qToMetres(point.positionSubvoxels[1])/coreMetres,playerZ/256.0});
|
||
}
|
||
std::vector<FPassageSegmentSupportAudit> segmentSupport;
|
||
const FWalkAudit passageAudit=auditPlayerPath(Shape,path,0.125,0.125,0.25,3,0.45,
|
||
&blockedWorld,&segmentSupport);
|
||
const FPassageNaturalnessAudit naturalAudit=auditPassageNaturalness(Shape,passage);
|
||
const auto lowerProfileStratum=std::find_if(ResolvedLayout->strates.begin(),
|
||
ResolvedLayout->strates.end(),[&](const auto& stratum)
|
||
{return stratum.strateIndex==passage.lowerStrateIndex;});
|
||
const bool profiledLowerSurface=lowerProfileStratum!=ResolvedLayout->strates.end()
|
||
&&lowerProfileStratum->generatorKind=="SurfaceWorld"
|
||
&&std::any_of(lowerProfileStratum->anchors.begin(),
|
||
lowerProfileStratum->anchors.end(),[](const auto& anchor)
|
||
{return anchor.kind=="heightfield_profile";});
|
||
const FPassageMouthProfileAudit mouthProfileAudit=
|
||
auditPassageMouthProfiles(Shape,passage,profiledLowerSurface);
|
||
const FPassageRippleAudit rippleAudit=auditPassageRipples(Shape,passage);
|
||
const FPassageOpenAirAudit openAirAudit=auditPassageOpenAir(Shape,blockedWorld,
|
||
passage,passageIndex);
|
||
const FPassageColumnAudit columnAudit=auditPassageColumns(Shape,blockedWorld,
|
||
passage,passageIndex);
|
||
openAirSamples+=openAirAudit.supportSamples;
|
||
openAirViolations+=openAirAudit.violations;
|
||
openAirLegacyViolations+=openAirAudit.legacyViolations;
|
||
openAirColumnSamples+=openAirAudit.columnSamples;
|
||
openAirShortSpanSamples+=openAirAudit.shortSpanSamples;
|
||
columnCrossings+=columnAudit.columns;
|
||
shortGroundedSpans+=columnAudit.shortSpans;
|
||
columnConnectedEnds+=columnAudit.connectedEnds;
|
||
columnRouteFailures+=columnAudit.routeSectionFailures;
|
||
columnWallFailures+=columnAudit.wallFailures;
|
||
longestShortSpanMetres=std::max(longestShortSpanMetres,
|
||
columnAudit.longestShortSpanMetres);
|
||
columnAuditsPassed&=columnAudit.passed;
|
||
columnDisconnectedEndControlApplicable|=columnAudit.disconnectedEndControlApplicable;
|
||
columnDisconnectedEndControlDetected|=columnAudit.disconnectedEndControlDetected;
|
||
++openAirAudits;
|
||
const auto radialSquared=[](const std::array<std::int64_t,3>& p)
|
||
{return p[0]*p[0]+p[1]*p[1];};
|
||
const bool mouthsInRadius=radialSquared(passage.upperMouthSubvoxels)
|
||
<=passage.placementRadiusSubvoxels*passage.placementRadiusSubvoxels
|
||
&&radialSquared(passage.lowerMouthSubvoxels)
|
||
<=passage.placementRadiusSubvoxels*passage.placementRadiusSubvoxels;
|
||
const bool mouthsInContent=mouthInContent(passage.upperStrateIndex,passage.upperMouthSubvoxels)
|
||
&&mouthInContent(passage.lowerStrateIndex,passage.lowerMouthSubvoxels);
|
||
++passageCount;naturalPasses+=naturalAudit.passed;
|
||
mouthProfilePasses+=mouthProfileAudit.passed;
|
||
ripplePasses+=rippleAudit.passed;
|
||
rippleControlsDetected+=rippleAudit.controlDetected;
|
||
floorRippleControlsDetected+=rippleAudit.floorControlDetected;
|
||
floorStepRippleControlsDetected+=rippleAudit.floorStepControlDetected;
|
||
passagePasses+=passageAudit.passed&&mouthsInRadius&&mouthsInContent
|
||
&&naturalAudit.passed&&mouthProfileAudit.passed&&rippleAudit.passed;
|
||
std::printf("[ResolvedPassageAudit] id=%s interface=%d-%d control_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu mouth_content=%d/2 mouth_radius=%d/2 walkability=%s\n",
|
||
passage.stableId.c_str(),passage.upperStrateIndex,passage.lowerStrateIndex,
|
||
passage.controlPoints.size(),static_cast<unsigned long long>(passageAudit.centers),
|
||
static_cast<unsigned long long>(passageAudit.capsuleSamples),
|
||
static_cast<unsigned long long>(passageAudit.blocked),
|
||
static_cast<unsigned long long>(passageAudit.supportSamples),
|
||
static_cast<unsigned long long>(passageAudit.unsupported),
|
||
static_cast<unsigned long long>(passageAudit.gradeFailures),mouthsInContent?2:0,
|
||
mouthsInRadius?2:0,passageAudit.passed?"PASS":"FAIL");
|
||
std::printf("[PassageNaturalnessAudit] id=%s flat_run_max_m=%.3f limit_m=%.3f wall_relief_p2p_m=%.3f minimum_m=%.3f samples=%llu missing=%llu status=%s\n",
|
||
passage.stableId.c_str(),naturalAudit.maximumFlatRunMetres,MaximumFlatRunMetres,
|
||
naturalAudit.wallReliefPeakToPeakMetres,MinimumWallReliefMetres,
|
||
static_cast<unsigned long long>(naturalAudit.wallSamples),
|
||
static_cast<unsigned long long>(naturalAudit.missingWallSamples),
|
||
naturalAudit.passed?"PASS":"FAIL");
|
||
std::printf("[PassageMouthProfileAudit] id=%s floor_samples=%llu floor_missing=%llu wall_samples=%llu wall_missing=%llu floor_step_scan=centerline profiled_lower_surface=%s floor_flat_step_run_m=%.3f floor_step_m=%.3f wall_flat_step_run_m=%.3f wall_step_m=%.3f spacing_m=%.3f plateau=(%.3f,%.3f) status=%s\n",
|
||
passage.stableId.c_str(),static_cast<unsigned long long>(mouthProfileAudit.floorSamples),
|
||
static_cast<unsigned long long>(mouthProfileAudit.missingFloorSamples),
|
||
static_cast<unsigned long long>(mouthProfileAudit.wallSamples),
|
||
static_cast<unsigned long long>(mouthProfileAudit.missingWallSamples),
|
||
profiledLowerSurface?"true":"false",
|
||
mouthProfileAudit.floorFlatStepRunMetres,mouthProfileAudit.floorStepMetres,
|
||
mouthProfileAudit.wallFlatStepRunMetres,mouthProfileAudit.wallStepMetres,
|
||
PassageProfileSpacingMetres,PassageProfilePlateauMinimumMetres,
|
||
PassageProfileStepThresholdMetres,mouthProfileAudit.passed?"PASS":"FAIL");
|
||
std::printf("[PassageRippleAudit] id=%s window_m=32.000 spacing_m=0.125 period_m=%.3f amplitude_m=%.4f amplitude_cm=%.2f coherent_period_m=%.3f coherent_amplitude_m=%.4f coherent_amplitude_cm=%.2f local_peak_amplitude_m=%.4f coverage=%llu/%llu threshold_m=0.035 synthetic_spheres_period_m=1.500 synthetic_spheres_amplitude_m=%.4f synthetic_spheres_detected=%s floor_period_m=%.3f floor_amplitude_m=%.4f floor_amplitude_cm=%.2f floor_coherent_period_m=%.3f floor_coherent_amplitude_m=%.4f floor_coverage=%llu/%llu floor_threshold_m=0.025 floor_beaded_control_amplitude_m=%.4f floor_beaded_control_detected=%s floor_step_control_amplitude_m=%.4f floor_step_control_detected=%s status=%s\n",
|
||
passage.stableId.c_str(),rippleAudit.periodMetres,
|
||
rippleAudit.amplitudeMetres,rippleAudit.amplitudeMetres*100.0,
|
||
rippleAudit.coherentPeriodMetres,rippleAudit.coherentAmplitudeMetres,
|
||
rippleAudit.coherentAmplitudeMetres*100.0,
|
||
rippleAudit.localPeakAmplitudeMetres,
|
||
static_cast<unsigned long long>(rippleAudit.samples),
|
||
static_cast<unsigned long long>(rippleAudit.expectedSamples),
|
||
rippleAudit.controlAmplitudeMetres,rippleAudit.controlDetected?"true":"false",
|
||
rippleAudit.floorPeriodMetres,rippleAudit.floorAmplitudeMetres,
|
||
rippleAudit.floorAmplitudeMetres*100.0,
|
||
rippleAudit.floorCoherentPeriodMetres,rippleAudit.floorCoherentAmplitudeMetres,
|
||
static_cast<unsigned long long>(rippleAudit.floorSamples),
|
||
static_cast<unsigned long long>(rippleAudit.floorExpectedSamples),
|
||
rippleAudit.floorControlAmplitudeMetres,
|
||
rippleAudit.floorControlDetected?"true":"false",
|
||
rippleAudit.floorStepControlAmplitudeMetres,
|
||
rippleAudit.floorStepControlDetected?"true":"false",
|
||
rippleAudit.passed?"PASS":"FAIL");
|
||
std::printf("[PassageFloorStepNegativeControl] id=%s source=synthetic_signed_distance_step height_m=0.250 threshold_m=0.025 amplitude_m=%.4f detected=%s\n",
|
||
passage.stableId.c_str(),rippleAudit.floorStepControlAmplitudeMetres,
|
||
rippleAudit.floorStepControlDetected?"true":"false");
|
||
for(std::size_t sample=0;sample<rippleAudit.floorPeakProfile.size();sample+=4)
|
||
{
|
||
const auto& value=rippleAudit.floorPeakProfile[sample];
|
||
std::printf("[PassageFloorRippleSample] id=%s distance_m=%.3f "
|
||
"expected_floor_m=%.4f sampled_floor_m=%.4f residual_m=%.4f "
|
||
"route_xyz_m=(%.3f,%.3f,%.3f)\n",
|
||
passage.stableId.c_str(),value[0],value[1],value[2],value[2]-value[1],
|
||
value[3],value[4],value[5]);
|
||
}
|
||
std::printf("[PassageOpenAirAudit] id=%s support_samples=%llu violations=%llu column_samples=%llu short_span_samples=%llu l24_ungated_violations=%llu route_support_samples=%llu route_support_violations=%llu spacing_m=%.3f status=%s\n",
|
||
passage.stableId.c_str(),static_cast<unsigned long long>(openAirAudit.supportSamples),
|
||
static_cast<unsigned long long>(openAirAudit.violations),
|
||
static_cast<unsigned long long>(openAirAudit.columnSamples),
|
||
static_cast<unsigned long long>(openAirAudit.shortSpanSamples),
|
||
static_cast<unsigned long long>(openAirAudit.legacyViolations),
|
||
static_cast<unsigned long long>(openAirAudit.routeSupportSamples),
|
||
static_cast<unsigned long long>(openAirAudit.routeSupportViolations),
|
||
PassageSupportAuditSpacingMetres,
|
||
openAirAudit.violations==0?"PASS":"FAIL");
|
||
for(std::size_t i=0;i<openAirAudit.firstViolationCount;++i)
|
||
std::printf("[PassageOpenAirAuditSample] id=%s kind=unsupported_support xyz_m=(%.3f,%.3f,%.3f) density=%.6f\n",
|
||
passage.stableId.c_str(),openAirAudit.firstViolations[i][0],
|
||
openAirAudit.firstViolations[i][1],openAirAudit.firstViolations[i][2],
|
||
openAirAudit.firstViolations[i][3]);
|
||
for(std::size_t i=0;i<passageAudit.firstBlockedCount;++i)
|
||
{
|
||
const auto& sample=passageAudit.firstBlocked[i];
|
||
double nearestDistanceSquared=std::numeric_limits<double>::infinity();
|
||
std::size_t nearestSegment=0;
|
||
double nearestT=0.0;
|
||
double nearestPassageSdf=std::numeric_limits<double>::infinity();
|
||
std::size_t nearestPassageSegment=0;
|
||
for(std::size_t segment=0;segment+1<path.size();++segment)
|
||
{
|
||
const auto& a=path[segment];const auto& b=path[segment+1];
|
||
const double dx=b[0]-a[0],dy=b[1]-a[1],dz=b[2]-a[2];
|
||
const double lengthSquared=dx*dx+dy*dy+dz*dz;
|
||
const double t=lengthSquared>0.0?std::clamp(
|
||
((sample[0]/coreMetres-a[0])*dx+(sample[1]/coreMetres-a[1])*dy
|
||
+(sample[2]/coreMetres-a[2])*dz)/lengthSquared,0.0,1.0):0.0;
|
||
const double ex=sample[0]/coreMetres-(a[0]+t*dx);
|
||
const double ey=sample[1]/coreMetres-(a[1]+t*dy);
|
||
const double ez=sample[2]/coreMetres-(a[2]+t*dz);
|
||
const double distanceSquared=ex*ex+ey*ey+ez*ez;
|
||
if(distanceSquared<nearestDistanceSquared)
|
||
{nearestDistanceSquared=distanceSquared;nearestSegment=segment;nearestT=t;}
|
||
}
|
||
for(std::size_t segment=0;segment+1<passage.controlPoints.size();++segment)
|
||
{
|
||
const auto& a=passage.controlPoints[segment];
|
||
const auto& b=passage.controlPoints[segment+1];
|
||
const double ax=qToMetres(a.positionSubvoxels[0]);
|
||
const double ay=qToMetres(a.positionSubvoxels[1]);
|
||
const double az=qToMetres(a.positionSubvoxels[2]);
|
||
const double dx=qToMetres(b.positionSubvoxels[0]-a.positionSubvoxels[0]);
|
||
const double dy=qToMetres(b.positionSubvoxels[1]-a.positionSubvoxels[1]);
|
||
const double dz=qToMetres(b.positionSubvoxels[2]-a.positionSubvoxels[2]);
|
||
const double lengthSquared=dx*dx+dy*dy+dz*dz;
|
||
const double t=lengthSquared>0.0?std::clamp(
|
||
((sample[0]-ax)*dx+(sample[1]-ay)*dy+(sample[2]-az)*dz)
|
||
/lengthSquared,0.0,1.0):0.0;
|
||
const double ex=sample[0]-(ax+t*dx),ey=sample[1]-(ay+t*dy),
|
||
ez=sample[2]-(az+t*dz);
|
||
const double radius=qToMetres(a.radiusSubvoxels)
|
||
+(qToMetres(b.radiusSubvoxels)-qToMetres(a.radiusSubvoxels))*t;
|
||
const double signedDistance=std::sqrt(ex*ex+ey*ey+ez*ez)-radius;
|
||
if(signedDistance<nearestPassageSdf)
|
||
{nearestPassageSdf=signedDistance;nearestPassageSegment=segment;}
|
||
}
|
||
const VoxelForgeCore::Vec3d nativePosition{sample[0]/coreMetres,
|
||
sample[1]/coreMetres,sample[2]/coreMetres};
|
||
const float nativeDensity=blockedWorld.densityAt(nativePosition);
|
||
const bool passageColumn=Shape.isPassageColumnSample(passageIndex,
|
||
nearestSegment,nativePosition);
|
||
std::printf("[ResolvedPassageAuditSample] id=%s kind=blocked xyz_m=(%.3f,%.3f,%.3f) density=%.6f native_density=%.6f segment=%zu t=%.3f passage_segment=%zu passage_sdf_m=%.3f passage_column=%d native_path=%s\n",
|
||
passage.stableId.c_str(),sample[0],sample[1],sample[2],sample[3],
|
||
nativeDensity,nearestSegment,nearestT,nearestPassageSegment,
|
||
nearestPassageSdf,passageColumn?1:0,
|
||
blockedWorld.densityDispatchPathAt(nativePosition).c_str());
|
||
}
|
||
for(std::size_t i=0;i<passageAudit.firstUnsupportedCount;++i)
|
||
{
|
||
std::printf("[ResolvedPassageAuditSample] id=%s kind=unsupported xyz_m=(%.3f,%.3f,%.3f) density=%.6f\n",
|
||
passage.stableId.c_str(),passageAudit.firstUnsupported[i][0],
|
||
passageAudit.firstUnsupported[i][1],passageAudit.firstUnsupported[i][2],
|
||
passageAudit.firstUnsupported[i][3]);
|
||
}
|
||
for(std::size_t segment=0;segment<segmentSupport.size();++segment)
|
||
{
|
||
const auto& item=segmentSupport[segment];
|
||
if(!item.hasUnsupported)continue;
|
||
const auto& a=path[segment];const auto& b=path[segment+1];
|
||
const auto& sample=item.firstUnsupported;
|
||
const double firstX=sample[0]/coreMetres;
|
||
const double firstY=sample[1]/coreMetres;
|
||
const double firstZ=(sample[2]-0.125)/coreMetres;
|
||
const VoxelForgeCore::Vec3d bridgeProbe{sample[0]/coreMetres,
|
||
sample[1]/coreMetres,(sample[2]-0.25)/coreMetres};
|
||
const bool columnAtSupport=Shape.isPassageColumnSample(passageIndex,
|
||
segment,bridgeProbe);
|
||
std::printf("[ResolvedPassageSupportSegment] id=%s segment=%zu route_z_m=%.3f..%.3f centers=%llu unsupported=%llu native_unsupported=%llu first_foot_m=(%.3f,%.3f,%.3f) column_at_d025=%d native_support_probes=%u/5 native_density_d0125_d1_d2_d4=(%.6f,%.6f,%.6f,%.6f) native_path=%s\n",
|
||
passage.stableId.c_str(),segment,
|
||
std::min(a[2],b[2])*coreMetres,std::max(a[2],b[2])*coreMetres,
|
||
static_cast<unsigned long long>(item.centers),
|
||
static_cast<unsigned long long>(item.unsupported),
|
||
static_cast<unsigned long long>(item.nativeUnsupported),
|
||
sample[0],sample[1],sample[2],columnAtSupport?1:0,
|
||
item.firstNativeSupportedProbes,item.firstNativeDensities[0],item.firstNativeDensities[1],
|
||
item.firstNativeDensities[2],item.firstNativeDensities[3],
|
||
blockedWorld.densityDispatchPathAt({firstX,firstY,firstZ}).c_str());
|
||
}
|
||
}
|
||
std::uint64_t routePoints=0,routePasses=0,routeWalkPasses=0,routeBindings=0,routeBindingFailures=0;
|
||
struct FRouteAuditInput
|
||
{
|
||
std::int32_t strateIndex=0;
|
||
std::vector<std::array<double,3>> path;
|
||
double supportStart=0.125,supportStep=0.125,supportMaximum=0.25,
|
||
supportProbeRadius=0.45;
|
||
std::uint32_t minimumSupportProbes=3;
|
||
};
|
||
std::vector<FRouteAuditInput> routeAuditInputs;
|
||
for(const auto& stratum:ResolvedLayout->strates)
|
||
{
|
||
std::vector<const VoxelForgeCore::ResolvedLayoutAnchorV2*> anchors;
|
||
for(const auto& anchor:stratum.anchors)
|
||
if(anchor.kind=="route_point"&&anchor.values.size()>=3)anchors.push_back(&anchor);
|
||
if(anchors.size()<2)continue;
|
||
std::vector<std::array<double,3>> route;route.reserve(anchors.size());
|
||
for(const auto* anchor:anchors)
|
||
{
|
||
route.push_back({qToMetres(anchor->values[0])/coreMetres,
|
||
qToMetres(anchor->values[1])/coreMetres,
|
||
qToMetres(anchor->values[2])/coreMetres});
|
||
++routePoints;
|
||
}
|
||
double supportStartMetres=0.125,supportStepMetres=0.125,
|
||
supportMaximumMetres=0.25,supportProbeRadiusMetres=0.45;
|
||
std::uint32_t minimumSupportProbes=3;
|
||
const VoxelForgeCore::WorldAssemblyRouteConstraintsV1* routeConstraints=nullptr;
|
||
if(Assembly)
|
||
{
|
||
const auto worldRoute=std::find_if(Assembly->plan.strates.begin(),
|
||
Assembly->plan.strates.end(),[&](const auto& candidate)
|
||
{return candidate.strateIndex==stratum.strateIndex;});
|
||
if(worldRoute!=Assembly->plan.strates.end()
|
||
&&worldRoute->routeConstraints.version==1)
|
||
routeConstraints=&worldRoute->routeConstraints;
|
||
const auto islandRoute=std::find_if(Assembly->floatingIslands.begin(),
|
||
Assembly->floatingIslands.end(),[&](const auto& candidate)
|
||
{return candidate.strateIndex==stratum.strateIndex;});
|
||
if(islandRoute!=Assembly->floatingIslands.end()
|
||
&&islandRoute->manifest.recipe.routeConstraints.version==1)
|
||
routeConstraints=&islandRoute->manifest.recipe.routeConstraints;
|
||
}
|
||
if(routeConstraints)
|
||
{
|
||
const double qMetres=coreMetres
|
||
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||
minimumSupportProbes=std::clamp<std::uint32_t>(
|
||
routeConstraints->minimumSupportProbes,1,5);
|
||
supportProbeRadiusMetres=routeConstraints->supportProbeRadiusSubvoxels*qMetres;
|
||
if(stratum.generatorKind=="FloatingIslands")
|
||
{
|
||
supportStartMetres=(routeConstraints->minimumClearanceSubvoxels
|
||
+VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/2)*qMetres;
|
||
supportStepMetres=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel
|
||
*0.5*qMetres;
|
||
supportMaximumMetres=(routeConstraints->minimumClearanceSubvoxels
|
||
+routeConstraints->supportSearchDepthSubvoxels)*qMetres;
|
||
}
|
||
else
|
||
{
|
||
const std::int64_t spacing=std::min<std::int64_t>(
|
||
routeConstraints->sweepSpacingSubvoxels,
|
||
VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/4);
|
||
supportStepMetres=std::max<std::int64_t>(1,spacing)*qMetres;
|
||
supportStartMetres=supportStepMetres;
|
||
supportMaximumMetres=routeConstraints->supportSearchDepthSubvoxels*qMetres;
|
||
}
|
||
}
|
||
const FWalkAudit routeAudit=auditPlayerPath(Shape,route,supportStartMetres,
|
||
supportStepMetres,supportMaximumMetres,minimumSupportProbes,
|
||
supportProbeRadiusMetres,nullptr,nullptr);
|
||
routeAuditInputs.push_back({stratum.strateIndex,route,supportStartMetres,
|
||
supportStepMetres,supportMaximumMetres,supportProbeRadiusMetres,
|
||
minimumSupportProbes});
|
||
bool binding=true;
|
||
for(const auto& passage:ResolvedLayout->passages)
|
||
{
|
||
const auto equalPosition=[](const auto& a,const auto& b)
|
||
{return a[0]==b[0]&&a[1]==b[1]&&a[2]==b[2];};
|
||
if(passage.upperStrateIndex==stratum.strateIndex)
|
||
{
|
||
const auto rootedMouth=std::find_if(stratum.anchors.begin(),
|
||
stratum.anchors.end(),[](const auto& anchor)
|
||
{return anchor.kind=="passage_anchor"&&anchor.values.size()>=10;});
|
||
const std::array<std::int64_t,3> departure=
|
||
rootedMouth!=stratum.anchors.end()
|
||
?std::array<std::int64_t,3>{{rootedMouth->values[7],
|
||
rootedMouth->values[8],rootedMouth->values[9]}}
|
||
:std::array<std::int64_t,3>{{anchors.back()->values[0],
|
||
anchors.back()->values[1],anchors.back()->values[2]}};
|
||
binding&=equalPosition(passage.upperMouthSubvoxels,
|
||
departure);
|
||
}
|
||
if(passage.lowerStrateIndex==stratum.strateIndex)
|
||
{
|
||
const auto passageArrival=std::find_if(stratum.anchors.begin(),
|
||
stratum.anchors.end(),[](const auto& anchor)
|
||
{return anchor.kind=="passage_arrival"&&anchor.values.size()>=3;});
|
||
const std::array<std::int64_t,3> arrival=passageArrival!=stratum.anchors.end()
|
||
?std::array<std::int64_t,3>{{passageArrival->values[0],
|
||
passageArrival->values[1],passageArrival->values[2]}}
|
||
:std::array<std::int64_t,3>{{anchors.front()->values[0],
|
||
anchors.front()->values[1],anchors.front()->values[2]}};
|
||
binding&=equalPosition(passage.lowerMouthSubvoxels,
|
||
arrival);
|
||
}
|
||
}
|
||
++routePasses;routeWalkPasses+=routeAudit.passed;
|
||
routeBindings+=binding;routeBindingFailures+=!binding;
|
||
std::printf("[ResolvedStrateRouteAudit] strate=%d route_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu mouth_binding=%s status=%s\n",
|
||
stratum.strateIndex,anchors.size(),static_cast<unsigned long long>(routeAudit.centers),
|
||
static_cast<unsigned long long>(routeAudit.capsuleSamples),
|
||
static_cast<unsigned long long>(routeAudit.blocked),
|
||
static_cast<unsigned long long>(routeAudit.supportSamples),
|
||
static_cast<unsigned long long>(routeAudit.unsupported),
|
||
static_cast<unsigned long long>(routeAudit.gradeFailures),binding?"PASS":"FAIL",
|
||
routeAudit.passed&&binding?"PASS":"FAIL");
|
||
for(std::size_t i=0;i<routeAudit.firstBlockedCount;++i)
|
||
std::printf("[ResolvedStrateRouteAuditSample] strate=%d kind=blocked xyz_m=(%.3f,%.3f,%.3f) density=%.6f\n",
|
||
stratum.strateIndex,routeAudit.firstBlocked[i][0],routeAudit.firstBlocked[i][1],
|
||
routeAudit.firstBlocked[i][2],routeAudit.firstBlocked[i][3]);
|
||
for(std::size_t i=0;i<routeAudit.firstUnsupportedCount;++i)
|
||
std::printf("[ResolvedStrateRouteAuditSample] strate=%d kind=unsupported xyz_m=(%.3f,%.3f,%.3f) density=%.6f\n",
|
||
stratum.strateIndex,routeAudit.firstUnsupported[i][0],
|
||
routeAudit.firstUnsupported[i][1],routeAudit.firstUnsupported[i][2],
|
||
routeAudit.firstUnsupported[i][3]);
|
||
}
|
||
for(const auto& passage:ResolvedLayout->passages)
|
||
{
|
||
std::vector<std::array<double,3>> path;path.reserve(passage.controlPoints.size());
|
||
for(const auto& point:passage.controlPoints)
|
||
{
|
||
const double playerZ=static_cast<double>(point.positionSubvoxels[2]
|
||
-point.radiusSubvoxels)+1.13*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel
|
||
*1000000.0/World.CoreVoxelSizeMicrons;
|
||
path.push_back({qToMetres(point.positionSubvoxels[0])/coreMetres,
|
||
qToMetres(point.positionSubvoxels[1])/coreMetres,playerZ/256.0});
|
||
}
|
||
const FWalkAudit blockedAudit=auditPlayerPath(blockedWorld,path,
|
||
0.125,0.125,0.25,3,0.45,nullptr,nullptr);
|
||
blockedControls+=blockedAudit.blocked>0&&!blockedAudit.passed;
|
||
std::printf("[ResolvedPassageBlockedControl] id=%s blocked_capsule_samples=%llu detected=%s\n",
|
||
passage.stableId.c_str(),static_cast<unsigned long long>(blockedAudit.blocked),
|
||
blockedAudit.blocked>0&&!blockedAudit.passed?"true":"false");
|
||
}
|
||
for(const auto& input:routeAuditInputs)
|
||
{
|
||
const FWalkAudit controlRoute=auditPlayerPath(blockedWorld,input.path,
|
||
input.supportStart,input.supportStep,input.supportMaximum,
|
||
input.minimumSupportProbes,input.supportProbeRadius,nullptr,nullptr);
|
||
std::printf("[ResolvedStrateRouteBlockedControl] strate=%d blocked=%llu unsupported=%llu status=%s\n",
|
||
input.strateIndex,static_cast<unsigned long long>(controlRoute.blocked),
|
||
static_cast<unsigned long long>(controlRoute.unsupported),
|
||
controlRoute.passed?"PASS":"FAIL");
|
||
}
|
||
const bool l24OpenAirControlDetected=openAirLegacyViolations>0;
|
||
std::printf("[PassageOpenAirNegativeControl] source=L24_ungated_support unsupported_samples=%llu detected=%s\n",
|
||
static_cast<unsigned long long>(openAirLegacyViolations),
|
||
l24OpenAirControlDetected?"true":"false");
|
||
std::printf("[PassageOpenAirAuditSummary] interfaces=%llu samples=%llu violations=%llu column_samples=%llu short_span_samples=%llu l24_negative_control=%s status=%s\n",
|
||
static_cast<unsigned long long>(openAirAudits),
|
||
static_cast<unsigned long long>(openAirSamples),
|
||
static_cast<unsigned long long>(openAirViolations),
|
||
static_cast<unsigned long long>(openAirColumnSamples),
|
||
static_cast<unsigned long long>(openAirShortSpanSamples),
|
||
l24OpenAirControlDetected?"DETECTED":"MISSED",
|
||
openAirAudits==passageCount&&openAirViolations==0&&l24OpenAirControlDetected
|
||
?"PASS":"FAIL");
|
||
const bool l26NoLongBridgeControlDetected=l26NegativeControlApplicable
|
||
&&l26LegacyBridgeSpanMetres>12.5;
|
||
const bool l26NoLongBridgeControlPass=!l26NegativeControlApplicable
|
||
||l26NoLongBridgeControlDetected;
|
||
noLongBridgeAuditPassed=longestUncolumnedSpanMetres<=12.5
|
||
&&l26NoLongBridgeControlPass;
|
||
std::printf("[PassageNoLongBridgeNegativeControl] source=L26_SurfaceWorld_bridge applicable=%s longest_uncolumned_m=%.3f threshold_m=12.500 detected=%s\n",
|
||
l26NegativeControlApplicable?"true":"false",l26LegacyBridgeSpanMetres,
|
||
l26NoLongBridgeControlDetected?"true":"false");
|
||
std::printf("[PassageNoLongBridgeAudit] columns=%llu short_grounded_spans=%llu longest_uncolumned_m=%.3f threshold_m=12.500 longest_short_span_m=%.3f l26_negative_control=%s status=%s\n",
|
||
static_cast<unsigned long long>(columnCrossings),
|
||
static_cast<unsigned long long>(shortGroundedSpans),longestUncolumnedSpanMetres,
|
||
longestShortSpanMetres,l26NegativeControlApplicable
|
||
?(l26NoLongBridgeControlDetected?"DETECTED":"MISSED"):"NOT_APPLICABLE",
|
||
noLongBridgeAuditPassed?"PASS":"FAIL");
|
||
std::printf("[PassageRockColumnAuditSummary] columns=%llu connected_ends=%llu/%llu short_spans=%llu wall_or_support_failures=%llu disconnected_end_control=%s status=%s\n",
|
||
static_cast<unsigned long long>(columnCrossings),
|
||
static_cast<unsigned long long>(columnConnectedEnds),
|
||
static_cast<unsigned long long>(columnCrossings*2),
|
||
static_cast<unsigned long long>(shortGroundedSpans),
|
||
static_cast<unsigned long long>(columnRouteFailures+columnWallFailures),
|
||
columnDisconnectedEndControlApplicable
|
||
?(columnDisconnectedEndControlDetected?"DETECTED":"MISSED")
|
||
:"NOT_APPLICABLE",
|
||
columnAuditsPassed?"PASS":"FAIL");
|
||
const bool l23SmoothWallDetected=!looksNatural(0.0,0.0);
|
||
std::printf("[PassageNaturalnessNegativeControl] source=L23_smooth_unrelieved_wall relief_p2p_m=0.000 threshold_m=%.3f detected=%s\n",
|
||
MinimumWallReliefMetres,l23SmoothWallDetected?"true":"false");
|
||
std::printf("[PassageMouthProfileNegativeControl] source=L23_terraced_geometry flat_run_m=%.3f following_step_m=%.3f thresholds=(%.3f,%.3f) detected=%s\n",
|
||
l23TerraceControlResult.first,l23TerraceControlResult.second,
|
||
PassageProfilePlateauMinimumMetres,PassageProfileStepThresholdMetres,
|
||
l23TerraceControlDetected?"true":"false");
|
||
std::printf("[PassageNaturalnessSummary] passed=%llu/%llu l23_negative_control=%s flat_run_limit_m=%.3f wall_relief_min_m=%.3f status=%s\n",
|
||
static_cast<unsigned long long>(naturalPasses),static_cast<unsigned long long>(passageCount),
|
||
l23SmoothWallDetected?"DETECTED":"MISSED",MaximumFlatRunMetres,
|
||
MinimumWallReliefMetres,naturalPasses==passageCount&&l23SmoothWallDetected?"PASS":"FAIL");
|
||
std::printf("[PassageMouthProfileSummary] passed=%llu/%llu spacing_m=%.3f plateau_min_m=%.3f following_step_min_m=%.3f l23_terrace_negative_control=%s status=%s\n",
|
||
static_cast<unsigned long long>(mouthProfilePasses),
|
||
static_cast<unsigned long long>(passageCount),PassageProfileSpacingMetres,
|
||
PassageProfilePlateauMinimumMetres,PassageProfileStepThresholdMetres,
|
||
l23TerraceControlDetected?"DETECTED":"MISSED",
|
||
mouthProfilePasses==passageCount&&l23TerraceControlDetected?"PASS":"FAIL");
|
||
std::printf("[PassageRippleAuditSummary] passed=%llu/%llu synthetic_spheres_negative_control=%s max_amplitude_m=0.035 floor_beaded_negative_control=%s max_floor_amplitude_m=0.025 floor_step_negative_control=%s status=%s\n",
|
||
static_cast<unsigned long long>(ripplePasses),
|
||
static_cast<unsigned long long>(passageCount),
|
||
rippleControlsDetected==passageCount?"DETECTED":"MISSED",
|
||
floorRippleControlsDetected==passageCount?"DETECTED":"MISSED",
|
||
floorStepRippleControlsDetected==passageCount?"DETECTED":"MISSED",
|
||
ripplePasses==passageCount?"PASS":"FAIL");
|
||
const bool auditPassed=passageCount>0&&passagePasses==passageCount
|
||
&&naturalPasses==passageCount&&l23SmoothWallDetected
|
||
&&mouthProfilePasses==passageCount&&l23TerraceControlDetected
|
||
&&ripplePasses==passageCount
|
||
&&openAirAudits==passageCount&&openAirViolations==0&&l24OpenAirControlDetected
|
||
&&columnAuditsPassed&&noLongBridgeAuditPassed
|
||
&&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings
|
||
&&routeBindingFailures==0
|
||
&&blockedCompiled&&blockedControls==passageCount;
|
||
std::printf("[ResolvedPassageAuditSummary] interfaces=%llu passed=%llu natural=%llu/%llu strate_routes=%llu route_walkable=%llu route_points=%llu route_bindings=%llu route_binding_failures=%llu blocked_controls=%llu/%llu status=%s\n",
|
||
static_cast<unsigned long long>(passageCount),static_cast<unsigned long long>(passagePasses),
|
||
static_cast<unsigned long long>(naturalPasses),static_cast<unsigned long long>(passageCount),
|
||
static_cast<unsigned long long>(routePasses),static_cast<unsigned long long>(routeWalkPasses),
|
||
static_cast<unsigned long long>(routePoints),
|
||
static_cast<unsigned long long>(routeBindings),static_cast<unsigned long long>(routeBindingFailures),
|
||
static_cast<unsigned long long>(blockedControls),static_cast<unsigned long long>(passageCount),
|
||
auditPassed?"PASS":"FAIL");
|
||
if(!auditPassed)
|
||
{Streamer.Stop();return 10;}
|
||
}
|
||
if(bAuditResolvedLayoutBoundaries)
|
||
{
|
||
if(ResolvedLayout==nullptr)
|
||
{Streamer.Stop();std::fprintf(stderr,"[ResolvedLayoutProbe] missing plan-time snapshot\n");return 2;}
|
||
std::vector<std::int64_t> endpoints;
|
||
for(const auto& Stratum:ResolvedLayout->strates)
|
||
{
|
||
endpoints.push_back(Stratum.interiorBottomMicrometres);
|
||
endpoints.push_back(Stratum.interiorTopMicrometres);
|
||
endpoints.push_back(Stratum.ceiling.bottomMicrometres);
|
||
endpoints.push_back(Stratum.ceiling.topMicrometres);
|
||
endpoints.push_back(Stratum.bedrock.bottomMicrometres);
|
||
endpoints.push_back(Stratum.bedrock.topMicrometres);
|
||
}
|
||
for(std::size_t StratumIndex=0;StratumIndex+1<ResolvedLayout->strates.size();++StratumIndex)
|
||
{
|
||
const auto& Upper=ResolvedLayout->strates[StratumIndex];
|
||
if(Upper.transitionMode=="Hard"||Upper.transitionWidthMicrometres<=0)continue;
|
||
endpoints.push_back(Upper.interiorBottomMicrometres-Upper.transitionWidthMicrometres);
|
||
endpoints.push_back(Upper.interiorBottomMicrometres+Upper.transitionWidthMicrometres);
|
||
}
|
||
std::sort(endpoints.begin(),endpoints.end());
|
||
endpoints.erase(std::unique(endpoints.begin(),endpoints.end()),endpoints.end());
|
||
std::uint64_t probes=0,disagreements=0;
|
||
const double CoreVoxelMicrometres=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
||
const auto expectedAt=[&](std::int64_t zMicrometres)
|
||
{
|
||
std::vector<std::string> candidates;
|
||
for(const auto& Stratum:ResolvedLayout->strates)
|
||
{
|
||
const auto contains=[zMicrometres](const VoxelForgeCore::ResolvedLayoutBandV2& Band)
|
||
{return Band.presentInCurrentField&&zMicrometres>=Band.bottomMicrometres
|
||
&&zMicrometres<Band.topMicrometres;};
|
||
if(contains(Stratum.ceiling))candidates.push_back(Stratum.generatorKind+"/ceiling");
|
||
if(contains(Stratum.content))candidates.push_back(Stratum.generatorKind+"/content");
|
||
if(contains(Stratum.bedrock))candidates.push_back(Stratum.generatorKind+"/bedrock");
|
||
}
|
||
return candidates;
|
||
};
|
||
const std::array<std::int64_t,3> offsets{{
|
||
-static_cast<std::int64_t>(World.CoreVoxelSizeMicrons),0,
|
||
static_cast<std::int64_t>(World.CoreVoxelSizeMicrons)}};
|
||
const std::int64_t stackMinimum=std::min_element(ResolvedLayout->strates.begin(),
|
||
ResolvedLayout->strates.end(),[](const auto& A,const auto& B)
|
||
{return A.bedrock.bottomMicrometres<B.bedrock.bottomMicrometres;})
|
||
->bedrock.bottomMicrometres;
|
||
const std::int64_t stackMaximum=std::max_element(ResolvedLayout->strates.begin(),
|
||
ResolvedLayout->strates.end(),[](const auto& A,const auto& B)
|
||
{return A.ceiling.topMicrometres<B.ceiling.topMicrometres;})
|
||
->ceiling.topMicrometres;
|
||
for(const std::int64_t Endpoint:endpoints)
|
||
for(const std::int64_t Offset:offsets)
|
||
{
|
||
const std::int64_t zMicrometres=Endpoint+Offset;
|
||
const double zVoxel=static_cast<double>(zMicrometres)/CoreVoxelMicrometres;
|
||
const VoxelForgeCore::Vec3d position{0.0,0.0,zVoxel};
|
||
const std::string actual=Shape.densityDispatchPathAt(position);
|
||
const auto expected=expectedAt(zMicrometres);
|
||
std::string expectedLabel;
|
||
std::vector<std::string> expectedGenerators;
|
||
for(const auto& Candidate:expected)
|
||
{
|
||
if(!expectedLabel.empty())expectedLabel+=',';
|
||
expectedLabel+=Candidate;
|
||
const auto Separator=Candidate.find('/');
|
||
const std::string Kind=Candidate.substr(0,Separator);
|
||
if(std::find(expectedGenerators.begin(),expectedGenerators.end(),Kind)==expectedGenerators.end())
|
||
expectedGenerators.push_back(Kind);
|
||
}
|
||
const float density=Shape.densityAt(position);
|
||
const bool solid=density<0.0f;
|
||
const bool outsideStack=zMicrometres<stackMinimum||zMicrometres>=stackMaximum;
|
||
if(expected.empty())expectedLabel=outsideStack?"outside-stack/air":"uncovered";
|
||
std::string actualGenerator;
|
||
if(actual=="SurfaceWorld-native"||actual=="SurfaceWorld-legacy")actualGenerator="SurfaceWorld";
|
||
else if(actual=="FloatingIslands-native")actualGenerator="FloatingIslands";
|
||
else if(actual.rfind("compiled-analytic/",0)==0)actualGenerator=actual.substr(18);
|
||
const bool mismatch=expected.empty()
|
||
?(!outsideStack||solid)
|
||
:std::find(expectedGenerators.begin(),expectedGenerators.end(),actualGenerator)==expectedGenerators.end();
|
||
++probes;disagreements+=mismatch?1u:0u;
|
||
std::printf("[ResolvedLayoutProbe] endpoint_um=%lld offset_um=%lld z_m=%.3f expected=%s actual=%s density=%.7g occupancy=%s disagreement=%s\n",
|
||
static_cast<long long>(Endpoint),static_cast<long long>(Offset),
|
||
static_cast<double>(zMicrometres)/1000000.0,expectedLabel.c_str(),actual.c_str(),density,
|
||
solid?"solid":"air",mismatch?"YES":"no");
|
||
}
|
||
std::printf("[ResolvedLayoutProbeSummary] probes=%llu disagreements=%llu status=%s\n",
|
||
static_cast<unsigned long long>(probes),static_cast<unsigned long long>(disagreements),
|
||
disagreements==0?"PASS":"DEVIATIONS_RECORDED");
|
||
Streamer.Stop();
|
||
return 0;
|
||
}
|
||
const auto ShapeReady=Clock::now();
|
||
const double ShapeSeconds=std::chrono::duration<double>(ShapeReady-ShapeBuildStart).count();
|
||
if(bAuditTileShapeReadiness)
|
||
{
|
||
VoxelForgeCore::TileGenerationOptions CompiledOptions;
|
||
VoxelForgeCore::TileGenerationOptions NativeOnlyOptions;
|
||
CompiledOptions.hashDensity=true;
|
||
NativeOnlyOptions.hashDensity=true;
|
||
NativeOnlyOptions.diagnosticForceNativeOnly=true;
|
||
int32 Compared=0,Mismatched=0;
|
||
for(const FVoxelTileKey& Tile:ShapeFreeTiles)
|
||
{
|
||
if(!Neighborhood.contains(Tile))continue;
|
||
const VoxelForgeCore::TileKey CoreKey{{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level};
|
||
std::string CompiledError,NativeOnlyError;
|
||
const auto Compiled=Shape.generateTile(CoreKey,CompiledOptions,CompiledError);
|
||
const auto NativeOnly=Shape.generateTile(CoreKey,NativeOnlyOptions,NativeOnlyError);
|
||
++Compared;
|
||
const bool bSame=Compiled&&NativeOnly
|
||
&&Compiled->densityHash==NativeOnly->densityHash
|
||
&&Compiled->meshHash==NativeOnly->meshHash
|
||
&&Compiled->payloadHash==NativeOnly->payloadHash
|
||
&&Compiled->topologyHash==NativeOnly->topologyHash;
|
||
if(!bSame)
|
||
{
|
||
++Mismatched;
|
||
std::printf("[ShapeReadinessEquivalence] key=(%d,%d,%d,lod%d) match=0 compiled=%s native_only=%s density=%s mesh=%s\n",
|
||
Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z,Tile.Level,
|
||
Compiled?"ok":CompiledError.c_str(),NativeOnly?"ok":NativeOnlyError.c_str(),
|
||
Compiled&&NativeOnly&&Compiled->densityHash==NativeOnly->densityHash?"same":"different",
|
||
Compiled&&NativeOnly&&Compiled->meshHash==NativeOnly->meshHash?"same":"different");
|
||
}
|
||
}
|
||
std::printf("[ShapeReadinessEquivalence] compared_near_ring=%d mismatches=%d status=%s\n",
|
||
Compared,Mismatched,Mismatched==0?"PASS":"FAIL");
|
||
if(Mismatched!=0)
|
||
{
|
||
Streamer.Stop();
|
||
std::fprintf(stderr,"[StreamPipeline] native-only tile generation differs from compiled generation.\n");
|
||
return 9;
|
||
}
|
||
}
|
||
if(bAuditBasePageEquivalence)
|
||
{
|
||
std::vector<VoxelForgeCore::TileKey> AuditKeys;
|
||
AuditKeys.reserve(48);
|
||
const auto AddAuditCandidates=[&](int32 PreferredLOD)
|
||
{
|
||
const std::size_t ExistingCount=AuditKeys.size();
|
||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||
{
|
||
if(Tile.Level!=PreferredLOD)continue;
|
||
const VoxelForgeCore::TileKey Key{{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},
|
||
Tile.Level};
|
||
if(Shape.isTileShapeFree(Key)||Shape.needsTileRegion(Key))continue;
|
||
if(std::find(AuditKeys.begin(),AuditKeys.end(),Key)==AuditKeys.end())
|
||
AuditKeys.push_back(Key);
|
||
if(AuditKeys.size()>=48||AuditKeys.size()-ExistingCount>=4)return;
|
||
}
|
||
};
|
||
for(int32 PreferredLOD:{1,0,2,3,4,5,6,7,8})
|
||
{
|
||
AddAuditCandidates(PreferredLOD);
|
||
if(AuditKeys.size()>=48)break;
|
||
}
|
||
if(AuditKeys.empty())
|
||
{
|
||
std::printf("[BasePageTileEquivalence] status=UNAVAILABLE reason=no_tile_reachable_through_both_routes\n");
|
||
Streamer.Stop();
|
||
return 9;
|
||
}
|
||
VoxelForgeCore::TileGenerationOptions HashOptions;
|
||
HashOptions.hashDensity=true;
|
||
HashOptions.profileMesherStages=true;
|
||
bool bFoundEquivalentTile=false;
|
||
std::size_t CandidatesTried=0;
|
||
VoxelForgeCore::TileKey EquivalentKey{};
|
||
for(const VoxelForgeCore::TileKey& AuditKey:AuditKeys)
|
||
{
|
||
++CandidatesTried;
|
||
std::string BaseError,PageError,CompileError;
|
||
const auto BaseGenerationStart=Clock::now();
|
||
const auto BaseTile=Shape.generateTile(AuditKey,HashOptions,BaseError);
|
||
const double BaseGenerationSeconds=std::chrono::duration<double>(
|
||
Clock::now()-BaseGenerationStart).count();
|
||
if(!BaseTile)
|
||
{
|
||
std::printf("[BasePageTileEquivalenceCandidate] key=(%d,%d,%d,lod%d) base=FAIL reason=%s\n",
|
||
AuditKey.coordinate.x,AuditKey.coordinate.y,AuditKey.coordinate.z,
|
||
AuditKey.lod,BaseError.c_str());
|
||
continue;
|
||
}
|
||
const std::array<VoxelForgeCore::TileKey,1> AuditTiles={AuditKey};
|
||
VoxelForgeCore::TileRegionCompileStats PageBuildStats;
|
||
std::size_t PageTileCount=0;
|
||
const auto PageCompileStart=Clock::now();
|
||
const bool bPageBuilt=Shape.compileTileRegion(AuditTiles,
|
||
static_cast<std::uint32_t>(std::clamp(WorkerCount,1,16)),Scheduler,
|
||
CompileError,&PageTileCount,&PageBuildStats,true,true);
|
||
const double PageCompileSeconds=std::chrono::duration<double>(
|
||
Clock::now()-PageCompileStart).count();
|
||
const auto PageGenerationStart=Clock::now();
|
||
const auto PageTile=bPageBuilt?Shape.generateTile(AuditKey,HashOptions,PageError)
|
||
:std::optional<VoxelForgeCore::TileMesh>{};
|
||
const double PageGenerationSeconds=std::chrono::duration<double>(
|
||
Clock::now()-PageGenerationStart).count();
|
||
const bool bDensitySame=PageTile&&BaseTile->densityHash==PageTile->densityHash;
|
||
const bool bMeshSame=PageTile&&BaseTile->meshHash==PageTile->meshHash;
|
||
const bool bPayloadSame=PageTile&&BaseTile->payloadHash==PageTile->payloadHash;
|
||
const bool bTopologySame=PageTile&&BaseTile->topologyHash==PageTile->topologyHash;
|
||
const bool bActualPage=PageTile&&PageTile->diagnostics.analyticRegionPageRoute;
|
||
const int32 Matches=static_cast<int32>(bDensitySame)+static_cast<int32>(bMeshSame)
|
||
+static_cast<int32>(bPayloadSame)+static_cast<int32>(bTopologySame);
|
||
const bool bSame=bActualPage&&Matches==4;
|
||
std::printf("[BasePageTileEquivalenceCandidate] key=(%d,%d,%d,lod%d) page_built=%d page_route_verified=%d page_keys=%zu page_nodes=%llu base_route=base-shape page_route=analytic-page base_generation_ms=%.3f page_compile_ms=%.3f page_generation_ms=%.3f density=%s mesh=%s payload=%s topology=%s base_density=0x%016llx page_density=0x%016llx status=%s error=%s\n",
|
||
AuditKey.coordinate.x,AuditKey.coordinate.y,AuditKey.coordinate.z,AuditKey.lod,
|
||
bPageBuilt?1:0,bActualPage?1:0,PageTileCount,
|
||
static_cast<unsigned long long>(PageBuildStats.latticeNodes),
|
||
BaseGenerationSeconds*1000.0,PageCompileSeconds*1000.0,
|
||
PageGenerationSeconds*1000.0,
|
||
bDensitySame?"same":"DIFF",bMeshSame?"same":"DIFF",
|
||
bPayloadSame?"same":"DIFF",bTopologySame?"same":"DIFF",
|
||
static_cast<unsigned long long>(BaseTile->densityHash),
|
||
static_cast<unsigned long long>(PageTile?PageTile->densityHash:0),
|
||
bSame?"PASS":"DIFF",bPageBuilt?PageError.c_str():CompileError.c_str());
|
||
if(bSame)
|
||
{
|
||
bFoundEquivalentTile=true;
|
||
EquivalentKey=AuditKey;
|
||
break;
|
||
}
|
||
}
|
||
std::printf("[BasePageTileEquivalence] candidates=%zu tried=%zu matching_key=(%d,%d,%d,lod%d) status=%s\n",
|
||
AuditKeys.size(),CandidatesTried,EquivalentKey.coordinate.x,
|
||
EquivalentKey.coordinate.y,EquivalentKey.coordinate.z,EquivalentKey.lod,
|
||
bFoundEquivalentTile?"PASS":"FAIL");
|
||
if(!bFoundEquivalentTile)
|
||
{
|
||
Streamer.Stop();
|
||
return 9;
|
||
}
|
||
}
|
||
if(bAuditRouteParity)
|
||
{
|
||
struct FRouteAuditCandidate
|
||
{
|
||
VoxelForgeCore::TileKey Key;
|
||
bool bWouldRequestPage=false;
|
||
};
|
||
struct FRouteAuditRow
|
||
{
|
||
FRouteAuditCandidate Candidate;
|
||
std::optional<VoxelForgeCore::TileMesh> Base;
|
||
};
|
||
VoxelForgeCore::TileGenerationOptions CaptureOptions;
|
||
CaptureOptions.hashDensity=true;
|
||
CaptureOptions.captureDensitySamples=true;
|
||
CaptureOptions.forceFullDensityCapture=true;
|
||
CaptureOptions.profileMesherStages=true;
|
||
std::vector<FRouteAuditRow> Rows;
|
||
Rows.reserve(static_cast<std::size_t>(World.FullResClipLevels)*8);
|
||
std::vector<VoxelForgeCore::TileKey> PageKeys;
|
||
PageKeys.reserve(Rows.capacity());
|
||
const int32 AuditPageLODs=std::min(World.FullResClipLevels,3);
|
||
std::vector<const FStandaloneDesiredTileSet*> CandidateSets{&Desired};
|
||
if(bHasCameraStep)CandidateSets.push_back(&MovedDesired);
|
||
for(int32 LOD=0;LOD<AuditPageLODs;++LOD)
|
||
{
|
||
for(const bool bPageRequested:{true,false})
|
||
{
|
||
int32 SurfaceCandidates=0;
|
||
bool bHaveFlatCandidate=false;
|
||
int32 Inspected=0;
|
||
for(const FStandaloneDesiredTileSet* CandidateSet:CandidateSets)
|
||
{
|
||
for(const FVoxelTileKey& Tile:CandidateSet->Tiles)
|
||
{
|
||
if(Tile.Level!=LOD)continue;
|
||
const VoxelForgeCore::TileKey Key{{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level};
|
||
if(Shape.isTileShapeFree(Key)
|
||
||Shape.needsTileRegion(Key)!=bPageRequested)continue;
|
||
if(Inspected++>=256)break;
|
||
std::string BaseError;
|
||
auto Base=Shape.generateTile(Key,CaptureOptions,BaseError);
|
||
if(!Base)
|
||
{
|
||
std::printf("[RouteParityCandidate] route=%s key=(%d,%d,%d,lod%d) base=FAIL error=%s\n",
|
||
bPageRequested?"actual-page":"base-covered-page",Key.coordinate.x,
|
||
Key.coordinate.y,Key.coordinate.z,Key.lod,BaseError.c_str());
|
||
continue;
|
||
}
|
||
const bool bHasSurface=!Base->vertices.empty()&&!Base->indices.empty();
|
||
if(bHasSurface&&SurfaceCandidates<3)
|
||
{
|
||
Rows.push_back({{Key,bPageRequested},std::move(Base)});
|
||
PageKeys.push_back(Key);
|
||
++SurfaceCandidates;
|
||
}
|
||
else if(!bHasSurface&&!bHaveFlatCandidate)
|
||
{
|
||
Rows.push_back({{Key,bPageRequested},std::move(Base)});
|
||
PageKeys.push_back(Key);
|
||
bHaveFlatCandidate=true;
|
||
}
|
||
if(SurfaceCandidates>=3&&bHaveFlatCandidate)break;
|
||
}
|
||
if(SurfaceCandidates>=3&&bHaveFlatCandidate)break;
|
||
}
|
||
}
|
||
}
|
||
if(Rows.empty())
|
||
{
|
||
std::printf("[RouteParityAudit] status=UNAVAILABLE reason=no_page_route_candidates\n");
|
||
Streamer.Stop();
|
||
return 9;
|
||
}
|
||
|
||
VoxelForgeCore::TileGenerationOptions PageOptions=CaptureOptions;
|
||
PageOptions.auditUniformBlockProofs=true;
|
||
int32 BasePageMismatches=0,PageRoutesVerified=0;
|
||
std::uint64_t PageProofBlocks=0,PageProofSamples=0,PageProofFallbacks=0,
|
||
PageProofUnknown=0,PageProofViolations=0;
|
||
std::uint64_t PageExactSamples=0,PageExactMismatches=0;
|
||
double MaxDensityDelta=0.0,SumDensityDelta=0.0;
|
||
std::uint64_t ComparedDensitySamples=0,SignMismatches=0,
|
||
CrossingEdgeMismatches=0,SharedCrossingEdges=0;
|
||
double MaxSurfaceDisplacement=0.0,SumSurfaceDisplacement=0.0;
|
||
for(const FRouteAuditRow& Row:Rows)
|
||
if(Row.Base)
|
||
{
|
||
const FRouteDensityComparison Query=CompareRouteDensities(
|
||
*Row.Base,*Row.Base,Shape);
|
||
PageExactSamples+=Query.exactQuerySamples;
|
||
PageExactMismatches+=Query.exactQueryMismatches;
|
||
}
|
||
else ++BasePageMismatches;
|
||
VoxelForgeCore::TileRegionCompileStats PageStats;
|
||
std::size_t CoveredPageKeys=0;
|
||
std::string PageCompileError;
|
||
const bool bPagesCompiled=Shape.compileTileRegion(PageKeys,
|
||
static_cast<std::uint32_t>(std::clamp(WorkerCount,1,16)),Scheduler,
|
||
PageCompileError,&CoveredPageKeys,&PageStats,true,true);
|
||
if(!bPagesCompiled)
|
||
{
|
||
std::printf("[RouteParityAudit] page_compile=FAIL error=%s\n",PageCompileError.c_str());
|
||
Streamer.Stop();
|
||
return 9;
|
||
}
|
||
for(FRouteAuditRow& Row:Rows)
|
||
{
|
||
std::string Error;
|
||
const auto Page=Shape.generateTile(Row.Candidate.Key,PageOptions,Error);
|
||
const bool bVerifiedPage=Page&&Page->diagnostics.analyticRegionPageRoute;
|
||
PageRoutesVerified+=bVerifiedPage?1:0;
|
||
const bool bSame=Row.Base&&bVerifiedPage&&SameTileOutput(*Row.Base,*Page);
|
||
const FRouteDensityComparison Comparison=Page&&Row.Base
|
||
?CompareRouteDensities(*Page,*Row.Base,Shape):FRouteDensityComparison{};
|
||
if(!bSame||!Comparison.valid||Comparison.exactQueryMismatches!=0)
|
||
++BasePageMismatches;
|
||
PageExactSamples+=Comparison.exactQuerySamples;
|
||
PageExactMismatches+=Comparison.exactQueryMismatches;
|
||
MaxDensityDelta=std::max(MaxDensityDelta,Comparison.maxAbsoluteDelta);
|
||
SumDensityDelta+=Comparison.sumAbsoluteDelta;
|
||
ComparedDensitySamples+=Comparison.samples;
|
||
SignMismatches+=Comparison.signMismatches;
|
||
CrossingEdgeMismatches+=Comparison.crossingEdgeMismatches;
|
||
SharedCrossingEdges+=Comparison.sharedCrossingEdges;
|
||
MaxSurfaceDisplacement=std::max(MaxSurfaceDisplacement,
|
||
Comparison.maxSurfaceDisplacementVoxels);
|
||
SumSurfaceDisplacement+=Comparison.sumSurfaceDisplacementVoxels;
|
||
if(Page)
|
||
{
|
||
PageProofBlocks+=Page->diagnostics.uniformBlockProofsChecked;
|
||
PageProofSamples+=Page->diagnostics.uniformBlockProofSamplesChecked;
|
||
PageProofFallbacks+=Page->diagnostics.uniformBlockProofFallbackBlocks;
|
||
PageProofUnknown+=Page->diagnostics.uniformBlockProofUnknownBlocks;
|
||
PageProofViolations+=Page->diagnostics.uniformBlockProofViolations;
|
||
}
|
||
std::printf("[RouteParityPair] route=base/page key=(%d,%d,%d,lod%d) page_requested=%d page_verified=%d same_density=%d same_mesh=%d same_payload=%d same_topology=%d samples=%llu max_abs_density_delta=%.9g sign_mismatches=%llu edge_crossing_mismatches=%llu shared_surface_edges=%llu max_surface_delta_vox=%.9g exact_query_mismatches=%llu base_native_float=%llu page_native_float=%llu first_density=(%.9g,%.9g) status=%s error=%s\n",
|
||
Row.Candidate.Key.coordinate.x,Row.Candidate.Key.coordinate.y,
|
||
Row.Candidate.Key.coordinate.z,Row.Candidate.Key.lod,
|
||
Row.Candidate.bWouldRequestPage?1:0,bVerifiedPage?1:0,
|
||
Row.Base&&Page&&Row.Base->densityHash==Page->densityHash?1:0,
|
||
Row.Base&&Page&&Row.Base->meshHash==Page->meshHash?1:0,
|
||
Row.Base&&Page&&Row.Base->payloadHash==Page->payloadHash?1:0,
|
||
Row.Base&&Page&&Row.Base->topologyHash==Page->topologyHash?1:0,
|
||
static_cast<unsigned long long>(Comparison.samples),
|
||
Comparison.maxAbsoluteDelta,
|
||
static_cast<unsigned long long>(Comparison.signMismatches),
|
||
static_cast<unsigned long long>(Comparison.crossingEdgeMismatches),
|
||
static_cast<unsigned long long>(Comparison.sharedCrossingEdges),
|
||
Comparison.maxSurfaceDisplacementVoxels,
|
||
static_cast<unsigned long long>(Comparison.exactQueryMismatches),
|
||
static_cast<unsigned long long>(Row.Base?Row.Base->diagnostics.floatFieldCalls:0),
|
||
static_cast<unsigned long long>(Page?Page->diagnostics.floatFieldCalls:0),
|
||
Row.Base&&!Row.Base->debugDensitySamples.empty()
|
||
?Row.Base->debugDensitySamples.front():0.0f,
|
||
Page&&!Page->debugDensitySamples.empty()?Page->debugDensitySamples.front():0.0f,
|
||
bSame&&Comparison.valid&&Comparison.exactQueryMismatches==0?"PASS":"FAIL",
|
||
Page?Error.c_str():Error.c_str());
|
||
}
|
||
|
||
int32 NativeMismatches=0,NativeRoutesVerified=0;
|
||
std::uint64_t NativeExactSamples=0,NativeExactMismatches=0;
|
||
int32 NativePairs=0;
|
||
VoxelForgeCore::TileGenerationOptions NativeOptions=CaptureOptions;
|
||
NativeOptions.diagnosticForceNativeOnly=true;
|
||
for(const FVoxelTileKey& Tile:ShapeFreeTiles)
|
||
{
|
||
if(NativePairs>=9)break;
|
||
if(!Neighborhood.contains(Tile))continue;
|
||
const VoxelForgeCore::TileKey Key{{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level};
|
||
std::string BaseError,NativeError;
|
||
const auto Base=Shape.generateTile(Key,CaptureOptions,BaseError);
|
||
const auto Native=Shape.generateTile(Key,NativeOptions,NativeError);
|
||
++NativePairs;
|
||
const bool bVerifiedNative=Native&&Native->diagnostics.nativeOnlyRoute;
|
||
NativeRoutesVerified+=bVerifiedNative?1:0;
|
||
const bool bSame=Base&&bVerifiedNative&&SameTileOutput(*Base,*Native);
|
||
const FRouteDensityComparison Comparison=Base&&Native
|
||
?CompareRouteDensities(*Native,*Base,Shape):FRouteDensityComparison{};
|
||
NativeExactSamples+=Comparison.exactQuerySamples;
|
||
NativeExactMismatches+=Comparison.exactQueryMismatches;
|
||
if(!bSame||!Comparison.valid||Comparison.exactQueryMismatches!=0)
|
||
++NativeMismatches;
|
||
std::printf("[RouteParityPair] route=compiled/native-only key=(%d,%d,%d,lod%d) native_verified=%d same_density=%d same_mesh=%d same_payload=%d same_topology=%d samples=%llu exact_query_mismatches=%llu status=%s error=%s\n",
|
||
Key.coordinate.x,Key.coordinate.y,Key.coordinate.z,Key.lod,
|
||
bVerifiedNative?1:0,Base&&Native&&Base->densityHash==Native->densityHash?1:0,
|
||
Base&&Native&&Base->meshHash==Native->meshHash?1:0,
|
||
Base&&Native&&Base->payloadHash==Native->payloadHash?1:0,
|
||
Base&&Native&&Base->topologyHash==Native->topologyHash?1:0,
|
||
static_cast<unsigned long long>(Comparison.samples),
|
||
static_cast<unsigned long long>(Comparison.exactQueryMismatches),
|
||
bSame&&Comparison.valid&&Comparison.exactQueryMismatches==0?"PASS":"FAIL",
|
||
Native?NativeError.c_str():NativeError.c_str());
|
||
}
|
||
|
||
int32 ExactQueryPairs=0,ExactQueryMismatches=0;
|
||
std::uint64_t ExactQuerySamples=0;
|
||
for(int32 LOD=World.FullResClipLevels;LOD<=World.MaxClipLevel&&LOD<9;++LOD)
|
||
{
|
||
int32 Added=0;
|
||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||
{
|
||
if(Tile.Level!=LOD||Added>=2)continue;
|
||
const VoxelForgeCore::TileKey Key{{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level};
|
||
std::string Error;
|
||
const auto Mesh=Shape.generateTile(Key,CaptureOptions,Error);
|
||
const FRouteDensityComparison Comparison=Mesh
|
||
?CompareRouteDensities(*Mesh,*Mesh,Shape):FRouteDensityComparison{};
|
||
++ExactQueryPairs;++Added;
|
||
ExactQuerySamples+=Comparison.exactQuerySamples;
|
||
if(!Mesh||!Comparison.valid||Comparison.exactQueryMismatches!=0)
|
||
++ExactQueryMismatches;
|
||
std::printf("[RouteParityPair] route=tile/exact-query key=(%d,%d,%d,lod%d) samples=%llu exact_query_mismatches=%llu status=%s error=%s\n",
|
||
Key.coordinate.x,Key.coordinate.y,Key.coordinate.z,Key.lod,
|
||
static_cast<unsigned long long>(Comparison.exactQuerySamples),
|
||
static_cast<unsigned long long>(Comparison.exactQueryMismatches),
|
||
Mesh&&Comparison.valid&&Comparison.exactQueryMismatches==0?"PASS":"FAIL",
|
||
Error.c_str());
|
||
}
|
||
}
|
||
int32 EditPairs=0,EditMismatches=0;
|
||
const auto EditRow=std::find_if(Rows.begin(),Rows.end(),[](const FRouteAuditRow& Row)
|
||
{
|
||
return Row.Base&&!Row.Candidate.bWouldRequestPage&&Row.Candidate.Key.lod<=1
|
||
&&!Row.Base->vertices.empty()&&!Row.Base->indices.empty();
|
||
});
|
||
if(EditRow!=Rows.end())
|
||
{
|
||
VoxelForgeCore::World EditWorld(CoreSnapshot);
|
||
std::string EditCompileError;
|
||
if(!EditWorld.compile(ShapeRequest,Scheduler,EditCompileError))
|
||
{
|
||
++EditMismatches;
|
||
std::printf("[RouteParityEdit] status=FAIL stage=compile error=%s\n",
|
||
EditCompileError.c_str());
|
||
}
|
||
else
|
||
{
|
||
const VoxelForgeCore::TileKey Key=EditRow->Candidate.Key;
|
||
const std::int32_t Extent=CHUNK_SIZE<<Key.lod;
|
||
const std::array<VoxelForgeCore::TileKey,1> EditKeys={Key};
|
||
VoxelForgeCore::TileGenerationOptions EditOptions=CaptureOptions;
|
||
EditOptions.auditUniformBlockProofs=true;
|
||
std::string BeforeError,ApplyError,BaseError,PageCompileError,PageError;
|
||
const auto Before=EditWorld.generateTile(Key,EditOptions,BeforeError);
|
||
VoxelForgeCore::EditTransaction Transaction;
|
||
Transaction.sequence=1;
|
||
VoxelForgeCore::EditBrush Brush;
|
||
Brush.shape=VoxelForgeCore::EditBrush::Shape::sphere;
|
||
Brush.operation=VoxelForgeCore::EditBrush::Operation::densityOffset;
|
||
bool bSurfaceCenterFound=false;
|
||
if(Before&&!Before->debugDensitySamples.empty())
|
||
{
|
||
const std::int32_t Dim=static_cast<std::int32_t>(std::llround(
|
||
std::cbrt(static_cast<double>(Before->debugDensitySamples.size()))));
|
||
const std::int32_t GridDim=Dim-3;
|
||
if(Dim>=4&&static_cast<std::size_t>(Dim)*Dim*Dim
|
||
==Before->debugDensitySamples.size()&&GridDim>0&&Extent%GridDim==0)
|
||
{
|
||
const std::int32_t Step=Extent/GridDim;
|
||
const std::int32_t OriginX=Key.coordinate.x*Extent;
|
||
const std::int32_t OriginY=Key.coordinate.y*Extent;
|
||
const std::int32_t OriginZ=Key.coordinate.z*Extent;
|
||
const auto Index=[Dim](std::int32_t X,std::int32_t Y,std::int32_t Z)
|
||
{return (static_cast<std::size_t>(Z)*Dim+Y)*Dim+X;};
|
||
for(std::int32_t Z=0;Z<Dim&&!bSurfaceCenterFound;++Z)
|
||
for(std::int32_t Y=0;Y<Dim&&!bSurfaceCenterFound;++Y)
|
||
for(std::int32_t X=0;X<Dim&&!bSurfaceCenterFound;++X)
|
||
{
|
||
for(const std::array<std::int32_t,3>& Delta:
|
||
{std::array<std::int32_t,3>{1,0,0},
|
||
std::array<std::int32_t,3>{0,1,0},
|
||
std::array<std::int32_t,3>{0,0,1}})
|
||
{
|
||
const std::int32_t NX=X+Delta[0],NY=Y+Delta[1],NZ=Z+Delta[2];
|
||
if(NX>=Dim||NY>=Dim||NZ>=Dim)continue;
|
||
const float D0=Before->debugDensitySamples[Index(X,Y,Z)];
|
||
const float D1=Before->debugDensitySamples[Index(NX,NY,NZ)];
|
||
if((D0<0.0f)==(D1<0.0f))continue;
|
||
const double T=static_cast<double>(D0)
|
||
/(static_cast<double>(D0)-D1);
|
||
Brush.a={OriginX+(X-1+T*Delta[0])*Step,
|
||
OriginY+(Y-1+T*Delta[1])*Step,
|
||
OriginZ+(Z-1+T*Delta[2])*Step};
|
||
bSurfaceCenterFound=true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if(!bSurfaceCenterFound)
|
||
Brush.a={static_cast<double>(Key.coordinate.x*Extent)+Extent*0.5,
|
||
static_cast<double>(Key.coordinate.y*Extent)+Extent*0.5,
|
||
static_cast<double>(Key.coordinate.z*Extent)+Extent*0.5};
|
||
Brush.radius=10.0;
|
||
Brush.strength=-48.0f;
|
||
Transaction.brushes.push_back(Brush);
|
||
const auto Applied=Before?EditWorld.applyEditTransaction(Transaction,
|
||
EditKeys,ApplyError):std::optional<VoxelForgeCore::EditResult>{};
|
||
const auto EditedBase=Applied
|
||
?EditWorld.generateTile(Key,EditOptions,BaseError)
|
||
:std::optional<VoxelForgeCore::TileMesh>{};
|
||
VoxelForgeCore::TileRegionCompileStats EditPageStats;
|
||
std::size_t EditPageKeys=0;
|
||
const bool bEditPageBuilt=EditedBase&&EditWorld.compileTileRegion(EditKeys,
|
||
static_cast<std::uint32_t>(std::clamp(WorkerCount,1,16)),Scheduler,
|
||
PageCompileError,&EditPageKeys,&EditPageStats,true,true);
|
||
const auto EditedPage=bEditPageBuilt
|
||
?EditWorld.generateTile(Key,EditOptions,PageError)
|
||
:std::optional<VoxelForgeCore::TileMesh>{};
|
||
const FRouteDensityComparison EditComparison=EditedBase&&EditedPage
|
||
?CompareRouteDensities(*EditedPage,*EditedBase,EditWorld)
|
||
:FRouteDensityComparison{};
|
||
const bool bEditChanged=Before&&EditedBase
|
||
&&Before->densityHash!=EditedBase->densityHash;
|
||
const bool bEditPageVerified=EditedPage
|
||
&&EditedPage->diagnostics.analyticRegionPageRoute;
|
||
const bool bEditSame=EditedBase&&EditedPage&&bEditPageVerified
|
||
&&SameTileOutput(*EditedBase,*EditedPage);
|
||
const bool bEditPass=Before&&Applied&&EditedBase&&bEditPageBuilt
|
||
&&bEditPageVerified&&bEditChanged&&bEditSame&&EditComparison.valid
|
||
&&EditComparison.exactQueryMismatches==0;
|
||
++EditPairs;
|
||
if(!bEditPass)++EditMismatches;
|
||
if(EditedPage)
|
||
{
|
||
PageProofBlocks+=EditedPage->diagnostics.uniformBlockProofsChecked;
|
||
PageProofSamples+=EditedPage->diagnostics.uniformBlockProofSamplesChecked;
|
||
PageProofFallbacks+=EditedPage->diagnostics.uniformBlockProofFallbackBlocks;
|
||
PageProofUnknown+=EditedPage->diagnostics.uniformBlockProofUnknownBlocks;
|
||
PageProofViolations+=EditedPage->diagnostics.uniformBlockProofViolations;
|
||
}
|
||
std::printf("[RouteParityEdit] key=(%d,%d,%d,lod%d) sequence=%llu before_hash=0x%016llx edited_base_hash=0x%016llx edited_page_hash=0x%016llx edit_changed=%d page_verified=%d same_mesh=%d same_payload=%d same_topology=%d samples=%llu exact_query_mismatches=%llu status=%s error=%s\n",
|
||
Key.coordinate.x,Key.coordinate.y,Key.coordinate.z,Key.lod,
|
||
static_cast<unsigned long long>(Transaction.sequence),
|
||
static_cast<unsigned long long>(Before?Before->densityHash:0),
|
||
static_cast<unsigned long long>(EditedBase?EditedBase->densityHash:0),
|
||
static_cast<unsigned long long>(EditedPage?EditedPage->densityHash:0),
|
||
bEditChanged?1:0,bEditPageVerified?1:0,
|
||
EditedBase&&EditedPage&&EditedBase->meshHash==EditedPage->meshHash?1:0,
|
||
EditedBase&&EditedPage&&EditedBase->payloadHash==EditedPage->payloadHash?1:0,
|
||
EditedBase&&EditedPage&&EditedBase->topologyHash==EditedPage->topologyHash?1:0,
|
||
static_cast<unsigned long long>(EditComparison.samples),
|
||
static_cast<unsigned long long>(EditComparison.exactQueryMismatches),
|
||
bEditPass?"PASS":"FAIL",
|
||
!Before?BeforeError.c_str():!Applied?ApplyError.c_str():
|
||
!EditedBase?BaseError.c_str():!bEditPageBuilt?PageCompileError.c_str():
|
||
PageError.c_str());
|
||
}
|
||
}
|
||
int32 HistoryPairs=0,HistoryMismatches=0;
|
||
if(bHasCameraStep&&bRegionCrossing)
|
||
{
|
||
const auto MovedAnchor=std::find_if(MovedDesired.Tiles.begin(),MovedDesired.Tiles.end(),
|
||
[](const FVoxelTileKey& Tile){return Tile.Level==0;});
|
||
const auto HistoryRow=std::find_if(Rows.begin(),Rows.end(),
|
||
[&](const FRouteAuditRow& Row)
|
||
{
|
||
if(!Row.Base||Row.Candidate.bWouldRequestPage)return false;
|
||
return std::none_of(MovedDesired.Tiles.begin(),MovedDesired.Tiles.end(),
|
||
[&](const FVoxelTileKey& Tile)
|
||
{
|
||
return Tile.Level==Row.Candidate.Key.lod
|
||
&&Tile.Coord.X==Row.Candidate.Key.coordinate.x
|
||
&&Tile.Coord.Y==Row.Candidate.Key.coordinate.y
|
||
&&Tile.Coord.Z==Row.Candidate.Key.coordinate.z;
|
||
});
|
||
});
|
||
if(HistoryRow!=Rows.end()&&MovedAnchor!=MovedDesired.Tiles.end())
|
||
{
|
||
VoxelForgeCore::ShapeBuildRequest MovedShapeRequest=ShapeRequest;
|
||
const FIntVector MovedOrigin=MovedAnchor->OriginVoxels();
|
||
const std::int32_t MovedExtent=MovedAnchor->ExtentVoxels();
|
||
MovedShapeRequest.minVoxel={MovedOrigin.X,MovedOrigin.Y,MovedOrigin.Z};
|
||
MovedShapeRequest.maxVoxel={MovedOrigin.X+MovedExtent,
|
||
MovedOrigin.Y+MovedExtent,MovedOrigin.Z+MovedExtent};
|
||
MovedShapeRequest.clipMinVoxel=MovedShapeRequest.minVoxel;
|
||
MovedShapeRequest.clipMaxVoxel=MovedShapeRequest.maxVoxel;
|
||
VoxelForgeCore::World MovedWorld(CoreSnapshot);
|
||
std::string MovedShapeError,MovedTileError;
|
||
const bool bMovedCompiled=MovedWorld.compile(MovedShapeRequest,Scheduler,
|
||
MovedShapeError);
|
||
const auto MovedTile=bMovedCompiled
|
||
?MovedWorld.generateTile(HistoryRow->Candidate.Key,CaptureOptions,MovedTileError)
|
||
:std::optional<VoxelForgeCore::TileMesh>{};
|
||
const FRouteDensityComparison HistoryComparison=MovedTile
|
||
?CompareRouteDensities(*MovedTile,*HistoryRow->Base,MovedWorld)
|
||
:FRouteDensityComparison{};
|
||
const bool bHistorySame=MovedTile
|
||
&&SameTileOutput(*MovedTile,*HistoryRow->Base)
|
||
&&HistoryComparison.valid&&HistoryComparison.exactQueryMismatches==0;
|
||
++HistoryPairs;
|
||
if(!bHistorySame)++HistoryMismatches;
|
||
std::printf("[RouteParityHistory] from=initial_base to=moved_world key=(%d,%d,%d,lod%d) same_density=%d same_mesh=%d same_payload=%d same_topology=%d samples=%llu max_abs_density_delta=%.9g exact_query_mismatches=%llu status=%s error=%s\n",
|
||
HistoryRow->Candidate.Key.coordinate.x,HistoryRow->Candidate.Key.coordinate.y,
|
||
HistoryRow->Candidate.Key.coordinate.z,HistoryRow->Candidate.Key.lod,
|
||
MovedTile&&MovedTile->densityHash==HistoryRow->Base->densityHash?1:0,
|
||
MovedTile&&MovedTile->meshHash==HistoryRow->Base->meshHash?1:0,
|
||
MovedTile&&MovedTile->payloadHash==HistoryRow->Base->payloadHash?1:0,
|
||
MovedTile&&MovedTile->topologyHash==HistoryRow->Base->topologyHash?1:0,
|
||
static_cast<unsigned long long>(HistoryComparison.samples),
|
||
HistoryComparison.maxAbsoluteDelta,
|
||
static_cast<unsigned long long>(HistoryComparison.exactQueryMismatches),
|
||
bHistorySame?"PASS":"FAIL",
|
||
!bMovedCompiled?MovedShapeError.c_str():MovedTileError.c_str());
|
||
}
|
||
}
|
||
const double MeanDensityDelta=ComparedDensitySamples
|
||
?SumDensityDelta/static_cast<double>(ComparedDensitySamples):0.0;
|
||
const double MeanSurfaceDisplacement=SharedCrossingEdges
|
||
?SumSurfaceDisplacement/static_cast<double>(SharedCrossingEdges):0.0;
|
||
const bool bRouteParityPassing=BasePageMismatches==0&&NativeMismatches==0
|
||
&&ExactQueryMismatches==0&&EditPairs==1&&EditMismatches==0
|
||
&&HistoryPairs==(bHasCameraStep&&bRegionCrossing?1:0)&&HistoryMismatches==0
|
||
&&PageRoutesVerified==static_cast<int32>(Rows.size())
|
||
&&NativeRoutesVerified==NativePairs&&PageProofViolations==0
|
||
&&PageExactMismatches==0&&NativeExactMismatches==0;
|
||
std::printf("[RouteParityAudit] seed=%d page_pairs=%zu page_verified=%d page_mismatches=%d native_only_pairs=%d native_verified=%d native_mismatches=%d higher_lod_exact_pairs=%d higher_lod_exact_mismatches=%d edit_pairs=%d edit_mismatches=%d history_pairs=%d history_mismatches=%d exact_query_samples=%llu exact_query_mismatches=%llu page_density_samples=%llu max_abs_density_delta=%.9g mean_abs_density_delta=%.9g sign_mismatches=%llu edge_crossing_mismatches=%llu shared_surface_edges=%llu max_surface_delta_vox=%.9g mean_surface_delta_vox=%.9g page_proof_blocks=%llu page_proof_samples=%llu page_proof_fallbacks=%llu page_proof_unknown=%llu page_proof_violations=%llu status=%s\n",
|
||
World.Seed,Rows.size(),PageRoutesVerified,BasePageMismatches,
|
||
NativePairs,NativeRoutesVerified,NativeMismatches,ExactQueryPairs,
|
||
ExactQueryMismatches,EditPairs,EditMismatches,HistoryPairs,HistoryMismatches,
|
||
static_cast<unsigned long long>(PageExactSamples
|
||
+NativeExactSamples+ExactQuerySamples),
|
||
static_cast<unsigned long long>(PageExactMismatches
|
||
+NativeExactMismatches+ExactQueryMismatches),
|
||
static_cast<unsigned long long>(ComparedDensitySamples),MaxDensityDelta,
|
||
MeanDensityDelta,static_cast<unsigned long long>(SignMismatches),
|
||
static_cast<unsigned long long>(CrossingEdgeMismatches),
|
||
static_cast<unsigned long long>(SharedCrossingEdges),MaxSurfaceDisplacement,
|
||
MeanSurfaceDisplacement,static_cast<unsigned long long>(PageProofBlocks),
|
||
static_cast<unsigned long long>(PageProofSamples),
|
||
static_cast<unsigned long long>(PageProofFallbacks),
|
||
static_cast<unsigned long long>(PageProofUnknown),
|
||
static_cast<unsigned long long>(PageProofViolations),
|
||
bRouteParityPassing?"PASS":"FAIL");
|
||
if(!bRouteParityPassing)
|
||
{
|
||
Streamer.Stop();
|
||
return 9;
|
||
}
|
||
}
|
||
const FStandaloneStreamingUpdate Update=Streamer.UpdateDesiredWithSet(Desired,{},0);
|
||
if(Update.DesiredTiles!=static_cast<int32>(Desired.Tiles.size()))
|
||
{
|
||
Streamer.Stop();
|
||
std::fprintf(stderr,"[StreamPipeline] expected %zu tiles after shape publication but queued %d.\n",
|
||
Desired.Tiles.size(),Update.DesiredTiles);return 7;
|
||
}
|
||
FSceneExportWriter SceneExport;
|
||
const bool bExportRequested=!MeshExportPath.empty();
|
||
bool bSceneExportPassing=true;
|
||
if(bExportRequested)
|
||
{
|
||
bSceneExportPassing=SceneExport.Open(MeshExportPath,World.CoreVoxelSizeMicrons,
|
||
CameraPositionVoxels,CameraYawDegrees,CameraPitchDegrees,
|
||
static_cast<std::uint32_t>(Update.DesiredTiles),
|
||
ResolvedLayout&&!ResolvedLayout->oreVeins.empty());
|
||
if(!bSceneExportPassing)
|
||
{
|
||
Streamer.Stop();
|
||
std::fprintf(stderr,"[MeshSceneExport] FAIL: could not open %s\n",
|
||
MeshExportPath.string().c_str());
|
||
return 8;
|
||
}
|
||
}
|
||
|
||
std::vector<FHashedTile> DensityRows,MeshRows,PayloadRows,TopologyRows,MaterialRows;
|
||
std::uint64_t MaterialQueries=0,MaterialCandidateChecks=0,MaterialOutsideTiles=0;
|
||
double MaterialQueryMilliseconds=0.0;
|
||
int32 Finished=0,NeighborhoodFinished=0,FullDetailFinished=0;
|
||
int32 FullDetailExpected=0;
|
||
uint64 WholeNonEmptyMeshTiles=0,WholeTriangleCount=0;
|
||
struct FColumnXY
|
||
{
|
||
std::int64_t X=0,Y=0;
|
||
bool operator==(const FColumnXY&)const=default;
|
||
};
|
||
struct FColumnXYHash
|
||
{
|
||
std::size_t operator()(const FColumnXY& Value)const noexcept
|
||
{
|
||
const std::size_t X=std::hash<std::int64_t>{}(Value.X);
|
||
const std::size_t Y=std::hash<std::int64_t>{}(Value.Y);
|
||
return X^(Y+static_cast<std::size_t>(0x9E3779B9u)+(X<<6)+(X>>2));
|
||
}
|
||
};
|
||
struct FColumnEvaluationEvent
|
||
{
|
||
FColumnXY XY;
|
||
FVoxelTileKey Tile;
|
||
std::uint64_t WorkerHash=0;
|
||
std::uint64_t Sequence=0;
|
||
bool bOutsideTileFootprint=false;
|
||
};
|
||
std::array<std::vector<FColumnEvaluationEvent>,9> ColumnEventsByLOD;
|
||
std::array<std::uint64_t,9> ColumnEvaluationEventsByLOD{},UniqueColumnCoordinatesByLOD{};
|
||
std::array<std::uint64_t,9> ColumnDuplicateEventsByLOD{},SameWorkerEvictedRepeatsByLOD{};
|
||
std::array<std::uint64_t,9> DifferentWorkerRepeatsByLOD{},HaloNormalRepeatsByLOD{};
|
||
std::array<std::uint64_t,9> HaloNormalEvaluationsByLOD{},NeighbourDuplicateEventsByLOD{};
|
||
std::array<std::uint64_t,9> CrossWorkerNeighbourDuplicateEventsByLOD{};
|
||
std::array<std::uint64_t,9> ColumnCaptureTilesByLOD{},ExpectedTilesByLOD{};
|
||
std::array<bool,9> ColumnCaptureCompleteByLOD{};
|
||
ColumnCaptureCompleteByLOD.fill(true);
|
||
std::array<double,9> WorkerSecondsByLOD{};
|
||
std::array<double,9> BaseRouteWorkerSecondsByLOD{},PageRouteWorkerSecondsByLOD{};
|
||
std::array<double,9> ColumnEvalMsByLOD{},MarchingExclusiveMsByLOD{};
|
||
std::array<double,9> GroundRefineExclusiveMsByLOD{},CapRefineExclusiveMsByLOD{};
|
||
std::array<double,9> MeshAssemblyMsByLOD{};
|
||
std::array<double,9> SkirtMsByLOD{};
|
||
struct FFarLODProfile
|
||
{
|
||
std::array<std::uint64_t,9> ColumnCalls{},ColumnHits{},ColumnMisses{};
|
||
std::array<std::uint64_t,9> ColumnConflictMisses{},SharedColumnHits{},ColumnEvaluations{};
|
||
std::array<double,9> McColumnMs{},GroundColumnMs{},CapColumnMs{};
|
||
std::array<double,9> McGridMs{},McCellClassMs{},McGradientMs{};
|
||
std::array<double,9> McInterpolationMs{},McDedupMs{},McOtherMs{};
|
||
std::array<double,9> StratumShellMs{},ConnectorShellMs{};
|
||
std::array<std::uint64_t,9> StratumFieldCalls{},ConnectorFieldCalls{},FarShellTriangles{};
|
||
std::array<std::uint64_t,9> StratumColumnPointCalls{},StratumColumnHits{};
|
||
std::array<std::uint64_t,9> StratumColumnMisses{},StratumSharedColumnHits{},StratumColumnEvaluations{};
|
||
std::array<std::uint64_t,9> ConnectorColumnPointCalls{},ConnectorColumnHits{};
|
||
std::array<std::uint64_t,9> ConnectorColumnMisses{},ConnectorSharedColumnHits{},ConnectorColumnEvaluations{};
|
||
std::array<double,9> StratumColumnComputeMs{},ConnectorColumnComputeMs{};
|
||
};
|
||
std::unique_ptr<FFarLODProfile> FarLODProfile;
|
||
if(bReportLodWorkProfile)FarLODProfile=std::make_unique<FFarLODProfile>();
|
||
std::array<std::uint64_t,9> DensitySamplesByLOD{};
|
||
std::array<std::uint64_t,9> SkirtTrianglesByLOD{};
|
||
std::array<std::uint64_t,9> TileCountByLOD{};
|
||
std::array<std::uint64_t,9> BaseRouteTilesByLOD{},PageRouteTilesByLOD{};
|
||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||
if(Tile.Level>=0&&Tile.Level<static_cast<int32>(ExpectedTilesByLOD.size()))
|
||
++ExpectedTilesByLOD[static_cast<std::size_t>(Tile.Level)];
|
||
const auto AreSameLODNeighbours=[](const FVoxelTileKey& A,const FVoxelTileKey& B)
|
||
{
|
||
if(A.Level!=B.Level)return false;
|
||
const std::int64_t DX=std::abs(static_cast<std::int64_t>(A.Coord.X)-B.Coord.X);
|
||
const std::int64_t DY=std::abs(static_cast<std::int64_t>(A.Coord.Y)-B.Coord.Y);
|
||
const std::int64_t DZ=std::abs(static_cast<std::int64_t>(A.Coord.Z)-B.Coord.Z);
|
||
return std::max({DX,DY,DZ})<=1&&(DX+DY+DZ)>0;
|
||
};
|
||
uint64 NativeSolidBoxes=0,NativeAirBoxes=0,NativeMixedBoxes=0,NativeUnknownBoxes=0;
|
||
uint64 NativeFieldCalls=0,NativeColumnHits=0,NativeColumnMisses=0,NativeMeshingSkips=0;
|
||
double NativeClassificationMilliseconds=0.0,NativeColumnComputeMilliseconds=0.0;
|
||
double NativeMeshingMilliseconds=0.0;
|
||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||
if(Tile.Level<World.FullResClipLevels)++FullDetailExpected;
|
||
std::array<std::optional<FStandaloneCpuTile>,9> GateSampleTiles{};
|
||
std::array<double,9> GateSampleDistances{};
|
||
GateSampleDistances.fill(std::numeric_limits<double>::infinity());
|
||
Clock::time_point FirstTileReady{},NeighborhoodReady{},FullDetailReady{},WholeViewReady{};
|
||
bool bFirstTileReported=false,bNeighborhoodReported=false,bFullDetailReported=false;
|
||
uint64 BlockProofBlocksChecked=0,BlockProofSamplesChecked=0;
|
||
uint64 BlockProofFallbackBlocks=0,BlockProofUnknownBlocks=0,BlockProofViolations=0;
|
||
while(Finished<Update.DesiredTiles)
|
||
{
|
||
std::vector<FStandaloneCpuTile> Ready=Streamer.TakeReady();
|
||
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
|
||
for(const FStandaloneCpuTile& Tile:Ready)
|
||
{
|
||
if(Tile.bGenerationFailed)
|
||
{
|
||
Streamer.Stop();
|
||
std::fprintf(stderr,"[StreamPipeline] a tile failed generation; refusing a partial result.\n");
|
||
return 9;
|
||
}
|
||
if(bExportRequested)SceneExport.Add(Tile);
|
||
++Finished;
|
||
if(bReportMeshCoverage)
|
||
{
|
||
const uint64 TileTriangleCount=static_cast<uint64>(Tile.Indices.size()/3);
|
||
WholeTriangleCount+=TileTriangleCount;
|
||
if(TileTriangleCount>0)++WholeNonEmptyMeshTiles;
|
||
}
|
||
if(Tile.Key.Level>=0&&Tile.Key.Level<static_cast<int32>(TileCountByLOD.size()))
|
||
{
|
||
const std::size_t LOD=static_cast<std::size_t>(Tile.Key.Level);
|
||
++TileCountByLOD[LOD];
|
||
WorkerSecondsByLOD[LOD]+=Tile.WorkerSeconds;
|
||
if(bReportLodWorkProfile)
|
||
{
|
||
if(Tile.CoreDiagnostics.analyticRegionPageRoute)
|
||
{
|
||
++PageRouteTilesByLOD[LOD];
|
||
PageRouteWorkerSecondsByLOD[LOD]+=Tile.WorkerSeconds;
|
||
}
|
||
else
|
||
{
|
||
++BaseRouteTilesByLOD[LOD];
|
||
BaseRouteWorkerSecondsByLOD[LOD]+=Tile.WorkerSeconds;
|
||
}
|
||
}
|
||
ColumnEvalMsByLOD[LOD]+=Tile.CoreDiagnostics.columnEvaluationExclusiveMilliseconds;
|
||
MarchingExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.marchingCubesExclusiveMilliseconds;
|
||
GroundRefineExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.groundRefinementExclusiveMilliseconds;
|
||
CapRefineExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.capRefinementExclusiveMilliseconds;
|
||
MeshAssemblyMsByLOD[LOD]+=Tile.CoreDiagnostics.meshAssemblyMilliseconds;
|
||
SkirtMsByLOD[LOD]+=Tile.CoreDiagnostics.marchingCubesSkirtMilliseconds;
|
||
SkirtTrianglesByLOD[LOD]+=Tile.SkirtTriangleCount;
|
||
if(bReportLodWorkProfile)
|
||
{
|
||
DensitySamplesByLOD[LOD]+=Tile.DensitySamples;
|
||
const auto& D=Tile.CoreDiagnostics;
|
||
FarLODProfile->ColumnCalls[LOD]+=D.fieldCalls;
|
||
FarLODProfile->ColumnHits[LOD]+=D.columnCacheHits;
|
||
FarLODProfile->ColumnMisses[LOD]+=D.columnCacheMisses;
|
||
FarLODProfile->ColumnConflictMisses[LOD]+=D.columnCacheConflictMisses;
|
||
FarLODProfile->SharedColumnHits[LOD]+=D.sharedColumnCacheHits;
|
||
FarLODProfile->ColumnEvaluations[LOD]+=D.columnEvaluations;
|
||
FarLODProfile->McColumnMs[LOD]+=D.marchingCubesColumnComputeMilliseconds;
|
||
FarLODProfile->GroundColumnMs[LOD]+=D.groundRefinementColumnComputeMilliseconds;
|
||
FarLODProfile->CapColumnMs[LOD]+=D.capRefinementColumnComputeMilliseconds;
|
||
FarLODProfile->McGridMs[LOD]+=D.marchingCubesDensityGridMilliseconds;
|
||
FarLODProfile->McCellClassMs[LOD]+=D.marchingCubesCellClassificationMilliseconds;
|
||
FarLODProfile->McGradientMs[LOD]+=D.marchingCubesGradientNormalsMilliseconds;
|
||
FarLODProfile->McInterpolationMs[LOD]+=D.marchingCubesEdgeInterpolationMilliseconds;
|
||
FarLODProfile->McDedupMs[LOD]+=D.marchingCubesVertexDedupStreamMilliseconds;
|
||
FarLODProfile->McOtherMs[LOD]+=D.marchingCubesOtherMilliseconds;
|
||
FarLODProfile->StratumShellMs[LOD]+=D.farStratumShellMilliseconds;
|
||
FarLODProfile->ConnectorShellMs[LOD]+=D.farConnectorShellMilliseconds;
|
||
FarLODProfile->StratumFieldCalls[LOD]+=D.farStratumFieldCalls;
|
||
FarLODProfile->ConnectorFieldCalls[LOD]+=D.farConnectorFieldCalls;
|
||
FarLODProfile->FarShellTriangles[LOD]+=D.farShellTrianglesAdded;
|
||
FarLODProfile->StratumColumnPointCalls[LOD]+=D.farStratumColumnPointCalls;
|
||
FarLODProfile->StratumColumnHits[LOD]+=D.farStratumColumnCacheHits;
|
||
FarLODProfile->StratumColumnMisses[LOD]+=D.farStratumColumnCacheMisses;
|
||
FarLODProfile->StratumSharedColumnHits[LOD]+=D.farStratumSharedColumnCacheHits;
|
||
FarLODProfile->StratumColumnEvaluations[LOD]+=D.farStratumColumnEvaluations;
|
||
FarLODProfile->ConnectorColumnPointCalls[LOD]+=D.farConnectorColumnPointCalls;
|
||
FarLODProfile->ConnectorColumnHits[LOD]+=D.farConnectorColumnCacheHits;
|
||
FarLODProfile->ConnectorColumnMisses[LOD]+=D.farConnectorColumnCacheMisses;
|
||
FarLODProfile->ConnectorSharedColumnHits[LOD]+=D.farConnectorSharedColumnCacheHits;
|
||
FarLODProfile->ConnectorColumnEvaluations[LOD]+=D.farConnectorColumnEvaluations;
|
||
FarLODProfile->StratumColumnComputeMs[LOD]+=D.farStratumColumnComputeMilliseconds;
|
||
FarLODProfile->ConnectorColumnComputeMs[LOD]+=D.farConnectorColumnComputeMilliseconds;
|
||
}
|
||
if(bAuditUniformBlockProofs)
|
||
{
|
||
BlockProofBlocksChecked+=Tile.CoreDiagnostics.uniformBlockProofsChecked;
|
||
BlockProofSamplesChecked+=Tile.CoreDiagnostics.uniformBlockProofSamplesChecked;
|
||
BlockProofFallbackBlocks+=Tile.CoreDiagnostics.uniformBlockProofFallbackBlocks;
|
||
BlockProofUnknownBlocks+=Tile.CoreDiagnostics.uniformBlockProofUnknownBlocks;
|
||
BlockProofViolations+=Tile.CoreDiagnostics.uniformBlockProofViolations;
|
||
}
|
||
if(bScreenSpaceGate&&Assembly&&Tile.Key.Level>=3&&Tile.Key.Level<9
|
||
&&!Tile.Vertices.empty()
|
||
&&StandaloneScreenSpaceGate::TileHasNativeSurface(Tile.Key,*Assembly,
|
||
static_cast<double>(World.CoreVoxelSizeMicrons)/1000000.0))
|
||
{
|
||
const double Distance=StandaloneScreenSpaceGate::TileAabbDistanceMeters(
|
||
Tile.Key,CameraPositionVoxels,
|
||
static_cast<double>(World.CoreVoxelSizeMicrons)/1000000.0);
|
||
const bool bCloser=Distance<GateSampleDistances[LOD]-1e-9;
|
||
const bool bTied=std::abs(Distance-GateSampleDistances[LOD])<=1e-9;
|
||
const bool bKeyLess=!GateSampleTiles[LOD]
|
||
||TileKeyLessForHash(Tile.Key,GateSampleTiles[LOD]->Key);
|
||
if(bCloser||(bTied&&bKeyLess))
|
||
{GateSampleDistances[LOD]=Distance;GateSampleTiles[LOD]=Tile;}
|
||
}
|
||
}
|
||
if(bProfileColumnOverlap&&Tile.Key.Level>=0
|
||
&&Tile.Key.Level<static_cast<int32>(ColumnCaptureTilesByLOD.size()))
|
||
{
|
||
const std::size_t LOD=static_cast<std::size_t>(Tile.Key.Level);
|
||
++ColumnCaptureTilesByLOD[LOD];
|
||
ColumnCaptureCompleteByLOD[LOD]=ColumnCaptureCompleteByLOD[LOD]
|
||
&&Tile.CoreDiagnostics.columnCoordinateCaptureComplete;
|
||
auto& Events=ColumnEventsByLOD[LOD];
|
||
for(const auto& Coordinate:Tile.CoreDiagnostics.evaluatedColumnCoordinates)
|
||
{
|
||
Events.push_back({{Coordinate.x,Coordinate.y},Tile.Key,
|
||
Tile.CoreDiagnostics.columnEvaluationWorkerHash,Coordinate.sequence,
|
||
Coordinate.outsideTileFootprint});
|
||
}
|
||
}
|
||
WholeViewReady=std::max(WholeViewReady,Tile.CompletedAt);
|
||
if(FirstTileReady.time_since_epoch().count()==0||Tile.CompletedAt<FirstTileReady)
|
||
FirstTileReady=Tile.CompletedAt;
|
||
if(Neighborhood.contains(Tile.Key))
|
||
{
|
||
++NeighborhoodFinished;
|
||
NeighborhoodReady=std::max(NeighborhoodReady,Tile.CompletedAt);
|
||
}
|
||
if(Tile.Key.Level<World.FullResClipLevels)
|
||
{
|
||
++FullDetailFinished;
|
||
FullDetailReady=std::max(FullDetailReady,Tile.CompletedAt);
|
||
NativeSolidBoxes+=Tile.CoreDiagnostics.solidBoxes;
|
||
NativeAirBoxes+=Tile.CoreDiagnostics.airBoxes;
|
||
NativeMixedBoxes+=Tile.CoreDiagnostics.mixedBoxes;
|
||
NativeUnknownBoxes+=Tile.CoreDiagnostics.unknownBoxes;
|
||
NativeFieldCalls+=Tile.CoreDiagnostics.fieldCalls;
|
||
NativeColumnHits+=Tile.CoreDiagnostics.columnCacheHits;
|
||
NativeColumnMisses+=Tile.CoreDiagnostics.columnCacheMisses;
|
||
NativeMeshingSkips+=Tile.CoreDiagnostics.meshingSkipped?1u:0u;
|
||
NativeClassificationMilliseconds+=Tile.CoreDiagnostics.classificationMilliseconds;
|
||
NativeColumnComputeMilliseconds+=Tile.CoreDiagnostics.columnComputeMilliseconds;
|
||
NativeMeshingMilliseconds+=Tile.CoreDiagnostics.meshingMilliseconds;
|
||
}
|
||
MaterialQueries+=Tile.CoreDiagnostics.materialQueries;
|
||
MaterialCandidateChecks+=Tile.CoreDiagnostics.materialCandidateChecks;
|
||
MaterialOutsideTiles+=Tile.CoreDiagnostics.materialTileOutsideEveryVein?1u:0u;
|
||
MaterialQueryMilliseconds+=Tile.CoreDiagnostics.materialQueryMilliseconds;
|
||
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(Tile.bHasMaterialHash)MaterialRows.push_back({Tile.Key,Tile.MaterialHash});
|
||
}
|
||
}
|
||
if(!bFirstTileReported&&FirstTileReady.time_since_epoch().count()!=0)
|
||
{
|
||
std::printf("[PipelineMilestone] first_tile_ready=%.3fs\n",
|
||
std::chrono::duration<double>(FirstTileReady-ColdStart).count());
|
||
bFirstTileReported=true;
|
||
}
|
||
if(!bNeighborhoodReported&&NeighborhoodFinished==static_cast<int32>(Neighborhood.size()))
|
||
{
|
||
std::printf("[PipelineMilestone] neighborhood_ready=%.3fs generated=%d/27\n",
|
||
std::chrono::duration<double>(NeighborhoodReady-ColdStart).count(),NeighborhoodFinished);
|
||
bNeighborhoodReported=true;
|
||
}
|
||
if(!bFullDetailReported&&FullDetailFinished==FullDetailExpected)
|
||
{
|
||
std::printf("[PipelineMilestone] full_detail_ready=%.3fs generated=%d/%d\n",
|
||
std::chrono::duration<double>(FullDetailReady-ColdStart).count(),
|
||
FullDetailFinished,FullDetailExpected);
|
||
bFullDetailReported=true;
|
||
}
|
||
}
|
||
if(bHasCameraStep)
|
||
{
|
||
const auto MovingStart=Clock::now();
|
||
double RegionCompileWallSeconds=0.0;
|
||
double RegionCompileCpuSeconds=0.0;
|
||
std::size_t RegionCompileRequested=0,RegionCompileCovered=0;
|
||
VoxelForgeCore::TileRegionCompileStats RegionCompileStats;
|
||
if(bRegionCrossing)
|
||
{
|
||
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> InitialKeys;
|
||
InitialKeys.reserve(Desired.Tiles.size()*2+1);
|
||
for(const FVoxelTileKey& Key:Desired.Tiles)InitialKeys.insert(Key);
|
||
std::vector<VoxelForgeCore::TileKey> EnteringCoreTiles;
|
||
EnteringCoreTiles.reserve(MovedDesired.Tiles.size());
|
||
for(const FVoxelTileKey& Key:MovedDesired.Tiles)
|
||
{
|
||
if(InitialKeys.contains(Key))continue;
|
||
EnteringCoreTiles.push_back({{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level});
|
||
}
|
||
RegionCompileRequested=EnteringCoreTiles.size();
|
||
const auto RegionCompileStart=Clock::now();
|
||
const std::clock_t RegionCompileCpuStart=std::clock();
|
||
std::string RegionCompileError;
|
||
if(!Shape.compileTileRegion(EnteringCoreTiles,
|
||
static_cast<uint32>(std::clamp(WorkerCount,1,16)),Scheduler,
|
||
RegionCompileError,&RegionCompileCovered,&RegionCompileStats,true))
|
||
{
|
||
Streamer.Stop();
|
||
std::fprintf(stderr,"[MovingRegionCompile] failed: %s\n",RegionCompileError.c_str());
|
||
return 2;
|
||
}
|
||
RegionCompileWallSeconds=std::chrono::duration<double>(
|
||
Clock::now()-RegionCompileStart).count();
|
||
RegionCompileCpuSeconds=static_cast<double>(std::clock()-RegionCompileCpuStart)
|
||
/CLOCKS_PER_SEC;
|
||
const std::size_t ParallelWorkers=RegionCompileStats.regionsBuilt>1
|
||
?std::min<std::size_t>(8,RegionCompileStats.regionsBuilt):0;
|
||
std::printf("[MovingRegionCompile] distance_m=%.3f entering_tiles=%zu covered_tiles=%zu regions=%zu lattice_nodes=%llu wall_s=%.6f cpu_s=%.6f worker_threads=%zu mode=analytic-shape-pages\n",
|
||
std::sqrt(static_cast<double>(CameraStepVoxels.X)*CameraStepVoxels.X
|
||
+static_cast<double>(CameraStepVoxels.Y)*CameraStepVoxels.Y
|
||
+static_cast<double>(CameraStepVoxels.Z)*CameraStepVoxels.Z)
|
||
*static_cast<double>(World.CoreVoxelSizeMicrons)/1000000.0,
|
||
RegionCompileRequested,RegionCompileCovered,RegionCompileStats.regionsBuilt,
|
||
static_cast<unsigned long long>(RegionCompileStats.latticeNodes),
|
||
RegionCompileWallSeconds,RegionCompileCpuSeconds,ParallelWorkers);
|
||
}
|
||
const auto MovingTileStart=Clock::now();
|
||
const auto MovingSubmitStart=Clock::now();
|
||
const auto MovingUpdate=Streamer.UpdateDesiredWithSet(MovedDesired,Desired.Tiles,1);
|
||
std::array<std::uint64_t,9> MovingTilesByLOD{};
|
||
std::array<double,9> MovingWorkerSecondsByLOD{};
|
||
std::array<std::uint64_t,9> MovingSamplesByLOD{},MovingNativeFloatCallsByLOD{},
|
||
MovingNativeLatticeCallsByLOD{},MovingNativeOnlyTilesByLOD{},
|
||
MovingAnalyticPageTilesByLOD{},MovingNativeSurfaceTilesByLOD{};
|
||
std::array<std::uint64_t,9> MovingBaseRouteTilesByLOD{};
|
||
std::array<double,9> MovingBaseRouteWorkerSecondsByLOD{},
|
||
MovingPageRouteWorkerSecondsByLOD{};
|
||
std::array<double,9> MovingColumnComputeMsByLOD{},MovingClassificationMsByLOD{},
|
||
MovingMcExclusiveMsByLOD{},MovingMcDensityGridMsByLOD{},
|
||
MovingMcCellClassMsByLOD{},MovingMcGradientMsByLOD{},
|
||
MovingMcInterpolationMsByLOD{},MovingMcDedupMsByLOD{},MovingMcOtherMsByLOD{};
|
||
std::vector<FHashedTile> MovingDensityRows,MovingMeshRows,
|
||
MovingPayloadRows,MovingTopologyRows;
|
||
std::vector<double> MovingEnteringLatencyMs;
|
||
MovingEnteringLatencyMs.reserve(static_cast<std::size_t>(std::max(0,MovingUpdate.NewJobs)));
|
||
int32 MovingFinished=0;
|
||
while(MovingFinished<MovingUpdate.NewJobs)
|
||
{
|
||
std::vector<FStandaloneCpuTile> Ready=Streamer.TakeReady();
|
||
if(Ready.empty())
|
||
{std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
|
||
for(const FStandaloneCpuTile& Tile:Ready)
|
||
{
|
||
++MovingFinished;
|
||
MovingEnteringLatencyMs.push_back(std::chrono::duration<double,std::milli>(
|
||
Clock::now()-MovingSubmitStart).count());
|
||
if(Tile.Key.Level>=0&&Tile.Key.Level<static_cast<int32>(MovingTilesByLOD.size()))
|
||
{
|
||
const std::size_t LOD=static_cast<std::size_t>(Tile.Key.Level);
|
||
++MovingTilesByLOD[LOD];
|
||
MovingWorkerSecondsByLOD[LOD]+=Tile.WorkerSeconds;
|
||
if(bReportLodWorkProfile)
|
||
{
|
||
const auto& D=Tile.CoreDiagnostics;
|
||
MovingSamplesByLOD[LOD]+=Tile.DensitySamples;
|
||
MovingNativeFloatCallsByLOD[LOD]+=D.floatFieldCalls;
|
||
MovingNativeLatticeCallsByLOD[LOD]+=D.latticeFieldCalls;
|
||
MovingNativeOnlyTilesByLOD[LOD]+=D.nativeOnlyRoute?1u:0u;
|
||
MovingAnalyticPageTilesByLOD[LOD]+=D.analyticRegionPageRoute?1u:0u;
|
||
MovingNativeSurfaceTilesByLOD[LOD]+=D.nativeSurfaceTile?1u:0u;
|
||
if(D.analyticRegionPageRoute)
|
||
MovingPageRouteWorkerSecondsByLOD[LOD]+=Tile.WorkerSeconds;
|
||
else
|
||
{
|
||
++MovingBaseRouteTilesByLOD[LOD];
|
||
MovingBaseRouteWorkerSecondsByLOD[LOD]+=Tile.WorkerSeconds;
|
||
}
|
||
MovingColumnComputeMsByLOD[LOD]+=D.columnEvaluationExclusiveMilliseconds;
|
||
MovingClassificationMsByLOD[LOD]+=D.classificationExclusiveMilliseconds;
|
||
MovingMcExclusiveMsByLOD[LOD]+=D.marchingCubesExclusiveMilliseconds;
|
||
MovingMcDensityGridMsByLOD[LOD]+=D.marchingCubesDensityGridMilliseconds;
|
||
MovingMcCellClassMsByLOD[LOD]+=D.marchingCubesCellClassificationMilliseconds;
|
||
MovingMcGradientMsByLOD[LOD]+=D.marchingCubesGradientNormalsMilliseconds;
|
||
MovingMcInterpolationMsByLOD[LOD]+=D.marchingCubesEdgeInterpolationMilliseconds;
|
||
MovingMcDedupMsByLOD[LOD]+=D.marchingCubesVertexDedupStreamMilliseconds;
|
||
MovingMcOtherMsByLOD[LOD]+=D.marchingCubesOtherMilliseconds;
|
||
}
|
||
}
|
||
if(bHashDensity)
|
||
{
|
||
if(Tile.bHasDensityHash)MovingDensityRows.push_back({Tile.Key,Tile.DensityHash});
|
||
if(Tile.bHasMeshHash)MovingMeshRows.push_back({Tile.Key,Tile.MeshHash});
|
||
if(Tile.bHasPayloadHash)MovingPayloadRows.push_back({Tile.Key,Tile.PayloadHash});
|
||
if(Tile.bHasTopologyHash)MovingTopologyRows.push_back({Tile.Key,Tile.TopologyHash});
|
||
}
|
||
}
|
||
}
|
||
const double MovingTileWallSeconds=std::chrono::duration<double>(
|
||
Clock::now()-MovingTileStart).count();
|
||
const double MovingWallSeconds=std::chrono::duration<double>(Clock::now()-MovingStart).count();
|
||
const double StepDistanceMeters=std::sqrt(
|
||
static_cast<double>(CameraStepVoxels.X)*CameraStepVoxels.X
|
||
+static_cast<double>(CameraStepVoxels.Y)*CameraStepVoxels.Y
|
||
+static_cast<double>(CameraStepVoxels.Z)*CameraStepVoxels.Z)
|
||
*static_cast<double>(World.CoreVoxelSizeMicrons)/1000000.0;
|
||
std::printf("[MovingCameraStep] step_voxels=(%.3f,%.3f,%.3f) distance_m=%.3f desired_tiles=%zu new_tiles=%d completed=%d wall_s=%.6f shared_column_cache=%s\n",
|
||
CameraStepVoxels.X,CameraStepVoxels.Y,CameraStepVoxels.Z,StepDistanceMeters,
|
||
MovedDesired.Tiles.size(),MovingUpdate.NewJobs,MovingFinished,MovingWallSeconds,
|
||
bUseSharedColumnCache?"on":"off");
|
||
std::printf("[MovingTileWork] tiles=%d wall_s=%.6f worker_sum_s=%.6f region_compile_wall_s=%.6f crossing_total_s=%.6f\n",
|
||
MovingFinished,MovingTileWallSeconds,
|
||
std::accumulate(MovingWorkerSecondsByLOD.begin(),MovingWorkerSecondsByLOD.end(),0.0),
|
||
RegionCompileWallSeconds,MovingWallSeconds);
|
||
std::sort(MovingEnteringLatencyMs.begin(),MovingEnteringLatencyMs.end());
|
||
const auto MovingLatencyPercentile=[&](double Percentile)
|
||
{
|
||
if(MovingEnteringLatencyMs.empty())return 0.0;
|
||
const std::size_t Rank=static_cast<std::size_t>(std::ceil(
|
||
Percentile*static_cast<double>(MovingEnteringLatencyMs.size())));
|
||
return MovingEnteringLatencyMs[std::min(
|
||
std::max<std::size_t>(1,Rank),MovingEnteringLatencyMs.size())-1];
|
||
};
|
||
std::printf("[MovingTileLatency] entering_tiles=%zu p50_ms=%.3f p95_ms=%.3f metric=submit_to_completion_observed\n",
|
||
MovingEnteringLatencyMs.size(),MovingLatencyPercentile(0.50),
|
||
MovingLatencyPercentile(0.95));
|
||
if(bHashDensity)
|
||
std::printf("[MovingTileHash] tiles=%d rows=(%zu,%zu,%zu,%zu) density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx status=%s\n",
|
||
MovingFinished,MovingDensityRows.size(),MovingMeshRows.size(),
|
||
MovingPayloadRows.size(),MovingTopologyRows.size(),
|
||
static_cast<unsigned long long>(HashTileRows(MovingDensityRows)),
|
||
static_cast<unsigned long long>(HashTileRows(MovingMeshRows)),
|
||
static_cast<unsigned long long>(HashTileRows(MovingPayloadRows)),
|
||
static_cast<unsigned long long>(HashTileRows(MovingTopologyRows)),
|
||
MovingDensityRows.size()==static_cast<std::size_t>(MovingFinished)
|
||
&&MovingMeshRows.size()==static_cast<std::size_t>(MovingFinished)
|
||
&&MovingPayloadRows.size()==static_cast<std::size_t>(MovingFinished)
|
||
&&MovingTopologyRows.size()==static_cast<std::size_t>(MovingFinished)
|
||
?"PASS":"FAIL");
|
||
for(std::size_t LOD=0;LOD<MovingTilesByLOD.size();++LOD)
|
||
if(MovingTilesByLOD[LOD]!=0)
|
||
{
|
||
std::printf("[MovingCameraStepLOD] lod=%zu tiles=%llu worker_sum_s=%.6f\n",
|
||
LOD,static_cast<unsigned long long>(MovingTilesByLOD[LOD]),
|
||
MovingWorkerSecondsByLOD[LOD]);
|
||
if(bReportLodWorkProfile)
|
||
std::printf("[MovingCameraStepLODProfile] lod=%zu density_samples=%llu samples_per_tile=%.1f native_float_calls=%llu native_lattice_calls=%llu native_surface_tiles=%llu native_only_tiles=%llu analytic_page_tiles=%llu base_route_tiles=%llu base_route_avg_ms=%.3f page_route_avg_ms=%.3f column_compute_ms=%.3f classification_ms=%.3f mc_exclusive_ms=%.3f mc_density_grid_ms=%.3f mc_cell_class_ms=%.3f mc_gradient_ms=%.3f mc_interpolation_ms=%.3f mc_dedup_ms=%.3f mc_other_ms=%.3f\n",
|
||
LOD,static_cast<unsigned long long>(MovingSamplesByLOD[LOD]),
|
||
static_cast<double>(MovingSamplesByLOD[LOD])/MovingTilesByLOD[LOD],
|
||
static_cast<unsigned long long>(MovingNativeFloatCallsByLOD[LOD]),
|
||
static_cast<unsigned long long>(MovingNativeLatticeCallsByLOD[LOD]),
|
||
static_cast<unsigned long long>(MovingNativeSurfaceTilesByLOD[LOD]),
|
||
static_cast<unsigned long long>(MovingNativeOnlyTilesByLOD[LOD]),
|
||
static_cast<unsigned long long>(MovingAnalyticPageTilesByLOD[LOD]),
|
||
static_cast<unsigned long long>(MovingBaseRouteTilesByLOD[LOD]),
|
||
MovingBaseRouteTilesByLOD[LOD]
|
||
?MovingBaseRouteWorkerSecondsByLOD[LOD]*1000.0
|
||
/MovingBaseRouteTilesByLOD[LOD]:0.0,
|
||
MovingAnalyticPageTilesByLOD[LOD]
|
||
?MovingPageRouteWorkerSecondsByLOD[LOD]*1000.0
|
||
/MovingAnalyticPageTilesByLOD[LOD]:0.0,
|
||
MovingColumnComputeMsByLOD[LOD],MovingClassificationMsByLOD[LOD],
|
||
MovingMcExclusiveMsByLOD[LOD],MovingMcDensityGridMsByLOD[LOD],
|
||
MovingMcCellClassMsByLOD[LOD],MovingMcGradientMsByLOD[LOD],
|
||
MovingMcInterpolationMsByLOD[LOD],MovingMcDedupMsByLOD[LOD],
|
||
MovingMcOtherMsByLOD[LOD]);
|
||
}
|
||
}
|
||
Streamer.Stop();
|
||
Streamer.DumpColdTimelineTrace();
|
||
bool bScreenSpaceGatePassing=true;
|
||
if(bScreenSpaceGate)
|
||
{
|
||
if(Assembly)
|
||
bScreenSpaceGatePassing=StandaloneScreenSpaceGate::Run(GateSampleTiles,
|
||
Streamer,*Assembly,ClosestDisplayDistances,World.CoreVoxelSizeMicrons,
|
||
World.MaxClipLevel,PixelBudgetsSubvoxels,CameraYawDegrees,
|
||
CameraPitchDegrees);
|
||
else
|
||
{std::fprintf(stderr,"[ScreenSpaceGateResult] no-world-assembly status=REJECT\n");bScreenSpaceGatePassing=false;}
|
||
}
|
||
if(bExportRequested)
|
||
{
|
||
bSceneExportPassing=SceneExport.Close(static_cast<std::uint32_t>(Update.DesiredTiles));
|
||
std::error_code FileSizeError;
|
||
const std::uintmax_t ExportBytes=std::filesystem::file_size(MeshExportPath,FileSizeError);
|
||
std::printf("[MeshSceneExport] tiles=%u expected=%d bytes=%llu status=%s path=%s\n",
|
||
SceneExport.TileCount,Update.DesiredTiles,
|
||
static_cast<unsigned long long>(FileSizeError?0:ExportBytes),
|
||
bSceneExportPassing?"PASS":"FAIL",MeshExportPath.string().c_str());
|
||
bSceneExportPassing=bSceneExportPassing&&!FileSizeError;
|
||
}
|
||
if(bProfileColumnOverlap)
|
||
{
|
||
struct FColumnPriorUse
|
||
{
|
||
FVoxelTileKey Tile;
|
||
std::uint64_t WorkerHash=0;
|
||
};
|
||
for(std::size_t LOD=0;LOD<ColumnEventsByLOD.size();++LOD)
|
||
{
|
||
auto& Events=ColumnEventsByLOD[LOD];
|
||
std::sort(Events.begin(),Events.end(),[](const FColumnEvaluationEvent& A,
|
||
const FColumnEvaluationEvent& B){return A.Sequence<B.Sequence;});
|
||
std::unordered_map<FColumnXY,std::vector<FColumnPriorUse>,FColumnXYHash> UsesByXY;
|
||
UsesByXY.reserve(Events.size());
|
||
for(const FColumnEvaluationEvent& Event:Events)
|
||
{
|
||
++ColumnEvaluationEventsByLOD[LOD];
|
||
if(Event.bOutsideTileFootprint)++HaloNormalEvaluationsByLOD[LOD];
|
||
auto [Found,Inserted]=UsesByXY.try_emplace(Event.XY);
|
||
auto& PriorUses=Found->second;
|
||
if(Inserted)
|
||
{
|
||
++UniqueColumnCoordinatesByLOD[LOD];
|
||
PriorUses.push_back({Event.Tile,Event.WorkerHash});
|
||
continue;
|
||
}
|
||
++ColumnDuplicateEventsByLOD[LOD];
|
||
if(Event.bOutsideTileFootprint)
|
||
++HaloNormalRepeatsByLOD[LOD];
|
||
else
|
||
{
|
||
const bool bSameWorkerSeen=std::any_of(PriorUses.begin(),PriorUses.end(),
|
||
[&](const FColumnPriorUse& Prior)
|
||
{return Prior.WorkerHash==Event.WorkerHash;});
|
||
if(bSameWorkerSeen)++SameWorkerEvictedRepeatsByLOD[LOD];
|
||
else ++DifferentWorkerRepeatsByLOD[LOD];
|
||
}
|
||
bool bHasNeighbourDuplicate=false,bHasCrossWorkerNeighbourDuplicate=false;
|
||
for(const FColumnPriorUse& Prior:PriorUses)
|
||
{
|
||
if(!AreSameLODNeighbours(Prior.Tile,Event.Tile))continue;
|
||
bHasNeighbourDuplicate=true;
|
||
bHasCrossWorkerNeighbourDuplicate=bHasCrossWorkerNeighbourDuplicate
|
||
||Prior.WorkerHash!=Event.WorkerHash;
|
||
}
|
||
if(bHasNeighbourDuplicate)++NeighbourDuplicateEventsByLOD[LOD];
|
||
if(bHasCrossWorkerNeighbourDuplicate)
|
||
++CrossWorkerNeighbourDuplicateEventsByLOD[LOD];
|
||
PriorUses.push_back({Event.Tile,Event.WorkerHash});
|
||
}
|
||
}
|
||
}
|
||
bool bColumnOverlapCapturePassing=true;
|
||
if(bProfileColumnOverlap)
|
||
{
|
||
for(std::size_t LOD=0;LOD<ColumnCaptureTilesByLOD.size();++LOD)
|
||
{
|
||
const bool bLODComplete=ColumnCaptureCompleteByLOD[LOD]
|
||
&&ColumnCaptureTilesByLOD[LOD]==ExpectedTilesByLOD[LOD];
|
||
bColumnOverlapCapturePassing=bColumnOverlapCapturePassing&&bLODComplete;
|
||
std::printf("[WorldAssemblerLODColumnOverlap] lod=%zu tiles=%llu expected_tiles=%llu total_column_evaluations=%llu unique_xy_columns=%llu repeated_column_evaluations=%llu same_worker_evicted_repeats=%llu different_worker_repeats=%llu halo_normal_repeats=%llu halo_normal_evaluations=%llu neighbouring_duplicate_evaluations=%llu cross_worker_neighbouring_duplicates=%llu capture_complete=%d\n",
|
||
LOD,static_cast<unsigned long long>(ColumnCaptureTilesByLOD[LOD]),
|
||
static_cast<unsigned long long>(ExpectedTilesByLOD[LOD]),
|
||
static_cast<unsigned long long>(ColumnEvaluationEventsByLOD[LOD]),
|
||
static_cast<unsigned long long>(UniqueColumnCoordinatesByLOD[LOD]),
|
||
static_cast<unsigned long long>(ColumnDuplicateEventsByLOD[LOD]),
|
||
static_cast<unsigned long long>(SameWorkerEvictedRepeatsByLOD[LOD]),
|
||
static_cast<unsigned long long>(DifferentWorkerRepeatsByLOD[LOD]),
|
||
static_cast<unsigned long long>(HaloNormalRepeatsByLOD[LOD]),
|
||
static_cast<unsigned long long>(HaloNormalEvaluationsByLOD[LOD]),
|
||
static_cast<unsigned long long>(NeighbourDuplicateEventsByLOD[LOD]),
|
||
static_cast<unsigned long long>(CrossWorkerNeighbourDuplicateEventsByLOD[LOD]),
|
||
bLODComplete?1:0);
|
||
}
|
||
}
|
||
for(std::size_t LOD=0;LOD<TileCountByLOD.size();++LOD)
|
||
{
|
||
std::printf("[WorldAssemblerLODWorker] lod=%zu tiles=%llu worker_sum_s=%.6f column_eval_exclusive_ms=%.3f mc_exclusive_ms=%.3f ground_refine_exclusive_ms=%.3f cap_refine_exclusive_ms=%.3f skirt_ms=%.3f skirt_triangles=%llu mesh_assembly_ms=%.3f\n",
|
||
LOD,static_cast<unsigned long long>(TileCountByLOD[LOD]),WorkerSecondsByLOD[LOD],
|
||
ColumnEvalMsByLOD[LOD],MarchingExclusiveMsByLOD[LOD],
|
||
GroundRefineExclusiveMsByLOD[LOD],CapRefineExclusiveMsByLOD[LOD],
|
||
SkirtMsByLOD[LOD],static_cast<unsigned long long>(SkirtTrianglesByLOD[LOD]),
|
||
MeshAssemblyMsByLOD[LOD]);
|
||
if(bReportLodWorkProfile)
|
||
{
|
||
const auto& Detail=*FarLODProfile;
|
||
const double SamplesPerTile=TileCountByLOD[LOD]
|
||
?static_cast<double>(DensitySamplesByLOD[LOD])/TileCountByLOD[LOD]:0.0;
|
||
std::printf("[WorldAssemblerLODSamples] lod=%zu samples=%llu samples_per_tile=%.1f\n",
|
||
LOD,static_cast<unsigned long long>(DensitySamplesByLOD[LOD]),SamplesPerTile);
|
||
std::printf("[WorldAssemblerLODRouteProfile] lod=%zu base_shape_tiles=%llu base_shape_avg_ms=%.3f analytic_page_tiles=%llu analytic_page_avg_ms=%.3f\n",
|
||
LOD,static_cast<unsigned long long>(BaseRouteTilesByLOD[LOD]),
|
||
BaseRouteTilesByLOD[LOD]
|
||
?BaseRouteWorkerSecondsByLOD[LOD]*1000.0/BaseRouteTilesByLOD[LOD]:0.0,
|
||
static_cast<unsigned long long>(PageRouteTilesByLOD[LOD]),
|
||
PageRouteTilesByLOD[LOD]
|
||
?PageRouteWorkerSecondsByLOD[LOD]*1000.0/PageRouteTilesByLOD[LOD]:0.0);
|
||
const double TileDivisor=TileCountByLOD[LOD]
|
||
?static_cast<double>(TileCountByLOD[LOD]):1.0;
|
||
std::printf("[WorldAssemblerLODCostDetail] lod=%zu tiles=%llu density_samples=%llu column_point_calls=%llu column_local_hits=%llu column_local_misses=%llu column_conflict_misses=%llu column_shared_hits=%llu column_formula_evaluations=%llu column_compute_ms=%.3f mc_column_ms=%.3f mc_exclusive_ms=%.3f mc_grid_ms=%.3f mc_cell_class_ms=%.3f mc_gradient_ms=%.3f mc_interpolation_ms=%.3f mc_dedup_stream_ms=%.3f mc_other_ms=%.3f ground_column_ms=%.3f ground_exclusive_ms=%.3f cap_column_ms=%.3f cap_exclusive_ms=%.3f stratum_shell_ms=%.3f stratum_field_calls=%llu stratum_column_calls=%llu stratum_column_hits=%llu stratum_column_misses=%llu stratum_shared_hits=%llu stratum_column_evaluations=%llu stratum_column_compute_ms=%.3f connector_shell_ms=%.3f connector_field_calls=%llu connector_column_calls=%llu connector_column_hits=%llu connector_column_misses=%llu connector_shared_hits=%llu connector_column_evaluations=%llu connector_column_compute_ms=%.3f far_shell_triangles=%llu skirt_ms=%.3f mesh_assembly_ms=%.3f worker_ms_per_tile=%.3f column_formula_evals_per_tile=%.1f connector_calls_per_tile=%.1f\n",
|
||
LOD,static_cast<unsigned long long>(TileCountByLOD[LOD]),
|
||
static_cast<unsigned long long>(DensitySamplesByLOD[LOD]),
|
||
static_cast<unsigned long long>(Detail.ColumnCalls[LOD]),
|
||
static_cast<unsigned long long>(Detail.ColumnHits[LOD]),
|
||
static_cast<unsigned long long>(Detail.ColumnMisses[LOD]),
|
||
static_cast<unsigned long long>(Detail.ColumnConflictMisses[LOD]),
|
||
static_cast<unsigned long long>(Detail.SharedColumnHits[LOD]),
|
||
static_cast<unsigned long long>(Detail.ColumnEvaluations[LOD]),
|
||
ColumnEvalMsByLOD[LOD],Detail.McColumnMs[LOD],MarchingExclusiveMsByLOD[LOD],
|
||
Detail.McGridMs[LOD],Detail.McCellClassMs[LOD],Detail.McGradientMs[LOD],
|
||
Detail.McInterpolationMs[LOD],Detail.McDedupMs[LOD],Detail.McOtherMs[LOD],
|
||
Detail.GroundColumnMs[LOD],GroundRefineExclusiveMsByLOD[LOD],
|
||
Detail.CapColumnMs[LOD],CapRefineExclusiveMsByLOD[LOD],
|
||
Detail.StratumShellMs[LOD],
|
||
static_cast<unsigned long long>(Detail.StratumFieldCalls[LOD]),
|
||
static_cast<unsigned long long>(Detail.StratumColumnPointCalls[LOD]),
|
||
static_cast<unsigned long long>(Detail.StratumColumnHits[LOD]),
|
||
static_cast<unsigned long long>(Detail.StratumColumnMisses[LOD]),
|
||
static_cast<unsigned long long>(Detail.StratumSharedColumnHits[LOD]),
|
||
static_cast<unsigned long long>(Detail.StratumColumnEvaluations[LOD]),
|
||
Detail.StratumColumnComputeMs[LOD],
|
||
Detail.ConnectorShellMs[LOD],
|
||
static_cast<unsigned long long>(Detail.ConnectorFieldCalls[LOD]),
|
||
static_cast<unsigned long long>(Detail.ConnectorColumnPointCalls[LOD]),
|
||
static_cast<unsigned long long>(Detail.ConnectorColumnHits[LOD]),
|
||
static_cast<unsigned long long>(Detail.ConnectorColumnMisses[LOD]),
|
||
static_cast<unsigned long long>(Detail.ConnectorSharedColumnHits[LOD]),
|
||
static_cast<unsigned long long>(Detail.ConnectorColumnEvaluations[LOD]),
|
||
Detail.ConnectorColumnComputeMs[LOD],
|
||
static_cast<unsigned long long>(Detail.FarShellTriangles[LOD]),
|
||
SkirtMsByLOD[LOD],MeshAssemblyMsByLOD[LOD],
|
||
WorkerSecondsByLOD[LOD]*1000.0/TileDivisor,
|
||
static_cast<double>(Detail.ColumnEvaluations[LOD])/TileDivisor,
|
||
static_cast<double>(Detail.ConnectorFieldCalls[LOD])/TileDivisor);
|
||
}
|
||
}
|
||
const double NeighborhoodSeconds=std::chrono::duration<double>(NeighborhoodReady-ColdStart).count();
|
||
const double NeighborhoodMeshSeconds=std::max(0.0,
|
||
std::chrono::duration<double>(NeighborhoodReady-ShapeReady).count());
|
||
const double FullDetailSeconds=std::chrono::duration<double>(FullDetailReady-ColdStart).count();
|
||
const double WholeViewSeconds=std::chrono::duration<double>(WholeViewReady-ColdStart).count();
|
||
const double StreamSeconds=std::chrono::duration<double>(WholeViewReady-StreamStart).count();
|
||
const double FirstTileSeconds=std::chrono::duration<double>(FirstTileReady-ColdStart).count();
|
||
std::printf("[StreamPipeline] workers=%d shape=%.3fs first_tile_ready=%.3fs neighborhood_mesh=%.3fs neighborhood_ready=%.3fs full_detail_ready=%.3fs whole_view_ready=%.3fs neighborhood_tiles=%d/27 full_detail_tiles=%d/%d whole_tiles=%d/%zu stream_wall=%.3fs cache_hits=0 full_payload=1\n",
|
||
WorkerCount,ShapeSeconds,FirstTileSeconds,NeighborhoodMeshSeconds,NeighborhoodSeconds,
|
||
FullDetailSeconds,WholeViewSeconds,NeighborhoodFinished,
|
||
FullDetailFinished,FullDetailExpected,Finished,Desired.Tiles.size(),StreamSeconds);
|
||
if(bReportMeshCoverage)
|
||
std::printf("[PipelineMeshCoverage] tiles=%d nonempty_mesh_tiles=%llu indexed_triangles=%llu\n",
|
||
Finished,static_cast<unsigned long long>(WholeNonEmptyMeshTiles),
|
||
static_cast<unsigned long long>(WholeTriangleCount));
|
||
std::printf("[WorldAssemblerTilePipeline] full_detail_tiles=%d solid_boxes=%llu air_boxes=%llu mixed_boxes=%llu unknown_boxes=%llu whole_tile_skips=%llu field_calls=%llu column_cache_hits=%llu column_cache_misses=%llu column_compute_worker_ms=%.3f classification_worker_ms=%.3f meshing_worker_ms=%.3f\n",
|
||
FullDetailExpected,static_cast<unsigned long long>(NativeSolidBoxes),
|
||
static_cast<unsigned long long>(NativeAirBoxes),
|
||
static_cast<unsigned long long>(NativeMixedBoxes),
|
||
static_cast<unsigned long long>(NativeUnknownBoxes),
|
||
static_cast<unsigned long long>(NativeMeshingSkips),
|
||
static_cast<unsigned long long>(NativeFieldCalls),
|
||
static_cast<unsigned long long>(NativeColumnHits),
|
||
static_cast<unsigned long long>(NativeColumnMisses),
|
||
NativeColumnComputeMilliseconds,NativeClassificationMilliseconds,
|
||
NativeMeshingMilliseconds);
|
||
std::printf("[MaterialQueryCost] queries=%llu candidate_checks=%llu outside_vein_tiles=%llu tiles=%d elapsed_ms=%.3f outside_query_ms=0 status=%s\n",
|
||
static_cast<unsigned long long>(MaterialQueries),
|
||
static_cast<unsigned long long>(MaterialCandidateChecks),
|
||
static_cast<unsigned long long>(MaterialOutsideTiles),Finished,
|
||
MaterialQueryMilliseconds,MaterialQueries==0?"zero":"measured");
|
||
if(bAuditUniformBlockProofs)
|
||
std::printf("[UniformBlockProofAudit] checked_blocks=%llu checked_samples=%llu fallback_blocks=%llu unknown_blocks=%llu violations=%llu\n",
|
||
static_cast<unsigned long long>(BlockProofBlocksChecked),
|
||
static_cast<unsigned long long>(BlockProofSamplesChecked),
|
||
static_cast<unsigned long long>(BlockProofFallbackBlocks),
|
||
static_cast<unsigned long long>(BlockProofUnknownBlocks),
|
||
static_cast<unsigned long long>(BlockProofViolations));
|
||
|
||
bool bHashesPassing=true;
|
||
if(bHashDensity)
|
||
{
|
||
bHashesPassing=DensityRows.size()==Desired.Tiles.size()
|
||
&&MeshRows.size()==Desired.Tiles.size()
|
||
&&PayloadRows.size()==Desired.Tiles.size()
|
||
&&TopologyRows.size()==Desired.Tiles.size()
|
||
&&MaterialRows.size()==Desired.Tiles.size();
|
||
if(bPrintTileHashes)
|
||
{
|
||
const auto makeHashIndex=[](const std::vector<FHashedTile>& rows)
|
||
{
|
||
std::unordered_map<FVoxelTileKey,uint64,FStandaloneTileKeyHash> index;
|
||
index.reserve(rows.size());
|
||
for(const FHashedTile& row:rows)index.emplace(row.Key,row.Hash);
|
||
return index;
|
||
};
|
||
const auto MeshIndex=makeHashIndex(MeshRows);
|
||
const auto PayloadIndex=makeHashIndex(PayloadRows);
|
||
const auto TopologyIndex=makeHashIndex(TopologyRows);
|
||
const auto MaterialIndex=makeHashIndex(MaterialRows);
|
||
const auto findHash=[](const auto& index,const FVoxelTileKey& key)
|
||
{
|
||
const auto found=index.find(key);
|
||
return found==index.end()?uint64{0}:found->second;
|
||
};
|
||
for(const FHashedTile& row:DensityRows)
|
||
std::printf("[PipelineTileHash] lod=%d x=%d y=%d z=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
|
||
row.Key.Level,row.Key.Coord.X,row.Key.Coord.Y,row.Key.Coord.Z,
|
||
static_cast<unsigned long long>(row.Hash),
|
||
static_cast<unsigned long long>(findHash(MeshIndex,row.Key)),
|
||
static_cast<unsigned long long>(findHash(PayloadIndex,row.Key)),
|
||
static_cast<unsigned long long>(findHash(TopologyIndex,row.Key)));
|
||
for(const FHashedTile& row:MaterialRows)
|
||
std::printf("[PipelineTileMaterialHash] lod=%d x=%d y=%d z=%d material=0x%016llx\n",
|
||
row.Key.Level,row.Key.Coord.X,row.Key.Coord.Y,row.Key.Coord.Z,
|
||
static_cast<unsigned long long>(row.Hash));
|
||
}
|
||
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)));
|
||
std::printf("[PipelineMaterialHash] tiles=%zu material=0x%016llx\n",
|
||
MaterialRows.size(),static_cast<unsigned long long>(HashTileRows(MaterialRows)));
|
||
for(std::size_t LOD=0;LOD<TileCountByLOD.size();++LOD)
|
||
{
|
||
const auto FilterLOD=[LOD](const std::vector<FHashedTile>& Rows)
|
||
{
|
||
std::vector<FHashedTile> Selected;
|
||
for(const FHashedTile& Row:Rows)
|
||
if(Row.Key.Level==static_cast<int32>(LOD))Selected.push_back(Row);
|
||
return Selected;
|
||
};
|
||
auto LodDensity=FilterLOD(DensityRows),LodMesh=FilterLOD(MeshRows);
|
||
auto LodPayload=FilterLOD(PayloadRows),LodTopology=FilterLOD(TopologyRows);
|
||
std::printf("[PipelineLODHash] lod=%zu tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
|
||
LOD,static_cast<unsigned long long>(TileCountByLOD[LOD]),
|
||
static_cast<unsigned long long>(HashTileRows(LodDensity)),
|
||
static_cast<unsigned long long>(HashTileRows(LodMesh)),
|
||
static_cast<unsigned long long>(HashTileRows(LodPayload)),
|
||
static_cast<unsigned long long>(HashTileRows(LodTopology)));
|
||
}
|
||
const std::array<VoxelForgeCore::Vec3d,9> QueryPoints={{
|
||
{CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z},
|
||
{CameraPositionVoxels.X+8.0,CameraPositionVoxels.Y,CameraPositionVoxels.Z},
|
||
{CameraPositionVoxels.X-8.0,CameraPositionVoxels.Y,CameraPositionVoxels.Z},
|
||
{CameraPositionVoxels.X,CameraPositionVoxels.Y+8.0,CameraPositionVoxels.Z},
|
||
{CameraPositionVoxels.X,CameraPositionVoxels.Y-8.0,CameraPositionVoxels.Z},
|
||
{CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z+8.0},
|
||
{CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z-8.0},
|
||
{CameraPositionVoxels.X+64.0,CameraPositionVoxels.Y-32.0,CameraPositionVoxels.Z+16.0},
|
||
{CameraPositionVoxels.X-128.0,CameraPositionVoxels.Y+96.0,CameraPositionVoxels.Z-64.0}}};
|
||
uint64 ExactDensityHash=14695981039346656037ull;
|
||
uint64 ExactUnclampedHash=14695981039346656037ull;
|
||
uint64 ExactDetailHash=14695981039346656037ull;
|
||
uint64 ExactCollisionHash=14695981039346656037ull;
|
||
for(const auto& Point:QueryPoints)
|
||
{
|
||
const float Density=Shape.densityAt(Point);
|
||
const float Unclamped=Shape.unclampedDensityAt(Point);
|
||
const auto Detail=Shape.detailSampleAt(Point);
|
||
HashWord(ExactDensityHash,std::bit_cast<uint32>(Density),4);
|
||
HashWord(ExactUnclampedHash,std::bit_cast<uint32>(Unclamped),4);
|
||
HashWord(ExactDetailHash,std::bit_cast<uint32>(Detail.displacement),4);
|
||
HashWord(ExactDetailHash,std::bit_cast<uint32>(Detail.caveInfluence),4);
|
||
HashWord(ExactDetailHash,std::bit_cast<uint32>(Detail.floorInfluence),4);
|
||
HashWord(ExactCollisionHash,Shape.isSolidAt(Point)?1u:0u,1);
|
||
}
|
||
std::printf("[PipelineExactQueryHash] probes=%zu density=0x%016llx unclamped=0x%016llx detail=0x%016llx collision=0x%016llx path=direct-Core-queries\n",
|
||
QueryPoints.size(),static_cast<unsigned long long>(ExactDensityHash),
|
||
static_cast<unsigned long long>(ExactUnclampedHash),
|
||
static_cast<unsigned long long>(ExactDetailHash),
|
||
static_cast<unsigned long long>(ExactCollisionHash));
|
||
}
|
||
return NeighborhoodFinished==static_cast<int32>(Neighborhood.size())
|
||
&&FullDetailFinished==FullDetailExpected&&Finished==Update.DesiredTiles&&bHashesPassing
|
||
&&(!bProfileColumnOverlap||bColumnOverlapCapturePassing)
|
||
&&(!bExportRequested||bSceneExportPassing)&&bScreenSpaceGatePassing
|
||
&&(!bAuditUniformBlockProofs||BlockProofViolations==0)?0:6;
|
||
}
|
||
} // namespace StandaloneStreamBenchmark
|