experiment: LivingPlane readable + alive mode (L163)

RGBA32F displacement/velocity/imprint field; rolling base relief, grazing light, elevation colours, 7.5 cm contours,
follow camera; scenes: roll, rest and wobble, lift-off, second sphere; alive mode = travelling swells + viscoelastic
imprint. Intel ADL GT2: ~0.17 ms/step at 512^2. Open: run-to-run difference with queries on/off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-11-02 10:55:32 +01:00
1 parent 5b15595e4a
commit e1d87eed26
3 files changed
+460 -128

No files matched your search

+388 -127
View File
@@ -8,6 +8,7 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <cerrno> #include <cerrno>
#include <chrono>
#include <cctype> #include <cctype>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
@@ -34,18 +35,33 @@ namespace
constexpr double DampingPerSecond=1.35; constexpr double DampingPerSecond=1.35;
constexpr double FoundationStiffnessPerSecondSquared=28.0; constexpr double FoundationStiffnessPerSecondSquared=28.0;
constexpr double SphereRadiusMeters=0.43; constexpr double SphereRadiusMeters=0.43;
constexpr double SphereSigmaMeters=0.46; constexpr double SphereSigmaMeters=0.58;
constexpr double SpherePushMetersPerSecondSquared=14.0; constexpr double SpherePushMetersPerSecondSquared=36.0;
constexpr GLuint TimerQueryRingSize=64;
constexpr double Pi=3.14159265358979323846; constexpr double Pi=3.14159265358979323846;
struct FBodyState
{
double X=0.0,Y=0.0,Z=0.0,Force=0.0;
};
struct FOptions struct FOptions
{ {
std::string Device="auto"; std::string Device="auto";
std::filesystem::path Output="Saved/Linux/l162/living-plane"; std::filesystem::path Output="Saved/Linux/l163/living-plane";
std::string Scene="wake";
int Size=128; int Size=128;
int Steps=360; int Steps=360;
int CaptureEvery=1; int CaptureEvery=1;
bool TimerQueries=true;
bool Breathing=false; 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 struct FDeviceCandidate
@@ -361,8 +377,8 @@ namespace
{ {
static constexpr char Source[]=R"GLSL(#version 430 core static constexpr char Source[]=R"GLSL(#version 430 core
layout(local_size_x=16,local_size_y=16) in; layout(local_size_x=16,local_size_y=16) in;
layout(binding=0,rg32f) readonly uniform image2D uSource; layout(binding=0,rgba32f) readonly uniform image2D uSource;
layout(binding=1,rg32f) writeonly uniform image2D uTarget; layout(binding=1,rgba32f) writeonly uniform image2D uTarget;
uniform int uGridSize; uniform int uGridSize;
uniform float uDt; uniform float uDt;
uniform float uDx; uniform float uDx;
@@ -371,38 +387,61 @@ uniform float uWaveSpeed;
uniform float uDamping; uniform float uDamping;
uniform float uFoundationStiffness; uniform float uFoundationStiffness;
uniform float uTime; uniform float uTime;
uniform vec2 uForcePosition; uniform vec2 uForcePositions[2];
uniform float uForceSigma; uniform float uForceSigma;
uniform float uForceStrength; uniform float uForceStrengths[2];
uniform int uBreathing; uniform int uBreathing;
uniform float uBreathingStrength;
uniform int uImprint;
uniform float uImprintCaptureRate;
uniform float uImprintRecoverySeconds;
uniform float uImprintMaxDepth;
void main() void main()
{ {
ivec2 p=ivec2(gl_GlobalInvocationID.xy); ivec2 p=ivec2(gl_GlobalInvocationID.xy);
if(p.x>=uGridSize||p.y>=uGridSize)return; if(p.x>=uGridSize||p.y>=uGridSize)return;
ivec2 lo=ivec2(0); ivec2 lo=ivec2(0);
ivec2 hi=ivec2(uGridSize-1); ivec2 hi=ivec2(uGridSize-1);
float h=imageLoad(uSource,p).r; vec4 state=imageLoad(uSource,p);
float v=imageLoad(uSource,p).g; 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 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 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 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 u=imageLoad(uSource,ivec2(p.x,min(p.y+1,hi.y))).r;
float laplacian=(l+r+d+u-4.0*h)/(uDx*uDx); float laplacian=(l+r+d+u-4.0*h)/(uDx*uDx);
vec2 world=(vec2(p)+vec2(0.5))*uDx-vec2(uPlaneLength*0.5); 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 sigma2=max(uForceSigma*uForceSigma,1.0e-8);
float interaction=-uForceStrength*exp(-dot(offset,offset)/(2.0*sigma2)); 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; float breathing=0.0;
if(uBreathing!=0) if(uBreathing!=0)
{ {
float spatial=sin(world.x*0.72)*sin(world.y*0.72); float swell=sin(world.x*0.48-uTime*0.22)
breathing=2.4*spatial*sin(uTime*1.45); *cos(world.y*0.42+uTime*0.16);
breathing=uBreathingStrength*swell*sin(uTime*0.48);
} }
float acceleration=uWaveSpeed*uWaveSpeed*laplacian-uFoundationStiffness*h 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; +interaction+breathing;
float nextVelocity=(v+acceleration*uDt)*exp(-uDamping*uDt); float nextVelocity=(v+acceleration*uDt)*exp(-uDamping*uDt);
float nextHeight=h+nextVelocity*uDt; float nextHeight=h+nextVelocity*uDt;
imageStore(uTarget,p,vec4(nextHeight,nextVelocity,0.0,0.0)); imageStore(uTarget,p,vec4(nextHeight,nextVelocity,nextImprint,0.0));
})GLSL"; })GLSL";
const GLuint Shader=CompileShader(GL_COMPUTE_SHADER,Source); const GLuint Shader=CompileShader(GL_COMPUTE_SHADER,Source);
if(!Shader)return 0; if(!Shader)return 0;
@@ -424,11 +463,17 @@ uniform float uPlaneLength;
uniform int uRenderSphere; uniform int uRenderSphere;
uniform vec3 uSphereCenter; uniform vec3 uSphereCenter;
uniform float uSphereRadius; uniform float uSphereRadius;
uniform int uSphereMaterial;
uniform mat4 uViewProjection; uniform mat4 uViewProjection;
out vec3 vWorld; out vec3 vWorld;
out vec3 vNormal; out vec3 vNormal;
out float vHeight; out float vHeight;
flat out int vSphere; 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() void main()
{ {
vec3 world; vec3 world;
@@ -442,11 +487,21 @@ void main()
ivec2 right=ivec2(min(texel.x+1,uGridSize-1),texel.y); ivec2 right=ivec2(min(texel.x+1,uGridSize-1),texel.y);
ivec2 down=ivec2(texel.x,max(texel.y-1,0)); ivec2 down=ivec2(texel.x,max(texel.y-1,0));
ivec2 up=ivec2(texel.x,min(texel.y+1,uGridSize-1)); ivec2 up=ivec2(texel.x,min(texel.y+1,uGridSize-1));
height=texelFetch(uField,texel,0).r; 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 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 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 dhdx=(rightHeight-leftHeight)/dx;
float dhdy=(texelFetch(uField,up,0).r-texelFetch(uField,down,0).r)/dy; float dhdy=(upHeight-downHeight)/dy;
world=vec3((aUv-vec2(0.5))*uPlaneLength,height); world=vec3((aUv-vec2(0.5))*uPlaneLength,height);
normal=normalize(vec3(-dhdx,-dhdy,1.0)); normal=normalize(vec3(-dhdx,-dhdy,1.0));
} }
@@ -459,37 +514,43 @@ void main()
vWorld=world; vWorld=world;
vNormal=normal; vNormal=normal;
vHeight=height; vHeight=height;
vSphere=uRenderSphere; vSphereMaterial=uRenderSphere==0?0:uSphereMaterial;
gl_Position=uViewProjection*vec4(world,1.0); gl_Position=uViewProjection*vec4(world,1.0);
})GLSL"; })GLSL";
static constexpr char FragmentSource[]=R"GLSL(#version 430 core static constexpr char FragmentSource[]=R"GLSL(#version 430 core
in vec3 vWorld; in vec3 vWorld;
in vec3 vNormal; in vec3 vNormal;
in float vHeight; in float vHeight;
flat in int vSphere; flat in int vSphereMaterial;
uniform vec3 uCameraPosition; uniform vec3 uCameraPosition;
out vec4 outColor; out vec4 outColor;
void main() void main()
{ {
vec3 n=normalize(vNormal); vec3 n=normalize(vNormal);
vec3 l=normalize(vec3(-0.45,-0.55,0.72)); vec3 l=normalize(vec3(-0.62,-0.54,0.32));
vec3 v=normalize(uCameraPosition-vWorld); vec3 v=normalize(uCameraPosition-vWorld);
float diffuse=max(dot(n,l),0.0); 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); float slope=1.0-clamp(n.z,0.0,1.0);
if(vSphere!=0) float spec=pow(max(dot(reflect(-l,n),v),0.0),vSphereMaterial==0?72.0:38.0);
if(vSphereMaterial!=0)
{ {
vec3 base=vec3(0.98,0.43,0.13); 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; float light=0.24+0.84*diffuse;
outColor=vec4(base*light+vec3(0.65,0.43,0.22)*spec,1.0); outColor=vec4(base*light+vec3(0.65,0.43,0.22)*spec,1.0);
} }
else else
{ {
float crest=clamp(0.50+vHeight*0.8,0.0,1.0); float low= smoothstep(-0.46,-0.08,vHeight);
vec3 deep=vec3(0.035,0.22,0.34); float high=smoothstep(0.02,0.32,vHeight);
vec3 crestColor=vec3(0.13,0.62,0.70); vec3 base=mix(vec3(0.025,0.075,0.13),vec3(0.055,0.30,0.31),low);
vec3 base=mix(deep,crestColor,crest); base=mix(base,vec3(0.28,0.46,0.28),high*0.82);
float light=0.28+0.88*diffuse; float contourPhase=fract((vHeight+0.60)/0.075);
outColor=vec4(base*light+vec3(0.31,0.62,0.68)*spec,1.0); 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"; })GLSL";
const GLuint VS=CompileShader(GL_VERTEX_SHADER,VertexSource); const GLuint VS=CompileShader(GL_VERTEX_SHADER,VertexSource);
@@ -509,6 +570,65 @@ void main()
struct FVec3 { double X=0.0,Y=0.0,Z=0.0; }; 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<FBodyState,2> 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<RollEnd)
{
Primary.X=-3.55+4.05*Smooth01(Time/RollEnd);
Primary.Z=TerrainHeight(Primary.X,Primary.Y)+0.41;
Primary.Force=SpherePushMetersPerSecondSquared;
}
else if(Time<RestEnd)
{
const double RestTime=Time-RollEnd;
Primary.X=0.50;
Primary.Z=TerrainHeight(Primary.X,Primary.Y)+0.41
+0.065*std::sin(RestTime*2.0*Pi*1.15);
Primary.Force=SpherePushMetersPerSecondSquared
*(0.82+0.18*std::sin(RestTime*2.0*Pi*1.15));
}
else if(Time<LiftEnd)
{
const double Lift=Smooth01((Time-RestEnd)/(LiftEnd-RestEnd));
Primary.X=0.50;
Primary.Z=TerrainHeight(Primary.X,Primary.Y)+0.41+1.10*Lift;
}
else
{
Primary.X=0.50;
Primary.Z=TerrainHeight(Primary.X,Primary.Y)+1.51;
}
FBodyState Secondary;
Secondary.Y=1.05+0.14*std::sin(Time*0.72);
const double Travel=Smooth01((Time-SecondaryStart)/(SecondaryEnd-SecondaryStart));
Secondary.X=5.15-10.0*Travel;
Secondary.Z=TerrainHeight(Secondary.X,Secondary.Y)+0.41;
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 Sub(FVec3 A,FVec3 B){return {A.X-B.X,A.Y-B.Y,A.Z-B.Z};}
FVec3 Cross(FVec3 A,FVec3 B) 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};} {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};}
@@ -519,9 +639,8 @@ void main()
return Length>1.0e-12?FVec3{V.X/Length,V.Y/Length,V.Z/Length}:FVec3{}; 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) std::array<float,16> MakeViewProjection(int Width,int Height,FVec3 Camera,FVec3 Target)
{ {
const FVec3 Target={0.0,0.0,0.0};
const FVec3 Forward=Normalize(Sub(Target,Camera)); const FVec3 Forward=Normalize(Sub(Target,Camera));
const FVec3 Side=Normalize(Cross(Forward,{0.0,0.0,1.0})); const FVec3 Side=Normalize(Cross(Forward,{0.0,0.0,1.0}));
const FVec3 Up=Cross(Side,Forward); const FVec3 Up=Cross(Side,Forward);
@@ -534,8 +653,8 @@ void main()
View[10]=static_cast<float>(-Forward.Z);View[14]=static_cast<float>(Dot(Forward,Camera)); View[10]=static_cast<float>(-Forward.Z);View[14]=static_cast<float>(Dot(Forward,Camera));
View[15]=1.0f; View[15]=1.0f;
const double Near=0.1,Far=100.0; const double Near=0.1,Far=60.0;
const double F=1.0/std::tan(42.0*Pi/360.0); const double F=1.0/std::tan(39.0*Pi/360.0);
const double Aspect=static_cast<double>(Width)/static_cast<double>(Height); const double Aspect=static_cast<double>(Width)/static_cast<double>(Height);
std::array<float,16> Projection{}; std::array<float,16> Projection{};
Projection[0]=static_cast<float>(F/Aspect); Projection[0]=static_cast<float>(F/Aspect);
@@ -635,7 +754,7 @@ void main()
GLsizei GridIndexCount=0,SphereIndexCount=0; GLsizei GridIndexCount=0,SphereIndexCount=0;
int Width=0,Height=0,GridSize=0; int Width=0,Height=0,GridSize=0;
std::array<float,16> ViewProjection{}; std::array<float,16> ViewProjection{};
FVec3 Camera{8.8,-12.2,9.4}; FVec3 Camera{};
void Destroy() void Destroy()
{ {
@@ -672,7 +791,7 @@ void main()
if(glCheckFramebufferStatus(GL_FRAMEBUFFER)!=GL_FRAMEBUFFER_COMPLETE) if(glCheckFramebufferStatus(GL_FRAMEBUFFER)!=GL_FRAMEBUFFER_COMPLETE)
{std::cerr<<"[LivingPlane] offscreen framebuffer is incomplete\n";return false;} {std::cerr<<"[LivingPlane] offscreen framebuffer is incomplete\n";return false;}
const int Segments=std::min(Size,256); const int Segments=std::min(Size,512);
std::vector<FGridVertex> GridVertices; std::vector<FGridVertex> GridVertices;
std::vector<std::uint32_t> GridIndices; std::vector<std::uint32_t> GridIndices;
MakeGrid(Segments,GridVertices,GridIndices); MakeGrid(Segments,GridVertices,GridIndices);
@@ -708,14 +827,16 @@ void main()
static_cast<GLsizeiptr>(SphereIndices.size()*sizeof(std::uint32_t)), static_cast<GLsizeiptr>(SphereIndices.size()*sizeof(std::uint32_t)),
SphereIndices.data(),GL_STATIC_DRAW); SphereIndices.data(),GL_STATIC_DRAW);
ViewProjection=MakeViewProjection(Width,Height,Camera);
glBindVertexArray(0); glBindVertexArray(0);
return glGetError()==GL_NO_ERROR; return glGetError()==GL_NO_ERROR;
} }
bool Render(GLuint FieldTexture,const std::filesystem::path& Output, bool Render(GLuint FieldTexture,const std::filesystem::path& Output,
double SphereX,double SphereY,double SphereZ) 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); glBindFramebuffer(GL_FRAMEBUFFER,Framebuffer);
glViewport(0,0,Width,Height); glViewport(0,0,Width,Height);
glDisable(GL_CULL_FACE); glDisable(GL_CULL_FACE);
@@ -739,11 +860,18 @@ void main()
glBindVertexArray(GridVao); glBindVertexArray(GridVao);
glDrawElements(GL_TRIANGLES,GridIndexCount,GL_UNSIGNED_INT,nullptr); glDrawElements(GL_TRIANGLES,GridIndexCount,GL_UNSIGNED_INT,nullptr);
glUniform1i(glGetUniformLocation(Program,"uRenderSphere"),1); 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"), glUniform1f(glGetUniformLocation(Program,"uSphereRadius"),
static_cast<float>(SphereRadiusMeters)); static_cast<float>(SphereRadiusMeters));
glBindVertexArray(SphereVao); glBindVertexArray(SphereVao);
glUniform1i(glGetUniformLocation(Program,"uSphereMaterial"),1);
glUniform3f(glGetUniformLocation(Program,"uSphereCenter"),
static_cast<float>(Primary.X),static_cast<float>(Primary.Y),
static_cast<float>(Primary.Z));
glDrawElements(GL_TRIANGLES,SphereIndexCount,GL_UNSIGNED_INT,nullptr);
glUniform1i(glGetUniformLocation(Program,"uSphereMaterial"),2);
glUniform3f(glGetUniformLocation(Program,"uSphereCenter"),
static_cast<float>(Secondary.X),static_cast<float>(Secondary.Y),
static_cast<float>(Secondary.Z));
glDrawElements(GL_TRIANGLES,SphereIndexCount,GL_UNSIGNED_INT,nullptr); glDrawElements(GL_TRIANGLES,SphereIndexCount,GL_UNSIGNED_INT,nullptr);
glFinish(); glFinish();
std::vector<std::uint8_t> Pixels(static_cast<std::size_t>(Width)*Height*4); std::vector<std::uint8_t> Pixels(static_cast<std::size_t>(Width)*Height*4);
@@ -768,6 +896,18 @@ void main()
return true; 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) bool ParseOptions(int Argc,char** Argv,FOptions& Options)
{ {
for(int I=1;I<Argc;++I) for(int I=1;I<Argc;++I)
@@ -782,13 +922,17 @@ void main()
std::string_view Value; std::string_view Value;
if(Arg=="--help"||Arg=="-h") if(Arg=="--help"||Arg=="-h")
{ {
std::cout<<"Usage: living-plane [--device=auto|gpu|cpu] [--size=N] [--steps=N] " std::cout<<"Usage: living-plane [--device=auto|gpu|cpu] [--scene=wake|alive] "
"[--out=DIR] [--capture-every=N] [--breathing=on|off]\n" "[--size=N] [--steps=N] [--out=DIR] [--capture-every=N]\n"
" --size is the square height-field and PNG resolution.\n" " [--queries=on|off] [--breathing=on|off] [--imprint=on|off]\n"
" --capture-every=0 writes only timing.json. Default is 1.\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); std::exit(0);
} }
else if(Take("--device",Value))Options.Device=Value; else if(Take("--device",Value))Options.Device=Value;
else if(Take("--scene",Value))Options.Scene=Value;
else if(Take("--size",Value)) else if(Take("--size",Value))
{if(!ParseInt(Value,Options.Size)){std::cerr<<"invalid --size\n";return false;}} {if(!ParseInt(Value,Options.Size)){std::cerr<<"invalid --size\n";return false;}}
else if(Take("--steps",Value)) else if(Take("--steps",Value))
@@ -796,22 +940,66 @@ void main()
else if(Take("--capture-every",Value)) else if(Take("--capture-every",Value))
{if(!ParseInt(Value,Options.CaptureEvery)){std::cerr<<"invalid --capture-every\n";return false;}} {if(!ParseInt(Value,Options.CaptureEvery)){std::cerr<<"invalid --capture-every\n";return false;}}
else if(Take("--out",Value))Options.Output=Value; 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)) else if(Take("--breathing",Value))
{ {
if(Value=="on")Options.Breathing=true; if(Value=="on")Options.Breathing=true;
else if(Value=="off")Options.Breathing=false; else if(Value=="off")Options.Breathing=false;
else {std::cerr<<"--breathing must be on or off\n";return 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: "<<Arg<<"\n";return false;} else {std::cerr<<"unknown option: "<<Arg<<"\n";return false;}
} }
if(Options.Device!="auto"&&Options.Device!="gpu"&&Options.Device!="cpu") if(Options.Device!="auto"&&Options.Device!="gpu"&&Options.Device!="cpu")
{std::cerr<<"--device must be auto, gpu, or cpu\n";return false;} {std::cerr<<"--device must be auto, gpu, or cpu\n";return false;}
if(Options.Scene!="wake"&&Options.Scene!="alive")
{std::cerr<<"--scene must be wake or alive\n";return false;}
if(!Options.BreathingConfigured)Options.Breathing=Options.Scene=="alive";
if(!Options.ImprintConfigured)Options.Imprint=Options.Scene=="alive";
if(Options.Size<16||Options.Size>4096) if(Options.Size<16||Options.Size>4096)
{std::cerr<<"--size must be in [16,4096]\n";return false;} {std::cerr<<"--size must be in [16,4096]\n";return false;}
if(Options.Steps<1||Options.Steps>100000) if(Options.Steps<1||Options.Steps>100000)
{std::cerr<<"--steps must be in [1,100000]\n";return false;} {std::cerr<<"--steps must be in [1,100000]\n";return false;}
if(Options.CaptureEvery<0||Options.CaptureEvery>100000) if(Options.CaptureEvery<0||Options.CaptureEvery>100000)
{std::cerr<<"--capture-every must be in [0,100000]\n";return false;} {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; return true;
} }
@@ -826,8 +1014,9 @@ void main()
bool WriteTimingJson(const FOptions& Options,const FContext& Context, bool WriteTimingJson(const FOptions& Options,const FContext& Context,
std::string_view Renderer,std::string_view Vendor,std::string_view Version, std::string_view Renderer,std::string_view Vendor,std::string_view Version,
double Dt,const std::vector<GLuint64>& Times,const std::vector<double>& Heights, double Dt,const std::vector<GLuint64>& Times,const std::vector<double>& Heights,
const std::vector<double>& Velocities,std::size_t Frames,bool FirstStepAudit, const std::vector<double>& Velocities,std::size_t Frames,int TimerQueryBits,
bool DynamicsAudit) int TimerQueryWaits,bool TimerResultsAvailable,double DispatchWallMsPerStep,
bool FirstStepAudit,bool DynamicsAudit)
{ {
std::error_code Error; std::error_code Error;
std::filesystem::create_directories(Options.Output,Error); std::filesystem::create_directories(Options.Output,Error);
@@ -862,6 +1051,7 @@ void main()
<<" \"gl_version\": \""<<JsonEscape(Version)<<"\",\n" <<" \"gl_version\": \""<<JsonEscape(Version)<<"\",\n"
<<" \"size\": "<<Options.Size<<",\n" <<" \"size\": "<<Options.Size<<",\n"
<<" \"steps\": "<<Options.Steps<<",\n" <<" \"steps\": "<<Options.Steps<<",\n"
<<" \"scene\": \""<<Options.Scene<<"\",\n"
<<" \"plane_length_m\": "<<PlaneLengthMeters<<",\n" <<" \"plane_length_m\": "<<PlaneLengthMeters<<",\n"
<<" \"dt_seconds\": "<<Dt<<",\n" <<" \"dt_seconds\": "<<Dt<<",\n"
<<" \"spectral_step_ratio\": "<<SpectralStep<<",\n" <<" \"spectral_step_ratio\": "<<SpectralStep<<",\n"
@@ -869,12 +1059,30 @@ void main()
<<" \"damping_per_s\": "<<DampingPerSecond<<",\n" <<" \"damping_per_s\": "<<DampingPerSecond<<",\n"
<<" \"foundation_stiffness_per_s2\": "<<FoundationStiffnessPerSecondSquared<<",\n" <<" \"foundation_stiffness_per_s2\": "<<FoundationStiffnessPerSecondSquared<<",\n"
<<" \"breathing\": "<<(Options.Breathing?"true":"false")<<",\n" <<" \"breathing\": "<<(Options.Breathing?"true":"false")<<",\n"
<<" \"breathing_strength_m_per_s2\": "<<Options.BreathingStrength<<",\n"
<<" \"imprint\": "<<(Options.Imprint?"true":"false")<<",\n"
<<" \"imprint_capture_m_per_s\": "<<Options.ImprintCaptureMetersPerSecond<<",\n"
<<" \"imprint_recovery_seconds\": "<<Options.ImprintRecoverySeconds<<",\n"
<<" \"imprint_max_depth_m\": "<<Options.ImprintMaxDepthMeters<<",\n"
<<" \"frames_written\": "<<Frames<<",\n" <<" \"frames_written\": "<<Frames<<",\n"
<<" \"first_step_equation_audit\": \""<<(FirstStepAudit?"pass":"fail")<<"\",\n" <<" \"first_step_equation_audit\": \""<<(FirstStepAudit?"pass":"fail")<<"\",\n"
<<" \"field_dynamics_audit\": \""<<(DynamicsAudit?"pass":"fail")<<"\",\n" <<" \"field_dynamics_audit\": \""<<(DynamicsAudit?"pass":"fail")<<"\",\n"
<<" \"timing_interpretation\": \"" <<" \"timer_queries_enabled\": "<<(Options.TimerQueries?"true":"false")<<",\n"
<<(Context.ExecutionDevice=="gpu"?"GPU GL_TIME_ELAPSED per compute dispatch" <<" \"timer_query_ring_size\": "<<(Options.TimerQueries
:"software-renderer GL_TIME_ELAPSED values; not GPU performance timings")<<"\",\n" ?std::min<std::size_t>(TimerQueryRingSize,Times.size()):0)<<",\n"
<<" \"timer_query_counter_bits\": "<<TimerQueryBits<<",\n"
<<" \"timer_query_results_available_after_completion\": ";
if(Options.TimerQueries)Json<<(TimerResultsAvailable?"true":"false");
else Json<<"null";
Json<<",\n"
<<" \"timer_query_waits_before_reuse\": "<<TimerQueryWaits<<",\n"
<<" \"dispatch_wall_ms_per_step\": ";
if(DispatchWallMsPerStep>=0.0)Json<<DispatchWallMsPerStep;
else Json<<"null";
Json<<",\n \"dispatch_wall_window\": \""
<<(Options.CaptureEvery==0
?"simulation loop plus final glFinish; no frame capture; first-step equation readback included"
:"not suitable for step cost because frame capture is enabled")<<"\",\n"
<<" \"field_min_m\": "<<HeightMin<<",\n" <<" \"field_min_m\": "<<HeightMin<<",\n"
<<" \"field_max_m\": "<<HeightMax<<",\n" <<" \"field_max_m\": "<<HeightMax<<",\n"
<<" \"field_mean_m\": "<<HeightMean<<",\n" <<" \"field_mean_m\": "<<HeightMean<<",\n"
@@ -924,7 +1132,7 @@ void main()
WaveSpeedMetersPerSecond/(Dx*Dx)+FoundationStiffnessPerSecondSquared); WaveSpeedMetersPerSecond/(Dx*Dx)+FoundationStiffnessPerSecondSquared);
std::array<GLuint,2> Fields{}; std::array<GLuint,2> Fields{};
glGenTextures(2,Fields.data()); glGenTextures(2,Fields.data());
const std::vector<float> Zeroes(static_cast<std::size_t>(Size)*Size*2,0.0f); const std::vector<float> Zeroes(static_cast<std::size_t>(Size)*Size*4,0.0f);
for(const GLuint Field:Fields) for(const GLuint Field:Fields)
{ {
glBindTexture(GL_TEXTURE_2D,Field); glBindTexture(GL_TEXTURE_2D,Field);
@@ -932,17 +1140,20 @@ void main()
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_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_S,GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,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()); glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA32F,Size,Size,0,GL_RGBA,GL_FLOAT,Zeroes.data());
} }
const int CaptureWidth=Size,CaptureHeight=Size;
FRenderResources Render; FRenderResources Render;
if(Options.CaptureEvery>0&&!Render.Initialize(Size,CaptureWidth,CaptureHeight)) if(Options.CaptureEvery>0&&!Render.Initialize(Size,Size,Size))
{Render.Destroy();glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;} {Render.Destroy();glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;}
const GLuint QueryCount=static_cast<GLuint>(Options.Steps); const int QueryCount=Options.TimerQueries
std::vector<GLuint> Queries(QueryCount); ?std::min<int>(static_cast<int>(TimerQueryRingSize),Options.Steps):0;
glGenQueries(static_cast<GLsizei>(QueryCount),Queries.data()); std::vector<GLuint> Queries(static_cast<std::size_t>(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 LocationGrid=glGetUniformLocation(ComputeProgram,"uGridSize");
const GLint LocationDt=glGetUniformLocation(ComputeProgram,"uDt"); const GLint LocationDt=glGetUniformLocation(ComputeProgram,"uDt");
const GLint LocationDx=glGetUniformLocation(ComputeProgram,"uDx"); const GLint LocationDx=glGetUniformLocation(ComputeProgram,"uDx");
@@ -951,14 +1162,20 @@ void main()
const GLint LocationDamping=glGetUniformLocation(ComputeProgram,"uDamping"); const GLint LocationDamping=glGetUniformLocation(ComputeProgram,"uDamping");
const GLint LocationFoundation=glGetUniformLocation(ComputeProgram,"uFoundationStiffness"); const GLint LocationFoundation=glGetUniformLocation(ComputeProgram,"uFoundationStiffness");
const GLint LocationTime=glGetUniformLocation(ComputeProgram,"uTime"); const GLint LocationTime=glGetUniformLocation(ComputeProgram,"uTime");
const GLint LocationForcePosition=glGetUniformLocation(ComputeProgram,"uForcePosition"); const GLint LocationForcePositions=glGetUniformLocation(ComputeProgram,"uForcePositions[0]");
const GLint LocationForceSigma=glGetUniformLocation(ComputeProgram,"uForceSigma"); const GLint LocationForceSigma=glGetUniformLocation(ComputeProgram,"uForceSigma");
const GLint LocationForceStrength=glGetUniformLocation(ComputeProgram,"uForceStrength"); const GLint LocationForceStrengths=glGetUniformLocation(ComputeProgram,"uForceStrengths[0]");
const GLint LocationBreathing=glGetUniformLocation(ComputeProgram,"uBreathing"); 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 if(LocationGrid<0||LocationDt<0||LocationDx<0||LocationPlane<0||LocationWaveSpeed<0
||LocationDamping<0||LocationFoundation<0||LocationTime<0 ||LocationDamping<0||LocationFoundation<0||LocationTime<0
||LocationForcePosition<0||LocationForceSigma<0 ||LocationForcePositions<0||LocationForceSigma<0||LocationForceStrengths<0
||LocationForceStrength<0||LocationBreathing<0) ||LocationBreathing<0||LocationBreathingStrength<0||LocationImprint<0
||LocationImprintCapture<0||LocationImprintRecovery<0||LocationImprintDepth<0)
{std::cerr<<"[LivingPlane] missing compute shader uniforms\n";return false;} {std::cerr<<"[LivingPlane] missing compute shader uniforms\n";return false;}
glUseProgram(ComputeProgram); glUseProgram(ComputeProgram);
@@ -971,19 +1188,30 @@ void main()
glUniform1f(LocationFoundation,static_cast<float>(FoundationStiffnessPerSecondSquared)); glUniform1f(LocationFoundation,static_cast<float>(FoundationStiffnessPerSecondSquared));
glUniform1f(LocationForceSigma,static_cast<float>(SphereSigmaMeters)); glUniform1f(LocationForceSigma,static_cast<float>(SphereSigmaMeters));
glUniform1i(LocationBreathing,Options.Breathing?1:0); glUniform1i(LocationBreathing,Options.Breathing?1:0);
glUniform1f(LocationBreathingStrength,static_cast<float>(Options.BreathingStrength));
glUniform1i(LocationImprint,Options.Imprint?1:0);
glUniform1f(LocationImprintCapture,
static_cast<float>(Options.ImprintCaptureMetersPerSecond));
glUniform1f(LocationImprintRecovery,static_cast<float>(Options.ImprintRecoverySeconds));
glUniform1f(LocationImprintDepth,static_cast<float>(Options.ImprintMaxDepthMeters));
int Current=0; int Current=0;
const auto Dispatch=[&](int Target,double Time,double SphereX,double SphereY, const auto Dispatch=[&](int Target,double Time,
double ForceStrength,GLuint Query) const std::array<FBodyState,2>& Bodies,GLuint Query)
{ {
glUseProgram(ComputeProgram); glUseProgram(ComputeProgram);
glUniform1f(LocationTime,static_cast<float>(Time)); glUniform1f(LocationTime,static_cast<float>(Time));
glUniform2f(LocationForcePosition,static_cast<float>(SphereX),static_cast<float>(SphereY)); const std::array<float,4> Positions={
glUniform1f(LocationForceStrength,static_cast<float>(ForceStrength)); static_cast<float>(Bodies[0].X),static_cast<float>(Bodies[0].Y),
static_cast<float>(Bodies[1].X),static_cast<float>(Bodies[1].Y)};
const std::array<float,2> Strengths={
static_cast<float>(Bodies[0].Force),static_cast<float>(Bodies[1].Force)};
glUniform2fv(LocationForcePositions,2,Positions.data());
glUniform1fv(LocationForceStrengths,2,Strengths.data());
glBindImageTexture(0,Fields[static_cast<std::size_t>(Current)],0,GL_FALSE,0, glBindImageTexture(0,Fields[static_cast<std::size_t>(Current)],0,GL_FALSE,0,
GL_READ_ONLY,GL_RG32F); GL_READ_ONLY,GL_RGBA32F);
glBindImageTexture(1,Fields[static_cast<std::size_t>(Target)],0,GL_FALSE,0, glBindImageTexture(1,Fields[static_cast<std::size_t>(Target)],0,GL_FALSE,0,
GL_WRITE_ONLY,GL_RG32F); GL_WRITE_ONLY,GL_RGBA32F);
if(Query)glBeginQuery(GL_TIME_ELAPSED,Query); if(Query)glBeginQuery(GL_TIME_ELAPSED,Query);
glDispatchCompute(static_cast<GLuint>((Size+15)/16), glDispatchCompute(static_cast<GLuint>((Size+15)/16),
static_cast<GLuint>((Size+15)/16),1); static_cast<GLuint>((Size+15)/16),1);
@@ -992,14 +1220,14 @@ void main()
Current=Target; Current=Target;
}; };
// Warm the runtime-compiled compute kernel with zero forcing before timing. // Warm the runtime-compiled compute kernel before measurement.
for(int Warm=0;Warm<8;++Warm) const std::array<FBodyState,2> NoBodies{};
Dispatch(1-Current,-8.0*Dt,0.0,0.0,0.0,0); for(int Warm=0;Warm<8;++Warm)Dispatch(1-Current,-8.0*Dt,NoBodies,0);
glFinish(); glFinish();
for(const GLuint Field:Fields) for(const GLuint Field:Fields)
{ {
glBindTexture(GL_TEXTURE_2D,Field); glBindTexture(GL_TEXTURE_2D,Field);
glTexSubImage2D(GL_TEXTURE_2D,0,0,0,Size,Size,GL_RG,GL_FLOAT,Zeroes.data()); 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); glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT|GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
Current=0; Current=0;
@@ -1007,62 +1235,72 @@ void main()
FRenderResources* RenderPtr=Options.CaptureEvery>0?&Render:nullptr; FRenderResources* RenderPtr=Options.CaptureEvery>0?&Render:nullptr;
std::size_t FramesWritten=0; std::size_t FramesWritten=0;
int FrameIndex=0; int FrameIndex=0;
auto SaveFrame=[&](double SphereX,double SphereY,double SphereZ)->bool auto SaveFrame=[&](double Time)->bool
{ {
if(!RenderPtr)return true; if(!RenderPtr)return true;
char Filename[32]{}; char Filename[32]{};
std::snprintf(Filename,sizeof(Filename),"frame_%06d.png",FrameIndex++); std::snprintf(Filename,sizeof(Filename),"frame_%06d.png",FrameIndex++);
const auto Bodies=GetBodiesAt(Time);
if(!Render.Render(Fields[static_cast<std::size_t>(Current)], if(!Render.Render(Fields[static_cast<std::size_t>(Current)],
Options.Output/Filename,SphereX,SphereY,SphereZ))return false; Options.Output/Filename,Bodies[0],Bodies[1]))return false;
++FramesWritten; ++FramesWritten;
return true; return true;
}; };
const int MoveSteps=std::max(2,static_cast<int>(std::lround(Options.Steps*0.58))); if(!SaveFrame(0.0))
const auto SphereAt=[&](int Step) {Render.Destroy();if(QueryCount>0)glDeleteQueries(QueryCount,Queries.data());
{
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;} glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;}
std::vector<GLuint64> Times(Options.TimerQueries
?static_cast<std::size_t>(Options.Steps):0);
int TimerQueryWaits=0;
bool TimerResultsAvailable=Options.TimerQueries;
auto ReadTimerQuery=[&](int QueryStep)
{
const GLuint Query=Queries[static_cast<std::size_t>(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<std::size_t>(QueryStep)]);
glGetQueryObjectiv(Query,GL_QUERY_RESULT_AVAILABLE,&Available);
TimerResultsAvailable=TimerResultsAvailable&&Available==GL_TRUE;
};
std::vector<float> FirstField; std::vector<float> FirstField;
bool FirstStepAudit=false; bool FirstStepAudit=false;
const auto WallStart=std::chrono::steady_clock::now();
for(int Step=0;Step<Options.Steps;++Step) for(int Step=0;Step<Options.Steps;++Step)
{ {
const auto Sphere=SphereAt(Step); if(QueryCount>0&&Step>=QueryCount)ReadTimerQuery(Step-QueryCount);
const double Time=static_cast<double>(Step)*Dt;
const auto Bodies=GetBodiesAt(Time);
const int Target=1-Current; const int Target=1-Current;
Dispatch(Target,static_cast<double>(Step)*Dt,Sphere[0],Sphere[1],Sphere[3], const GLuint Query=QueryCount>0
Queries[static_cast<std::size_t>(Step)]); ?Queries[static_cast<std::size_t>(Step%QueryCount)]:0;
Dispatch(Target,Time,Bodies,Query);
if(Step==0) if(Step==0)
{ {
glFinish(); glFinish();
FirstField.resize(static_cast<std::size_t>(Size)*Size*2); FirstField.resize(static_cast<std::size_t>(Size)*Size*4);
glBindTexture(GL_TEXTURE_2D,Fields[static_cast<std::size_t>(Current)]); glBindTexture(GL_TEXTURE_2D,Fields[static_cast<std::size_t>(Current)]);
glGetTexImage(GL_TEXTURE_2D,0,GL_RG,GL_FLOAT,FirstField.data()); glGetTexImage(GL_TEXTURE_2D,0,GL_RGBA,GL_FLOAT,FirstField.data());
const int X=std::clamp(static_cast<int>(std::floor( const int X=std::clamp(static_cast<int>(std::floor(
(Sphere[0]+PlaneLengthMeters*0.5)/Dx)),0,Size-1); (Bodies[0].X+PlaneLengthMeters*0.5)/Dx)),0,Size-1);
const int Y=std::clamp(static_cast<int>(std::floor( const int Y=std::clamp(static_cast<int>(std::floor(
(Sphere[1]+PlaneLengthMeters*0.5)/Dx)),0,Size-1); (Bodies[0].Y+PlaneLengthMeters*0.5)/Dx)),0,Size-1);
const double CellX=(X+0.5)*Dx-PlaneLengthMeters*0.5; const double CellX=(X+0.5)*Dx-PlaneLengthMeters*0.5;
const double CellY=(Y+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]) double Force=0.0;
+(CellY-Sphere[1])*(CellY-Sphere[1]); for(const FBodyState& Body:Bodies)
const double Force=-Sphere[3]*std::exp(-Distance2/ {
(2.0*SphereSigmaMeters*SphereSigmaMeters)); 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 double Expected=Force*Dt*Dt*std::exp(-DampingPerSecond*Dt);
const std::size_t Index=(static_cast<std::size_t>(Y)*Size+X)*2; const std::size_t Index=(static_cast<std::size_t>(Y)*Size+X)*4;
const double Actual=FirstField[Index]; const double Actual=FirstField[Index];
const double Tolerance=std::max(2.0e-7,std::abs(Expected)*2.5e-3); const double Tolerance=std::max(2.0e-7,std::abs(Expected)*2.5e-3);
FirstStepAudit=std::isfinite(Actual)&&std::abs(Actual-Expected)<=Tolerance; FirstStepAudit=std::isfinite(Actual)&&std::abs(Actual-Expected)<=Tolerance;
@@ -1073,54 +1311,77 @@ void main()
if(RenderPtr&&(Options.CaptureEvery==1 if(RenderPtr&&(Options.CaptureEvery==1
||((Step+1)%Options.CaptureEvery==0)||Step==Options.Steps-1)) ||((Step+1)%Options.CaptureEvery==0)||Step==Options.Steps-1))
{ {
const auto FrameSphere=SphereAt(Step); if(!SaveFrame(static_cast<double>(Step+1)*Dt))
if(!SaveFrame(FrameSphere[0],FrameSphere[1],FrameSphere[2])) {Render.Destroy();if(QueryCount>0)glDeleteQueries(QueryCount,Queries.data());
{Render.Destroy();glDeleteQueries(static_cast<GLsizei>(Queries.size()),Queries.data());
glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;} glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;}
} }
} }
glFinish(); glFinish();
const auto WallEnd=std::chrono::steady_clock::now();
const double WallMs=std::chrono::duration<double,std::milli>(WallEnd-WallStart).count();
const double DispatchWallMsPerStep=Options.CaptureEvery==0
?WallMs/Options.Steps:-1.0;
std::vector<GLuint64> Times(static_cast<std::size_t>(Options.Steps)); if(QueryCount>0)
for(int Step=0;Step<Options.Steps;++Step) for(int Step=std::max(0,Options.Steps-QueryCount);Step<Options.Steps;++Step)
glGetQueryObjectui64v(Queries[static_cast<std::size_t>(Step)],GL_QUERY_RESULT, ReadTimerQuery(Step);
&Times[static_cast<std::size_t>(Step)]); const bool QueryAudit=!Options.TimerQueries||TimerResultsAvailable;
std::vector<float> FinalField(static_cast<std::size_t>(Size)*Size*2);
std::vector<float> FinalField(static_cast<std::size_t>(Size)*Size*4);
glBindTexture(GL_TEXTURE_2D,Fields[static_cast<std::size_t>(Current)]); glBindTexture(GL_TEXTURE_2D,Fields[static_cast<std::size_t>(Current)]);
glGetTexImage(GL_TEXTURE_2D,0,GL_RG,GL_FLOAT,FinalField.data()); glGetTexImage(GL_TEXTURE_2D,0,GL_RGBA,GL_FLOAT,FinalField.data());
std::vector<double> Heights,Velocities; std::vector<double> Heights,Velocities;
Heights.reserve(static_cast<std::size_t>(Size)*Size); Heights.reserve(static_cast<std::size_t>(Size)*Size);
Velocities.reserve(static_cast<std::size_t>(Size)*Size); Velocities.reserve(static_cast<std::size_t>(Size)*Size);
bool Finite=true; bool Finite=true;
for(std::size_t I=0;I<FinalField.size();I+=2) double ImprintMin=0.0;
for(std::size_t I=0;I<FinalField.size();I+=4)
{ {
Finite=Finite&&std::isfinite(FinalField[I])&&std::isfinite(FinalField[I+1]); Finite=Finite&&std::isfinite(FinalField[I])&&std::isfinite(FinalField[I+1])
&&std::isfinite(FinalField[I+2]);
Heights.push_back(FinalField[I]); Heights.push_back(FinalField[I]);
Velocities.push_back(FinalField[I+1]); Velocities.push_back(FinalField[I+1]);
ImprintMin=std::min(ImprintMin,static_cast<double>(FinalField[I+2]));
} }
const auto [HeightMinIt,HeightMaxIt]=std::minmax_element(Heights.begin(),Heights.end()); 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 DynamicsAudit=Finite&&*HeightMinIt<-1.0e-7&&*HeightMaxIt-*HeightMinIt<2.0
const bool Good=FirstStepAudit&&DynamicsAudit&&glGetError()==GL_NO_ERROR; &&(!Options.Imprint||ImprintMin>=-Options.ImprintMaxDepthMeters-1.0e-5);
const bool Good=FirstStepAudit&&DynamicsAudit&&QueryAudit&&glGetError()==GL_NO_ERROR;
if(!DynamicsAudit) if(!DynamicsAudit)
std::cerr<<"[LivingPlane] field audit failed: finite="<<Finite<<" min=" std::cerr<<"[LivingPlane] field audit failed: finite="<<Finite<<" min="
<<*HeightMinIt<<" max="<<*HeightMaxIt<<"\n"; <<*HeightMinIt<<" max="<<*HeightMaxIt<<" imprint_min="<<ImprintMin<<"\n";
if(!QueryAudit)
std::cerr<<"[LivingPlane] timer-query result availability audit failed after glFinish\n";
if(!WriteTimingJson(Options,Context,Renderer,Vendor,Version,Dt,Times,Heights,Velocities, if(!WriteTimingJson(Options,Context,Renderer,Vendor,Version,Dt,Times,Heights,Velocities,
FramesWritten,FirstStepAudit,DynamicsAudit)) FramesWritten,TimerQueryBits,TimerQueryWaits,TimerResultsAvailable,
{Render.Destroy();glDeleteQueries(static_cast<GLsizei>(Queries.size()),Queries.data()); DispatchWallMsPerStep,FirstStepAudit,DynamicsAudit))
{Render.Destroy();if(QueryCount>0)glDeleteQueries(QueryCount,Queries.data());
glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;} glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;}
double MeanNs=0.0; const std::vector<double> QueryMs=[&]
for(const GLuint64 Ns:Times)MeanNs+=static_cast<double>(Ns); {
MeanNs/=Times.size(); std::vector<double> Result;
std::cout<<"[LivingPlane] size="<<Size<<" steps="<<Options.Steps<<" dt_ms=" Result.reserve(Times.size());
<<Dt*1000.0<<" GL_TIME_ELAPSED_mean_ms="<<MeanNs*1.0e-6 for(const GLuint64 Ns:Times)Result.push_back(static_cast<double>(Ns)*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) return Result;
<<" field_min_m="<<*HeightMinIt<<" field_max_m="<<*HeightMaxIt }();
<<" frames="<<FramesWritten<<" equation_audit="<<(FirstStepAudit?"PASS":"FAIL") std::cout<<"[LivingPlane] scene="<<Options.Scene<<" size="<<Size
<<" steps="<<Options.Steps<<" dt_ms="<<Dt*1000.0
<<" dispatch_wall_ms_per_step=";
if(DispatchWallMsPerStep>=0.0)std::cout<<DispatchWallMsPerStep;
else std::cout<<"n/a(capture enabled)";
if(Options.TimerQueries)
std::cout<<" GL_TIME_ELAPSED_median_ms="<<Quantile(QueryMs,0.50)
<<" timer_query_bits="<<TimerQueryBits<<" timer_query_waits="<<TimerQueryWaits;
std::cout<<" field_min_m="<<*HeightMinIt<<" field_max_m="<<*HeightMaxIt
<<" imprint_min_m="<<ImprintMin<<" frames="<<FramesWritten
<<" equation_audit="<<(FirstStepAudit?"PASS":"FAIL")
<<" timer_query_audit="<<(Options.TimerQueries
?(QueryAudit?"PASS":"FAIL"):"NOT_RUN")
<<" status="<<(Good?"PASS":"FAIL")<<" out="<<Options.Output.string()<<"\n"; <<" status="<<(Good?"PASS":"FAIL")<<" out="<<Options.Output.string()<<"\n";
Render.Destroy(); Render.Destroy();
glDeleteQueries(static_cast<GLsizei>(Queries.size()),Queries.data()); if(QueryCount>0)glDeleteQueries(QueryCount,Queries.data());
glDeleteTextures(2,Fields.data()); glDeleteTextures(2,Fields.data());
glDeleteProgram(ComputeProgram); glDeleteProgram(ComputeProgram);
return Good; return Good;
+71
View File
@@ -60,3 +60,74 @@ The only changed file outside `Experiments/LivingPlane/` is `.gitignore`, which
submission, so it's an upper bound): **0.080 ms/step at 512²** and **0.292 ms/step at 1024²**. submission, so it's an upper bound): **0.080 ms/step at 512²** and **0.292 ms/step at 1024²**.
- Visual read: the effect is very subtle in the current render (a slight dip under the sphere, faint rings). It needs - Visual read: the effect is very subtle in the current render (a slight dip under the sphere, faint rings). It needs
a larger push amplitude, grazing light or height colouring, and a closer camera to read. a larger push amplitude, grazing light or height colouring, and a closer camera to read.
## L163 — readable, living plane
The plane now reads as rolling terrain, and the objects leave visible troughs and wakes. `alive` adds travelling
breathing and a viscoelastic imprint that remains after lift-off. The simulation still runs in one GPU dispatch per
step; CPU work only supplies the two object states.
`LivingPlane.cpp` now keeps the dynamic displacement, velocity, and imprint in an `RGBA32F` ping-pong field. The
surface shader adds broad static terrain relief, calculates normals from relief plus displacement, and uses grazing
light, elevation colours, slope tint, and 7.5 cm contour bands. The camera follows the first sphere at a closer angle.
The wake scene rolls the first sphere, pauses and wobbles it, lifts it, then sends a second sphere through the wake.
The alive scene runs that same sequence with low-frequency travelling swells and a decaying contact imprint. Its
defaults are breathing strength 4.8 m/s², imprint capture 0.15 m/s, recovery time 6 s, and maximum imprint depth
0.34 m. These values have command-line overrides.
The timer query check found no early read or reuse in L162: it allocated a unique query per step, called `glFinish`,
then read every result. The saved Intel/Mesa 25.0.7 samples are 0 or 52 ns for most dispatches (512² median 0,
p95 52 ns; largest sample 17.552 µs), which cannot be a credible per-dispatch cost. The old usage did not reuse
query objects, so lifetime is not the cause; the saved data point to Intel/Mesa's timer-query reporting or timing
scope, but the sandbox cannot isolate which. L163 uses a 64-query ring, reads each query before reusing its slot,
checks availability after `glFinish`, and records `GL_QUERY_COUNTER_BITS`. On llvmpipe the availability check passed
with 64 counter bits and zero waits. The Intel query result still needs the GPU diagnostic command below.
### Measurements and checks
- llvmpipe wake: 128², 600 steps (`dt=9.56814 ms`, 5.741 s simulation window), 151 frames at 30 fps. First-step
equation audit and timer-query lifetime check passed; final displacement range was −0.177488 m to +0.104854 m.
- llvmpipe alive: same grid, steps, and window; equation audit and timer-query lifetime check passed. Final
displacement range was −0.200437 m to +0.115904 m, with a −0.111667 m stored imprint.
- llvmpipe cost: 512², 1,800 steps, `--queries=off`, no frame capture: **0.767473 ms/step** in one timed
simulation-loop window. `timing.json` includes dispatch submission, the step-0 equation readback, and final
`glFinish`, but excludes setup and frame capture. This is a software-renderer measurement, not the Intel GPU cost.
- Both five-second llvmpipe previews are in `Saved/Linux/l163/llvmpipe-wake.mp4` and
`Saved/Linux/l163/llvmpipe-alive.mp4`. They are 128² review previews; use the GPU commands below for the owner
videos.
- Equation-audit negative control: a temporary copy with its expected first-step height forced to zero exited FAIL
at actual −0.0488777 m. The final source passes that same check.
- The fast verification tier passed in 278.5 s. Build and logs are under `Saved/Linux/l163/verify/`.
- No Unreal `Source/` files changed and no Unreal build ran.
### Intel GPU run
Run from the plugin root as `jahni`. First build the runner. For timed runs, check `uptime` and keep the machine
quiet. The wall-time field is valid only when `--capture-every=0`; use `--queries=off` for the step-cost measurement.
```sh
bash Experiments/LivingPlane/build.sh
uptime
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l163/living-plane --device=gpu --scene=wake --size=512 --steps=2400 --capture-every=16 --queries=off --out=Saved/Linux/l163/gpu-wake-512
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l163/living-plane --device=gpu --scene=alive --size=512 --steps=2400 --capture-every=16 --queries=off --out=Saved/Linux/l163/gpu-alive-512
ffmpeg -y -framerate 30 -i Saved/Linux/l163/gpu-wake-512/frame_%06d.png -c:v libx264 -pix_fmt yuv420p Saved/Linux/l163/living-plane-wake-512.mp4
ffmpeg -y -framerate 30 -i Saved/Linux/l163/gpu-alive-512/frame_%06d.png -c:v libx264 -pix_fmt yuv420p Saved/Linux/l163/living-plane-alive-512.mp4
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l163/living-plane --device=gpu --scene=alive --size=512 --steps=1800 --capture-every=0 --queries=off --out=Saved/Linux/l163/gpu-timing-512
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l163/living-plane --device=gpu --scene=alive --size=512 --steps=1800 --capture-every=0 --queries=on --out=Saved/Linux/l163/gpu-query-check-512
```
`gpu-timing-512/timing.json` reports the 512² batch wall time per step, including dispatch submission and completion.
`gpu-query-check-512/timing.json` contains query counter bits, waits before slot reuse, availability status, and raw
per-step query samples. The sandbox has no accessible Intel DRM node, so the current Intel step cost and new query
diagnostic remain unmeasured here.
### L163 Intel GPU pass (run by the orchestrator, 2026-11-02)
- Intel ADL GT2 (Mesa 25.0.7, `renderD128`): wake and alive scenes, 512², 2,400 steps, 151 frames each; equation
audits PASS. Videos (local): `Saved/Linux/l163/living-plane-wake-512.mp4`, `living-plane-alive-512.mp4`.
- Step cost, alive scene, 512², 1,800 steps, no capture: **0.172 ms/step** with queries off, 0.152 ms/step with
queries on (batch wall time per step).
- The Intel `GL_TIME_ELAPSED` median is still 0 with 64 counter bits and the ring/availability checks passing. That
looks like a Mesa/Intel reporting issue; use wall time.
- **Open issue:** the same scene with the same steps gave different final fields (min −0.637 m with queries off vs
−0.327 m with queries on). Something depends on more than the step index (e.g. wall-clock time); investigate.
+1 -1
View File
@@ -3,7 +3,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" PLUGIN_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
OUTPUT_DIR="${PLUGIN_ROOT}/Saved/Linux/l162" OUTPUT_DIR="${PLUGIN_ROOT}/Saved/Linux/l163"
CXX="${CXX:-g++}" CXX="${CXX:-g++}"
command -v "${CXX}" >/dev/null || { echo "C++ compiler not found: ${CXX}" >&2; exit 2; } command -v "${CXX}" >/dev/null || { echo "C++ compiler not found: ${CXX}" >&2; exit 2; }