Linux offscreen capture (EGL/GLES pbuffer; llvmpipe in the sandbox) and an opt-in screen-space gate for LOD>=3 (normal distance projected at each LOD's closest display distance, 1 px at 1440p = 0.61 mrad). The gate rejects the S13 octave-drop and stride-2 controls, fault injections, and a vertical-vs-screen control. Finding: today's far meshes differ from the field by up to 92 px, and LOD swaps snap mesa rims by tens of px (p50 0 px), a silhouette effect of plain LOD switching. The octave cutoff was dropped: nothing qualified, and octaves are a small share of far time. Off-path cost: reviewer interleaved W12 within noise vs L4 (+0.6% median). Hashes = L4. Largest far lever measured: column work outside noise, 34% of far worker time (duplicate XY across tiles). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ED33VnqepD4YY6gDAAA832
722 lines
37 KiB
C++
722 lines
37 KiB
C++
#pragma once
|
|
|
|
#include "StreamingWorld.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <span>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace StandaloneScreenSpaceGate
|
|
{
|
|
inline constexpr double OnePixelRadians=1.57079632679489661923/2560.0;
|
|
|
|
struct FErrorRow
|
|
{
|
|
int Lod=0;
|
|
std::uint64_t Samples=0,Missing=0,Floating=0;
|
|
double P50=0.0,P99=0.0,Maximum=0.0;
|
|
double VerticalP50=0.0,VerticalP99=0.0,VerticalMaximum=0.0;
|
|
double MaximumNormalDistanceMeters=0.0,MaximumX=0.0,MaximumY=0.0;
|
|
double MaximumReferenceZ=0.0,MaximumDisplayedZ=0.0;
|
|
double MaximumSlopeDegrees=0.0;
|
|
std::array<std::uint64_t,4> SlopeSamples{};
|
|
std::array<double,4> SlopeP99{},SlopeMaximum{};
|
|
bool Valid=false;
|
|
};
|
|
|
|
struct FPopRow
|
|
{
|
|
int CoarseLod=0;
|
|
std::uint64_t Samples=0,Cracks=0,Floating=0;
|
|
double P50=0.0,P99=0.0,Maximum=0.0;
|
|
double VerticalP50=0.0,VerticalP99=0.0,VerticalMaximum=0.0;
|
|
double MaximumNormalDistanceMeters=0.0,MaximumX=0.0,MaximumY=0.0;
|
|
double MaximumReferenceZ=0.0,MaximumDisplayedZ=0.0;
|
|
double MaximumSlopeDegrees=0.0;
|
|
std::array<std::uint64_t,4> SlopeSamples{};
|
|
std::array<double,4> SlopeP99{},SlopeMaximum{};
|
|
bool Valid=false;
|
|
};
|
|
|
|
struct FSurfaceRoot
|
|
{
|
|
double ZMeters=0.0;
|
|
double SlopeX=0.0;
|
|
double SlopeY=0.0;
|
|
};
|
|
|
|
struct FViewBasis
|
|
{
|
|
std::array<double,3> Right{};
|
|
std::array<double,3> Up{};
|
|
};
|
|
|
|
struct FErrorAccumulator
|
|
{
|
|
std::vector<double> Pixels,VerticalPixels;
|
|
std::array<std::vector<double>,4> BySlope;
|
|
double Maximum=-1.0,MaximumVertical=0.0,MaximumNormalDistanceMeters=0.0;
|
|
double MaximumX=0.0,MaximumY=0.0,MaximumSlopeDegrees=0.0;
|
|
double MaximumReferenceZ=0.0,MaximumDisplayedZ=0.0;
|
|
std::array<std::uint64_t,4> SlopeSamples{};
|
|
};
|
|
|
|
inline double Percentile(std::vector<double>& Values,double P);
|
|
|
|
inline FViewBasis MakeViewBasis(double YawDegrees,double PitchDegrees)
|
|
{
|
|
constexpr double Pi=3.14159265358979323846;
|
|
const double Yaw=YawDegrees*Pi/180.0,Pitch=PitchDegrees*Pi/180.0;
|
|
const std::array<double,3> Forward={std::sin(Yaw)*std::cos(Pitch),
|
|
std::cos(Yaw)*std::cos(Pitch),std::sin(Pitch)};
|
|
FViewBasis Basis;
|
|
Basis.Right={std::cos(Yaw),-std::sin(Yaw),0.0};
|
|
Basis.Up={Basis.Right[1]*Forward[2]-Basis.Right[2]*Forward[1],
|
|
Basis.Right[2]*Forward[0]-Basis.Right[0]*Forward[2],
|
|
Basis.Right[0]*Forward[1]-Basis.Right[1]*Forward[0]};
|
|
return Basis;
|
|
}
|
|
|
|
inline std::size_t SlopeBin(double Degrees)
|
|
{
|
|
if(Degrees<30.0)return 0;
|
|
if(Degrees<45.0)return 1;
|
|
if(Degrees<60.0)return 2;
|
|
return 3;
|
|
}
|
|
|
|
inline void AddError(FErrorAccumulator& Accumulator,double DeltaZMeters,
|
|
const FSurfaceRoot& Root,double PixelBudgetMeters,const FViewBasis& View,
|
|
double X,double Y)
|
|
{
|
|
constexpr double Pi=3.14159265358979323846;
|
|
const double GradientSquared=Root.SlopeX*Root.SlopeX+Root.SlopeY*Root.SlopeY;
|
|
const double NormalLength=std::sqrt(1.0+GradientSquared);
|
|
const double NormalX=-Root.SlopeX/NormalLength;
|
|
const double NormalY=-Root.SlopeY/NormalLength;
|
|
const double NormalZ=1.0/NormalLength;
|
|
const double NormalDistance=std::abs(DeltaZMeters)/NormalLength;
|
|
const double ProjectedNormal=std::hypot(
|
|
NormalX*View.Right[0]+NormalY*View.Right[1]+NormalZ*View.Right[2],
|
|
NormalX*View.Up[0]+NormalY*View.Up[1]+NormalZ*View.Up[2]);
|
|
const double ProjectedVertical=std::hypot(View.Right[2],View.Up[2]);
|
|
const double NormalPixels=NormalDistance*ProjectedNormal/PixelBudgetMeters;
|
|
const double VerticalPixels=std::abs(DeltaZMeters)*ProjectedVertical/PixelBudgetMeters;
|
|
const double SlopeDegrees=std::atan(std::sqrt(GradientSquared))*180.0/Pi;
|
|
Accumulator.Pixels.push_back(NormalPixels);
|
|
Accumulator.VerticalPixels.push_back(VerticalPixels);
|
|
const std::size_t Bin=SlopeBin(SlopeDegrees);
|
|
Accumulator.BySlope[Bin].push_back(NormalPixels);
|
|
++Accumulator.SlopeSamples[Bin];
|
|
if(NormalPixels>Accumulator.Maximum)
|
|
{
|
|
Accumulator.Maximum=NormalPixels;
|
|
Accumulator.MaximumVertical=VerticalPixels;
|
|
Accumulator.MaximumNormalDistanceMeters=NormalDistance;
|
|
Accumulator.MaximumX=X;Accumulator.MaximumY=Y;
|
|
Accumulator.MaximumSlopeDegrees=SlopeDegrees;
|
|
Accumulator.MaximumReferenceZ=Root.ZMeters;
|
|
Accumulator.MaximumDisplayedZ=Root.ZMeters+DeltaZMeters;
|
|
}
|
|
}
|
|
|
|
inline void AddMissing(FErrorAccumulator& Accumulator)
|
|
{
|
|
Accumulator.Pixels.push_back(2.0);
|
|
Accumulator.VerticalPixels.push_back(2.0);
|
|
}
|
|
|
|
inline void FinalizeError(FErrorRow& Row,FErrorAccumulator& Accumulator)
|
|
{
|
|
Row.Samples=Accumulator.Pixels.size();
|
|
Row.P50=Percentile(Accumulator.Pixels,0.50);
|
|
Row.P99=Percentile(Accumulator.Pixels,0.99);
|
|
Row.Maximum=Accumulator.Pixels.empty()?0.0
|
|
:*std::max_element(Accumulator.Pixels.begin(),Accumulator.Pixels.end());
|
|
Row.VerticalP50=Percentile(Accumulator.VerticalPixels,0.50);
|
|
Row.VerticalP99=Percentile(Accumulator.VerticalPixels,0.99);
|
|
Row.VerticalMaximum=Accumulator.VerticalPixels.empty()?0.0
|
|
:*std::max_element(Accumulator.VerticalPixels.begin(),Accumulator.VerticalPixels.end());
|
|
Row.MaximumNormalDistanceMeters=Accumulator.MaximumNormalDistanceMeters;
|
|
Row.MaximumX=Accumulator.MaximumX;Row.MaximumY=Accumulator.MaximumY;
|
|
Row.MaximumReferenceZ=Accumulator.MaximumReferenceZ;
|
|
Row.MaximumDisplayedZ=Accumulator.MaximumDisplayedZ;
|
|
Row.MaximumSlopeDegrees=Accumulator.MaximumSlopeDegrees;
|
|
Row.SlopeSamples=Accumulator.SlopeSamples;
|
|
for(std::size_t Bin=0;Bin<Accumulator.BySlope.size();++Bin)
|
|
{
|
|
Row.SlopeP99[Bin]=Percentile(Accumulator.BySlope[Bin],0.99);
|
|
Row.SlopeMaximum[Bin]=Accumulator.BySlope[Bin].empty()?0.0
|
|
:*std::max_element(Accumulator.BySlope[Bin].begin(),Accumulator.BySlope[Bin].end());
|
|
}
|
|
}
|
|
|
|
struct FHeightGrid
|
|
{
|
|
int Side=65;
|
|
double MinimumX=0.0,MaximumX=0.0,MinimumY=0.0,MaximumY=0.0;
|
|
std::vector<std::vector<double>> Hits;
|
|
|
|
FHeightGrid()=default;
|
|
FHeightGrid(int InSide,double MinX,double MaxX,double MinY,double MaxY)
|
|
:Side(InSide),MinimumX(MinX),MaximumX(MaxX),MinimumY(MinY),MaximumY(MaxY),
|
|
Hits(static_cast<std::size_t>(InSide)*InSide)
|
|
{}
|
|
|
|
double X(int Ix) const
|
|
{return MinimumX+(MaximumX-MinimumX)*Ix/(Side-1);}
|
|
double Y(int Iy) const
|
|
{return MinimumY+(MaximumY-MinimumY)*Iy/(Side-1);}
|
|
|
|
void Add(int Ix,int Iy,double Z)
|
|
{
|
|
auto& Values=Hits[static_cast<std::size_t>(Iy)*Side+Ix];
|
|
Values.push_back(Z);
|
|
}
|
|
|
|
const std::vector<double>& At(int Ix,int Iy) const
|
|
{return Hits[static_cast<std::size_t>(Iy)*Side+Ix];}
|
|
};
|
|
|
|
inline double Percentile(std::vector<double>& Values,double P)
|
|
{
|
|
if(Values.empty())return 0.0;
|
|
std::sort(Values.begin(),Values.end());
|
|
const std::size_t Index=std::min(Values.size()-1,
|
|
static_cast<std::size_t>(std::ceil(P*Values.size())-1));
|
|
return Values[Index];
|
|
}
|
|
|
|
inline double TileAabbDistanceMeters(const FVoxelTileKey& Key,
|
|
const FVector& CameraVoxels,double VoxelMeters)
|
|
{
|
|
const double Extent=static_cast<double>(Key.ExtentVoxels());
|
|
const double Min[3]={static_cast<double>(Key.OriginVoxels().X),
|
|
static_cast<double>(Key.OriginVoxels().Y),static_cast<double>(Key.OriginVoxels().Z)};
|
|
const double Cam[3]={CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z};
|
|
double Squared=0.0;
|
|
for(int Axis=0;Axis<3;++Axis)
|
|
{
|
|
const double Delta=Cam[Axis]<Min[Axis]?Min[Axis]-Cam[Axis]
|
|
:Cam[Axis]>Min[Axis]+Extent?Cam[Axis]-(Min[Axis]+Extent):0.0;
|
|
Squared+=Delta*Delta;
|
|
}
|
|
return std::sqrt(Squared)*VoxelMeters;
|
|
}
|
|
|
|
inline bool TileHasNativeSurface(const FVoxelTileKey& Key,
|
|
const VoxelForgeCore::WorldAssemblyManifest& Assembly,
|
|
double VoxelMeters)
|
|
{
|
|
const double Extent=static_cast<double>(Key.ExtentVoxels());
|
|
const double X=(static_cast<double>(Key.OriginVoxels().X)+Extent*0.5)*256.0;
|
|
const double Y=(static_cast<double>(Key.OriginVoxels().Y)+Extent*0.5)*256.0;
|
|
const double MinZ=static_cast<double>(Key.OriginVoxels().Z);
|
|
const double MaxZ=MinZ+Extent;
|
|
for(const auto& Plan:Assembly.plan.strates)
|
|
{
|
|
VoxelForgeCore::Heightfield2D Field(Assembly.recipe,Plan);
|
|
if(!Field.isValid())continue;
|
|
const auto Column=Field.point(static_cast<std::int64_t>(std::llround(X)),
|
|
static_cast<std::int64_t>(std::llround(Y)),0);
|
|
const double Ground=static_cast<double>(Column.groundHeightSubvoxels)/256.0;
|
|
const double Cap=static_cast<double>(Column.capHeightSubvoxels)/256.0;
|
|
const double Margin=std::max(2.0,0.25/VoxelMeters);
|
|
if(Ground<Cap&&((Ground>=MinZ-Margin&&Ground<=MaxZ+Margin)
|
|
||(Cap>=MinZ-Margin&&Cap<=MaxZ+Margin)))return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
inline FHeightGrid Rasterize(std::span<const FStandaloneCpuTile* const> Tiles,
|
|
const FVoxelTileKey& Patch,double VoxelMeters,int Side=65)
|
|
{
|
|
const double Extent=static_cast<double>(Patch.ExtentVoxels())*VoxelMeters;
|
|
const double MinX=static_cast<double>(Patch.OriginVoxels().X)*VoxelMeters;
|
|
const double MinY=static_cast<double>(Patch.OriginVoxels().Y)*VoxelMeters;
|
|
FHeightGrid Grid(Side,MinX,MinX+Extent,MinY,MinY+Extent);
|
|
for(const FStandaloneCpuTile* Tile:Tiles)
|
|
{
|
|
if(!Tile||Tile->Indices.size()<3)continue;
|
|
const std::size_t TriangleCount=Tile->Indices.size()/3;
|
|
for(std::size_t Triangle=0;Triangle<TriangleCount;++Triangle)
|
|
{
|
|
if(Triangle<Tile->TriangleSkirtFaceMasks.size()
|
|
&&Tile->TriangleSkirtFaceMasks[Triangle]!=0)continue;
|
|
const std::uint32_t I0=Tile->Indices[Triangle*3];
|
|
const std::uint32_t I1=Tile->Indices[Triangle*3+1];
|
|
const std::uint32_t I2=Tile->Indices[Triangle*3+2];
|
|
if(I0>=Tile->Vertices.size()||I1>=Tile->Vertices.size()||I2>=Tile->Vertices.size())continue;
|
|
const auto& A=Tile->Vertices[I0].Position;
|
|
const auto& B=Tile->Vertices[I1].Position;
|
|
const auto& C=Tile->Vertices[I2].Position;
|
|
const double Denominator=(B[1]-C[1])*(A[0]-C[0])+(C[0]-B[0])*(A[1]-C[1]);
|
|
if(std::abs(Denominator)<1e-10)continue;
|
|
const double MinTriX=std::min({A[0],B[0],C[0]}),MaxTriX=std::max({A[0],B[0],C[0]});
|
|
const double MinTriY=std::min({A[1],B[1],C[1]}),MaxTriY=std::max({A[1],B[1],C[1]});
|
|
const double StepX=Extent/(Side-1),StepY=Extent/(Side-1);
|
|
const int Ix0=std::clamp(static_cast<int>(std::ceil((MinTriX-MinX)/StepX)),0,Side-1);
|
|
const int Ix1=std::clamp(static_cast<int>(std::floor((MaxTriX-MinX)/StepX)),0,Side-1);
|
|
const int Iy0=std::clamp(static_cast<int>(std::ceil((MinTriY-MinY)/StepY)),0,Side-1);
|
|
const int Iy1=std::clamp(static_cast<int>(std::floor((MaxTriY-MinY)/StepY)),0,Side-1);
|
|
for(int Iy=Iy0;Iy<=Iy1;++Iy)
|
|
for(int Ix=Ix0;Ix<=Ix1;++Ix)
|
|
{
|
|
const double X=Grid.X(Ix),Y=Grid.Y(Iy);
|
|
const double W0=((B[1]-C[1])*(X-C[0])+(C[0]-B[0])*(Y-C[1]))/Denominator;
|
|
const double W1=((C[1]-A[1])*(X-C[0])+(A[0]-C[0])*(Y-C[1]))/Denominator;
|
|
const double W2=1.0-W0-W1;
|
|
if(W0<-1e-6||W1<-1e-6||W2<-1e-6)continue;
|
|
Grid.Add(Ix,Iy,W0*A[2]+W1*B[2]+W2*C[2]);
|
|
}
|
|
}
|
|
}
|
|
for(auto& Values:Grid.Hits)
|
|
{
|
|
std::sort(Values.begin(),Values.end());
|
|
Values.erase(std::unique(Values.begin(),Values.end(),[](double A,double B)
|
|
{return std::abs(A-B)<0.02;}),Values.end());
|
|
}
|
|
return Grid;
|
|
}
|
|
|
|
inline std::vector<FSurfaceRoot> ExactHeightRoots(const FHeightGrid& Grid,int Ix,int Iy,
|
|
const FVoxelTileKey& Patch,double VoxelMeters,
|
|
const VoxelForgeCore::WorldAssemblyManifest& Assembly)
|
|
{
|
|
std::vector<FSurfaceRoot> Roots;
|
|
const double XSubvoxels=Grid.X(Ix)/VoxelMeters*256.0;
|
|
const double YSubvoxels=Grid.Y(Iy)/VoxelMeters*256.0;
|
|
const auto X=static_cast<std::int64_t>(std::llround(XSubvoxels));
|
|
const auto Y=static_cast<std::int64_t>(std::llround(YSubvoxels));
|
|
const double MinZ=static_cast<double>(Patch.OriginVoxels().Z);
|
|
const double MaxZ=MinZ+Patch.ExtentVoxels();
|
|
constexpr std::int64_t NormalProbeSubvoxels=256;
|
|
for(const auto& Plan:Assembly.plan.strates)
|
|
{
|
|
VoxelForgeCore::Heightfield2D Field(Assembly.recipe,Plan);
|
|
if(!Field.isValid())continue;
|
|
const auto Column=Field.point(X,Y,0);
|
|
if(Column.groundHeightSubvoxels>=Column.capHeightSubvoxels)continue;
|
|
const auto XMinus=Field.point(X-NormalProbeSubvoxels,Y,0);
|
|
const auto XPlus=Field.point(X+NormalProbeSubvoxels,Y,0);
|
|
const auto YMinus=Field.point(X,Y-NormalProbeSubvoxels,0);
|
|
const auto YPlus=Field.point(X,Y+NormalProbeSubvoxels,0);
|
|
const auto Slope=[&](std::int64_t Minus,std::int64_t Plus)
|
|
{return static_cast<double>(Plus-Minus)/(2.0*NormalProbeSubvoxels);};
|
|
const double Ground=static_cast<double>(Column.groundHeightSubvoxels)/256.0*VoxelMeters;
|
|
const double Cap=static_cast<double>(Column.capHeightSubvoxels)/256.0*VoxelMeters;
|
|
const double PlanMinZ=static_cast<double>(Plan.bottomChunkZ)*32.0*VoxelMeters;
|
|
const double PlanMaxZ=static_cast<double>(Plan.topChunkZ+1)*32.0*VoxelMeters;
|
|
if(Ground>=MinZ*VoxelMeters-0.25*VoxelMeters
|
|
&&Ground<=MaxZ*VoxelMeters+0.25*VoxelMeters
|
|
&&Ground>=PlanMinZ-0.25*VoxelMeters&&Ground<=PlanMaxZ+0.25*VoxelMeters)
|
|
Roots.push_back({Ground,Slope(XMinus.groundHeightSubvoxels,
|
|
XPlus.groundHeightSubvoxels),Slope(YMinus.groundHeightSubvoxels,
|
|
YPlus.groundHeightSubvoxels)});
|
|
if(Cap>=MinZ*VoxelMeters-0.25*VoxelMeters
|
|
&&Cap<=MaxZ*VoxelMeters+0.25*VoxelMeters
|
|
&&Cap>=PlanMinZ-0.25*VoxelMeters&&Cap<=PlanMaxZ+0.25*VoxelMeters)
|
|
Roots.push_back({Cap,Slope(XMinus.capHeightSubvoxels,
|
|
XPlus.capHeightSubvoxels),Slope(YMinus.capHeightSubvoxels,
|
|
YPlus.capHeightSubvoxels)});
|
|
}
|
|
std::sort(Roots.begin(),Roots.end(),[](const FSurfaceRoot& A,const FSurfaceRoot& B)
|
|
{return A.ZMeters<B.ZMeters;});
|
|
Roots.erase(std::unique(Roots.begin(),Roots.end(),[](const FSurfaceRoot& A,
|
|
const FSurfaceRoot& B){return std::abs(A.ZMeters-B.ZMeters)<0.01;}),Roots.end());
|
|
return Roots;
|
|
}
|
|
|
|
inline bool MatchNearest(const std::vector<double>& Hits,const std::vector<bool>& Used,
|
|
double Root,std::size_t& OutIndex,double& OutError)
|
|
{
|
|
double Best=std::numeric_limits<double>::infinity();std::size_t BestIndex=Hits.size();
|
|
for(std::size_t I=0;I<Hits.size();++I)
|
|
if(I>=Used.size()||!Used[I])
|
|
{
|
|
const double Error=std::abs(Hits[I]-Root);
|
|
if(Error<Best){Best=Error;BestIndex=I;}
|
|
}
|
|
if(BestIndex==Hits.size())return false;
|
|
OutIndex=BestIndex;OutError=Best;return true;
|
|
}
|
|
|
|
inline FErrorRow CompareToExact(const FHeightGrid& MeshGrid,const FVoxelTileKey& Patch,
|
|
double VoxelMeters,double PixelBudgetMeters,
|
|
const VoxelForgeCore::WorldAssemblyManifest& Assembly,const FViewBasis& View)
|
|
{
|
|
FErrorRow Row;Row.Lod=Patch.Level;Row.Valid=true;
|
|
FErrorAccumulator Accumulator;
|
|
for(int Iy=0;Iy<MeshGrid.Side;++Iy)
|
|
for(int Ix=0;Ix<MeshGrid.Side;++Ix)
|
|
{
|
|
const auto Roots=ExactHeightRoots(MeshGrid,Ix,Iy,Patch,VoxelMeters,Assembly);
|
|
if(Roots.empty()&&MeshGrid.At(Ix,Iy).empty())continue;
|
|
std::vector<bool> Used(MeshGrid.At(Ix,Iy).size(),false);
|
|
for(const FSurfaceRoot& Root:Roots)
|
|
{
|
|
std::size_t HitIndex=0;double ErrorMeters=0.0;
|
|
if(!MatchNearest(MeshGrid.At(Ix,Iy),Used,Root.ZMeters,HitIndex,ErrorMeters))
|
|
{
|
|
++Row.Missing;
|
|
AddMissing(Accumulator);
|
|
continue;
|
|
}
|
|
Used[HitIndex]=true;
|
|
AddError(Accumulator,MeshGrid.At(Ix,Iy)[HitIndex]-Root.ZMeters,
|
|
Root,PixelBudgetMeters,View,MeshGrid.X(Ix),MeshGrid.Y(Iy));
|
|
++Row.Samples;
|
|
}
|
|
// Heightfield2D declares ground and cap roots only. Other mesh intersections may
|
|
// be valid tunnel geometry; the exact-mesh comparison checks extra pieces.
|
|
}
|
|
FinalizeError(Row,Accumulator);
|
|
return Row;
|
|
}
|
|
|
|
inline FErrorRow CompareToReference(const FHeightGrid& MeshGrid,
|
|
const FHeightGrid& ReferenceGrid,const FVoxelTileKey& Patch,double VoxelMeters,
|
|
double PixelBudgetMeters,const VoxelForgeCore::WorldAssemblyManifest& Assembly,
|
|
const FViewBasis& View)
|
|
{
|
|
FErrorRow Row;Row.Lod=Patch.Level;Row.Valid=true;
|
|
FErrorAccumulator Accumulator;
|
|
for(int Iy=0;Iy<ReferenceGrid.Side;++Iy)
|
|
for(int Ix=0;Ix<ReferenceGrid.Side;++Ix)
|
|
{
|
|
const auto& Reference=ReferenceGrid.At(Ix,Iy);
|
|
const auto& Mesh=MeshGrid.At(Ix,Iy);
|
|
if(Reference.empty()&&Mesh.empty())continue;
|
|
const auto Roots=ExactHeightRoots(ReferenceGrid,Ix,Iy,Patch,VoxelMeters,Assembly);
|
|
std::vector<bool> UsedReference(Reference.size(),false),UsedMesh(Mesh.size(),false);
|
|
for(std::size_t ReferenceIndex=0;ReferenceIndex<Reference.size();++ReferenceIndex)
|
|
{
|
|
const double ReferenceZ=Reference[ReferenceIndex];
|
|
UsedReference[ReferenceIndex]=true;
|
|
std::size_t MeshIndex=0;double MeshError=0.0;
|
|
if(!MatchNearest(Mesh,UsedMesh,ReferenceZ,MeshIndex,MeshError))
|
|
{
|
|
++Row.Missing;AddMissing(Accumulator);continue;
|
|
}
|
|
UsedMesh[MeshIndex]=true;
|
|
FSurfaceRoot Root{ReferenceZ,0.0,0.0};
|
|
if(!Roots.empty())
|
|
{
|
|
const auto BestRoot=std::min_element(Roots.begin(),Roots.end(),
|
|
[ReferenceZ](const FSurfaceRoot& A,const FSurfaceRoot& B)
|
|
{return std::abs(A.ZMeters-ReferenceZ)<std::abs(B.ZMeters-ReferenceZ);});
|
|
if(BestRoot!=Roots.end()&&std::abs(BestRoot->ZMeters-ReferenceZ)<1.0)
|
|
Root=*BestRoot;
|
|
}
|
|
AddError(Accumulator,Mesh[MeshIndex]-Reference[ReferenceIndex],
|
|
Root,PixelBudgetMeters,View,ReferenceGrid.X(Ix),ReferenceGrid.Y(Iy));
|
|
}
|
|
for(std::size_t I=0;I<Mesh.size();++I)
|
|
if(!UsedMesh[I]){++Row.Floating;AddMissing(Accumulator);}
|
|
}
|
|
FinalizeError(Row,Accumulator);
|
|
return Row;
|
|
}
|
|
|
|
inline FPopRow ComparePop(const FHeightGrid& Coarse,const FHeightGrid& Fine,
|
|
const FVoxelTileKey& Patch,double VoxelMeters,double PixelBudgetMeters,
|
|
const VoxelForgeCore::WorldAssemblyManifest& Assembly,const FViewBasis& View)
|
|
{
|
|
FPopRow Row;Row.CoarseLod=Patch.Level;Row.Valid=true;
|
|
FErrorAccumulator Accumulator;
|
|
for(int Iy=0;Iy<Coarse.Side;++Iy)
|
|
for(int Ix=0;Ix<Coarse.Side;++Ix)
|
|
{
|
|
const auto& CoarseHits=Coarse.At(Ix,Iy);
|
|
const auto& FineHits=Fine.At(Ix,Iy);
|
|
const auto Roots=ExactHeightRoots(Coarse,Ix,Iy,Patch,VoxelMeters,Assembly);
|
|
if(Roots.empty()&&CoarseHits.empty()&&FineHits.empty())continue;
|
|
std::vector<bool> CoarseUsed(CoarseHits.size(),false);
|
|
std::vector<bool> FineUsed(FineHits.size(),false);
|
|
for(const FSurfaceRoot& Root:Roots)
|
|
{
|
|
std::size_t CoarseIndex=0,FineIndex=0;
|
|
double CoarseError=0.0,FineError=0.0;
|
|
if(!MatchNearest(CoarseHits,CoarseUsed,Root.ZMeters,CoarseIndex,CoarseError)
|
|
||!MatchNearest(FineHits,FineUsed,Root.ZMeters,FineIndex,FineError))
|
|
{
|
|
++Row.Cracks;AddMissing(Accumulator);continue;
|
|
}
|
|
CoarseUsed[CoarseIndex]=true;
|
|
FineUsed[FineIndex]=true;
|
|
AddError(Accumulator,CoarseHits[CoarseIndex]-FineHits[FineIndex],
|
|
Root,PixelBudgetMeters,View,Coarse.X(Ix),Coarse.Y(Iy));
|
|
++Row.Samples;
|
|
}
|
|
}
|
|
FErrorRow Stats;Stats.Lod=Patch.Level;Stats.Valid=true;
|
|
FinalizeError(Stats,Accumulator);
|
|
Row.Samples=Stats.Samples;Row.P50=Stats.P50;Row.P99=Stats.P99;Row.Maximum=Stats.Maximum;
|
|
Row.VerticalP50=Stats.VerticalP50;Row.VerticalP99=Stats.VerticalP99;
|
|
Row.VerticalMaximum=Stats.VerticalMaximum;
|
|
Row.MaximumNormalDistanceMeters=Stats.MaximumNormalDistanceMeters;
|
|
Row.MaximumX=Stats.MaximumX;Row.MaximumY=Stats.MaximumY;
|
|
Row.MaximumReferenceZ=Stats.MaximumReferenceZ;
|
|
Row.MaximumDisplayedZ=Stats.MaximumDisplayedZ;
|
|
Row.MaximumSlopeDegrees=Stats.MaximumSlopeDegrees;
|
|
Row.SlopeSamples=Stats.SlopeSamples;Row.SlopeP99=Stats.SlopeP99;
|
|
Row.SlopeMaximum=Stats.SlopeMaximum;
|
|
return Row;
|
|
}
|
|
|
|
inline std::array<double,9> ClosestDisplayedDistancesMeters(
|
|
const FStandaloneOwnerWorld& World,const FVector& CameraVoxels,
|
|
std::uint32_t VoxelSizeMicrons)
|
|
{
|
|
const double VoxelMeters=static_cast<double>(VoxelSizeMicrons)/1000000.0;
|
|
std::array<double,9> Minimums{};
|
|
Minimums.fill(std::numeric_limits<double>::infinity());
|
|
for(int Lod=0;Lod<=8;++Lod)
|
|
{
|
|
const int PhaseCount=1<<Lod;
|
|
for(int Phase=0;Phase<PhaseCount;++Phase)
|
|
{
|
|
FVector Probe=CameraVoxels;
|
|
Probe.X=static_cast<float>(Phase*32.0+32.0-1.0/256.0);
|
|
const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,Probe);
|
|
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
|
{
|
|
if(Tile.Level!=Lod)continue;
|
|
Minimums[Lod]=std::min(Minimums[Lod],
|
|
TileAabbDistanceMeters(Tile,Probe,VoxelMeters));
|
|
}
|
|
}
|
|
}
|
|
for(double& Distance:Minimums)
|
|
if(!std::isfinite(Distance))Distance=0.0;
|
|
return Minimums;
|
|
}
|
|
|
|
inline std::array<double,9> ClipmapClosestDisplayedDistancesMeters(
|
|
const FStandaloneOwnerWorld& World,std::uint32_t VoxelSizeMicrons)
|
|
{
|
|
const double VoxelMeters=static_cast<double>(VoxelSizeMicrons)/1000000.0;
|
|
std::array<double,9> Distances{};
|
|
for(int Lod=1;Lod<=8;++Lod)
|
|
Distances[Lod]=static_cast<double>(World.ClipRadius)
|
|
*static_cast<double>(CHUNK_SIZE)*(1ull<<(Lod-1))*VoxelMeters;
|
|
return Distances;
|
|
}
|
|
|
|
inline void PrintSlopeTail(const char* Kind,int Lod,const std::array<std::uint64_t,4>& Counts,
|
|
const std::array<double,4>& P99,const std::array<double,4>& Maximum)
|
|
{
|
|
constexpr const char* Labels[4]={"lt30","30to45","45to60","ge60"};
|
|
for(std::size_t Bin=0;Bin<Counts.size();++Bin)
|
|
std::printf("[ScreenSpaceSlopeTail] kind=%s lod=%d slope_deg=%s samples=%llu p99_px=%.4f max_px=%.4f\n",
|
|
Kind,Lod,Labels[Bin],static_cast<unsigned long long>(Counts[Bin]),P99[Bin],Maximum[Bin]);
|
|
}
|
|
|
|
inline void PrintKnownFailureControl(double CellMeters,double DisplayDistanceMeters,
|
|
const FViewBasis& View)
|
|
{
|
|
constexpr double Slope=100.0;
|
|
const double LateralMeters=CellMeters/200.0;
|
|
const double VerticalMeters=Slope*LateralMeters;
|
|
const double NormalLength=std::sqrt(1.0+Slope*Slope);
|
|
const double NormalDistance=VerticalMeters/NormalLength;
|
|
const std::array<double,3> Normal={-Slope/NormalLength,0.0,1.0/NormalLength};
|
|
const double ProjectedNormal=std::hypot(
|
|
Normal[0]*View.Right[0]+Normal[1]*View.Right[1]+Normal[2]*View.Right[2],
|
|
Normal[0]*View.Up[0]+Normal[1]*View.Up[1]+Normal[2]*View.Up[2]);
|
|
const double PixelBudgetMeters=DisplayDistanceMeters*OnePixelRadians;
|
|
const double VerticalPixels=VerticalMeters/PixelBudgetMeters;
|
|
const double NormalPixels=NormalDistance*ProjectedNormal/PixelBudgetMeters;
|
|
const bool Passed=VerticalPixels>4.0&&NormalPixels<1.0;
|
|
std::printf("[ScreenSpaceKnownFailureControl] case=steep-plane-lateral-offset slope_deg=%.4f cell_m=%.6f lateral_m=%.6f cell_fraction=%.6f vertical_error_m=%.6f vertical_height_px=%.4f closest_normal_m=%.6f projected_normal_px=%.4f status=%s\n",
|
|
std::atan(Slope)*180.0/3.14159265358979323846,CellMeters,LateralMeters,
|
|
LateralMeters/CellMeters,VerticalMeters,VerticalPixels,NormalDistance,NormalPixels,
|
|
Passed?"EXPECTED_VERTICAL_FALSE_POSITIVE":"CONTROL_FAILED");
|
|
}
|
|
|
|
inline bool Run(const std::array<std::optional<FStandaloneCpuTile>,9>& SampleTiles,
|
|
FStandaloneStreamingGenerator& Streamer,
|
|
const VoxelForgeCore::WorldAssemblyManifest& Assembly,
|
|
const std::array<double,9>& Distances,std::uint32_t VoxelSizeMicrons,int MaxLod,
|
|
const std::array<std::int64_t,9>& PixelBudgetsSubvoxels,
|
|
double CameraYawDegrees,double CameraPitchDegrees)
|
|
{
|
|
const double VoxelMeters=static_cast<double>(VoxelSizeMicrons)/1000000.0;
|
|
const FViewBasis View=MakeViewBasis(CameraYawDegrees,CameraPitchDegrees);
|
|
std::array<double,9> Budgets{};
|
|
std::array<FErrorRow,9> ErrorRows{};
|
|
std::array<FPopRow,9> PopRows{};
|
|
bool GatePass=true;
|
|
std::printf("[ScreenSpaceGate] angular_pixel=%.9f rad (horizontal_fov=90deg,width=2560) lod>=3_limit=1.0px sampling=65x65 skirts=excluded metric=local_surface_normal_projected field_reference=direct-Heightfield2D-roots normal_probe_vox=1 view_yaw_deg=%.3f view_pitch_deg=%.3f\n",
|
|
OnePixelRadians,CameraYawDegrees,CameraPitchDegrees);
|
|
for(int Lod=0;Lod<=MaxLod&&Lod<9;++Lod)
|
|
{
|
|
Budgets[Lod]=Distances[Lod]*OnePixelRadians;
|
|
std::printf("[ScreenSpaceDistance] lod=%d closest_m=%.3f pixel_budget_m=%.6f pixel_budget_cm=%.3f pixel_budget_subvoxels=%lld rule=%s\n",
|
|
Lod,Distances[Lod],Budgets[Lod],Budgets[Lod]*100.0,
|
|
static_cast<long long>(PixelBudgetsSubvoxels[Lod]),Lod<3?"exact-2cm":"1px");
|
|
}
|
|
const double LOD3CellMeters=static_cast<double>(CHUNK_SIZE*(1<<3))*VoxelMeters/64.0;
|
|
PrintKnownFailureControl(LOD3CellMeters,Distances[3],View);
|
|
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true,0);
|
|
for(int Lod=3;Lod<=MaxLod&&Lod<9;++Lod)
|
|
{
|
|
if(!SampleTiles[Lod]||SampleTiles[Lod]->Vertices.empty())
|
|
{
|
|
std::printf("[ScreenSpaceGateLOD] lod=%d status=NO_SURFACE_TILE\n",Lod);
|
|
GatePass=false;continue;
|
|
}
|
|
const FStandaloneCpuTile& CoarseTile=*SampleTiles[Lod];
|
|
const FStandaloneCpuTile* CoarsePtr=&CoarseTile;
|
|
const std::span<const FStandaloneCpuTile* const> CoarseSpan(&CoarsePtr,1);
|
|
const FHeightGrid CoarseGrid=Rasterize(CoarseSpan,CoarseTile.Key,VoxelMeters);
|
|
const FStandaloneCpuTile ExactTile=Streamer.GenerateDiagnostic(
|
|
CoarseTile.Key,0,1,true);
|
|
const FStandaloneCpuTile* ExactPointer=&ExactTile;
|
|
const std::span<const FStandaloneCpuTile* const> ExactSpan(&ExactPointer,1);
|
|
const FHeightGrid ExactGrid=Rasterize(ExactSpan,ExactTile.Key,VoxelMeters);
|
|
ErrorRows[Lod]=CompareToReference(CoarseGrid,ExactGrid,CoarseTile.Key,VoxelMeters,
|
|
Budgets[Lod],Assembly,View);
|
|
const FErrorRow& Error=ErrorRows[Lod];
|
|
const bool LodPass=Error.Valid&&Error.Missing==0&&Error.Floating==0
|
|
&&Error.Samples>=16&&Error.Maximum<=1.0;
|
|
GatePass=GatePass&&LodPass;
|
|
std::printf("[ScreenSpaceGateLOD] lod=%d key=(%d,%d,%d) samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xy_m=(%.3f,%.3f) max_slope_deg=%.2f missing=%llu floating=%llu status=%s\n",
|
|
Lod,CoarseTile.Key.Coord.X,CoarseTile.Key.Coord.Y,CoarseTile.Key.Coord.Z,
|
|
static_cast<unsigned long long>(Error.Samples),Error.P50,Error.P99,Error.Maximum,
|
|
Error.VerticalP50,Error.VerticalP99,Error.VerticalMaximum,
|
|
Error.MaximumNormalDistanceMeters,Error.MaximumX,Error.MaximumY,
|
|
Error.MaximumSlopeDegrees,
|
|
static_cast<unsigned long long>(Error.Missing),static_cast<unsigned long long>(Error.Floating),
|
|
LodPass?"PASS":"REJECT");
|
|
PrintSlopeTail("same_lod",Lod,Error.SlopeSamples,Error.SlopeP99,Error.SlopeMaximum);
|
|
|
|
const FErrorRow FieldError=CompareToExact(CoarseGrid,CoarseTile.Key,VoxelMeters,
|
|
Budgets[Lod],Assembly,View);
|
|
const bool FieldPass=FieldError.Valid&&FieldError.Missing==0&&FieldError.Floating==0
|
|
&&FieldError.Samples>=16&&FieldError.Maximum<=1.0;
|
|
GatePass=GatePass&&FieldPass;
|
|
std::printf("[ScreenSpaceGateFieldLOD] lod=%d samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xyz_m=(%.3f,%.3f,%.3f) max_slope_deg=%.2f missing=%llu floating=%llu status=%s\n",
|
|
Lod,static_cast<unsigned long long>(FieldError.Samples),FieldError.P50,
|
|
FieldError.P99,FieldError.Maximum,FieldError.VerticalP50,FieldError.VerticalP99,
|
|
FieldError.VerticalMaximum,FieldError.MaximumNormalDistanceMeters,
|
|
FieldError.MaximumX,FieldError.MaximumY,FieldError.MaximumDisplayedZ,
|
|
FieldError.MaximumSlopeDegrees,
|
|
static_cast<unsigned long long>(FieldError.Missing),
|
|
static_cast<unsigned long long>(FieldError.Floating),FieldPass?"PASS":"REJECT");
|
|
PrintSlopeTail("field",Lod,FieldError.SlopeSamples,FieldError.SlopeP99,
|
|
FieldError.SlopeMaximum);
|
|
|
|
bool FaultControlsPass=true;
|
|
const FHeightGrid MissingGrid(ExactGrid.Side,ExactGrid.MinimumX,ExactGrid.MaximumX,
|
|
ExactGrid.MinimumY,ExactGrid.MaximumY);
|
|
const FErrorRow MissingError=CompareToReference(MissingGrid,ExactGrid,
|
|
CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View);
|
|
const bool MissingRejected=MissingError.Missing>0&&MissingError.Maximum>1.0;
|
|
FaultControlsPass=FaultControlsPass&&MissingRejected;
|
|
std::printf("[ScreenSpaceFaultInjection] lod=%d case=missing-displayed-surface missing=%llu status=%s\n",
|
|
Lod,static_cast<unsigned long long>(MissingError.Missing),
|
|
MissingRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
|
|
|
FStandaloneCpuTile FloatingTile=CoarseTile;
|
|
const std::uint32_t FloatingVertexBase=static_cast<std::uint32_t>(FloatingTile.Vertices.size());
|
|
for(FStandaloneCpuVertex Vertex:CoarseTile.Vertices)
|
|
{
|
|
Vertex.Position[2]+=2.0f;
|
|
FloatingTile.Vertices.push_back(Vertex);
|
|
}
|
|
for(const std::uint32_t Index:CoarseTile.Indices)
|
|
FloatingTile.Indices.push_back(FloatingVertexBase+Index);
|
|
FloatingTile.TriangleSkirtFaceMasks.insert(FloatingTile.TriangleSkirtFaceMasks.end(),
|
|
CoarseTile.TriangleSkirtFaceMasks.begin(),CoarseTile.TriangleSkirtFaceMasks.end());
|
|
const FStandaloneCpuTile* FloatingPointer=&FloatingTile;
|
|
const std::span<const FStandaloneCpuTile* const> FloatingSpan(&FloatingPointer,1);
|
|
const FHeightGrid FloatingGrid=Rasterize(FloatingSpan,CoarseTile.Key,VoxelMeters);
|
|
const FErrorRow FloatingError=CompareToReference(FloatingGrid,ExactGrid,
|
|
CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View);
|
|
const bool FloatingRejected=FloatingError.Floating>0||FloatingError.Maximum>1.0;
|
|
FaultControlsPass=FaultControlsPass&&FloatingRejected;
|
|
std::printf("[ScreenSpaceFaultInjection] lod=%d case=2m-floating-surface floating=%llu max_px=%.4f status=%s\n",
|
|
Lod,static_cast<unsigned long long>(FloatingError.Floating),FloatingError.Maximum,
|
|
FloatingRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
|
|
|
std::vector<FStandaloneCpuTile> FineTiles;
|
|
FineTiles.reserve(8);
|
|
for(int Dz=0;Dz<=1;++Dz)
|
|
for(int Dy=0;Dy<=1;++Dy)
|
|
for(int Dx=0;Dx<=1;++Dx)
|
|
{
|
|
const FVoxelTileKey FineKey(FIntVector(2*CoarseTile.Key.Coord.X+Dx,
|
|
2*CoarseTile.Key.Coord.Y+Dy,2*CoarseTile.Key.Coord.Z+Dz),Lod-1);
|
|
FStandaloneCpuTile FineTile=Streamer.GenerateDiagnostic(FineKey);
|
|
if(!FineTile.Vertices.empty())FineTiles.push_back(std::move(FineTile));
|
|
}
|
|
std::vector<const FStandaloneCpuTile*> FinePointers;
|
|
FinePointers.reserve(FineTiles.size());
|
|
for(const auto& Tile:FineTiles)FinePointers.push_back(&Tile);
|
|
const auto FineSpan=std::span<const FStandaloneCpuTile* const>(
|
|
FinePointers.data(),FinePointers.size());
|
|
const FHeightGrid FineGrid=Rasterize(FineSpan,CoarseTile.Key,VoxelMeters);
|
|
PopRows[Lod]=ComparePop(CoarseGrid,FineGrid,CoarseTile.Key,VoxelMeters,
|
|
Budgets[Lod],Assembly,View);
|
|
const FPopRow& Pop=PopRows[Lod];
|
|
const bool PopPass=Pop.Valid&&Pop.Cracks==0&&Pop.Floating==0&&Pop.Maximum<=1.0;
|
|
GatePass=GatePass&&PopPass;
|
|
std::printf("[ScreenSpaceSwap] lod=%d_to_%d samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xyz_m=(%.3f,%.3f,%.3f) max_slope_deg=%.2f cracks=%llu floating=%llu status=%s\n",
|
|
Lod,Lod-1,static_cast<unsigned long long>(Pop.Samples),
|
|
Pop.P50,Pop.P99,Pop.Maximum,Pop.VerticalP50,Pop.VerticalP99,Pop.VerticalMaximum,
|
|
Pop.MaximumNormalDistanceMeters,Pop.MaximumX,Pop.MaximumY,
|
|
Pop.MaximumDisplayedZ,
|
|
Pop.MaximumSlopeDegrees,static_cast<unsigned long long>(Pop.Cracks),
|
|
static_cast<unsigned long long>(Pop.Floating),
|
|
PopPass?"PASS":"REJECT");
|
|
PrintSlopeTail("swap",Lod,Pop.SlopeSamples,Pop.SlopeP99,Pop.SlopeMaximum);
|
|
|
|
const FHeightGrid MissingFineGrid(CoarseGrid.Side,CoarseGrid.MinimumX,
|
|
CoarseGrid.MaximumX,CoarseGrid.MinimumY,CoarseGrid.MaximumY);
|
|
const FPopRow MissingFineError=ComparePop(CoarseGrid,MissingFineGrid,
|
|
CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View);
|
|
const bool CrackRejected=MissingFineError.Cracks>0&&MissingFineError.Maximum>1.0;
|
|
FaultControlsPass=FaultControlsPass&&CrackRejected;
|
|
GatePass=GatePass&&FaultControlsPass;
|
|
std::printf("[ScreenSpaceFaultInjection] lod=%d case=missing-fine-ring-surface cracks=%llu max_px=%.4f status=%s\n",
|
|
Lod,static_cast<unsigned long long>(MissingFineError.Cracks),
|
|
MissingFineError.Maximum,CrackRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
|
|
|
bool NegativeControlsPass=true;
|
|
const auto CheckNegative=[&](const char* Name,std::uint32_t Drop,std::uint32_t Stride)
|
|
{
|
|
FStandaloneCpuTile Bad=Streamer.GenerateDiagnostic(
|
|
CoarseTile.Key,Drop,Stride,true);
|
|
const FStandaloneCpuTile* BadPointer=&Bad;
|
|
const std::span<const FStandaloneCpuTile* const> BadSpan(&BadPointer,1);
|
|
const FHeightGrid BadGrid=Rasterize(BadSpan,Bad.Key,VoxelMeters);
|
|
const FErrorRow BadError=CompareToReference(BadGrid,ExactGrid,
|
|
CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View);
|
|
const bool Rejected=BadError.Missing>0||BadError.Floating>0
|
|
||BadError.Samples<16||BadError.Maximum>1.0;
|
|
NegativeControlsPass=NegativeControlsPass&&Rejected;
|
|
std::printf("[ScreenSpaceNegative] lod=%d case=%s p50_px=%.4f p99_px=%.4f max_px=%.4f missing=%llu floating=%llu status=%s\n",
|
|
Lod,Name,BadError.P50,BadError.P99,BadError.Maximum,
|
|
static_cast<unsigned long long>(BadError.Missing),
|
|
static_cast<unsigned long long>(BadError.Floating),
|
|
Rejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
|
};
|
|
CheckNegative("s13-global-octave-drop",1,1);
|
|
CheckNegative("stride-2-column-interpolation",0,2);
|
|
GatePass=GatePass&&NegativeControlsPass;
|
|
}
|
|
std::printf("[ScreenSpaceGateResult] status=%s\n",GatePass?"PASS":"REJECT");
|
|
return GatePass;
|
|
}
|
|
}
|
|
|