#define GL_GLEXT_PROTOTYPES 1 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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.58; constexpr double SpherePushMetersPerSecondSquared=36.0; constexpr GLuint TimerQueryRingSize=64; constexpr double Pi=3.14159265358979323846; struct FBodyState { double X=0.0,Y=0.0,Z=0.0,Force=0.0; }; struct FOptions { std::string Device="auto"; std::filesystem::path Output="Saved/Linux/l164/living-plane"; std::string Scene="wake"; int Size=128; int Steps=360; int CaptureEvery=1; bool TimerQueries=true; bool Breathing=false; bool BreathingConfigured=false; bool Imprint=false; bool ImprintConfigured=false; double BreathingStrength=4.8; double ImprintCaptureMetersPerSecond=0.15; double ImprintRecoverySeconds=6.0; double ImprintMaxDepthMeters=0.34; }; 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(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(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(C);break; } } return Result; } bool HasExtension(std::string_view Extensions,std::string_view Wanted) { std::size_t At=0; while(At FindHardwareDevices(std::string& Error) { using TQueryDevices=PFNEGLQUERYDEVICESEXTPROC; using TQueryDeviceString=PFNEGLQUERYDEVICESTRINGEXTPROC; using TGetPlatformDisplay=PFNEGLGETPLATFORMDISPLAYEXTPROC; auto QueryDevices=reinterpret_cast(eglGetProcAddress("eglQueryDevicesEXT")); auto QueryDeviceString=reinterpret_cast( eglGetProcAddress("eglQueryDeviceStringEXT")); auto GetPlatformDisplay=reinterpret_cast( eglGetProcAddress("eglGetPlatformDisplayEXT")); const auto* ClientExtensions=reinterpret_cast( 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 Devices(static_cast(Count)); if(QueryDevices(Count,Devices.data(),&Count)!=EGL_TRUE) {Error="eglQueryDevicesEXT failed while listing devices";return {};} std::vector Candidates; for(EGLint I=0;I(I)]; const auto* Vendor=QueryDeviceString(Candidate.Handle,EGL_VENDOR); const auto* Extensions=QueryDeviceString(Candidate.Handle,EGL_EXTENSIONS); if(Vendor)Candidate.Vendor=reinterpret_cast(Vendor); if(Extensions)Candidate.Extensions=reinterpret_cast(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(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( 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: "<( 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: "<( 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: "<(std::max(Length,1)),0); glGetShaderInfoLog(Shader,Length,nullptr,Log.data()); std::cerr<<"[LivingPlane] shader compilation failed:\n"< 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::max(Length,1)),0); glGetProgramInfoLog(Program,Length,nullptr,Log.data()); std::cerr<<"[LivingPlane] program link failed:\n"<=uGridSize||p.y>=uGridSize)return; ivec2 lo=ivec2(0); ivec2 hi=ivec2(uGridSize-1); vec4 state=imageLoad(uSource,p); float h=state.r; float v=state.g; float imprint=state.b; 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); float sigma2=max(uForceSigma*uForceSigma,1.0e-8); float interaction=0.0; float contact=0.0; for(int i=0;i<2;++i) { vec2 offset=world-uForcePositions[i]; float weight=exp(-dot(offset,offset)/(2.0*sigma2)); interaction-=uForceStrengths[i]*weight; if(uForceStrengths[i]>0.0)contact=max(contact,weight); } float breathing=0.0; if(uBreathing!=0) { float swell=sin(world.x*0.48-uTime*0.22) *cos(world.y*0.42+uTime*0.16); breathing=uBreathingStrength*swell*sin(uTime*0.48); } float nextImprint=imprint; if(uImprint!=0) { nextImprint=clamp(imprint*exp(-uDt/max(uImprintRecoverySeconds,1.0e-4)) -contact*uImprintCaptureRate*uDt,-uImprintMaxDepth,0.0); } float foundationTarget=uImprint!=0?imprint:0.0; float acceleration=uWaveSpeed*uWaveSpeed*laplacian -uFoundationStiffness*(h-foundationTarget) +interaction+breathing; float nextVelocity=(v+acceleration*uDt)*exp(-uDamping*uDt); float nextHeight=h+nextVelocity*uDt; imageStore(uTarget,p,vec4(nextHeight,nextVelocity,nextImprint,0.0)); })GLSL"; const GLuint Shader=CompileShader(GL_COMPUTE_SHADER,Source); if(!Shader)return 0; const std::array 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 int uSphereMaterial; uniform mat4 uViewProjection; out vec3 vWorld; out vec3 vNormal; out float vHeight; flat out int vSphereMaterial; float TerrainHeight(vec2 p) { return 0.105*sin(p.x*0.62+p.y*0.13)*cos(p.y*0.57) +0.045*sin(p.x*1.04-p.y*0.78); } 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)); float sampleDx=uPlaneLength/float(uGridSize); vec2 leftWorld=(vec2(left)+vec2(0.5))*sampleDx-vec2(uPlaneLength*0.5); vec2 rightWorld=(vec2(right)+vec2(0.5))*sampleDx-vec2(uPlaneLength*0.5); vec2 downWorld=(vec2(down)+vec2(0.5))*sampleDx-vec2(uPlaneLength*0.5); vec2 upWorld=(vec2(up)+vec2(0.5))*sampleDx-vec2(uPlaneLength*0.5); float leftHeight=texelFetch(uField,left,0).r+TerrainHeight(leftWorld); float rightHeight=texelFetch(uField,right,0).r+TerrainHeight(rightWorld); float downHeight=texelFetch(uField,down,0).r+TerrainHeight(downWorld); float upHeight=texelFetch(uField,up,0).r+TerrainHeight(upWorld); height=texelFetch(uField,texel,0).r+TerrainHeight( (vec2(texel)+vec2(0.5))*sampleDx-vec2(uPlaneLength*0.5)); 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=(rightHeight-leftHeight)/dx; float dhdy=(upHeight-downHeight)/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; vSphereMaterial=uRenderSphere==0?0:uSphereMaterial; 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 vSphereMaterial; uniform vec3 uCameraPosition; out vec4 outColor; void main() { vec3 n=normalize(vNormal); vec3 l=normalize(vec3(-0.62,-0.54,0.32)); vec3 v=normalize(uCameraPosition-vWorld); float diffuse=max(dot(n,l),0.0); float slope=1.0-clamp(n.z,0.0,1.0); float spec=pow(max(dot(reflect(-l,n),v),0.0),vSphereMaterial==0?72.0:38.0); if(vSphereMaterial!=0) { vec3 base=vSphereMaterial==1?vec3(0.96,0.34,0.10):vec3(0.18,0.70,0.92); float light=0.24+0.84*diffuse; outColor=vec4(base*light+vec3(0.65,0.43,0.22)*spec,1.0); } else { float low= smoothstep(-0.46,-0.08,vHeight); float high=smoothstep(0.02,0.32,vHeight); vec3 base=mix(vec3(0.025,0.075,0.13),vec3(0.055,0.30,0.31),low); base=mix(base,vec3(0.28,0.46,0.28),high*0.82); float contourPhase=fract((vHeight+0.60)/0.075); float contourDistance=min(contourPhase,1.0-contourPhase); float contour=smoothstep(0.025,0.16,contourDistance); base=mix(base*0.35,base,contour); base+=vec3(0.20,0.22,0.12)*smoothstep(0.08,0.36,slope); float light=0.34+0.98*diffuse; outColor=vec4(base*light+vec3(0.30,0.52,0.48)*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 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; }; double TerrainHeight(double X,double Y) { return 0.105*std::sin(X*0.62+Y*0.13)*std::cos(Y*0.57) +0.045*std::sin(X*1.04-Y*0.78); } double Smooth01(double T) { T=std::clamp(T,0.0,1.0); return T*T*(3.0-2.0*T); } std::array GetBodiesAt(double Time) { constexpr double RollEnd=1.62; constexpr double RestEnd=2.55; constexpr double LiftEnd=2.95; constexpr double SecondaryStart=2.70; constexpr double SecondaryEnd=5.55; FBodyState Primary; Primary.Y=-0.15; if(Time=SecondaryStart&&Time<=SecondaryEnd) Secondary.Force=SpherePushMetersPerSecondSquared*0.78; return {Primary,Secondary}; } 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 MakeViewProjection(int Width,int Height,FVec3 Camera,FVec3 Target) { 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 View{}; View[0]=static_cast(Side.X);View[4]=static_cast(Side.Y); View[8]=static_cast(Side.Z);View[12]=static_cast(-Dot(Side,Camera)); View[1]=static_cast(Up.X);View[5]=static_cast(Up.Y); View[9]=static_cast(Up.Z);View[13]=static_cast(-Dot(Up,Camera)); View[2]=static_cast(-Forward.X);View[6]=static_cast(-Forward.Y); View[10]=static_cast(-Forward.Z);View[14]=static_cast(Dot(Forward,Camera)); View[15]=1.0f; const double Near=0.1,Far=60.0; const double F=1.0/std::tan(39.0*Pi/360.0); const double Aspect=static_cast(Width)/static_cast(Height); std::array Projection{}; Projection[0]=static_cast(F/Aspect); Projection[5]=static_cast(F); Projection[10]=static_cast((Far+Near)/(Near-Far)); Projection[11]=-1.0f; Projection[14]=static_cast((2.0*Far*Near)/(Near-Far)); std::array 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(Column*4+Row)]+= Projection[static_cast(K*4+Row)] *View[static_cast(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& Vertices, std::vector& Indices) { const int Side=Segments+1; Vertices.reserve(static_cast(Side)*Side); for(int Y=0;Y<=Segments;++Y)for(int X=0;X<=Segments;++X) Vertices.push_back({static_cast(X)/Segments,static_cast(Y)/Segments}); Indices.reserve(static_cast(Segments)*Segments*6); for(int Y=0;Y(Y*Side+X); const std::uint32_t B=A+1; const std::uint32_t D=A+static_cast(Side); const std::uint32_t C=D+1; Indices.insert(Indices.end(),{A,B,C,A,C,D}); } } void MakeSphere(std::vector& Vertices, std::vector& Indices) { constexpr int Rings=24,Segments=40; for(int Ring=0;Ring<=Rings;++Ring) { const double Theta=Pi*static_cast(Ring)/Rings; for(int Segment=0;Segment<=Segments;++Segment) { const double Phi=2.0*Pi*static_cast(Segment)/Segments; const float X=static_cast(std::sin(Theta)*std::cos(Phi)); const float Y=static_cast(std::sin(Theta)*std::sin(Phi)); const float Z=static_cast(std::cos(Theta)); Vertices.push_back({{X,Y,Z},{X,Y,Z}}); } } const int Side=Segments+1; for(int Ring=0;Ring(Ring*Side+Segment); const std::uint32_t B=A+static_cast(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& 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(Width),static_cast(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(Width)*4; for(int Y=Height-1;Y>=0;--Y) png_write_row(Png,const_cast(Pixels.data()+static_cast(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 ViewProjection{}; FVec3 Camera{}; 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,512); std::vector GridVertices; std::vector GridIndices; MakeGrid(Segments,GridVertices,GridIndices); GridIndexCount=static_cast(GridIndices.size()); glGenVertexArrays(1,&GridVao);glBindVertexArray(GridVao); glGenBuffers(1,&GridVbo);glBindBuffer(GL_ARRAY_BUFFER,GridVbo); glBufferData(GL_ARRAY_BUFFER,static_cast(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(GridIndices.size()*sizeof(std::uint32_t)), GridIndices.data(),GL_STATIC_DRAW); std::vector SphereVertices; std::vector SphereIndices; MakeSphere(SphereVertices,SphereIndices); SphereIndexCount=static_cast(SphereIndices.size()); glGenVertexArrays(1,&SphereVao);glBindVertexArray(SphereVao); glGenBuffers(1,&SphereVbo);glBindBuffer(GL_ARRAY_BUFFER,SphereVbo); glBufferData(GL_ARRAY_BUFFER, static_cast(SphereVertices.size()*sizeof(FSphereVertex)), SphereVertices.data(),GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(FSphereVertex), reinterpret_cast(offsetof(FSphereVertex,Position))); glEnableVertexAttribArray(2); glVertexAttribPointer(2,3,GL_FLOAT,GL_FALSE,sizeof(FSphereVertex), reinterpret_cast(offsetof(FSphereVertex,Normal))); glGenBuffers(1,&SphereEbo);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,SphereEbo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, static_cast(SphereIndices.size()*sizeof(std::uint32_t)), SphereIndices.data(),GL_STATIC_DRAW); glBindVertexArray(0); return glGetError()==GL_NO_ERROR; } bool Render(GLuint FieldTexture,const std::filesystem::path& Output, const FBodyState& Primary,const FBodyState& Secondary) { const FVec3 Target={Primary.X,Primary.Y,0.0}; Camera={Primary.X+2.65,Primary.Y-4.45,3.65}; ViewProjection=MakeViewProjection(Width,Height,Camera,Target); 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(PlaneLengthMeters)); glUniformMatrix4fv(glGetUniformLocation(Program,"uViewProjection"),1,GL_FALSE, ViewProjection.data()); glUniform3f(glGetUniformLocation(Program,"uCameraPosition"), static_cast(Camera.X),static_cast(Camera.Y),static_cast(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); glUniform1f(glGetUniformLocation(Program,"uSphereRadius"), static_cast(SphereRadiusMeters)); glBindVertexArray(SphereVao); glUniform1i(glGetUniformLocation(Program,"uSphereMaterial"),1); glUniform3f(glGetUniformLocation(Program,"uSphereCenter"), static_cast(Primary.X),static_cast(Primary.Y), static_cast(Primary.Z)); glDrawElements(GL_TRIANGLES,SphereIndexCount,GL_UNSIGNED_INT,nullptr); glUniform1i(glGetUniformLocation(Program,"uSphereMaterial"),2); glUniform3f(glGetUniformLocation(Program,"uSphereCenter"), static_cast(Secondary.X),static_cast(Secondary.Y), static_cast(Secondary.Z)); glDrawElements(GL_TRIANGLES,SphereIndexCount,GL_UNSIGNED_INT,nullptr); glFinish(); std::vector Pixels(static_cast(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: "<::min() ||Parsed>std::numeric_limits::max())return false; Value=static_cast(Parsed); return true; } bool ParseDouble(std::string_view Text,double& Value) { if(Text.empty())return false; char* End=nullptr; errno=0; const std::string Copy(Text); const double Parsed=std::strtod(Copy.c_str(),&End); if(errno||!End||*End!='\0'||!std::isfinite(Parsed))return false; Value=Parsed; return true; } bool ParseOptions(int Argc,char** Argv,FOptions& Options) { for(int I=1;Ibool { 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] [--scene=wake|alive] " "[--size=N] [--steps=N] [--out=DIR] [--capture-every=N]\n" " [--queries=on|off] [--breathing=on|off] [--imprint=on|off]\n" " [--breathing-strength=MPS2] [--imprint-capture=MPS] " "[--imprint-recovery=SECONDS] [--imprint-depth=M]\n" " --capture-every=0 writes only timing.json. --queries=off measures " "batched dispatch wall time without per-step query overhead.\n"; std::exit(0); } else if(Take("--device",Value))Options.Device=Value; else if(Take("--scene",Value))Options.Scene=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("--queries",Value)) { if(Value=="on")Options.TimerQueries=true; else if(Value=="off")Options.TimerQueries=false; else {std::cerr<<"--queries must be on or off\n";return false;} } 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;} Options.BreathingConfigured=true; } else if(Take("--imprint",Value)) { if(Value=="on")Options.Imprint=true; else if(Value=="off")Options.Imprint=false; else {std::cerr<<"--imprint must be on or off\n";return false;} Options.ImprintConfigured=true; } else if(Take("--breathing-strength",Value)) { if(!ParseDouble(Value,Options.BreathingStrength)) {std::cerr<<"invalid --breathing-strength\n";return false;} } else if(Take("--imprint-capture",Value)) { if(!ParseDouble(Value,Options.ImprintCaptureMetersPerSecond)) {std::cerr<<"invalid --imprint-capture\n";return false;} } else if(Take("--imprint-recovery",Value)) { if(!ParseDouble(Value,Options.ImprintRecoverySeconds)) {std::cerr<<"invalid --imprint-recovery\n";return false;} } else if(Take("--imprint-depth",Value)) { if(!ParseDouble(Value,Options.ImprintMaxDepthMeters)) {std::cerr<<"invalid --imprint-depth\n";return false;} } else {std::cerr<<"unknown option: "<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;} if(Options.BreathingStrength<0.0||Options.BreathingStrength>30.0 ||Options.ImprintCaptureMetersPerSecond<0.0 ||Options.ImprintCaptureMetersPerSecond>1.0 ||Options.ImprintRecoverySeconds<0.1||Options.ImprintRecoverySeconds>60.0 ||Options.ImprintMaxDepthMeters<0.0||Options.ImprintMaxDepthMeters>1.0) {std::cerr<<"alive-mode parameters are outside their supported ranges\n";return false;} return true; } double Quantile(std::vector Values,double Q) { if(Values.empty())return 0.0; std::sort(Values.begin(),Values.end()); const std::size_t I=static_cast(std::ceil(Q*Values.size())-1.0); return Values[std::min(I,Values.size()-1)]; } std::uint64_t FieldChecksum(const std::vector& Field) { // Hash float bit patterns in canonical little-endian byte order so the // result does not depend on the host's native byte order. std::uint64_t Hash=14695981039346656037ull; for(const float Value:Field) { const std::uint32_t Bits=std::bit_cast(Value); for(unsigned Byte=0;Byte<4;++Byte) { Hash^=static_cast((Bits>>(Byte*8))&0xffu); Hash*=1099511628211ull; } } return Hash; } 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& Times,const std::vector& Heights, const std::vector& Velocities,std::size_t Frames,int TimerQueryBits, int TimerQueryWaits,bool TimerResultsAvailable,double DispatchWallMsPerStep, std::uint64_t FieldChecksumValue,bool FirstStepAudit,bool DynamicsAudit) { std::error_code Error; std::filesystem::create_directories(Options.Output,Error); if(Error){std::cerr<<"cannot create output directory: "< Ms; Ms.reserve(Times.size()); for(const GLuint64 Ns:Times)Ms.push_back(static_cast(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()); char FieldChecksumHex[17]{}; std::snprintf(FieldChecksumHex,sizeof(FieldChecksumHex),"%016llx", static_cast(FieldChecksumValue)); 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)(TimerQueryRingSize,Times.size()):0)<<",\n" <<" \"timer_query_counter_bits\": "<=0.0)Json<(Times[I])*1.0e-6; } Json<<"\n ],\n \"gl_time_elapsed_summary_ms\": {\n" <<" \"mean\": "<(Json); } bool Run(const FOptions& Options) { FContext Context; if(!CreateContext(Options.Device,Context))return false; const auto* RendererPtr=reinterpret_cast(glGetString(GL_RENDERER)); const auto* VendorPtr=reinterpret_cast(glGetString(GL_VENDOR)); const auto* VersionPtr=reinterpret_cast(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: " < Fields{}; glGenTextures(2,Fields.data()); const std::vector Zeroes(static_cast(Size)*Size*4,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_RGBA32F,Size,Size,0,GL_RGBA,GL_FLOAT,Zeroes.data()); } FRenderResources Render; if(Options.CaptureEvery>0&&!Render.Initialize(Size,Size,Size)) {Render.Destroy();glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;} const int QueryCount=Options.TimerQueries ?std::min(static_cast(TimerQueryRingSize),Options.Steps):0; std::vector Queries(static_cast(QueryCount)); if(QueryCount>0)glGenQueries(QueryCount,Queries.data()); GLint TimerQueryBits=0; if(Options.TimerQueries)glGetQueryiv(GL_TIME_ELAPSED,GL_QUERY_COUNTER_BITS,&TimerQueryBits); 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 LocationForcePositions=glGetUniformLocation(ComputeProgram,"uForcePositions[0]"); const GLint LocationForceSigma=glGetUniformLocation(ComputeProgram,"uForceSigma"); const GLint LocationForceStrengths=glGetUniformLocation(ComputeProgram,"uForceStrengths[0]"); const GLint LocationBreathing=glGetUniformLocation(ComputeProgram,"uBreathing"); const GLint LocationBreathingStrength=glGetUniformLocation(ComputeProgram,"uBreathingStrength"); const GLint LocationImprint=glGetUniformLocation(ComputeProgram,"uImprint"); const GLint LocationImprintCapture=glGetUniformLocation(ComputeProgram,"uImprintCaptureRate"); const GLint LocationImprintRecovery=glGetUniformLocation(ComputeProgram,"uImprintRecoverySeconds"); const GLint LocationImprintDepth=glGetUniformLocation(ComputeProgram,"uImprintMaxDepth"); if(LocationGrid<0||LocationDt<0||LocationDx<0||LocationPlane<0||LocationWaveSpeed<0 ||LocationDamping<0||LocationFoundation<0||LocationTime<0 ||LocationForcePositions<0||LocationForceSigma<0||LocationForceStrengths<0 ||LocationBreathing<0||LocationBreathingStrength<0||LocationImprint<0 ||LocationImprintCapture<0||LocationImprintRecovery<0||LocationImprintDepth<0) {std::cerr<<"[LivingPlane] missing compute shader uniforms\n";return false;} glUseProgram(ComputeProgram); glUniform1i(LocationGrid,Size); glUniform1f(LocationDt,static_cast(Dt)); glUniform1f(LocationDx,static_cast(Dx)); glUniform1f(LocationPlane,static_cast(PlaneLengthMeters)); glUniform1f(LocationWaveSpeed,static_cast(WaveSpeedMetersPerSecond)); glUniform1f(LocationDamping,static_cast(DampingPerSecond)); glUniform1f(LocationFoundation,static_cast(FoundationStiffnessPerSecondSquared)); glUniform1f(LocationForceSigma,static_cast(SphereSigmaMeters)); glUniform1i(LocationBreathing,Options.Breathing?1:0); glUniform1f(LocationBreathingStrength,static_cast(Options.BreathingStrength)); glUniform1i(LocationImprint,Options.Imprint?1:0); glUniform1f(LocationImprintCapture, static_cast(Options.ImprintCaptureMetersPerSecond)); glUniform1f(LocationImprintRecovery,static_cast(Options.ImprintRecoverySeconds)); glUniform1f(LocationImprintDepth,static_cast(Options.ImprintMaxDepthMeters)); int Current=0; const auto Dispatch=[&](int Target,double Time, const std::array& Bodies,GLuint Query) { glUseProgram(ComputeProgram); glUniform1f(LocationTime,static_cast(Time)); const std::array Positions={ static_cast(Bodies[0].X),static_cast(Bodies[0].Y), static_cast(Bodies[1].X),static_cast(Bodies[1].Y)}; const std::array Strengths={ static_cast(Bodies[0].Force),static_cast(Bodies[1].Force)}; glUniform2fv(LocationForcePositions,2,Positions.data()); glUniform1fv(LocationForceStrengths,2,Strengths.data()); glBindImageTexture(0,Fields[static_cast(Current)],0,GL_FALSE,0, GL_READ_ONLY,GL_RGBA32F); glBindImageTexture(1,Fields[static_cast(Target)],0,GL_FALSE,0, GL_WRITE_ONLY,GL_RGBA32F); if(Query)glBeginQuery(GL_TIME_ELAPSED,Query); glDispatchCompute(static_cast((Size+15)/16), static_cast((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 before measurement. const std::array NoBodies{}; for(int Warm=0;Warm<8;++Warm)Dispatch(1-Current,-8.0*Dt,NoBodies,0); glFinish(); for(const GLuint Field:Fields) { glBindTexture(GL_TEXTURE_2D,Field); glTexSubImage2D(GL_TEXTURE_2D,0,0,0,Size,Size,GL_RGBA,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 Time)->bool { if(!RenderPtr)return true; char Filename[32]{}; std::snprintf(Filename,sizeof(Filename),"frame_%06d.png",FrameIndex++); const auto Bodies=GetBodiesAt(Time); if(!Render.Render(Fields[static_cast(Current)], Options.Output/Filename,Bodies[0],Bodies[1]))return false; ++FramesWritten; return true; }; if(!SaveFrame(0.0)) {Render.Destroy();if(QueryCount>0)glDeleteQueries(QueryCount,Queries.data()); glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;} std::vector Times(Options.TimerQueries ?static_cast(Options.Steps):0); int TimerQueryWaits=0; bool TimerResultsAvailable=Options.TimerQueries; auto ReadTimerQuery=[&](int QueryStep) { const GLuint Query=Queries[static_cast(QueryStep%QueryCount)]; GLint Available=GL_FALSE; glGetQueryObjectiv(Query,GL_QUERY_RESULT_AVAILABLE,&Available); if(Available!=GL_TRUE)++TimerQueryWaits; glGetQueryObjectui64v(Query,GL_QUERY_RESULT, &Times[static_cast(QueryStep)]); glGetQueryObjectiv(Query,GL_QUERY_RESULT_AVAILABLE,&Available); TimerResultsAvailable=TimerResultsAvailable&&Available==GL_TRUE; }; std::vector FirstField; bool FirstStepAudit=false; const auto WallStart=std::chrono::steady_clock::now(); for(int Step=0;Step0&&Step>=QueryCount)ReadTimerQuery(Step-QueryCount); const double Time=static_cast(Step)*Dt; const auto Bodies=GetBodiesAt(Time); const int Target=1-Current; const GLuint Query=QueryCount>0 ?Queries[static_cast(Step%QueryCount)]:0; Dispatch(Target,Time,Bodies,Query); if(Step==0) { glFinish(); FirstField.resize(static_cast(Size)*Size*4); glBindTexture(GL_TEXTURE_2D,Fields[static_cast(Current)]); // glGetTexImage is a texture-update access. The image/fetch // barriers used for simulation and rendering do not cover this // readback visibility path. glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT); glGetTexImage(GL_TEXTURE_2D,0,GL_RGBA,GL_FLOAT,FirstField.data()); const int X=std::clamp(static_cast(std::floor( (Bodies[0].X+PlaneLengthMeters*0.5)/Dx)),0,Size-1); const int Y=std::clamp(static_cast(std::floor( (Bodies[0].Y+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; double Force=0.0; for(const FBodyState& Body:Bodies) { const double Distance2=(CellX-Body.X)*(CellX-Body.X) +(CellY-Body.Y)*(CellY-Body.Y); Force-=Body.Force*std::exp(-Distance2/ (2.0*SphereSigmaMeters*SphereSigmaMeters)); } const double Expected=Force*Dt*Dt*std::exp(-DampingPerSecond*Dt); const std::size_t Index=(static_cast(Y)*Size+X)*4; 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 " <(Step+1)*Dt)) {Render.Destroy();if(QueryCount>0)glDeleteQueries(QueryCount,Queries.data()); glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;} } } glFinish(); const auto WallEnd=std::chrono::steady_clock::now(); const double WallMs=std::chrono::duration(WallEnd-WallStart).count(); const double DispatchWallMsPerStep=Options.CaptureEvery==0 ?WallMs/Options.Steps:-1.0; if(QueryCount>0) for(int Step=std::max(0,Options.Steps-QueryCount);Step FinalField(static_cast(Size)*Size*4); glBindTexture(GL_TEXTURE_2D,Fields[static_cast(Current)]); glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT); glGetTexImage(GL_TEXTURE_2D,0,GL_RGBA,GL_FLOAT,FinalField.data()); std::vector Heights,Velocities; Heights.reserve(static_cast(Size)*Size); Velocities.reserve(static_cast(Size)*Size); bool Finite=true; double ImprintMin=0.0; for(std::size_t I=0;I(FinalField[I+2])); } const auto [HeightMinIt,HeightMaxIt]=std::minmax_element(Heights.begin(),Heights.end()); const bool DynamicsAudit=Finite&&*HeightMinIt<-1.0e-7&&*HeightMaxIt-*HeightMinIt<2.0 &&(!Options.Imprint||ImprintMin>=-Options.ImprintMaxDepthMeters-1.0e-5); const std::uint64_t FinalFieldChecksum=FieldChecksum(FinalField); const bool Good=FirstStepAudit&&DynamicsAudit&&QueryAudit&&glGetError()==GL_NO_ERROR; if(!DynamicsAudit) std::cerr<<"[LivingPlane] field audit failed: finite="<0)glDeleteQueries(QueryCount,Queries.data()); glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;} const std::vector QueryMs=[&] { std::vector Result; Result.reserve(Times.size()); for(const GLuint64 Ns:Times)Result.push_back(static_cast(Ns)*1.0e-6); return Result; }(); std::cout<<"[LivingPlane] scene="<0)glDeleteQueries(QueryCount,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; }