experiment: LivingPlane repeatable + standalone UE runtime module (L164)

Readback barrier fix (GL_TEXTURE_UPDATE_BARRIER_BIT before glGetTexImage); check_repeatability.py compares full-field
checksums across query/capture settings - Intel GPU PASS (3b9d45905c7aafff), llvmpipe PASS, mismatch control fails.
New runtime module VoxelForgeLivingPlane (separate from generation): RDG global shader (Shaders/LivingPlane.usf),
ping-pong RGBA32F render targets, actor with WPO plane and object inputs, no CPU readback. Compiles in Linux UE 5.7.
Windows build: Source/VoxelForgeLivingPlane/*, Shaders/LivingPlane.usf, VoxelForge.uplugin. Setup: UE-SETUP.md.

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 11:10:43 +01:00
1 parent e1d87eed26
commit 3b71d35eba
13 files changed
+868 -8

No files matched your search

+34 -3
View File
@@ -7,6 +7,7 @@
#include <algorithm>
#include <array>
#include <bit>
#include <cerrno>
#include <chrono>
#include <cctype>
@@ -17,6 +18,7 @@
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <numeric>
@@ -48,7 +50,7 @@ namespace
struct FOptions
{
std::string Device="auto";
std::filesystem::path Output="Saved/Linux/l163/living-plane";
std::filesystem::path Output="Saved/Linux/l164/living-plane";
std::string Scene="wake";
int Size=128;
int Steps=360;
@@ -1011,12 +1013,29 @@ void main()
return Values[std::min(I,Values.size()-1)];
}
std::uint64_t FieldChecksum(const std::vector<float>& 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<std::uint32_t>(Value);
for(unsigned Byte=0;Byte<4;++Byte)
{
Hash^=static_cast<std::uint8_t>((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<GLuint64>& Times,const std::vector<double>& Heights,
const std::vector<double>& Velocities,std::size_t Frames,int TimerQueryBits,
int TimerQueryWaits,bool TimerResultsAvailable,double DispatchWallMsPerStep,
bool FirstStepAudit,bool DynamicsAudit)
std::uint64_t FieldChecksumValue,bool FirstStepAudit,bool DynamicsAudit)
{
std::error_code Error;
std::filesystem::create_directories(Options.Output,Error);
@@ -1035,6 +1054,9 @@ void main()
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<unsigned long long>(FieldChecksumValue));
const double Dx=PlaneLengthMeters/Options.Size;
const double SpectralStep=Dt*std::sqrt(8.0*WaveSpeedMetersPerSecond*
WaveSpeedMetersPerSecond/(Dx*Dx)+FoundationStiffnessPerSecondSquared);
@@ -1087,6 +1109,7 @@ void main()
<<" \"field_max_m\": "<<HeightMax<<",\n"
<<" \"field_mean_m\": "<<HeightMean<<",\n"
<<" \"field_rms_m\": "<<HeightRms<<",\n"
<<" \"field_checksum_fnv1a64\": \""<<FieldChecksumHex<<"\",\n"
<<" \"max_abs_velocity_m_per_s\": "<<std::abs(VelocityMax)<<",\n"
<<" \"gl_time_elapsed_ms_per_step\": [";
for(std::size_t I=0;I<Times.size();++I)
@@ -1284,6 +1307,10 @@ void main()
glFinish();
FirstField.resize(static_cast<std::size_t>(Size)*Size*4);
glBindTexture(GL_TEXTURE_2D,Fields[static_cast<std::size_t>(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<int>(std::floor(
(Bodies[0].X+PlaneLengthMeters*0.5)/Dx)),0,Size-1);
@@ -1329,6 +1356,7 @@ void main()
std::vector<float> FinalField(static_cast<std::size_t>(Size)*Size*4);
glBindTexture(GL_TEXTURE_2D,Fields[static_cast<std::size_t>(Current)]);
glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT);
glGetTexImage(GL_TEXTURE_2D,0,GL_RGBA,GL_FLOAT,FinalField.data());
std::vector<double> Heights,Velocities;
Heights.reserve(static_cast<std::size_t>(Size)*Size);
@@ -1346,6 +1374,7 @@ void main()
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="<<Finite<<" min="
@@ -1354,7 +1383,7 @@ void main()
std::cerr<<"[LivingPlane] timer-query result availability audit failed after glFinish\n";
if(!WriteTimingJson(Options,Context,Renderer,Vendor,Version,Dt,Times,Heights,Velocities,
FramesWritten,TimerQueryBits,TimerQueryWaits,TimerResultsAvailable,
DispatchWallMsPerStep,FirstStepAudit,DynamicsAudit))
DispatchWallMsPerStep,FinalFieldChecksum,FirstStepAudit,DynamicsAudit))
{Render.Destroy();if(QueryCount>0)glDeleteQueries(QueryCount,Queries.data());
glDeleteTextures(2,Fields.data());glDeleteProgram(ComputeProgram);return false;}
@@ -1375,6 +1404,8 @@ void main()
<<" timer_query_bits="<<TimerQueryBits<<" timer_query_waits="<<TimerQueryWaits;
std::cout<<" field_min_m="<<*HeightMinIt<<" field_max_m="<<*HeightMaxIt
<<" imprint_min_m="<<ImprintMin<<" frames="<<FramesWritten
<<" field_checksum_fnv1a64="<<std::hex<<std::setw(16)<<std::setfill('0')
<<FinalFieldChecksum<<std::dec<<std::setfill(' ')
<<" equation_audit="<<(FirstStepAudit?"PASS":"FAIL")
<<" timer_query_audit="<<(Options.TimerQueries
?(QueryAudit?"PASS":"FAIL"):"NOT_RUN")