#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 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(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); 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 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 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 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 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=100.0; const double F=1.0/std::tan(42.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{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 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); 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(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); glUniform3f(glGetUniformLocation(Program,"uSphereCenter"), static_cast(SphereX),static_cast(SphereY),static_cast(SphereZ)); glUniform1f(glGetUniformLocation(Program,"uSphereRadius"), static_cast(SphereRadiusMeters)); glBindVertexArray(SphereVao); 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 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] [--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: "<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 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)]; } 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,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()); 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)(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*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(Options.Steps); std::vector Queries(QueryCount); glGenQueries(static_cast(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(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); int Current=0; const auto Dispatch=[&](int Target,double Time,double SphereX,double SphereY, double ForceStrength,GLuint Query) { glUseProgram(ComputeProgram); glUniform1f(LocationTime,static_cast(Time)); glUniform2f(LocationForcePosition,static_cast(SphereX),static_cast(SphereY)); glUniform1f(LocationForceStrength,static_cast(ForceStrength)); glBindImageTexture(0,Fields[static_cast(Current)],0,GL_FALSE,0, GL_READ_ONLY,GL_RG32F); glBindImageTexture(1,Fields[static_cast(Target)],0,GL_FALSE,0, GL_WRITE_ONLY,GL_RG32F); 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 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(Current)], Options.Output/Filename,SphereX,SphereY,SphereZ))return false; ++FramesWritten; return true; }; const int MoveSteps=std::max(2,static_cast(std::lround(Options.Steps*0.58))); const auto SphereAt=[&](int Step) { const double MoveT=std::clamp(static_cast(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(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{X,0.0,Z,Force}; }; const auto Initial=SphereAt(0); if(!SaveFrame(Initial[0],Initial[1],Initial[2])) {Render.Destroy();glDeleteQueries(static_cast(Queries.size()),Queries.data()); glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;} std::vector FirstField; bool FirstStepAudit=false; for(int Step=0;Step(Step)*Dt,Sphere[0],Sphere[1],Sphere[3], Queries[static_cast(Step)]); if(Step==0) { glFinish(); FirstField.resize(static_cast(Size)*Size*2); glBindTexture(GL_TEXTURE_2D,Fields[static_cast(Current)]); glGetTexImage(GL_TEXTURE_2D,0,GL_RG,GL_FLOAT,FirstField.data()); const int X=std::clamp(static_cast(std::floor( (Sphere[0]+PlaneLengthMeters*0.5)/Dx)),0,Size-1); const int Y=std::clamp(static_cast(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(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 " <(Queries.size()),Queries.data()); glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;} } } glFinish(); std::vector Times(static_cast(Options.Steps)); for(int Step=0;Step(Step)],GL_QUERY_RESULT, &Times[static_cast(Step)]); std::vector FinalField(static_cast(Size)*Size*2); glBindTexture(GL_TEXTURE_2D,Fields[static_cast(Current)]); glGetTexImage(GL_TEXTURE_2D,0,GL_RG,GL_FLOAT,FinalField.data()); std::vector Heights,Velocities; Heights.reserve(static_cast(Size)*Size); Velocities.reserve(static_cast(Size)*Size); bool Finite=true; for(std::size_t I=0;I(Queries.size()),Queries.data()); glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;} double MeanNs=0.0; for(const GLuint64 Ns:Times)MeanNs+=static_cast(Ns); MeanNs/=Times.size(); std::cout<<"[LivingPlane] size="<(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; }