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
202 lines
16 KiB
Markdown
202 lines
16 KiB
Markdown
# L162 — Living Plane
|
||
|
||
The EGL/OpenGL 4.3 runner builds and passes its llvmpipe correctness and frame checks. The sandbox has no accessible DRM render node, so it did not run or report Intel GPU performance. The runner refuses `--device=gpu` here instead of falling back to software.
|
||
|
||
## Implementation
|
||
|
||
`LivingPlane.cpp` runs a ping-pong `RG32F` height/velocity field through a runtime-compiled GLSL compute shader. The damped wave update includes a weak foundation spring, a Gaussian sphere push, and an optional slow breathing force. The spring is needed because a persistent local push otherwise accelerates the zero-frequency height mode and sinks the whole plane. The sphere moves across the plane, stops, and bobs. A vertex-displaced mesh and lit sphere render to an EGL pbuffer-backed framebuffer and save as PNGs.
|
||
|
||
The timestep is selected from the largest 2D grid eigenfrequency:
|
||
|
||
`dt = 1.30 / sqrt(8*c*c/(dx*dx) + foundationStiffness)`
|
||
|
||
The resulting spectral step ratio is 1.30, below the explicit update's 2.0 stability limit. The runner warms the compute kernel for eight unmeasured dispatches, then records one `GL_TIME_ELAPSED` query per requested simulation step in `timing.json`.
|
||
|
||
The first-step equation audit also caught a GL state error during bring-up: frame rendering left the graphics program active before the next compute dispatch. `Dispatch` now binds the compute program before every step, so the audit fails if a frame silently stops simulation.
|
||
|
||
Device selection enumerates EGL devices and uses `EGL_EXT_platform_device` for an accessible Intel render node first. On this host, the known integrated device is Intel on `renderD128`. Automatic mode falls back to a surfaceless Mesa context with `LIBGL_ALWAYS_SOFTWARE=1`; explicit GPU mode fails when no accessible hardware node is present. EGL does not expose Vulkan's integrated/discrete enum, so Intel vendor/render-node identity is the selection heuristic.
|
||
|
||
The Unreal handoff is in [UNREAL-MAPPING.md](UNREAL-MAPPING.md). It keeps the prototype render-only and describes the density mapping if the surface later needs to affect voxel queries. No Unreal `Source/` files changed.
|
||
|
||
## Measurements and checks
|
||
|
||
- Built with `Experiments/LivingPlane/build.sh` using the installed EGL, desktop GL, and libpng development files; no shader compiler or package install was needed.
|
||
- Final llvmpipe run: 128² field, 360 steps, `dt=9.56814 ms`, spectral ratio 1.30. The first-step equation audit and finite-field/dynamics audit passed. Final field range was −0.139533 m to +0.0366704 m. It wrote 16 128×128 PNGs and a JSON file containing 360 per-step query samples. The frames were inspected; the moving sphere and wake are visible.
|
||
- Auto-selection smoke check: 32², 12 steps; selected llvmpipe and passed both audits. Breathing smoke check: 64², 180 steps with `--breathing=on`; passed both audits.
|
||
- Negative device-selection control: `--device=gpu` returned an error because the sandbox has no accessible EGL DRM render node.
|
||
- Existing fast verification tier passed: 278.9 seconds, ending in `SUMMARY PASS`. Its build and logs are under `Saved/Linux/l162/verify/` and `Saved/Linux/l162/`.
|
||
- The llvmpipe `GL_TIME_ELAPSED` samples are preserved in JSON but are software-driver query values, not GPU timings. The requested hardware timing pass remains to be run outside the sandbox.
|
||
- No Unreal build was run; no Unreal `Source/` files changed.
|
||
|
||
## Intel GPU pass
|
||
|
||
Run these from the plugin root as `jahni`. The frame run writes 512×512 PNGs to `Saved/Linux/l162/gpu-frames-512/`; the two timing runs write `timing.json` with one GPU query per step and skip PNG capture. They still read the field for the first-step and final-state correctness audits.
|
||
Before the timing runs, check `uptime` and run them on a quiet machine (load below about 1.5, with no other build active).
|
||
|
||
```sh
|
||
uptime
|
||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l162/living-plane --device=gpu --size=512 --steps=1800 --capture-every=12 --out=Saved/Linux/l162/gpu-frames-512
|
||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l162/living-plane --device=gpu --size=512 --steps=1800 --capture-every=0 --out=Saved/Linux/l162/gpu-timing-512
|
||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s ./Saved/Linux/l162/living-plane --device=gpu --size=1024 --steps=1800 --capture-every=0 --out=Saved/Linux/l162/gpu-timing-1024
|
||
```
|
||
|
||
Encode the captured sequence as MP4 with:
|
||
|
||
```sh
|
||
ffmpeg -y -framerate 30 -i Saved/Linux/l162/gpu-frames-512/frame_%06d.png -c:v libx264 -pix_fmt yuv420p Saved/Linux/l162/living-plane-512.mp4
|
||
```
|
||
|
||
## Files and remaining risk
|
||
|
||
The only changed file outside `Experiments/LivingPlane/` is `.gitignore`, which now allows `Experiments/` and its contents. GPU timings and Intel render-node selection remain unverified until the commands above run on the host outside this sandbox.
|
||
|
||
## Intel GPU pass (run by the orchestrator outside the sandbox, 2026-11-02)
|
||
|
||
- The device was selected correctly: `Mesa Intel(R) Graphics (ADL GT2)`, OpenGL 4.6, `/dev/dri/renderD128`. 512²,
|
||
1,800 steps, 151 frames; the equation audit passes. MP4: `Saved/Linux/l162/living-plane-512.mp4` (local).
|
||
- **The `GL_TIME_ELAPSED` per-dispatch values (~0.0001 ms) are not credible** for 262k cells, so they aren't
|
||
reported as timings.
|
||
- Wall-time difference between 1,800 and 200 steps, divided by the 1,600 steps (start-up cancels out; this includes
|
||
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
|
||
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.
|
||
|
||
## L164 — repeatable field readback and UE runtime actor
|
||
|
||
**Result:** the repeatability check passes on llvmpipe, and the new `VoxelForgeLivingPlane` runtime module compiles
|
||
in the Linux UE 5.7 host. The surface actor is independent of VoxelForge generation and sends no field readback to
|
||
the CPU.
|
||
|
||
### Repeatability finding
|
||
|
||
`uTime` is `Step * dt`, and `GetBodiesAt` receives that same value. Neither the object schedule nor the breathing
|
||
force uses wall time or a rendered-frame counter. Every compute step already issued an image-access barrier before
|
||
the next ping-pong image read. The defect was on the diagnostic side: the step-zero and final `glGetTexImage`
|
||
readbacks followed shader image writes without `GL_TEXTURE_UPDATE_BARRIER_BIT`. The Khronos OpenGL specification
|
||
requires that barrier for `GetTexImage` to observe earlier shader writes ([§7.12](https://registry.khronos.org/OpenGL/specs/gl/glspec44.compatibility.pdf)). Query/capture pacing could therefore affect the reported field values without changing any simulation input. The barrier is now issued before both readbacks.
|
||
|
||
`check_repeatability.py` runs the same 512² alive scene for 1,800 steps twice: queries off with no captures, then
|
||
queries on with captures every 900 steps. The checksum covers every final RGBA32F field value using canonical
|
||
little-endian float bytes.
|
||
|
||
- llvmpipe, queries off/capture off: field range −0.637382 m to +0.109308 m; checksum
|
||
`1ba08f5dafc01f82`.
|
||
- llvmpipe, queries on/capture every 900: the same range and checksum; equation and timer-query audits pass; three
|
||
frames written.
|
||
- Negative control: changing one saved checksum made the comparison exit 1 with `FAIL field checksum mismatch`.
|
||
- The llvmpipe runs were each one 1,800-step simulation-loop window. Logs and JSON are under
|
||
`Saved/Linux/l164/repeatability/`.
|
||
|
||
The Intel run is not available in this sandbox. The owner should repeat the checksum comparison on the GPU to
|
||
confirm that the missing transfer-readback barrier accounts for the earlier Intel min mismatch:
|
||
|
||
```sh
|
||
python3 Experiments/LivingPlane/check_repeatability.py --device=gpu --out=Saved/Linux/l164/gpu-repeatability
|
||
```
|
||
|
||
### Unreal implementation and checks
|
||
|
||
The plugin descriptor adds a separate `VoxelForgeLivingPlane` runtime module. Its RDG global shader is in
|
||
`Shaders/LivingPlane.usf`; module startup maps `/Plugin/VoxelForgeLivingPlane` to the plugin shader directory. The
|
||
actor owns two UAV-capable RGBA32F render targets, advances them at the stable fixed step, and supplies the current
|
||
texture to a movable plane mesh material. Its Blueprint/C++ object input includes world position, radius, velocity,
|
||
base push, and active state. The CPU converts those values to plane-local metres and uploads uniforms; it does not
|
||
read the field back. The ignore rules now include `Shaders/` so that the shader ships in the worktree diff.
|
||
`Experiments/LivingPlane/UE-SETUP.md` gives level setup, the full material graph, and object driving steps.
|
||
|
||
- Linux UE 5.7 `VFHostEditor Linux Development`: **PASS**, final incremental build compiled
|
||
`VoxelForgeLivingPlaneActor.cpp` and linked `libUnrealEditor-VoxelForgeLivingPlane.so`. Command used
|
||
`-NoUBA -MaxParallelActions=4`, `taskset -c 0-11`, and no `prlimit`. Log:
|
||
`Saved/Linux/l164/ue-build-final.log`. The initial attempt exposed an incorrect RDG texture registration overload;
|
||
it had already compiled `VoxelForgeLivingPlaneModule.cpp` and the generated module code. The corrected final
|
||
build passed.
|
||
- The host `PluginDeps/VoxelForge/Source` and `.uplugin` links were temporarily pointed at this worktree for the
|
||
build and restored to `/home/jahni/VoxelM/Plugins/VoxelForge` afterward. The temporary shader link was removed.
|
||
- Standalone build, final repeatability comparison, checksum-mismatch negative control, plugin JSON parse, and
|
||
`git diff --check` passed.
|
||
- Not run: fast generation verification (generation sources are untouched), Intel GPU checksum comparison, or a UE
|
||
editor render/shader-runtime check. The Linux UBT build verifies C++ and module integration, not first-use shader
|
||
compilation or material appearance.
|
||
|
||
New Unreal source and shader files for the owner's Windows build:
|
||
|
||
- `Source/VoxelForgeLivingPlane/VoxelForgeLivingPlane.Build.cs`
|
||
- `Source/VoxelForgeLivingPlane/Public/VoxelForgeLivingPlaneActor.h`
|
||
- `Source/VoxelForgeLivingPlane/Private/VoxelForgeLivingPlaneActor.cpp`
|
||
- `Source/VoxelForgeLivingPlane/Private/VoxelForgeLivingPlaneModule.cpp`
|
||
- `Shaders/LivingPlane.usf`
|
||
|
||
No VoxelForge generation module files changed. Remaining risks are the Intel checksum confirmation and first-use UE
|
||
shader/material rendering, which require the owner's GPU/editor run.
|