wip(assembler): rounds L5+L6 -- Linux far-terrain captures and screen-space gate (world unchanged)
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
This commit is contained in:
1 parent
7770074503
commit
9898e057fd
36 files changed
+1821
-11
No files matched your search
@@ -0,0 +1,397 @@
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <png.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct FVertex
|
||||
{
|
||||
float Position[3];
|
||||
float Normal[3];
|
||||
float Height;
|
||||
};
|
||||
|
||||
struct FScene
|
||||
{
|
||||
std::uint32_t VoxelSizeMicrons=0;
|
||||
std::array<double,3> CameraMeters{};
|
||||
double YawDegrees=0.0,PitchDegrees=0.0;
|
||||
std::vector<FVertex> Vertices;
|
||||
std::vector<std::uint32_t> Indices;
|
||||
std::uint64_t Triangles=0,SkirtTriangles=0;
|
||||
std::uint32_t TileCount=0;
|
||||
};
|
||||
|
||||
template<typename T> bool Read(std::ifstream& File,T& Value)
|
||||
{
|
||||
File.read(reinterpret_cast<char*>(&Value),sizeof(Value));
|
||||
return static_cast<bool>(File);
|
||||
}
|
||||
|
||||
bool LoadScene(const std::filesystem::path& Path,FScene& Scene,std::string& Error)
|
||||
{
|
||||
std::ifstream File(Path,std::ios::binary);
|
||||
if(!File){Error="cannot open mesh scene";return false;}
|
||||
char Magic[8]{};
|
||||
File.read(Magic,sizeof(Magic));
|
||||
if(!File||std::memcmp(Magic,"VFMSH001",8)!=0)
|
||||
{Error="mesh scene magic is invalid";return false;}
|
||||
std::uint32_t Version=0,ExpectedTiles=0;
|
||||
if(!Read(File,Version)||!Read(File,Scene.VoxelSizeMicrons)||!Read(File,ExpectedTiles)
|
||||
||Version!=1||Scene.VoxelSizeMicrons==0||ExpectedTiles>1000000)
|
||||
{Error="mesh scene header is malformed";return false;}
|
||||
for(double& Value:Scene.CameraMeters)if(!Read(File,Value))
|
||||
{Error="mesh scene camera is truncated";return false;}
|
||||
if(!Read(File,Scene.YawDegrees)||!Read(File,Scene.PitchDegrees))
|
||||
{Error="mesh scene camera orientation is truncated";return false;}
|
||||
Scene.TileCount=ExpectedTiles;
|
||||
std::uint64_t TotalVertices=0,TotalIndices=0;
|
||||
for(std::uint32_t Tile=0;Tile<ExpectedTiles;++Tile)
|
||||
{
|
||||
std::int32_t Key[4]{};
|
||||
std::uint32_t SurfaceVertices=0,SkirtCount=0,VertexCount=0,IndexCount=0,MaskCount=0;
|
||||
for(auto& Value:Key)if(!Read(File,Value)){Error="tile key is truncated";return false;}
|
||||
if(!Read(File,SurfaceVertices)||!Read(File,SkirtCount)||!Read(File,VertexCount)
|
||||
||!Read(File,IndexCount)||!Read(File,MaskCount)
|
||||
||VertexCount>10000000||IndexCount>60000000||MaskCount>20000000
|
||||
||IndexCount%3!=0||(MaskCount!=0&&MaskCount!=IndexCount/3))
|
||||
{Error="tile mesh header is malformed";return false;}
|
||||
if(TotalVertices+VertexCount>std::numeric_limits<std::uint32_t>::max()
|
||||
||TotalIndices+IndexCount>std::numeric_limits<std::size_t>::max())
|
||||
{Error="mesh scene exceeds supported index space";return false;}
|
||||
const std::size_t Base=Scene.Vertices.size();
|
||||
Scene.Vertices.resize(Base+VertexCount);
|
||||
Scene.Indices.reserve(Scene.Indices.size()+IndexCount);
|
||||
for(std::size_t I=Base;I<Base+VertexCount;++I)
|
||||
for(float& Value:Scene.Vertices[I].Position)if(!Read(File,Value))
|
||||
{Error="tile positions are truncated";return false;}
|
||||
for(std::size_t I=Base;I<Base+VertexCount;++I)
|
||||
{
|
||||
for(float& Value:Scene.Vertices[I].Normal)if(!Read(File,Value))
|
||||
{Error="tile normals are truncated";return false;}
|
||||
Scene.Vertices[I].Height=Scene.Vertices[I].Position[2];
|
||||
}
|
||||
const std::size_t IndexOffset=Scene.Indices.size();
|
||||
Scene.Indices.resize(IndexOffset+IndexCount);
|
||||
if(IndexCount)File.read(reinterpret_cast<char*>(Scene.Indices.data()+IndexOffset),
|
||||
static_cast<std::streamsize>(sizeof(std::uint32_t)*IndexCount));
|
||||
std::vector<std::uint8_t> Masks(MaskCount);
|
||||
if(MaskCount)File.read(reinterpret_cast<char*>(Masks.data()),MaskCount);
|
||||
if(!File){Error="tile index or skirt data is truncated";return false;}
|
||||
for(std::size_t Index=IndexOffset;Index<Scene.Indices.size();++Index)
|
||||
{
|
||||
if(Scene.Indices[Index]>=VertexCount)
|
||||
{Error="tile index points outside its vertex array";return false;}
|
||||
Scene.Indices[Index]+=static_cast<std::uint32_t>(Base);
|
||||
}
|
||||
Scene.Triangles+=IndexCount/3;
|
||||
Scene.SkirtTriangles+=SkirtCount;
|
||||
TotalVertices+=VertexCount;TotalIndices+=IndexCount;
|
||||
}
|
||||
if(File.peek()!=std::char_traits<char>::eof())
|
||||
{Error="mesh scene has trailing bytes";return false;}
|
||||
if(Scene.Indices.empty()){Error="mesh scene contains no triangles";return false;}
|
||||
return true;
|
||||
}
|
||||
|
||||
EGLDisplay MakeDisplay()
|
||||
{
|
||||
using TGetPlatformDisplay=EGLDisplay(*)(EGLenum,void*,const EGLint*);
|
||||
auto GetPlatformDisplay=reinterpret_cast<TGetPlatformDisplay>(
|
||||
eglGetProcAddress("eglGetPlatformDisplayEXT"));
|
||||
#ifdef EGL_PLATFORM_SURFACELESS_MESA
|
||||
if(GetPlatformDisplay)
|
||||
{
|
||||
const EGLDisplay Display=GetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA,
|
||||
EGL_DEFAULT_DISPLAY,nullptr);
|
||||
if(Display!=EGL_NO_DISPLAY)return Display;
|
||||
}
|
||||
#endif
|
||||
return eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
}
|
||||
|
||||
GLuint CompileShader(GLenum Type,const char* Source)
|
||||
{
|
||||
const GLuint Shader=glCreateShader(Type);
|
||||
glShaderSource(Shader,1,&Source,nullptr);
|
||||
glCompileShader(Shader);
|
||||
GLint Success=GL_FALSE;
|
||||
glGetShaderiv(Shader,GL_COMPILE_STATUS,&Success);
|
||||
if(Success==GL_FALSE)
|
||||
{
|
||||
GLint Length=0;glGetShaderiv(Shader,GL_INFO_LOG_LENGTH,&Length);
|
||||
std::string Log(static_cast<std::size_t>(std::max(Length,1)),0);
|
||||
glGetShaderInfoLog(Shader,Length,nullptr,Log.data());
|
||||
std::fprintf(stderr,"[OffscreenGL] shader compile failed: %s\n",Log.c_str());
|
||||
glDeleteShader(Shader);return 0;
|
||||
}
|
||||
return Shader;
|
||||
}
|
||||
|
||||
GLuint MakeProgram()
|
||||
{
|
||||
static constexpr char VertexSource[]=R"GLSL(#version 300 es
|
||||
precision highp float;
|
||||
layout(location=0) in vec3 inCameraPosition;
|
||||
layout(location=1) in vec3 inNormal;
|
||||
layout(location=2) in float inHeight;
|
||||
out vec3 vNormal;
|
||||
out float vHeight;
|
||||
uniform float uTanHalfHorizontal;
|
||||
uniform float uTanHalfVertical;
|
||||
uniform float uNear;
|
||||
uniform float uFar;
|
||||
void main()
|
||||
{
|
||||
float z=max(inCameraPosition.z,0.0001);
|
||||
float clipZ=((uFar+uNear)/(uFar-uNear))*z-(2.0*uFar*uNear)/(uFar-uNear);
|
||||
gl_Position=vec4(inCameraPosition.x/uTanHalfHorizontal,
|
||||
inCameraPosition.y/uTanHalfVertical,clipZ,z);
|
||||
vNormal=normalize(inNormal);
|
||||
vHeight=inHeight;
|
||||
})GLSL";
|
||||
static constexpr char FragmentSource[]=R"GLSL(#version 300 es
|
||||
precision highp float;
|
||||
in vec3 vNormal;
|
||||
in float vHeight;
|
||||
out vec4 outColor;
|
||||
void main()
|
||||
{
|
||||
vec3 n=normalize(vNormal);
|
||||
vec3 normalColor=abs(n)*0.78+vec3(0.12);
|
||||
float band=mod(floor((vHeight+10000.0)/80.0),2.0);
|
||||
vec3 bandColor=mix(vec3(0.24,0.31,0.37),vec3(0.39,0.42,0.35),band);
|
||||
outColor=vec4(mix(bandColor,normalColor,0.68),1.0);
|
||||
})GLSL";
|
||||
const GLuint VS=CompileShader(GL_VERTEX_SHADER,VertexSource);
|
||||
const GLuint FS=CompileShader(GL_FRAGMENT_SHADER,FragmentSource);
|
||||
if(!VS||!FS)return 0;
|
||||
const GLuint Program=glCreateProgram();
|
||||
glAttachShader(Program,VS);glAttachShader(Program,FS);glLinkProgram(Program);
|
||||
glDeleteShader(VS);glDeleteShader(FS);
|
||||
GLint Success=GL_FALSE;glGetProgramiv(Program,GL_LINK_STATUS,&Success);
|
||||
if(Success==GL_FALSE)
|
||||
{
|
||||
GLint Length=0;glGetProgramiv(Program,GL_INFO_LOG_LENGTH,&Length);
|
||||
std::string Log(static_cast<std::size_t>(std::max(Length,1)),0);
|
||||
glGetProgramInfoLog(Program,Length,nullptr,Log.data());
|
||||
std::fprintf(stderr,"[OffscreenGL] program link failed: %s\n",Log.c_str());
|
||||
glDeleteProgram(Program);return 0;
|
||||
}
|
||||
return Program;
|
||||
}
|
||||
|
||||
bool WritePng(const std::filesystem::path& Path,int Width,int Height,
|
||||
const std::vector<std::uint8_t>& Pixels)
|
||||
{
|
||||
std::error_code Error;
|
||||
if(!Path.parent_path().empty())std::filesystem::create_directories(Path.parent_path(),Error);
|
||||
if(Error)return false;
|
||||
FILE* File=std::fopen(Path.c_str(),"wb");
|
||||
if(!File)return false;
|
||||
png_structp Png=png_create_write_struct(PNG_LIBPNG_VER_STRING,nullptr,nullptr,nullptr);
|
||||
png_infop Info=Png?png_create_info_struct(Png):nullptr;
|
||||
if(!Png||!Info){if(Png)png_destroy_write_struct(&Png,nullptr);std::fclose(File);return false;}
|
||||
if(setjmp(png_jmpbuf(Png)))
|
||||
{png_destroy_write_struct(&Png,&Info);std::fclose(File);return false;}
|
||||
png_init_io(Png,File);
|
||||
png_set_IHDR(Png,Info,static_cast<png_uint_32>(Width),static_cast<png_uint_32>(Height),
|
||||
8,PNG_COLOR_TYPE_RGBA,PNG_INTERLACE_NONE,PNG_COMPRESSION_TYPE_DEFAULT,PNG_FILTER_TYPE_DEFAULT);
|
||||
png_write_info(Png,Info);
|
||||
const std::size_t Stride=static_cast<std::size_t>(Width)*4;
|
||||
for(int Y=Height-1;Y>=0;--Y)
|
||||
png_write_row(Png,const_cast<png_bytep>(Pixels.data()+static_cast<std::size_t>(Y)*Stride));
|
||||
png_write_end(Png,nullptr);
|
||||
png_destroy_write_struct(&Png,&Info);
|
||||
const bool Good=std::fclose(File)==0;
|
||||
return Good;
|
||||
}
|
||||
|
||||
bool Render(FScene& Scene,const std::filesystem::path& Output,int Width,int Height,
|
||||
double HorizontalFovDegrees)
|
||||
{
|
||||
EGLDisplay Display=MakeDisplay();
|
||||
EGLint Major=0,Minor=0;
|
||||
if(Display==EGL_NO_DISPLAY||eglInitialize(Display,&Major,&Minor)!=EGL_TRUE)
|
||||
{std::fprintf(stderr,"[OffscreenEGL] eglInitialize failed (0x%x)\n",eglGetError());return false;}
|
||||
if(eglBindAPI(EGL_OPENGL_ES_API)!=EGL_TRUE)
|
||||
{std::fprintf(stderr,"[OffscreenEGL] eglBindAPI failed (0x%x)\n",eglGetError());eglTerminate(Display);return false;}
|
||||
const EGLint ConfigAttributes[]={EGL_SURFACE_TYPE,EGL_PBUFFER_BIT,
|
||||
EGL_RENDERABLE_TYPE,EGL_OPENGL_ES3_BIT,EGL_RED_SIZE,8,EGL_GREEN_SIZE,8,
|
||||
EGL_BLUE_SIZE,8,EGL_ALPHA_SIZE,8,EGL_DEPTH_SIZE,24,EGL_NONE};
|
||||
EGLConfig Config{};EGLint ConfigCount=0;
|
||||
if(eglChooseConfig(Display,ConfigAttributes,&Config,1,&ConfigCount)!=EGL_TRUE||ConfigCount==0)
|
||||
{std::fprintf(stderr,"[OffscreenEGL] no GLES3 pbuffer config (0x%x)\n",eglGetError());eglTerminate(Display);return false;}
|
||||
const EGLint SurfaceAttributes[]={EGL_WIDTH,Width,EGL_HEIGHT,Height,EGL_NONE};
|
||||
EGLSurface Surface=eglCreatePbufferSurface(Display,Config,SurfaceAttributes);
|
||||
const EGLint ContextAttributes[]={EGL_CONTEXT_CLIENT_VERSION,3,EGL_NONE};
|
||||
EGLContext Context=eglCreateContext(Display,Config,EGL_NO_CONTEXT,ContextAttributes);
|
||||
if(Surface==EGL_NO_SURFACE||Context==EGL_NO_CONTEXT
|
||||
||eglMakeCurrent(Display,Surface,Surface,Context)!=EGL_TRUE)
|
||||
{std::fprintf(stderr,"[OffscreenEGL] context/surface creation failed (0x%x)\n",eglGetError());
|
||||
if(Context!=EGL_NO_CONTEXT)eglDestroyContext(Display,Context);
|
||||
if(Surface!=EGL_NO_SURFACE)eglDestroySurface(Display,Surface);
|
||||
eglTerminate(Display);return false;}
|
||||
|
||||
const GLubyte* Renderer=glGetString(GL_RENDERER);
|
||||
const GLubyte* GlVersion=glGetString(GL_VERSION);
|
||||
std::printf("[OffscreenEGL] renderer=%s version=%s pbuffer=%dx%d\n",
|
||||
Renderer?reinterpret_cast<const char*>(Renderer):"unknown",
|
||||
GlVersion?reinterpret_cast<const char*>(GlVersion):"unknown",Width,Height);
|
||||
GLuint ColorTexture=0,DepthBuffer=0,Framebuffer=0;
|
||||
glGenTextures(1,&ColorTexture);glBindTexture(GL_TEXTURE_2D,ColorTexture);
|
||||
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,Width,Height,0,GL_RGBA,GL_UNSIGNED_BYTE,nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
|
||||
glGenRenderbuffers(1,&DepthBuffer);glBindRenderbuffer(GL_RENDERBUFFER,DepthBuffer);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER,GL_DEPTH_COMPONENT24,Width,Height);
|
||||
glGenFramebuffers(1,&Framebuffer);glBindFramebuffer(GL_FRAMEBUFFER,Framebuffer);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,ColorTexture,0);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_RENDERBUFFER,DepthBuffer);
|
||||
if(glCheckFramebufferStatus(GL_FRAMEBUFFER)!=GL_FRAMEBUFFER_COMPLETE)
|
||||
{std::fprintf(stderr,"[OffscreenGL] framebuffer is incomplete\n");return false;}
|
||||
|
||||
constexpr double Pi=3.14159265358979323846;
|
||||
const double Yaw=Scene.YawDegrees*Pi/180.0,Pitch=Scene.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)};
|
||||
const std::array<double,3> Right={std::cos(Yaw),-std::sin(Yaw),0.0};
|
||||
const std::array<double,3> Up={Right[1]*Forward[2]-Right[2]*Forward[1],
|
||||
Right[2]*Forward[0]-Right[0]*Forward[2],Right[0]*Forward[1]-Right[1]*Forward[0]};
|
||||
const double TanHalfHorizontal=std::tan(HorizontalFovDegrees*Pi/360.0);
|
||||
const double Aspect=static_cast<double>(Width)/Height;
|
||||
const double TanHalfVertical=TanHalfHorizontal/Aspect;
|
||||
const double Near=0.1,Far=20000.0;
|
||||
for(FVertex& Vertex:Scene.Vertices)
|
||||
{
|
||||
const double DX=Vertex.Position[0]-Scene.CameraMeters[0];
|
||||
const double DY=Vertex.Position[1]-Scene.CameraMeters[1];
|
||||
const double DZ=Vertex.Position[2]-Scene.CameraMeters[2];
|
||||
const float CameraX=static_cast<float>(DX*Right[0]+DY*Right[1]+DZ*Right[2]);
|
||||
const float CameraY=static_cast<float>(DX*Up[0]+DY*Up[1]+DZ*Up[2]);
|
||||
const float CameraZ=static_cast<float>(DX*Forward[0]+DY*Forward[1]+DZ*Forward[2]);
|
||||
Vertex.Position[0]=CameraX;Vertex.Position[1]=CameraY;Vertex.Position[2]=CameraZ;
|
||||
}
|
||||
const GLuint Program=MakeProgram();
|
||||
if(!Program)return false;
|
||||
GLuint Vao=0,Vbo=0,Ebo=0;
|
||||
glGenVertexArrays(1,&Vao);glBindVertexArray(Vao);
|
||||
glGenBuffers(1,&Vbo);glBindBuffer(GL_ARRAY_BUFFER,Vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER,static_cast<GLsizeiptr>(Scene.Vertices.size()*sizeof(FVertex)),
|
||||
Scene.Vertices.data(),GL_STATIC_DRAW);
|
||||
glGenBuffers(1,&Ebo);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,Ebo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER,static_cast<GLsizeiptr>(Scene.Indices.size()*sizeof(std::uint32_t)),
|
||||
Scene.Indices.data(),GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
||||
reinterpret_cast<void*>(offsetof(FVertex,Position)));
|
||||
glEnableVertexAttribArray(1);glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
||||
reinterpret_cast<void*>(offsetof(FVertex,Normal)));
|
||||
glEnableVertexAttribArray(2);glVertexAttribPointer(2,1,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
||||
reinterpret_cast<void*>(offsetof(FVertex,Height)));
|
||||
glViewport(0,0,Width,Height);glDisable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LEQUAL);glClearColor(0.035f,0.055f,0.075f,1.0f);
|
||||
glClearDepthf(1.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
|
||||
glUseProgram(Program);
|
||||
glUniform1f(glGetUniformLocation(Program,"uTanHalfHorizontal"),static_cast<float>(TanHalfHorizontal));
|
||||
glUniform1f(glGetUniformLocation(Program,"uTanHalfVertical"),static_cast<float>(TanHalfVertical));
|
||||
glUniform1f(glGetUniformLocation(Program,"uNear"),static_cast<float>(Near));
|
||||
glUniform1f(glGetUniformLocation(Program,"uFar"),static_cast<float>(Far));
|
||||
glDrawElements(GL_TRIANGLES,static_cast<GLsizei>(Scene.Indices.size()),GL_UNSIGNED_INT,nullptr);
|
||||
glFinish();
|
||||
std::vector<std::uint8_t> Pixels(static_cast<std::size_t>(Width)*Height*4);
|
||||
glReadPixels(0,0,Width,Height,GL_RGBA,GL_UNSIGNED_BYTE,Pixels.data());
|
||||
const GLenum GlError=glGetError();
|
||||
const bool PngGood=GlError==GL_NO_ERROR&&WritePng(Output,Width,Height,Pixels);
|
||||
if(!PngGood)std::fprintf(stderr,"[OffscreenGL] render or PNG write failed (GL=0x%x)\n",GlError);
|
||||
else
|
||||
{
|
||||
std::error_code SizeError;
|
||||
const auto Bytes=std::filesystem::file_size(Output,SizeError);
|
||||
std::printf("[OffscreenCapture] triangles=%llu tiles=%u skirts=%llu png_bytes=%llu status=PASS path=%s\n",
|
||||
static_cast<unsigned long long>(Scene.Triangles),Scene.TileCount,
|
||||
static_cast<unsigned long long>(Scene.SkirtTriangles),
|
||||
static_cast<unsigned long long>(SizeError?0:Bytes),Output.string().c_str());
|
||||
}
|
||||
glDeleteProgram(Program);glDeleteBuffers(1,&Ebo);glDeleteBuffers(1,&Vbo);
|
||||
glDeleteVertexArrays(1,&Vao);glDeleteFramebuffers(1,&Framebuffer);
|
||||
glDeleteRenderbuffers(1,&DepthBuffer);glDeleteTextures(1,&ColorTexture);
|
||||
eglMakeCurrent(Display,EGL_NO_SURFACE,EGL_NO_SURFACE,EGL_NO_CONTEXT);
|
||||
eglDestroyContext(Display,Context);eglDestroySurface(Display,Surface);eglTerminate(Display);
|
||||
return PngGood;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc,char** argv)
|
||||
{
|
||||
if(argc<3)
|
||||
{
|
||||
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N]\n",argv[0]);
|
||||
return 2;
|
||||
}
|
||||
int Width=2560,Height=1440;double Fov=90.0;
|
||||
bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0;
|
||||
bool HasCamera=false;std::array<double,3> Camera{};
|
||||
for(int I=3;I<argc;++I)
|
||||
{
|
||||
const std::string_view Arg(argv[I]);
|
||||
if(Arg.starts_with("--size="))
|
||||
{
|
||||
const std::string Value(Arg.substr(7));
|
||||
if(std::sscanf(Value.c_str(),"%dx%d",&Width,&Height)!=2||Width<=0||Height<=0)
|
||||
{std::fprintf(stderr,"Invalid image size\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--fov-deg="))
|
||||
{
|
||||
char* End=nullptr;const std::string Value(Arg.substr(10));Fov=std::strtod(Value.c_str(),&End);
|
||||
if(End==Value.c_str()||*End!='\0'||Fov<=1.0||Fov>=179.0)
|
||||
{std::fprintf(stderr,"Invalid horizontal FOV\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--camera-m="))
|
||||
{
|
||||
const std::string Value(Arg.substr(11));
|
||||
if(std::sscanf(Value.c_str(),"%lf,%lf,%lf",&Camera[0],&Camera[1],&Camera[2])!=3
|
||||
||!std::isfinite(Camera[0])||!std::isfinite(Camera[1])||!std::isfinite(Camera[2]))
|
||||
{std::fprintf(stderr,"Invalid camera position\n");return 2;}
|
||||
HasCamera=true;
|
||||
}
|
||||
else if(Arg.starts_with("--yaw-deg=")||Arg.starts_with("--pitch-deg="))
|
||||
{
|
||||
const bool IsYaw=Arg.starts_with("--yaw-deg=");
|
||||
const std::size_t Prefix=IsYaw?10:12;
|
||||
const std::string Value(Arg.substr(Prefix));char* End=nullptr;
|
||||
const double Parsed=std::strtod(Value.c_str(),&End);
|
||||
if(End==Value.c_str()||*End!='\0'||!std::isfinite(Parsed))
|
||||
{std::fprintf(stderr,"Invalid camera angle\n");return 2;}
|
||||
if(IsYaw){Yaw=Parsed;HasYaw=true;}else{Pitch=Parsed;HasPitch=true;}
|
||||
}
|
||||
else {std::fprintf(stderr,"Unknown option: %.*s\n",static_cast<int>(Arg.size()),Arg.data());return 2;}
|
||||
}
|
||||
FScene Scene;std::string Error;
|
||||
if(!LoadScene(argv[1],Scene,Error))
|
||||
{std::fprintf(stderr,"[OffscreenCapture] FAIL: %s\n",Error.c_str());return 2;}
|
||||
if(HasCamera)Scene.CameraMeters=Camera;
|
||||
if(HasYaw)Scene.YawDegrees=Yaw;
|
||||
if(HasPitch)Scene.PitchDegrees=Pitch;
|
||||
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f vertices=%zu triangles=%llu\n",
|
||||
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
|
||||
Scene.YawDegrees,Scene.PitchDegrees,Scene.Vertices.size(),
|
||||
static_cast<unsigned long long>(Scene.Triangles));
|
||||
return Render(Scene,argv[2],Width,Height,Fov)?0:3;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,721 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace
|
||||
bool ProfileColumnOverlap=false;
|
||||
bool ForceGenerate=false;
|
||||
bool MeshCoverage=false;
|
||||
bool ScreenSpaceGate=false;
|
||||
bool HasSeedOverride=false;
|
||||
int SeedOverride=0;
|
||||
bool HasRenderDistanceOverride=false;
|
||||
@@ -48,6 +49,9 @@ namespace
|
||||
bool HasCamera=false;
|
||||
bool AutoSurfaceCamera=false;
|
||||
FVector CameraMeters=FVector::ZeroVector;
|
||||
float CameraYawDegrees=0.0f;
|
||||
float CameraPitchDegrees=0.0f;
|
||||
std::filesystem::path MeshExportPath;
|
||||
};
|
||||
|
||||
bool ParseInteger(std::string_view text,int& output)
|
||||
@@ -61,13 +65,15 @@ namespace
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseCamera(std::string_view text,FVector& meters)
|
||||
bool ParseCamera(std::string_view text,FVector& meters,float& yawDegrees,float& pitchDegrees)
|
||||
{
|
||||
const std::string values(text);
|
||||
float x=0.0f,y=0.0f,z=0.0f,yaw=0.0f,pitch=0.0f;
|
||||
if(std::sscanf(values.c_str(),"%f,%f,%f,%f,%f",&x,&y,&z,&yaw,&pitch)!=5)
|
||||
return false;
|
||||
meters={x,y,z};
|
||||
yawDegrees=yaw;
|
||||
pitchDegrees=pitch;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -85,10 +91,12 @@ namespace
|
||||
std::printf("Usage: %s [owner_world.json] --world-assembler-prototype --stream-benchmark --stream-pipeline [options]\n"
|
||||
" --workers=N --clip-radius=N --max-clip-level=N --max-level=N\n"
|
||||
" --camera=X,Y,Z,YAW,PITCH (position in metres) --stream-hash\n"
|
||||
" --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n"
|
||||
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
||||
" --seed=N --recipe=surface-world|floating-islands\n"
|
||||
" --auto-surface-camera (place the 27-tile cube on this seed's surface)\n"
|
||||
" --mesh-coverage (report non-empty tiles and indexed triangles)\n"
|
||||
" --screen-space-gate (measure display error and historical negative controls)\n"
|
||||
" --render-distance-chunks=N (0 disables the extra far ring)\n"
|
||||
" --tls-probe=N (Linux worker TLS/stack measurement; N=0..12)\n"
|
||||
" --tls-probe-touch=none|surface-world|floating-islands (first-use probe)\n"
|
||||
@@ -103,6 +111,7 @@ namespace
|
||||
else if(arg=="--profile-column-overlap")options.ProfileColumnOverlap=true;
|
||||
else if(arg=="--force-generate")options.ForceGenerate=true;
|
||||
else if(arg=="--mesh-coverage")options.MeshCoverage=true;
|
||||
else if(arg=="--screen-space-gate")options.ScreenSpaceGate=true;
|
||||
else if(arg=="--auto-surface-camera")options.AutoSurfaceCamera=true;
|
||||
else if(arg.starts_with("--recipe="))
|
||||
{
|
||||
@@ -135,10 +144,17 @@ namespace
|
||||
}
|
||||
else if(arg.starts_with("--camera="))
|
||||
{
|
||||
if(!ParseCamera(arg.substr(9),options.CameraMeters))
|
||||
if(!ParseCamera(arg.substr(9),options.CameraMeters,
|
||||
options.CameraYawDegrees,options.CameraPitchDegrees))
|
||||
{std::fprintf(stderr,"Invalid --camera value: %.*s\n",static_cast<int>(arg.size()),arg.data());return false;}
|
||||
options.HasCamera=true;
|
||||
}
|
||||
else if(arg.starts_with("--mesh-export="))
|
||||
{
|
||||
options.MeshExportPath=std::filesystem::path(arg.substr(14));
|
||||
if(options.MeshExportPath.empty())
|
||||
{std::fprintf(stderr,"--mesh-export needs a file path\n");return false;}
|
||||
}
|
||||
else if(arg.starts_with("--workers="))
|
||||
{if(!readInt("--workers=",options.WorkerCount))return false;}
|
||||
else if(arg.starts_with("--clip-radius="))
|
||||
@@ -297,5 +313,6 @@ int main(int argc,char** argv)
|
||||
cameraVoxels,options.WorkerCount,options.MaxLevel,VoxelForgeCore::DetailVariant::coherent,
|
||||
scheduler,options.StreamHash,false,false,false,false,coldStart,
|
||||
options.ProfileColumnOverlap,options.FarHorizonLOD8InteriorStepVoxels,
|
||||
options.MeshCoverage);
|
||||
options.MeshCoverage,options.MeshExportPath,options.CameraYawDegrees,
|
||||
options.CameraPitchDegrees,options.ScreenSpaceGate);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "OwnerWorldLoader.h"
|
||||
#include "StreamingWorld.h"
|
||||
#include "LinuxScreenSpaceGate.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -9,6 +10,8 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
@@ -17,6 +20,76 @@
|
||||
|
||||
namespace StandaloneStreamBenchmark
|
||||
{
|
||||
struct FSceneExportWriter
|
||||
{
|
||||
std::ofstream File;
|
||||
bool bGood=true;
|
||||
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)
|
||||
{
|
||||
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;
|
||||
constexpr char Magic[8]={'V','F','M','S','H','0','0','1'};
|
||||
File.write(Magic,sizeof(Magic));
|
||||
const std::uint32_t Version=1;
|
||||
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(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)
|
||||
@@ -62,10 +135,26 @@ namespace StandaloneStreamBenchmark
|
||||
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)
|
||||
int32 FarHorizonLOD8InteriorStepVoxels,bool bReportMeshCoverage=false,
|
||||
const std::filesystem::path& MeshExportPath={},float CameraYawDegrees=0.0f,
|
||||
float CameraPitchDegrees=0.0f,bool bScreenSpaceGate=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);
|
||||
@@ -154,6 +243,7 @@ namespace StandaloneStreamBenchmark
|
||||
FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
|
||||
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap,
|
||||
FarHorizonLOD8InteriorStepVoxels);
|
||||
if(bScreenSpaceGate)Streamer.SetMesherStageProfilingEnabled(true);
|
||||
const auto StreamStart=Clock::now();
|
||||
const FStandaloneStreamingUpdate Update=Streamer.StartWithDesired(Desired);
|
||||
if(Update.DesiredTiles!=static_cast<int32>(Desired.Tiles.size()))
|
||||
@@ -162,6 +252,22 @@ namespace StandaloneStreamBenchmark
|
||||
std::fprintf(stderr,"[StreamPipeline] expected %zu tiles 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));
|
||||
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;
|
||||
int32 Finished=0,NeighborhoodFinished=0,FullDetailFinished=0;
|
||||
@@ -196,6 +302,8 @@ namespace StandaloneStreamBenchmark
|
||||
std::array<double,9> ColumnEvalMsByLOD{},MarchingExclusiveMsByLOD{};
|
||||
std::array<double,9> GroundRefineExclusiveMsByLOD{},CapRefineExclusiveMsByLOD{};
|
||||
std::array<double,9> MeshAssemblyMsByLOD{};
|
||||
std::array<double,9> SkirtMsByLOD{};
|
||||
std::array<std::uint64_t,9> SkirtTrianglesByLOD{};
|
||||
std::array<std::uint64_t,9> TileCountByLOD{};
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
if(Tile.Level>=0&&Tile.Level<static_cast<int32>(ExpectedTilesByLOD.size()))
|
||||
@@ -214,6 +322,9 @@ namespace StandaloneStreamBenchmark
|
||||
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 NeighborhoodReady{},FullDetailReady{},WholeViewReady{};
|
||||
bool bNeighborhoodReported=false,bFullDetailReported=false;
|
||||
while(Finished<Update.DesiredTiles)
|
||||
@@ -222,6 +333,7 @@ namespace StandaloneStreamBenchmark
|
||||
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
|
||||
for(const FStandaloneCpuTile& Tile:Ready)
|
||||
{
|
||||
if(bExportRequested)SceneExport.Add(Tile);
|
||||
++Finished;
|
||||
if(bReportMeshCoverage)
|
||||
{
|
||||
@@ -239,6 +351,23 @@ namespace StandaloneStreamBenchmark
|
||||
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(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()))
|
||||
@@ -321,6 +450,28 @@ namespace StandaloneStreamBenchmark
|
||||
}
|
||||
}
|
||||
Streamer.Stop();
|
||||
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;
|
||||
}
|
||||
bool bColumnOverlapCapturePassing=true;
|
||||
if(bProfileColumnOverlap)
|
||||
{
|
||||
@@ -341,10 +492,12 @@ namespace StandaloneStreamBenchmark
|
||||
}
|
||||
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 mesh_assembly_ms=%.3f\n",
|
||||
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],MeshAssemblyMsByLOD[LOD]);
|
||||
GroundRefineExclusiveMsByLOD[LOD],CapRefineExclusiveMsByLOD[LOD],
|
||||
SkirtMsByLOD[LOD],static_cast<unsigned long long>(SkirtTrianglesByLOD[LOD]),
|
||||
MeshAssemblyMsByLOD[LOD]);
|
||||
}
|
||||
const double NeighborhoodSeconds=std::chrono::duration<double>(NeighborhoodReady-ColdStart).count();
|
||||
const double NeighborhoodMeshSeconds=std::chrono::duration<double>(NeighborhoodReady-ShapeReady).count();
|
||||
@@ -393,9 +546,59 @@ namespace StandaloneStreamBenchmark
|
||||
static_cast<unsigned long long>(HashTileRows(MeshRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(PayloadRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(TopologyRows)));
|
||||
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)?0:6;
|
||||
&&(!bProfileColumnOverlap||bColumnOverlapCapturePassing)
|
||||
&&(!bExportRequested||bSceneExportPassing)&&bScreenSpaceGatePassing?0:6;
|
||||
}
|
||||
} // namespace StandaloneStreamBenchmark
|
||||
@@ -646,6 +646,16 @@ public:
|
||||
int32 GetWorkerCount()const{return WorkerCount;}
|
||||
uint64 GetCacheHits()const{return CacheHitCount.load(std::memory_order_relaxed);}
|
||||
|
||||
void SetMesherStageProfilingEnabled(bool Enabled)
|
||||
{bProfileMesherStages=Enabled;}
|
||||
FStandaloneCpuTile GenerateDiagnostic(const FVoxelTileKey& Key,
|
||||
std::uint32_t GlobalOctaveDrop=0,std::uint32_t CellStride=1,
|
||||
bool ForceGenerate=false)
|
||||
{
|
||||
FStandaloneTileJob Job;Job.Key=Key;Job.StartedAt=FClock::now();Job.QueuedAt=Job.StartedAt;
|
||||
return Generate(Job,GlobalOctaveDrop,CellStride,ForceGenerate);
|
||||
}
|
||||
|
||||
private:
|
||||
FStandaloneStreamingUpdate UpdateDesiredSet(FStandaloneDesiredTileSet NewDesired,
|
||||
const std::vector<FVoxelTileKey>& ResidentKeys,uint64 CrossingIndex)
|
||||
@@ -761,12 +771,14 @@ private:
|
||||
Wake.notify_all();
|
||||
return Stats;
|
||||
}
|
||||
FStandaloneCpuTile Generate(const FStandaloneTileJob& Job)
|
||||
FStandaloneCpuTile Generate(const FStandaloneTileJob& Job,
|
||||
std::uint32_t DiagnosticGlobalOctaveDrop=0,std::uint32_t DiagnosticCellStride=1,
|
||||
bool ForceGenerate=false)
|
||||
{
|
||||
const auto StartTime=Job.StartedAt;
|
||||
FStandaloneCpuTile Out;Out.Key=Job.Key;Out.RequestedCrossing=Job.RequestedCrossing;
|
||||
Out.QueuedAt=Job.QueuedAt;Out.StartedAt=Job.StartedAt;
|
||||
if(MeshCache)
|
||||
if(MeshCache&&!ForceGenerate)
|
||||
{
|
||||
if(const FStandaloneCpuTile* Cached=MeshCache->Find(Job.Key))
|
||||
{
|
||||
@@ -805,6 +817,11 @@ private:
|
||||
Options.skipMaterialGroups=bSkipMaterialGroups;Options.skipUv=bSkipUV;Options.skipColors=bSkipColors;
|
||||
Options.captureColumnCoordinates=bCaptureColumnCoordinates;
|
||||
Options.lod8HorizonInteriorStepVoxels=Lod8HorizonInteriorStepVoxels;
|
||||
Options.profileMesherStages=bProfileMesherStages&&!ForceGenerate;
|
||||
Options.diagnosticGlobalOctaveDrop=DiagnosticGlobalOctaveDrop;
|
||||
Options.diagnosticCellStride=std::max<std::uint32_t>(1,DiagnosticCellStride);
|
||||
Options.diagnosticColumnInterpolationStride=
|
||||
std::max<std::uint32_t>(1,DiagnosticCellStride);
|
||||
std::string Error;
|
||||
auto Result=CoreWorld->generateTile(
|
||||
{{Job.Key.Coord.X,Job.Key.Coord.Y,Job.Key.Coord.Z},Job.Key.Level},Options,Error);
|
||||
@@ -977,6 +994,7 @@ private:
|
||||
bool bSkipColors=false;
|
||||
bool bCaptureColumnCoordinates=false;
|
||||
int32 Lod8HorizonInteriorStepVoxels=0;
|
||||
bool bProfileMesherStages=false;
|
||||
std::atomic<uint64> CacheHitCount{0};
|
||||
mutable std::mutex Mutex;
|
||||
std::condition_variable Wake;
|
||||
|
||||
@@ -1768,6 +1768,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
const int32 GroundSurfaceTriangleCount=GroundTris.Num()/3;
|
||||
const int32 CapSurfaceTriangleCount=CapTris.Num()/3;
|
||||
TArray<uint8> GroundSkirtFaceMasks,CapSkirtFaceMasks;
|
||||
const uint64 SkirtStageStartCycles=OutStageTimings?FPlatformTime::Cycles64():0;
|
||||
if (bGenerateSkirts && (GroundTris.Num() + CapTris.Num()) > 0)
|
||||
{
|
||||
const float ExtentCm = (float)(CellsPerAxis * Step) * GeometryVoxelSize;
|
||||
@@ -2007,6 +2008,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
EmitSkirts(GroundTris,GroundSkirtFaceMasks);
|
||||
EmitSkirts(CapTris,CapSkirtFaceMasks);
|
||||
}
|
||||
if(OutStageTimings)
|
||||
OutStageTimings->SkirtMilliseconds=static_cast<double>(
|
||||
FPlatformTime::Cycles64()-SkirtStageStartCycles)
|
||||
*FPlatformTime::GetSecondsPerCycle64()*1000.0;
|
||||
|
||||
// F17 — concatène sol PUIS sky-cap : un run d'indices contigu par polygroup (voir
|
||||
// FVoxelMeshData::NumCeilingTriangles). Mêmes triangles qu'avant, seul l'ordre change.
|
||||
@@ -2059,6 +2064,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
AuditInterpolationCycles,AuditInterpolationSamples,SurfaceCellCalls);
|
||||
OutStageTimings->VertexDedupStreamMilliseconds=ScaleSampledCycles(
|
||||
AuditDedupStreamCycles,AuditDedupStreamSamples,SurfaceCellCalls);
|
||||
OutStageTimings->SkirtMilliseconds=std::max(0.0,
|
||||
OutStageTimings->SkirtMilliseconds);
|
||||
const double TotalMilliseconds=static_cast<double>(
|
||||
FPlatformTime::Cycles64()-StageMesherStartCycles)*CycleToMs;
|
||||
OutStageTimings->OtherMilliseconds=FMath::Max(0.0,TotalMilliseconds
|
||||
@@ -2066,7 +2073,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
-OutStageTimings->CellClassificationMilliseconds
|
||||
-OutStageTimings->GradientNormalsMilliseconds
|
||||
-OutStageTimings->EdgeInterpolationMilliseconds
|
||||
-OutStageTimings->VertexDedupStreamMilliseconds);
|
||||
-OutStageTimings->VertexDedupStreamMilliseconds
|
||||
-OutStageTimings->SkirtMilliseconds);
|
||||
}
|
||||
|
||||
return MeshData;
|
||||
|
||||
@@ -50,6 +50,7 @@ struct FVoxelMarchingCubesStageTimings
|
||||
double GradientNormalsMilliseconds = 0.0;
|
||||
double EdgeInterpolationMilliseconds = 0.0;
|
||||
double VertexDedupStreamMilliseconds = 0.0;
|
||||
double SkirtMilliseconds = 0.0;
|
||||
double OtherMilliseconds = 0.0;
|
||||
uint64 GradientFallbackDensityCalls = 0;
|
||||
uint64 EdgeMidpointDensityCalls = 0;
|
||||
|
||||
Reference in new issue
Block a user