Files
VoxelForge/Experiments/LivingPlane/LivingPlane.cpp
T
Fr0zkaandClaude Opus 5.5 5b15595e4a experiment: LivingPlane - GPU wave-equation height field reacting to a moving sphere (L162)
Standalone EGL/OpenGL 4.3 compute runner (runtime GLSL, no shader compiler needed), ping-pong height/velocity field,
damped wave + foundation spring + Gaussian sphere push + optional breathing, vertex-displaced render to PNG.
Independent of generation. Intel ADL GT2: ~0.08 ms/step at 512^2, ~0.29 ms/step at 1024^2 (wall-time difference).
.gitignore now allows Experiments/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-11-02 10:35:37 +01:00

1137 lines
53 KiB
C++

#define GL_GLEXT_PROTOTYPES 1
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GL/glcorearb.h>
#include <png.h>
#include <algorithm>
#include <array>
#include <cerrno>
#include <cctype>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <limits>
#include <numeric>
#include <set>
#include <span>
#include <string>
#include <string_view>
#include <unistd.h>
#include <utility>
#include <vector>
namespace
{
constexpr double PlaneLengthMeters=12.0;
constexpr double WaveSpeedMetersPerSecond=4.5;
constexpr double DampingPerSecond=1.35;
constexpr double FoundationStiffnessPerSecondSquared=28.0;
constexpr double SphereRadiusMeters=0.43;
constexpr double SphereSigmaMeters=0.46;
constexpr double SpherePushMetersPerSecondSquared=14.0;
constexpr double Pi=3.14159265358979323846;
struct FOptions
{
std::string Device="auto";
std::filesystem::path Output="Saved/Linux/l162/living-plane";
int Size=128;
int Steps=360;
int CaptureEvery=1;
bool Breathing=false;
};
struct FDeviceCandidate
{
EGLDeviceEXT Handle=EGL_NO_DEVICE_EXT;
std::string Vendor;
std::string Extensions;
std::string RenderNode;
int Score=0;
};
struct FContext
{
EGLDisplay Display=EGL_NO_DISPLAY;
EGLSurface Surface=EGL_NO_SURFACE;
EGLContext Context=EGL_NO_CONTEXT;
EGLDeviceEXT Device=EGL_NO_DEVICE_EXT;
std::string DeviceLabel="surfaceless Mesa";
std::string RenderNode;
std::string ExecutionDevice;
std::string Error;
~FContext() { Reset(); }
void Reset()
{
if(Display!=EGL_NO_DISPLAY)
{
eglMakeCurrent(Display,EGL_NO_SURFACE,EGL_NO_SURFACE,EGL_NO_CONTEXT);
if(Context!=EGL_NO_CONTEXT)eglDestroyContext(Display,Context);
if(Surface!=EGL_NO_SURFACE)eglDestroySurface(Display,Surface);
eglTerminate(Display);
}
Display=EGL_NO_DISPLAY;
Surface=EGL_NO_SURFACE;
Context=EGL_NO_CONTEXT;
}
bool TryDisplay(EGLDisplay Candidate,const std::string& Label)
{
Reset();
if(Candidate==EGL_NO_DISPLAY)
{Error="eglGetPlatformDisplay returned EGL_NO_DISPLAY";return false;}
Display=Candidate;
EGLint Major=0,Minor=0;
if(eglInitialize(Display,&Major,&Minor)!=EGL_TRUE)
{Error="eglInitialize failed: "+EglError();Reset();return false;}
if(eglBindAPI(EGL_OPENGL_API)!=EGL_TRUE)
{Error="eglBindAPI(EGL_OPENGL_API) failed: "+EglError();Reset();return false;}
const EGLint ConfigAttributes[]={EGL_SURFACE_TYPE,EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE,EGL_OPENGL_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)
{Error="no desktop OpenGL pbuffer config: "+EglError();Reset();return false;}
const EGLint SurfaceAttributes[]={EGL_WIDTH,16,EGL_HEIGHT,16,EGL_NONE};
Surface=eglCreatePbufferSurface(Display,Config,SurfaceAttributes);
const EGLint ContextAttributes[]={EGL_CONTEXT_MAJOR_VERSION_KHR,4,
EGL_CONTEXT_MINOR_VERSION_KHR,3,EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR,
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR,EGL_NONE};
Context=eglCreateContext(Display,Config,EGL_NO_CONTEXT,ContextAttributes);
if(Surface==EGL_NO_SURFACE||Context==EGL_NO_CONTEXT
||eglMakeCurrent(Display,Surface,Surface,Context)!=EGL_TRUE)
{Error="OpenGL 4.3 context creation failed: "+EglError();Reset();return false;}
GLint GlMajor=0,GlMinor=0;
glGetIntegerv(GL_MAJOR_VERSION,&GlMajor);
glGetIntegerv(GL_MINOR_VERSION,&GlMinor);
if(GlMajor<4||(GlMajor==4&&GlMinor<3))
{Error="driver returned OpenGL "+std::to_string(GlMajor)+"."+
std::to_string(GlMinor)+", need 4.3";Reset();return false;}
const auto* Renderer=reinterpret_cast<const char*>(glGetString(GL_RENDERER));
if(!Renderer)
{Error="glGetString(GL_RENDERER) returned null";Reset();return false;}
if(IsSoftwareRenderer(Renderer))ExecutionDevice="cpu";
else ExecutionDevice="gpu";
DeviceLabel=Label;
return true;
}
static std::string EglError()
{
char Text[24]{};
std::snprintf(Text,sizeof(Text),"0x%04x",eglGetError());
return Text;
}
static std::string Lower(std::string_view Input)
{
std::string Result(Input);
std::transform(Result.begin(),Result.end(),Result.begin(),[](unsigned char C)
{return static_cast<char>(std::tolower(C));});
return Result;
}
static bool IsSoftwareRenderer(std::string_view Renderer)
{
const std::string LowerRenderer=Lower(Renderer);
return LowerRenderer.find("llvmpipe")!=std::string::npos
||LowerRenderer.find("softpipe")!=std::string::npos
||LowerRenderer.find("software rasterizer")!=std::string::npos;
}
};
std::string JsonEscape(std::string_view Text)
{
std::string Result;
for(const unsigned char C:Text)
{
switch(C)
{
case '"':Result+="\\\"";break;
case '\\':Result+="\\\\";break;
case '\n':Result+="\\n";break;
case '\r':Result+="\\r";break;
case '\t':Result+="\\t";break;
default:if(C<0x20)Result+=' ';else Result+=static_cast<char>(C);break;
}
}
return Result;
}
bool HasExtension(std::string_view Extensions,std::string_view Wanted)
{
std::size_t At=0;
while(At<Extensions.size())
{
while(At<Extensions.size()&&Extensions[At]==' ')++At;
const std::size_t End=Extensions.find(' ',At);
const std::string_view Part=Extensions.substr(At,
End==std::string_view::npos?Extensions.size()-At:End-At);
if(Part==Wanted)return true;
if(End==std::string_view::npos)break;
At=End+1;
}
return false;
}
std::vector<FDeviceCandidate> FindHardwareDevices(std::string& Error)
{
using TQueryDevices=PFNEGLQUERYDEVICESEXTPROC;
using TQueryDeviceString=PFNEGLQUERYDEVICESTRINGEXTPROC;
using TGetPlatformDisplay=PFNEGLGETPLATFORMDISPLAYEXTPROC;
auto QueryDevices=reinterpret_cast<TQueryDevices>(eglGetProcAddress("eglQueryDevicesEXT"));
auto QueryDeviceString=reinterpret_cast<TQueryDeviceString>(
eglGetProcAddress("eglQueryDeviceStringEXT"));
auto GetPlatformDisplay=reinterpret_cast<TGetPlatformDisplay>(
eglGetProcAddress("eglGetPlatformDisplayEXT"));
const auto* ClientExtensions=reinterpret_cast<const char*>(
eglQueryString(EGL_NO_DISPLAY,EGL_EXTENSIONS));
if(!QueryDevices||!QueryDeviceString||!GetPlatformDisplay||!ClientExtensions
||!HasExtension(ClientExtensions,"EGL_EXT_device_enumeration")
||!HasExtension(ClientExtensions,"EGL_EXT_platform_device"))
{Error="EGL device enumeration/platform-device extensions are unavailable";return {};}
EGLint Count=0;
if(QueryDevices(0,nullptr,&Count)!=EGL_TRUE||Count<=0)
{Error="eglQueryDevicesEXT found no EGL devices";return {};}
std::vector<EGLDeviceEXT> Devices(static_cast<std::size_t>(Count));
if(QueryDevices(Count,Devices.data(),&Count)!=EGL_TRUE)
{Error="eglQueryDevicesEXT failed while listing devices";return {};}
std::vector<FDeviceCandidate> Candidates;
for(EGLint I=0;I<Count;++I)
{
FDeviceCandidate Candidate;
Candidate.Handle=Devices[static_cast<std::size_t>(I)];
const auto* Vendor=QueryDeviceString(Candidate.Handle,EGL_VENDOR);
const auto* Extensions=QueryDeviceString(Candidate.Handle,EGL_EXTENSIONS);
if(Vendor)Candidate.Vendor=reinterpret_cast<const char*>(Vendor);
if(Extensions)Candidate.Extensions=reinterpret_cast<const char*>(Extensions);
if(!HasExtension(Candidate.Extensions,"EGL_EXT_device_drm_render_node"))continue;
const auto* Node=QueryDeviceString(Candidate.Handle,EGL_DRM_RENDER_NODE_FILE_EXT);
if(Node)Candidate.RenderNode=reinterpret_cast<const char*>(Node);
if(Candidate.RenderNode.empty()
||access(Candidate.RenderNode.c_str(),R_OK|W_OK)!=0)continue;
const std::string VendorLower=FContext::Lower(Candidate.Vendor);
const bool IsIntel=VendorLower.find("intel")!=std::string::npos
||Candidate.RenderNode.find("renderD128")!=std::string::npos;
// This host's integrated Alder Lake device is exposed by Mesa as Intel
// on renderD128. EGL does not expose Vulkan's physical-device-type enum.
Candidate.Score=IsIntel?100:50;
Candidates.push_back(std::move(Candidate));
}
std::stable_sort(Candidates.begin(),Candidates.end(),[](const auto& A,const auto& B)
{return A.Score>B.Score;});
if(Candidates.empty()&&Error.empty())
Error="no accessible EGL DRM render node was found";
return Candidates;
}
bool CreateContext(const std::string& Request,FContext& Out)
{
if(Request=="cpu")
{
setenv("LIBGL_ALWAYS_SOFTWARE","1",1);
const auto GetPlatformDisplay=reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(
eglGetProcAddress("eglGetPlatformDisplayEXT"));
#ifdef EGL_PLATFORM_SURFACELESS_MESA
EGLDisplay Display=EGL_NO_DISPLAY;
if(GetPlatformDisplay)Display=GetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA,
EGL_DEFAULT_DISPLAY,nullptr);
if(Display==EGL_NO_DISPLAY)Display=eglGetDisplay(EGL_DEFAULT_DISPLAY);
if(!Out.TryDisplay(Display,"Mesa surfaceless (LIBGL_ALWAYS_SOFTWARE=1)"))
{std::cerr<<"[LivingPlane] CPU context failed: "<<Out.Error<<"\n";return false;}
#else
const EGLDisplay Display=eglGetDisplay(EGL_DEFAULT_DISPLAY);
if(!Out.TryDisplay(Display,"EGL default display (LIBGL_ALWAYS_SOFTWARE=1"))
{std::cerr<<"[LivingPlane] CPU context failed: "<<Out.Error<<"\n";return false;}
#endif
if(Out.ExecutionDevice!="cpu")
{std::cerr<<"[LivingPlane] --device=cpu selected a non-software renderer\n";return false;}
return true;
}
unsetenv("LIBGL_ALWAYS_SOFTWARE");
std::string EnumerationError;
const auto Candidates=FindHardwareDevices(EnumerationError);
if(Candidates.empty())
{
if(Request=="gpu")
{std::cerr<<"[LivingPlane] --device=gpu unavailable: "<<EnumerationError
<<" (or no accessible DRM render node)\n";return false;}
}
else
{
auto GetPlatformDisplay=reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(
eglGetProcAddress("eglGetPlatformDisplayEXT"));
for(const auto& Candidate:Candidates)
{
// Automatic selection is intentionally limited to this host's
// integrated Intel target; a discrete-only host falls back to CPU.
if(Request=="auto"&&Candidate.Score<100)continue;
EGLDisplay Display=GetPlatformDisplay(EGL_PLATFORM_DEVICE_EXT,
Candidate.Handle,nullptr);
const std::string Label=Candidate.Vendor+" "+Candidate.RenderNode;
if(Out.TryDisplay(Display,Label))
{
Out.Device=Candidate.Handle;
Out.RenderNode=Candidate.RenderNode;
if(Out.ExecutionDevice=="gpu")return true;
Out.Reset();
Out.Error="hardware EGL device produced a software renderer";
}
}
if(Request=="gpu")
{std::cerr<<"[LivingPlane] --device=gpu context failed: "<<Out.Error<<"\n";return false;}
}
setenv("LIBGL_ALWAYS_SOFTWARE","1",1);
auto GetPlatformDisplay=reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(
eglGetProcAddress("eglGetPlatformDisplayEXT"));
EGLDisplay SoftwareDisplay=EGL_NO_DISPLAY;
#ifdef EGL_PLATFORM_SURFACELESS_MESA
if(GetPlatformDisplay)SoftwareDisplay=GetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA,
EGL_DEFAULT_DISPLAY,nullptr);
#endif
if(SoftwareDisplay==EGL_NO_DISPLAY)SoftwareDisplay=eglGetDisplay(EGL_DEFAULT_DISPLAY);
if(!Out.TryDisplay(SoftwareDisplay,"Mesa surfaceless (LIBGL_ALWAYS_SOFTWARE=1)"))
{std::cerr<<"[LivingPlane] llvmpipe fallback failed: "<<Out.Error<<"\n";return false;}
if(Out.ExecutionDevice!="cpu")
{std::cerr<<"[LivingPlane] software fallback did not return a software renderer\n";return false;}
return true;
}
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::cerr<<"[LivingPlane] shader compilation failed:\n"<<Log<<"\n";
glDeleteShader(Shader);
return 0;
}
return Shader;
}
GLuint LinkProgram(std::span<const GLuint> Shaders)
{
const GLuint Program=glCreateProgram();
for(const GLuint Shader:Shaders)glAttachShader(Program,Shader);
glLinkProgram(Program);
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::cerr<<"[LivingPlane] program link failed:\n"<<Log<<"\n";
glDeleteProgram(Program);
return 0;
}
return Program;
}
GLuint MakeComputeProgram()
{
static constexpr char Source[]=R"GLSL(#version 430 core
layout(local_size_x=16,local_size_y=16) in;
layout(binding=0,rg32f) readonly uniform image2D uSource;
layout(binding=1,rg32f) writeonly uniform image2D uTarget;
uniform int uGridSize;
uniform float uDt;
uniform float uDx;
uniform float uPlaneLength;
uniform float uWaveSpeed;
uniform float uDamping;
uniform float uFoundationStiffness;
uniform float uTime;
uniform vec2 uForcePosition;
uniform float uForceSigma;
uniform float uForceStrength;
uniform int uBreathing;
void main()
{
ivec2 p=ivec2(gl_GlobalInvocationID.xy);
if(p.x>=uGridSize||p.y>=uGridSize)return;
ivec2 lo=ivec2(0);
ivec2 hi=ivec2(uGridSize-1);
float h=imageLoad(uSource,p).r;
float v=imageLoad(uSource,p).g;
float l=imageLoad(uSource,ivec2(max(p.x-1,lo.x),p.y)).r;
float r=imageLoad(uSource,ivec2(min(p.x+1,hi.x),p.y)).r;
float d=imageLoad(uSource,ivec2(p.x,max(p.y-1,lo.y))).r;
float u=imageLoad(uSource,ivec2(p.x,min(p.y+1,hi.y))).r;
float laplacian=(l+r+d+u-4.0*h)/(uDx*uDx);
vec2 world=(vec2(p)+vec2(0.5))*uDx-vec2(uPlaneLength*0.5);
vec2 offset=world-uForcePosition;
float sigma2=max(uForceSigma*uForceSigma,1.0e-8);
float interaction=-uForceStrength*exp(-dot(offset,offset)/(2.0*sigma2));
float breathing=0.0;
if(uBreathing!=0)
{
float spatial=sin(world.x*0.72)*sin(world.y*0.72);
breathing=2.4*spatial*sin(uTime*1.45);
}
float acceleration=uWaveSpeed*uWaveSpeed*laplacian-uFoundationStiffness*h
+interaction+breathing;
float nextVelocity=(v+acceleration*uDt)*exp(-uDamping*uDt);
float nextHeight=h+nextVelocity*uDt;
imageStore(uTarget,p,vec4(nextHeight,nextVelocity,0.0,0.0));
})GLSL";
const GLuint Shader=CompileShader(GL_COMPUTE_SHADER,Source);
if(!Shader)return 0;
const std::array<GLuint,1> Shaders={Shader};
const GLuint Program=LinkProgram(Shaders);
glDeleteShader(Shader);
return Program;
}
GLuint MakeRenderProgram()
{
static constexpr char VertexSource[]=R"GLSL(#version 430 core
layout(location=0) in vec3 aPosition;
layout(location=1) in vec2 aUv;
layout(location=2) in vec3 aNormal;
uniform sampler2D uField;
uniform int uGridSize;
uniform float uPlaneLength;
uniform int uRenderSphere;
uniform vec3 uSphereCenter;
uniform float uSphereRadius;
uniform mat4 uViewProjection;
out vec3 vWorld;
out vec3 vNormal;
out float vHeight;
flat out int vSphere;
void main()
{
vec3 world;
vec3 normal;
float height;
if(uRenderSphere==0)
{
ivec2 texel=clamp(ivec2(round(aUv*float(uGridSize-1))),ivec2(0),
ivec2(uGridSize-1));
ivec2 left=ivec2(max(texel.x-1,0),texel.y);
ivec2 right=ivec2(min(texel.x+1,uGridSize-1),texel.y);
ivec2 down=ivec2(texel.x,max(texel.y-1,0));
ivec2 up=ivec2(texel.x,min(texel.y+1,uGridSize-1));
height=texelFetch(uField,texel,0).r;
float dx=max(float(right.x-left.x)*uPlaneLength/float(uGridSize),1.0e-6);
float dy=max(float(up.y-down.y)*uPlaneLength/float(uGridSize),1.0e-6);
float dhdx=(texelFetch(uField,right,0).r-texelFetch(uField,left,0).r)/dx;
float dhdy=(texelFetch(uField,up,0).r-texelFetch(uField,down,0).r)/dy;
world=vec3((aUv-vec2(0.5))*uPlaneLength,height);
normal=normalize(vec3(-dhdx,-dhdy,1.0));
}
else
{
world=uSphereCenter+aPosition*uSphereRadius;
normal=normalize(aNormal);
height=world.z;
}
vWorld=world;
vNormal=normal;
vHeight=height;
vSphere=uRenderSphere;
gl_Position=uViewProjection*vec4(world,1.0);
})GLSL";
static constexpr char FragmentSource[]=R"GLSL(#version 430 core
in vec3 vWorld;
in vec3 vNormal;
in float vHeight;
flat in int vSphere;
uniform vec3 uCameraPosition;
out vec4 outColor;
void main()
{
vec3 n=normalize(vNormal);
vec3 l=normalize(vec3(-0.45,-0.55,0.72));
vec3 v=normalize(uCameraPosition-vWorld);
float diffuse=max(dot(n,l),0.0);
float spec=pow(max(dot(reflect(-l,n),v),0.0),vSphere==0?58.0:38.0);
if(vSphere!=0)
{
vec3 base=vec3(0.98,0.43,0.13);
float light=0.24+0.84*diffuse;
outColor=vec4(base*light+vec3(0.65,0.43,0.22)*spec,1.0);
}
else
{
float crest=clamp(0.50+vHeight*0.8,0.0,1.0);
vec3 deep=vec3(0.035,0.22,0.34);
vec3 crestColor=vec3(0.13,0.62,0.70);
vec3 base=mix(deep,crestColor,crest);
float light=0.28+0.88*diffuse;
outColor=vec4(base*light+vec3(0.31,0.62,0.68)*spec,1.0);
}
})GLSL";
const GLuint VS=CompileShader(GL_VERTEX_SHADER,VertexSource);
const GLuint FS=CompileShader(GL_FRAGMENT_SHADER,FragmentSource);
if(!VS||!FS)
{
if(VS)glDeleteShader(VS);
if(FS)glDeleteShader(FS);
return 0;
}
const std::array<GLuint,2> Shaders={VS,FS};
const GLuint Program=LinkProgram(Shaders);
glDeleteShader(VS);
glDeleteShader(FS);
return Program;
}
struct FVec3 { double X=0.0,Y=0.0,Z=0.0; };
FVec3 Sub(FVec3 A,FVec3 B){return {A.X-B.X,A.Y-B.Y,A.Z-B.Z};}
FVec3 Cross(FVec3 A,FVec3 B)
{return {A.Y*B.Z-A.Z*B.Y,A.Z*B.X-A.X*B.Z,A.X*B.Y-A.Y*B.X};}
double Dot(FVec3 A,FVec3 B){return A.X*B.X+A.Y*B.Y+A.Z*B.Z;}
FVec3 Normalize(FVec3 V)
{
const double Length=std::sqrt(Dot(V,V));
return Length>1.0e-12?FVec3{V.X/Length,V.Y/Length,V.Z/Length}:FVec3{};
}
std::array<float,16> MakeViewProjection(int Width,int Height,FVec3 Camera)
{
const FVec3 Target={0.0,0.0,0.0};
const FVec3 Forward=Normalize(Sub(Target,Camera));
const FVec3 Side=Normalize(Cross(Forward,{0.0,0.0,1.0}));
const FVec3 Up=Cross(Side,Forward);
std::array<float,16> View{};
View[0]=static_cast<float>(Side.X);View[4]=static_cast<float>(Side.Y);
View[8]=static_cast<float>(Side.Z);View[12]=static_cast<float>(-Dot(Side,Camera));
View[1]=static_cast<float>(Up.X);View[5]=static_cast<float>(Up.Y);
View[9]=static_cast<float>(Up.Z);View[13]=static_cast<float>(-Dot(Up,Camera));
View[2]=static_cast<float>(-Forward.X);View[6]=static_cast<float>(-Forward.Y);
View[10]=static_cast<float>(-Forward.Z);View[14]=static_cast<float>(Dot(Forward,Camera));
View[15]=1.0f;
const double Near=0.1,Far=100.0;
const double F=1.0/std::tan(42.0*Pi/360.0);
const double Aspect=static_cast<double>(Width)/static_cast<double>(Height);
std::array<float,16> Projection{};
Projection[0]=static_cast<float>(F/Aspect);
Projection[5]=static_cast<float>(F);
Projection[10]=static_cast<float>((Far+Near)/(Near-Far));
Projection[11]=-1.0f;
Projection[14]=static_cast<float>((2.0*Far*Near)/(Near-Far));
std::array<float,16> Result{};
for(int Column=0;Column<4;++Column)for(int Row=0;Row<4;++Row)
for(int K=0;K<4;++K)
Result[static_cast<std::size_t>(Column*4+Row)]+=
Projection[static_cast<std::size_t>(K*4+Row)]
*View[static_cast<std::size_t>(Column*4+K)];
return Result;
}
struct FGridVertex { float U=0.0f,V=0.0f; };
struct FSphereVertex { float Position[3]{},Normal[3]{}; };
void MakeGrid(int Segments,std::vector<FGridVertex>& Vertices,
std::vector<std::uint32_t>& Indices)
{
const int Side=Segments+1;
Vertices.reserve(static_cast<std::size_t>(Side)*Side);
for(int Y=0;Y<=Segments;++Y)for(int X=0;X<=Segments;++X)
Vertices.push_back({static_cast<float>(X)/Segments,static_cast<float>(Y)/Segments});
Indices.reserve(static_cast<std::size_t>(Segments)*Segments*6);
for(int Y=0;Y<Segments;++Y)for(int X=0;X<Segments;++X)
{
const std::uint32_t A=static_cast<std::uint32_t>(Y*Side+X);
const std::uint32_t B=A+1;
const std::uint32_t D=A+static_cast<std::uint32_t>(Side);
const std::uint32_t C=D+1;
Indices.insert(Indices.end(),{A,B,C,A,C,D});
}
}
void MakeSphere(std::vector<FSphereVertex>& Vertices,
std::vector<std::uint32_t>& Indices)
{
constexpr int Rings=24,Segments=40;
for(int Ring=0;Ring<=Rings;++Ring)
{
const double Theta=Pi*static_cast<double>(Ring)/Rings;
for(int Segment=0;Segment<=Segments;++Segment)
{
const double Phi=2.0*Pi*static_cast<double>(Segment)/Segments;
const float X=static_cast<float>(std::sin(Theta)*std::cos(Phi));
const float Y=static_cast<float>(std::sin(Theta)*std::sin(Phi));
const float Z=static_cast<float>(std::cos(Theta));
Vertices.push_back({{X,Y,Z},{X,Y,Z}});
}
}
const int Side=Segments+1;
for(int Ring=0;Ring<Rings;++Ring)for(int Segment=0;Segment<Segments;++Segment)
{
const std::uint32_t A=static_cast<std::uint32_t>(Ring*Side+Segment);
const std::uint32_t B=A+static_cast<std::uint32_t>(Side);
const std::uint32_t C=B+1;
const std::uint32_t D=A+1;
Indices.insert(Indices.end(),{A,B,C,A,C,D});
}
}
bool WritePng(const std::filesystem::path& Path,int Width,int Height,
const std::vector<std::uint8_t>& Pixels)
{
std::error_code Error;
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);
return std::fclose(File)==0;
}
struct FRenderResources
{
GLuint Program=0,Framebuffer=0,ColorTexture=0,DepthBuffer=0;
GLuint GridVao=0,GridVbo=0,GridEbo=0,SphereVao=0,SphereVbo=0,SphereEbo=0;
GLsizei GridIndexCount=0,SphereIndexCount=0;
int Width=0,Height=0,GridSize=0;
std::array<float,16> ViewProjection{};
FVec3 Camera{8.8,-12.2,9.4};
void Destroy()
{
if(SphereEbo)glDeleteBuffers(1,&SphereEbo);
if(SphereVbo)glDeleteBuffers(1,&SphereVbo);
if(SphereVao)glDeleteVertexArrays(1,&SphereVao);
if(GridEbo)glDeleteBuffers(1,&GridEbo);
if(GridVbo)glDeleteBuffers(1,&GridVbo);
if(GridVao)glDeleteVertexArrays(1,&GridVao);
if(DepthBuffer)glDeleteRenderbuffers(1,&DepthBuffer);
if(ColorTexture)glDeleteTextures(1,&ColorTexture);
if(Framebuffer)glDeleteFramebuffers(1,&Framebuffer);
if(Program)glDeleteProgram(Program);
*this=FRenderResources{};
}
bool Initialize(int Size,int InWidth,int InHeight)
{
Width=InWidth;Height=InHeight;GridSize=Size;
Program=MakeRenderProgram();
if(!Program)return false;
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_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
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::cerr<<"[LivingPlane] offscreen framebuffer is incomplete\n";return false;}
const int Segments=std::min(Size,256);
std::vector<FGridVertex> GridVertices;
std::vector<std::uint32_t> GridIndices;
MakeGrid(Segments,GridVertices,GridIndices);
GridIndexCount=static_cast<GLsizei>(GridIndices.size());
glGenVertexArrays(1,&GridVao);glBindVertexArray(GridVao);
glGenBuffers(1,&GridVbo);glBindBuffer(GL_ARRAY_BUFFER,GridVbo);
glBufferData(GL_ARRAY_BUFFER,static_cast<GLsizeiptr>(GridVertices.size()*sizeof(FGridVertex)),
GridVertices.data(),GL_STATIC_DRAW);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1,2,GL_FLOAT,GL_FALSE,sizeof(FGridVertex),nullptr);
glGenBuffers(1,&GridEbo);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,GridEbo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
static_cast<GLsizeiptr>(GridIndices.size()*sizeof(std::uint32_t)),
GridIndices.data(),GL_STATIC_DRAW);
std::vector<FSphereVertex> SphereVertices;
std::vector<std::uint32_t> SphereIndices;
MakeSphere(SphereVertices,SphereIndices);
SphereIndexCount=static_cast<GLsizei>(SphereIndices.size());
glGenVertexArrays(1,&SphereVao);glBindVertexArray(SphereVao);
glGenBuffers(1,&SphereVbo);glBindBuffer(GL_ARRAY_BUFFER,SphereVbo);
glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(SphereVertices.size()*sizeof(FSphereVertex)),
SphereVertices.data(),GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(FSphereVertex),
reinterpret_cast<void*>(offsetof(FSphereVertex,Position)));
glEnableVertexAttribArray(2);
glVertexAttribPointer(2,3,GL_FLOAT,GL_FALSE,sizeof(FSphereVertex),
reinterpret_cast<void*>(offsetof(FSphereVertex,Normal)));
glGenBuffers(1,&SphereEbo);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,SphereEbo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
static_cast<GLsizeiptr>(SphereIndices.size()*sizeof(std::uint32_t)),
SphereIndices.data(),GL_STATIC_DRAW);
ViewProjection=MakeViewProjection(Width,Height,Camera);
glBindVertexArray(0);
return glGetError()==GL_NO_ERROR;
}
bool Render(GLuint FieldTexture,const std::filesystem::path& Output,
double SphereX,double SphereY,double SphereZ)
{
glBindFramebuffer(GL_FRAMEBUFFER,Framebuffer);
glViewport(0,0,Width,Height);
glDisable(GL_CULL_FACE);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glClearColor(0.018f,0.055f,0.095f,1.0f);
glClearDepth(1.0);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
glUseProgram(Program);
glUniform1i(glGetUniformLocation(Program,"uField"),0);
glUniform1i(glGetUniformLocation(Program,"uGridSize"),GridSize);
glUniform1f(glGetUniformLocation(Program,"uPlaneLength"),
static_cast<float>(PlaneLengthMeters));
glUniformMatrix4fv(glGetUniformLocation(Program,"uViewProjection"),1,GL_FALSE,
ViewProjection.data());
glUniform3f(glGetUniformLocation(Program,"uCameraPosition"),
static_cast<float>(Camera.X),static_cast<float>(Camera.Y),static_cast<float>(Camera.Z));
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,FieldTexture);
glUniform1i(glGetUniformLocation(Program,"uRenderSphere"),0);
glBindVertexArray(GridVao);
glDrawElements(GL_TRIANGLES,GridIndexCount,GL_UNSIGNED_INT,nullptr);
glUniform1i(glGetUniformLocation(Program,"uRenderSphere"),1);
glUniform3f(glGetUniformLocation(Program,"uSphereCenter"),
static_cast<float>(SphereX),static_cast<float>(SphereY),static_cast<float>(SphereZ));
glUniform1f(glGetUniformLocation(Program,"uSphereRadius"),
static_cast<float>(SphereRadiusMeters));
glBindVertexArray(SphereVao);
glDrawElements(GL_TRIANGLES,SphereIndexCount,GL_UNSIGNED_INT,nullptr);
glFinish();
std::vector<std::uint8_t> Pixels(static_cast<std::size_t>(Width)*Height*4);
glPixelStorei(GL_PACK_ALIGNMENT,1);
glReadPixels(0,0,Width,Height,GL_RGBA,GL_UNSIGNED_BYTE,Pixels.data());
if(glGetError()!=GL_NO_ERROR||!WritePng(Output,Width,Height,Pixels))
{std::cerr<<"[LivingPlane] frame render/write failed: "<<Output<<"\n";return false;}
return true;
}
};
bool ParseInt(std::string_view Text,int& Value)
{
if(Text.empty())return false;
char* End=nullptr;
errno=0;
const std::string Copy(Text);
const long Parsed=std::strtol(Copy.c_str(),&End,10);
if(errno||!End||*End!='\0'||Parsed<std::numeric_limits<int>::min()
||Parsed>std::numeric_limits<int>::max())return false;
Value=static_cast<int>(Parsed);
return true;
}
bool ParseOptions(int Argc,char** Argv,FOptions& Options)
{
for(int I=1;I<Argc;++I)
{
const std::string_view Arg(Argv[I]);
const auto Take=[&](std::string_view Key,std::string_view& Value)->bool
{
if(Arg.starts_with(Key)&&Arg.size()>Key.size()&&Arg[Key.size()]=='=')
{Value=Arg.substr(Key.size()+1);return true;}
return false;
};
std::string_view Value;
if(Arg=="--help"||Arg=="-h")
{
std::cout<<"Usage: living-plane [--device=auto|gpu|cpu] [--size=N] [--steps=N] "
"[--out=DIR] [--capture-every=N] [--breathing=on|off]\n"
" --size is the square height-field and PNG resolution.\n"
" --capture-every=0 writes only timing.json. Default is 1.\n";
std::exit(0);
}
else if(Take("--device",Value))Options.Device=Value;
else if(Take("--size",Value))
{if(!ParseInt(Value,Options.Size)){std::cerr<<"invalid --size\n";return false;}}
else if(Take("--steps",Value))
{if(!ParseInt(Value,Options.Steps)){std::cerr<<"invalid --steps\n";return false;}}
else if(Take("--capture-every",Value))
{if(!ParseInt(Value,Options.CaptureEvery)){std::cerr<<"invalid --capture-every\n";return false;}}
else if(Take("--out",Value))Options.Output=Value;
else if(Take("--breathing",Value))
{
if(Value=="on")Options.Breathing=true;
else if(Value=="off")Options.Breathing=false;
else {std::cerr<<"--breathing must be on or off\n";return false;}
}
else {std::cerr<<"unknown option: "<<Arg<<"\n";return false;}
}
if(Options.Device!="auto"&&Options.Device!="gpu"&&Options.Device!="cpu")
{std::cerr<<"--device must be auto, gpu, or cpu\n";return false;}
if(Options.Size<16||Options.Size>4096)
{std::cerr<<"--size must be in [16,4096]\n";return false;}
if(Options.Steps<1||Options.Steps>100000)
{std::cerr<<"--steps must be in [1,100000]\n";return false;}
if(Options.CaptureEvery<0||Options.CaptureEvery>100000)
{std::cerr<<"--capture-every must be in [0,100000]\n";return false;}
return true;
}
double Quantile(std::vector<double> Values,double Q)
{
if(Values.empty())return 0.0;
std::sort(Values.begin(),Values.end());
const std::size_t I=static_cast<std::size_t>(std::ceil(Q*Values.size())-1.0);
return Values[std::min(I,Values.size()-1)];
}
bool WriteTimingJson(const FOptions& Options,const FContext& Context,
std::string_view Renderer,std::string_view Vendor,std::string_view Version,
double Dt,const std::vector<GLuint64>& Times,const std::vector<double>& Heights,
const std::vector<double>& Velocities,std::size_t Frames,bool FirstStepAudit,
bool DynamicsAudit)
{
std::error_code Error;
std::filesystem::create_directories(Options.Output,Error);
if(Error){std::cerr<<"cannot create output directory: "<<Error.message()<<"\n";return false;}
std::ofstream Json(Options.Output/"timing.json",std::ios::trunc);
if(!Json){std::cerr<<"cannot write timing.json\n";return false;}
std::vector<double> Ms;
Ms.reserve(Times.size());
for(const GLuint64 Ns:Times)Ms.push_back(static_cast<double>(Ns)*1.0e-6);
const double Mean=Ms.empty()?0.0:std::accumulate(Ms.begin(),Ms.end(),0.0)/Ms.size();
const double Min=Ms.empty()?0.0:*std::min_element(Ms.begin(),Ms.end());
const double Max=Ms.empty()?0.0:*std::max_element(Ms.begin(),Ms.end());
const double HeightMin=*std::min_element(Heights.begin(),Heights.end());
const double HeightMax=*std::max_element(Heights.begin(),Heights.end());
const double HeightMean=std::accumulate(Heights.begin(),Heights.end(),0.0)/Heights.size();
double HeightSquare=0.0;
for(const double H:Heights)HeightSquare+=H*H;
const double HeightRms=std::sqrt(HeightSquare/Heights.size());
const double Dx=PlaneLengthMeters/Options.Size;
const double SpectralStep=Dt*std::sqrt(8.0*WaveSpeedMetersPerSecond*
WaveSpeedMetersPerSecond/(Dx*Dx)+FoundationStiffnessPerSecondSquared);
const double VelocityMax=*std::max_element(Velocities.begin(),Velocities.end(),
[](double A,double B){return std::abs(A)<std::abs(B);});
Json<<"{\n \"schema\": 1,\n"
<<" \"requested_device\": \""<<JsonEscape(Options.Device)<<"\",\n"
<<" \"execution_device\": \""<<Context.ExecutionDevice<<"\",\n"
<<" \"egl_device\": \""<<JsonEscape(Context.DeviceLabel)<<"\",\n"
<<" \"drm_render_node\": \""<<JsonEscape(Context.RenderNode)<<"\",\n"
<<" \"gl_vendor\": \""<<JsonEscape(Vendor)<<"\",\n"
<<" \"gl_renderer\": \""<<JsonEscape(Renderer)<<"\",\n"
<<" \"gl_version\": \""<<JsonEscape(Version)<<"\",\n"
<<" \"size\": "<<Options.Size<<",\n"
<<" \"steps\": "<<Options.Steps<<",\n"
<<" \"plane_length_m\": "<<PlaneLengthMeters<<",\n"
<<" \"dt_seconds\": "<<Dt<<",\n"
<<" \"spectral_step_ratio\": "<<SpectralStep<<",\n"
<<" \"wave_speed_m_per_s\": "<<WaveSpeedMetersPerSecond<<",\n"
<<" \"damping_per_s\": "<<DampingPerSecond<<",\n"
<<" \"foundation_stiffness_per_s2\": "<<FoundationStiffnessPerSecondSquared<<",\n"
<<" \"breathing\": "<<(Options.Breathing?"true":"false")<<",\n"
<<" \"frames_written\": "<<Frames<<",\n"
<<" \"first_step_equation_audit\": \""<<(FirstStepAudit?"pass":"fail")<<"\",\n"
<<" \"field_dynamics_audit\": \""<<(DynamicsAudit?"pass":"fail")<<"\",\n"
<<" \"timing_interpretation\": \""
<<(Context.ExecutionDevice=="gpu"?"GPU GL_TIME_ELAPSED per compute dispatch"
:"software-renderer GL_TIME_ELAPSED values; not GPU performance timings")<<"\",\n"
<<" \"field_min_m\": "<<HeightMin<<",\n"
<<" \"field_max_m\": "<<HeightMax<<",\n"
<<" \"field_mean_m\": "<<HeightMean<<",\n"
<<" \"field_rms_m\": "<<HeightRms<<",\n"
<<" \"max_abs_velocity_m_per_s\": "<<std::abs(VelocityMax)<<",\n"
<<" \"gl_time_elapsed_ms_per_step\": [";
for(std::size_t I=0;I<Times.size();++I)
{
if(I)Json<<",";
if(I%8==0)Json<<"\n ";
Json<<static_cast<double>(Times[I])*1.0e-6;
}
Json<<"\n ],\n \"gl_time_elapsed_summary_ms\": {\n"
<<" \"mean\": "<<Mean<<",\n"
<<" \"median\": "<<Quantile(Ms,0.50)<<",\n"
<<" \"p95\": "<<Quantile(Ms,0.95)<<",\n"
<<" \"min\": "<<Min<<",\n"
<<" \"max\": "<<Max<<"\n }\n}\n";
return static_cast<bool>(Json);
}
bool Run(const FOptions& Options)
{
FContext Context;
if(!CreateContext(Options.Device,Context))return false;
const auto* RendererPtr=reinterpret_cast<const char*>(glGetString(GL_RENDERER));
const auto* VendorPtr=reinterpret_cast<const char*>(glGetString(GL_VENDOR));
const auto* VersionPtr=reinterpret_cast<const char*>(glGetString(GL_VERSION));
if(!RendererPtr||!VendorPtr||!VersionPtr)
{std::cerr<<"[LivingPlane] OpenGL identity query failed\n";return false;}
const std::string Renderer=RendererPtr,Vendor=VendorPtr,Version=VersionPtr;
if(Options.Device=="gpu"&&Context.ExecutionDevice!="gpu")
{std::cerr<<"[LivingPlane] refused software renderer for requested GPU run: "
<<Renderer<<"\n";return false;}
if(Options.Device=="cpu"&&Context.ExecutionDevice!="cpu")
{std::cerr<<"[LivingPlane] refused non-software renderer for requested CPU run: "
<<Renderer<<"\n";return false;}
std::cout<<"[LivingPlane] device="<<Context.ExecutionDevice<<" renderer=\""
<<Renderer<<"\" version=\""<<Version<<"\" egl_device=\""
<<Context.DeviceLabel<<"\" render_node=\""<<Context.RenderNode<<"\"\n";
const GLuint ComputeProgram=MakeComputeProgram();
if(!ComputeProgram)return false;
const int Size=Options.Size;
const double Dx=PlaneLengthMeters/Size;
const double Dt=1.30/std::sqrt(8.0*WaveSpeedMetersPerSecond*
WaveSpeedMetersPerSecond/(Dx*Dx)+FoundationStiffnessPerSecondSquared);
std::array<GLuint,2> Fields{};
glGenTextures(2,Fields.data());
const std::vector<float> Zeroes(static_cast<std::size_t>(Size)*Size*2,0.0f);
for(const GLuint Field:Fields)
{
glBindTexture(GL_TEXTURE_2D,Field);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D,0,GL_RG32F,Size,Size,0,GL_RG,GL_FLOAT,Zeroes.data());
}
const int CaptureWidth=Size,CaptureHeight=Size;
FRenderResources Render;
if(Options.CaptureEvery>0&&!Render.Initialize(Size,CaptureWidth,CaptureHeight))
{Render.Destroy();glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;}
const GLuint QueryCount=static_cast<GLuint>(Options.Steps);
std::vector<GLuint> Queries(QueryCount);
glGenQueries(static_cast<GLsizei>(QueryCount),Queries.data());
const GLint LocationGrid=glGetUniformLocation(ComputeProgram,"uGridSize");
const GLint LocationDt=glGetUniformLocation(ComputeProgram,"uDt");
const GLint LocationDx=glGetUniformLocation(ComputeProgram,"uDx");
const GLint LocationPlane=glGetUniformLocation(ComputeProgram,"uPlaneLength");
const GLint LocationWaveSpeed=glGetUniformLocation(ComputeProgram,"uWaveSpeed");
const GLint LocationDamping=glGetUniformLocation(ComputeProgram,"uDamping");
const GLint LocationFoundation=glGetUniformLocation(ComputeProgram,"uFoundationStiffness");
const GLint LocationTime=glGetUniformLocation(ComputeProgram,"uTime");
const GLint LocationForcePosition=glGetUniformLocation(ComputeProgram,"uForcePosition");
const GLint LocationForceSigma=glGetUniformLocation(ComputeProgram,"uForceSigma");
const GLint LocationForceStrength=glGetUniformLocation(ComputeProgram,"uForceStrength");
const GLint LocationBreathing=glGetUniformLocation(ComputeProgram,"uBreathing");
if(LocationGrid<0||LocationDt<0||LocationDx<0||LocationPlane<0||LocationWaveSpeed<0
||LocationDamping<0||LocationFoundation<0||LocationTime<0
||LocationForcePosition<0||LocationForceSigma<0
||LocationForceStrength<0||LocationBreathing<0)
{std::cerr<<"[LivingPlane] missing compute shader uniforms\n";return false;}
glUseProgram(ComputeProgram);
glUniform1i(LocationGrid,Size);
glUniform1f(LocationDt,static_cast<float>(Dt));
glUniform1f(LocationDx,static_cast<float>(Dx));
glUniform1f(LocationPlane,static_cast<float>(PlaneLengthMeters));
glUniform1f(LocationWaveSpeed,static_cast<float>(WaveSpeedMetersPerSecond));
glUniform1f(LocationDamping,static_cast<float>(DampingPerSecond));
glUniform1f(LocationFoundation,static_cast<float>(FoundationStiffnessPerSecondSquared));
glUniform1f(LocationForceSigma,static_cast<float>(SphereSigmaMeters));
glUniform1i(LocationBreathing,Options.Breathing?1:0);
int Current=0;
const auto Dispatch=[&](int Target,double Time,double SphereX,double SphereY,
double ForceStrength,GLuint Query)
{
glUseProgram(ComputeProgram);
glUniform1f(LocationTime,static_cast<float>(Time));
glUniform2f(LocationForcePosition,static_cast<float>(SphereX),static_cast<float>(SphereY));
glUniform1f(LocationForceStrength,static_cast<float>(ForceStrength));
glBindImageTexture(0,Fields[static_cast<std::size_t>(Current)],0,GL_FALSE,0,
GL_READ_ONLY,GL_RG32F);
glBindImageTexture(1,Fields[static_cast<std::size_t>(Target)],0,GL_FALSE,0,
GL_WRITE_ONLY,GL_RG32F);
if(Query)glBeginQuery(GL_TIME_ELAPSED,Query);
glDispatchCompute(static_cast<GLuint>((Size+15)/16),
static_cast<GLuint>((Size+15)/16),1);
if(Query)glEndQuery(GL_TIME_ELAPSED);
glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT|GL_TEXTURE_FETCH_BARRIER_BIT);
Current=Target;
};
// Warm the runtime-compiled compute kernel with zero forcing before timing.
for(int Warm=0;Warm<8;++Warm)
Dispatch(1-Current,-8.0*Dt,0.0,0.0,0.0,0);
glFinish();
for(const GLuint Field:Fields)
{
glBindTexture(GL_TEXTURE_2D,Field);
glTexSubImage2D(GL_TEXTURE_2D,0,0,0,Size,Size,GL_RG,GL_FLOAT,Zeroes.data());
}
glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT|GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
Current=0;
FRenderResources* RenderPtr=Options.CaptureEvery>0?&Render:nullptr;
std::size_t FramesWritten=0;
int FrameIndex=0;
auto SaveFrame=[&](double SphereX,double SphereY,double SphereZ)->bool
{
if(!RenderPtr)return true;
char Filename[32]{};
std::snprintf(Filename,sizeof(Filename),"frame_%06d.png",FrameIndex++);
if(!Render.Render(Fields[static_cast<std::size_t>(Current)],
Options.Output/Filename,SphereX,SphereY,SphereZ))return false;
++FramesWritten;
return true;
};
const int MoveSteps=std::max(2,static_cast<int>(std::lround(Options.Steps*0.58)));
const auto SphereAt=[&](int Step)
{
const double MoveT=std::clamp(static_cast<double>(Step)/std::max(1,MoveSteps-1),0.0,1.0);
const double Smooth=MoveT*MoveT*(3.0-2.0*MoveT);
const double X=-4.8+9.6*Smooth;
const bool Rest=Step>=MoveSteps;
const double RestTime=Rest?static_cast<double>(Step-MoveSteps)*Dt:0.0;
const double Bob=Rest?std::sin(RestTime*2.0*Pi*1.25):0.0;
const double Z=0.36+0.08*Bob;
const double Force=SpherePushMetersPerSecondSquared*(1.0-0.32*Bob);
return std::array<double,4>{X,0.0,Z,Force};
};
const auto Initial=SphereAt(0);
if(!SaveFrame(Initial[0],Initial[1],Initial[2]))
{Render.Destroy();glDeleteQueries(static_cast<GLsizei>(Queries.size()),Queries.data());
glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;}
std::vector<float> FirstField;
bool FirstStepAudit=false;
for(int Step=0;Step<Options.Steps;++Step)
{
const auto Sphere=SphereAt(Step);
const int Target=1-Current;
Dispatch(Target,static_cast<double>(Step)*Dt,Sphere[0],Sphere[1],Sphere[3],
Queries[static_cast<std::size_t>(Step)]);
if(Step==0)
{
glFinish();
FirstField.resize(static_cast<std::size_t>(Size)*Size*2);
glBindTexture(GL_TEXTURE_2D,Fields[static_cast<std::size_t>(Current)]);
glGetTexImage(GL_TEXTURE_2D,0,GL_RG,GL_FLOAT,FirstField.data());
const int X=std::clamp(static_cast<int>(std::floor(
(Sphere[0]+PlaneLengthMeters*0.5)/Dx)),0,Size-1);
const int Y=std::clamp(static_cast<int>(std::floor(
(Sphere[1]+PlaneLengthMeters*0.5)/Dx)),0,Size-1);
const double CellX=(X+0.5)*Dx-PlaneLengthMeters*0.5;
const double CellY=(Y+0.5)*Dx-PlaneLengthMeters*0.5;
const double Distance2=(CellX-Sphere[0])*(CellX-Sphere[0])
+(CellY-Sphere[1])*(CellY-Sphere[1]);
const double Force=-Sphere[3]*std::exp(-Distance2/
(2.0*SphereSigmaMeters*SphereSigmaMeters));
const double Expected=Force*Dt*Dt*std::exp(-DampingPerSecond*Dt);
const std::size_t Index=(static_cast<std::size_t>(Y)*Size+X)*2;
const double Actual=FirstField[Index];
const double Tolerance=std::max(2.0e-7,std::abs(Expected)*2.5e-3);
FirstStepAudit=std::isfinite(Actual)&&std::abs(Actual-Expected)<=Tolerance;
if(!FirstStepAudit)
std::cerr<<"[LivingPlane] first-step equation audit failed: expected "
<<Expected<<" actual "<<Actual<<" tolerance "<<Tolerance<<"\n";
}
if(RenderPtr&&(Options.CaptureEvery==1
||((Step+1)%Options.CaptureEvery==0)||Step==Options.Steps-1))
{
const auto FrameSphere=SphereAt(Step);
if(!SaveFrame(FrameSphere[0],FrameSphere[1],FrameSphere[2]))
{Render.Destroy();glDeleteQueries(static_cast<GLsizei>(Queries.size()),Queries.data());
glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;}
}
}
glFinish();
std::vector<GLuint64> Times(static_cast<std::size_t>(Options.Steps));
for(int Step=0;Step<Options.Steps;++Step)
glGetQueryObjectui64v(Queries[static_cast<std::size_t>(Step)],GL_QUERY_RESULT,
&Times[static_cast<std::size_t>(Step)]);
std::vector<float> FinalField(static_cast<std::size_t>(Size)*Size*2);
glBindTexture(GL_TEXTURE_2D,Fields[static_cast<std::size_t>(Current)]);
glGetTexImage(GL_TEXTURE_2D,0,GL_RG,GL_FLOAT,FinalField.data());
std::vector<double> Heights,Velocities;
Heights.reserve(static_cast<std::size_t>(Size)*Size);
Velocities.reserve(static_cast<std::size_t>(Size)*Size);
bool Finite=true;
for(std::size_t I=0;I<FinalField.size();I+=2)
{
Finite=Finite&&std::isfinite(FinalField[I])&&std::isfinite(FinalField[I+1]);
Heights.push_back(FinalField[I]);
Velocities.push_back(FinalField[I+1]);
}
const auto [HeightMinIt,HeightMaxIt]=std::minmax_element(Heights.begin(),Heights.end());
const bool DynamicsAudit=Finite&&*HeightMinIt<-1.0e-7&&*HeightMaxIt-*HeightMinIt<2.0;
const bool Good=FirstStepAudit&&DynamicsAudit&&glGetError()==GL_NO_ERROR;
if(!DynamicsAudit)
std::cerr<<"[LivingPlane] field audit failed: finite="<<Finite<<" min="
<<*HeightMinIt<<" max="<<*HeightMaxIt<<"\n";
if(!WriteTimingJson(Options,Context,Renderer,Vendor,Version,Dt,Times,Heights,Velocities,
FramesWritten,FirstStepAudit,DynamicsAudit))
{Render.Destroy();glDeleteQueries(static_cast<GLsizei>(Queries.size()),Queries.data());
glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;}
double MeanNs=0.0;
for(const GLuint64 Ns:Times)MeanNs+=static_cast<double>(Ns);
MeanNs/=Times.size();
std::cout<<"[LivingPlane] size="<<Size<<" steps="<<Options.Steps<<" dt_ms="
<<Dt*1000.0<<" GL_TIME_ELAPSED_mean_ms="<<MeanNs*1.0e-6
<<" median_ms="<<Quantile([&]{std::vector<double> V;for(auto N:Times)V.push_back(N*1.0e-6);return V;}(),0.5)
<<" field_min_m="<<*HeightMinIt<<" field_max_m="<<*HeightMaxIt
<<" frames="<<FramesWritten<<" equation_audit="<<(FirstStepAudit?"PASS":"FAIL")
<<" status="<<(Good?"PASS":"FAIL")<<" out="<<Options.Output.string()<<"\n";
Render.Destroy();
glDeleteQueries(static_cast<GLsizei>(Queries.size()),Queries.data());
glDeleteTextures(2,Fields.data());
glDeleteProgram(ComputeProgram);
return Good;
}
}
int main(int Argc,char** Argv)
{
FOptions Options;
if(!ParseOptions(Argc,Argv,Options))return 2;
if(!Run(Options))return 1;
return 0;
}