Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82eb5b1c66 | ||
|
|
52cb3d7a23 | ||
|
|
4d7b470ebc | ||
|
|
c3a1610b22 | ||
|
|
9d088ef96c | ||
|
|
24803ab771 | ||
|
|
786ecc4cdf | ||
|
|
ca0bf767ca | ||
|
|
ec89b29853 | ||
|
|
c6357b573f | ||
|
|
9c70053543 | ||
|
|
3b71d35eba | ||
|
|
e1d87eed26 | ||
|
|
5b15595e4a | ||
|
|
ff52c9b576 | ||
|
|
aa458d3607 | ||
|
|
323fbe3736 | ||
|
|
4290672d1c | ||
|
|
7d654ff861 | ||
|
|
a45ba9bb10 | ||
|
|
a4e214561f | ||
|
|
6cd9e4da76 | ||
|
|
9c6d261d01 | ||
|
|
eb47d72258 | ||
|
|
95bf654fbc |
No files matched your search
@@ -9,12 +9,20 @@
|
||||
!/Source/
|
||||
!/Source/**
|
||||
|
||||
# Plugin runtime shader source.
|
||||
!/Shaders/
|
||||
!/Shaders/**
|
||||
|
||||
# Portable CPU generator and D3D11 viewer.
|
||||
!/Standalone/
|
||||
!/Standalone/**
|
||||
/Standalone/Build/
|
||||
/Standalone/bin/
|
||||
|
||||
# Research and prototype experiments.
|
||||
!/Experiments/
|
||||
!/Experiments/**
|
||||
|
||||
# The guarded single-entry test harness is source, not generated output.
|
||||
!/Tools/
|
||||
!/Tools/**
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# AGENTS.md — VoxelForge (read automatically by Codex)
|
||||
|
||||
You are the implementation agent. The orchestrator (Claude) writes each round's brief, reviews your diff against the
|
||||
code, and commits. The owner decides direction. Be warm and direct; credit what worked; say plainly what didn't.
|
||||
|
||||
Read first, as the task needs: `CURRENT-STATE.md` (what ships, reference numbers, **closed experiments: don't reopen
|
||||
them without new evidence**), then `CODEMAP.md` and `ARCHITECTURE.md`. Round reports live in
|
||||
`Standalone/Research/WorldAssemblerM3/Linux*/REPORT.md`.
|
||||
|
||||
## Standing rules (every round, unless the brief says otherwise)
|
||||
|
||||
- Run as user `jahni`. Write only inside this plugin, plus `/home/jahni/VFHost` and `/home/jahni/UnrealEngine` when
|
||||
the brief asks for a UE build. No installs, no system changes.
|
||||
- **No `git commit`, `push` or `stash`.** Leave changes in the working tree; the orchestrator reviews and commits.
|
||||
- Headless processes and builds: `taskset -c 0-11 prlimit --as=6442450944` (12 P-core threads, 6 GiB). Never use all
|
||||
cores.
|
||||
- Unreal (UBT, the editor, commandlets): `taskset -c 0-11` only, **never `prlimit`** (CoreCLR fails). UE builds use
|
||||
`-NoUBA -MaxParallelActions=4`, never while timing.
|
||||
- Stop any single run that passes 30 minutes. Logs go in `Saved/Linux/l<round>/`.
|
||||
- **Timing:** quiet machine only (check `uptime` first; load < ~1.5, nothing else building). L80 protocol: one warm-up
|
||||
per binary, ≥5 interleaved runs, W8 and W12, all four owner layers. Every number states the window that produced it.
|
||||
- **Same world for everyone:** a seed must give the same world on every machine and worker count (Clang and GCC,
|
||||
W1/W8/W12). Byte-identity with older versions is NOT required. If the output changes: bump
|
||||
`WorldAssemblyEvaluatorVersion`, add baselines, and show captures.
|
||||
- Density sign: negative = solid, positive = air. Core voxels are 25 cm. Never mix metres and voxels without an
|
||||
explicit conversion (v225 shipped a reach bug from exactly that).
|
||||
- The fast tier (`Standalone/Tools/verify/verify.py fast`) must stay green. Don't loosen an audit to make a change pass.
|
||||
|
||||
## How to work
|
||||
|
||||
1. **Count before you build.** Measure where the time or the failure actually is (stack sampler, census, counters).
|
||||
State which parts are measured and which are inferred.
|
||||
2. **Suspect the measurement first.** An alarming or contradictory number is the instrument until a control rules it
|
||||
out (A/A runs, an ablation that verifiably removes the work).
|
||||
3. **No fix without a root cause.** Reproduce it, trace it to its source, fix the cause. A guard that hides the symptom
|
||||
is not a fix.
|
||||
4. **One change at a time,** each with its own check.
|
||||
5. **Subtract before you add.** Prefer removing work to reorganising it. The smallest edit that solves the problem.
|
||||
6. **Attack the premise.** When two fixes sharing one premise fail the same gate, write that premise in one sentence
|
||||
and question it before trying a third. **After three failed fixes, stop:** it is probably the design, not the
|
||||
hypothesis. Report it instead of trying fix #4.
|
||||
7. **Tests must be able to fail.** Name what would make each check fail. Add a negative control when you add an audit.
|
||||
8. **Refactors:** pin the behaviour (hashes), change the structure, prove nothing changed.
|
||||
9. **Verify before you claim.** Don't report a check as passed unless it ran on the final source. Say which checks
|
||||
didn't run and why.
|
||||
10. Changes that speed things up go in the shared layer, so every generator benefits.
|
||||
|
||||
## Report
|
||||
|
||||
Write the round's `REPORT.md`, short and plain:
|
||||
- the result first;
|
||||
- what changed and why;
|
||||
- the measurements, with their windows;
|
||||
- the checks run, and those not run;
|
||||
- remaining risks.
|
||||
|
||||
List any changed Unreal `Source/` files, because the owner compiles them on Windows.
|
||||
@@ -7,7 +7,7 @@ As of 2026-10-31. L131 is on `assembler/world` as `8107196`; this L132 worktree
|
||||
- **Core:** evaluator version **227**. Unreal's `AVoxelWorld::bUseVoxelForgeCore` is opt-in and defaults to `false`; the legacy generator remains the default runtime path. [Core version](Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h), [runtime switch](Source/VoxelForge/Public/VoxelWorld.h), [L130 report](Standalone/Research/WorldAssemblerM3/Linux130/REPORT.md).
|
||||
- **Verified owner4 generator dispatch (L132):** SW binds `SurfaceWorld-native` because the owner export contains a matching `SurfaceWorld.Heightfield2D` v2 plan. FI binds `compiled-analytic/FloatingIslands` because its `FloatingIslands.ClusterField` v0 layout has no matching native island plan. TN and VS bind `compiled-analytic/TunnelNetwork` and `compiled-analytic/VerticalShafts`; their legacy recipe IDs provide neither a `NativeCaves`/`NativeLattice` graph recipe nor native field capability. The L132 fast-tier report prints the binding and generated tile-center share at each owner strate. [Dispatch](Standalone/Core/src/ShapeWorldInternal.h), [adapter recipe checks](Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h), [L132 report](Standalone/Research/WorldAssemblerM3/Linux132/REPORT.md).
|
||||
- **Passages:** continuous smooth swept tubes with a continuous floor profile. L125 stamps conservative route slabs and outside sentinels into the tile cache; exact route geometry is still evaluated. All 21,425 route samples passed the open-air walk check; the nine-station floor probe's maximum root offset was 7.884 mm. L130 sets the shared influence reach to **1.0533 m** (0.75 m shell + 0.21 m relief + 0.50 m smooth-max support / 6 + 0.01 m margin), converted once from metres to Core voxels for the proof, feature, fallback, and off-grid bounds. [L125](Standalone/Research/WorldAssemblerM3/Linux125/REPORT.md), [L130](Standalone/Research/WorldAssemblerM3/Linux130/REPORT.md).
|
||||
- **Streaming:** arrival uses a bounded startup apply burst: `voxel.StreamingStartupApplyBoost=1` by default, up to 24 visible meshes or 14 ms per frame for at most 30 seconds, until the initial desired set and walkable ring are ready. `voxel.StreamingFarApplySupplement=1` is also default-on: after the normal visible apply cap, at most one far result applies when under half the apply-time budget; it is inactive during the startup burst. Dynamic near reserve defaults on; coarse-first remains off; the legacy pending-result cap defaults off. The far-apply lane is Unreal-only. [CVar source](Source/VoxelForge/Private/VoxelWorld.cpp), [L121 report](Standalone/Research/WorldAssemblerM3/Linux121/REPORT.md), [architecture](ARCHITECTURE.md).
|
||||
- **Streaming:** arrival uses a bounded startup apply burst: `voxel.StreamingStartupApplyBoost=1` by default, up to 24 visible meshes or 14 ms per frame for at most 30 seconds, until the initial desired set and walkable ring are ready. `voxel.StreamingFarApplySupplement=1` is also default-on: after the normal visible apply cap, at most one far result applies when under half the apply-time budget; it is inactive during the startup burst. Dynamic near reserve defaults on; coarse-first remains off; the legacy pending-result cap defaults off. The L165/L166 incremental desired-set path now defaults on (`voxel.StreamingIncrementalDesiredSet=1`): Core clipmap keys, overlay membership, and priority order update incrementally, with same-center Core keys cached. No separate overlay or priority incremental switches are required. Audits stay off (`StreamingIncrementalDesiredAuditEvery=0`, `StreamingIncrementalDesiredMaterializationAuditEvery=0`, mismatch injection `=0`). L157 early admission and apply-cap controls stay off (`StreamingFastTravelWaitForAllCriticalTiles=1`, `StreamingFastTravelApplyBudgetMilliseconds=0`). L160 ahead-first and same-center rebuild suppression stay off (`StreamingFastTravelForwardFirst=0`, `StreamingRebuildOnCoreRegionCompletion=1`). Its separate Core desired-cache switch stays off (`StreamingCoreDesiredCacheByCenter=0`); the new incremental path enables its own same-center Core cache. The far-apply lane is Unreal-only. [CVar source](Source/VoxelForge/Private/VoxelWorld.cpp), [L121 report](Standalone/Research/WorldAssemblerM3/Linux121/REPORT.md), [architecture](ARCHITECTURE.md).
|
||||
- **Determinism:** proven on Linux with Clang and GCC at W1/W8/W12. L131's unchanged-output comparison covers owner4's SW/FI/TN/VS views in 24 compiler/worker cases; the canonical owner4/owner2 recipe matrix also passes. Windows determinism is unverified. [L131 checks](Standalone/Research/WorldAssemblerM3/Linux131/REPORT.md), [v227 canonical hashes](Standalone/Tools/verify/baselines/owner-hash/canonical/).
|
||||
|
||||
## Current performance references
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Living Plane in Unreal Engine 5.7
|
||||
|
||||
`VoxelForgeLivingPlane` is a separate runtime module. It does not call the VoxelForge terrain generators and it does not read the simulated texture back to the CPU. The actor owns two transient `RGBA32f` render targets and advances them with an RDG compute pass. The CPU sends object state and fixed-step uniforms only.
|
||||
|
||||
## Add it to a level
|
||||
|
||||
1. Enable the VoxelForge plugin and restart the editor so the `VoxelForgeLivingPlane` runtime module and its shader mapping load.
|
||||
2. Create a material named `M_LivingPlane` using the graph below.
|
||||
3. Place `VoxelForgeLivingPlaneActor` in the level. Assign `M_LivingPlane` to **Plane Material**.
|
||||
4. Keep the actor on the world XY plane with unit actor scale. `Plane Length Meters` controls the mesh width and simulation width; the built-in plane mesh is scaled to that size.
|
||||
5. The actor creates and clears its two GPU state targets on `BeginPlay`. The mesh has collision disabled; this surface is render-only and does not change VoxelForge density or collision queries.
|
||||
|
||||
## Material graph
|
||||
|
||||
Use a Surface, Opaque material. The actor binds these parameters on its dynamic material instance:
|
||||
|
||||
- `LivingField`: Texture Object Parameter. The texture is the current RGBA32f simulation state; **R** is displacement in metres.
|
||||
- `PlaneLengthMeters`: Scalar Parameter.
|
||||
- `FieldSize`: Scalar Parameter, equal to the render target width and height.
|
||||
|
||||
For World Position Offset:
|
||||
|
||||
1. Read `LivingField` at `TextureCoordinate[0]` with a Texture Sample.
|
||||
2. Multiply its **R** output by `100` to convert metres to Unreal centimetres.
|
||||
3. Append `(0, 0)` to that value, making `(0, 0, heightCm)`, and connect it to **World Position Offset**.
|
||||
|
||||
For the displaced normal, sample the height at four UV offsets using the same texture object:
|
||||
|
||||
- Left/right UV: `TexCoord + (-1/FieldSize, 0)` and `TexCoord + (1/FieldSize, 0)`.
|
||||
- Down/up UV: `TexCoord + (0, -1/FieldSize)` and `TexCoord + (0, 1/FieldSize)`.
|
||||
- `dHdx = (Right.R - Left.R) * FieldSize / (2 * PlaneLengthMeters)`.
|
||||
- `dHdy = (Up.R - Down.R) * FieldSize / (2 * PlaneLengthMeters)`.
|
||||
- Make and normalize the tangent-space vector `(-dHdx, -dHdy, 1)`, then connect it to **Normal**.
|
||||
|
||||
The built-in plane's U/V tangent directions align with local X/Y. Use a neutral base colour and roughness around `0.7` to start. The graph assumes a horizontal plane; add a local-to-world transform if the plane needs arbitrary rotation.
|
||||
|
||||
## Drive objects
|
||||
|
||||
`ObjectInputs` has two slots. Each `FVFLivingPlaneObjectInput` contains world position in centimetres, world velocity in centimetres per second, radius in centimetres, base push acceleration in metres per second squared, and an active flag. Blueprint can use **Set Object State** or set the array entry; C++ can call the same method. Set the object's center about one radius above the plane while it contacts it, and mark the slot inactive after lift-off.
|
||||
|
||||
The compute shader uses the radius to scale the Gaussian contact footprint. The original wave equation keeps the L163 wave speed, damping, foundation spring, travelling breathing force, and recovering imprint. The base push acceleration supplies the object force; lateral speed adds `0.25 m/s²` per `1 m/s`, and downward speed adds `0.5 m/s²` per `1 m/s`. Upward velocity does not add force. Object positions and velocities are transformed into the plane's local frame and converted from centimetres to metres before upload.
|
||||
|
||||
Each step uses `dt = 1.30 / sqrt(8*c*c/(dx*dx) + foundationStiffness)`, with `dx = PlaneLengthMeters / FieldSize`. Simulation time passed to breathing is the fixed step index times `dt`. Game-thread delta time only determines how many fixed steps are queued; no GPU field data is read back in the normal path.
|
||||
|
||||
## Build files
|
||||
|
||||
The owner's Windows build needs every added source and shader file:
|
||||
|
||||
- `Source/VoxelForgeLivingPlane/VoxelForgeLivingPlane.Build.cs`
|
||||
- `Source/VoxelForgeLivingPlane/Public/VoxelForgeLivingPlaneActor.h`
|
||||
- `Source/VoxelForgeLivingPlane/Private/VoxelForgeLivingPlaneActor.cpp`
|
||||
- `Source/VoxelForgeLivingPlane/Private/VoxelForgeLivingPlaneModule.cpp`
|
||||
- `Shaders/LivingPlane.usf`
|
||||
|
||||
The `VoxelForge.uplugin` runtime-module entry must ship with them.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Unreal mapping note
|
||||
|
||||
The prototype stores height, vertical velocity, a recovering imprint, and a spare
|
||||
channel in a pair of `RGBA32F` textures. A world-owned Unreal component can keep
|
||||
equivalent transient render graph textures and advance them with a compute pass. It
|
||||
should own simulation time and clear or restore its buffers when the world is recreated.
|
||||
|
||||
The sphere path becomes a world-space interaction event: position, contact radius,
|
||||
and impulse strength are supplied to the compute pass. For multiplayer or deterministic
|
||||
replay, replicate the compact events and simulation tick, or record the resulting field;
|
||||
do not assume that independent GPU floating-point runs will produce byte-identical
|
||||
textures.
|
||||
|
||||
The rendered top surface can remain a render-only displaced mesh. If it must participate
|
||||
in voxel collision or queries, its height must also reach the density evaluator. For a
|
||||
height `h(x,y)`, the plugin's density convention is negative inside solid and positive
|
||||
in air, so the plane's signed density is `z - h(x,y)`. Keep this calculation in metres
|
||||
until the existing voxel/Core boundary converts with the active voxel size (25 cm by
|
||||
default); do not mix metre distances and voxel counts in the wave solver.
|
||||
|
||||
The current sample is an isolated visual field. Wiring this into `AVoxelWorld` would
|
||||
need an explicit owner for simulation lifetime, collision/update cadence, save/load,
|
||||
and the replication policy before it could affect shipped terrain.
|
||||
|
||||
## L164 render-only Unreal implementation
|
||||
|
||||
`Source/VoxelForgeLivingPlane/` now implements this render-only path as a separate
|
||||
runtime module. `AVoxelForgeLivingPlaneActor` owns two UAV-capable RGBA32F render
|
||||
targets, advances the wave, breathing, and imprint equations with an RDG global shader,
|
||||
and binds the current texture to its plane material. Blueprint/C++ supplies object
|
||||
position, radius, velocity, push, and active state. The actor does not call the
|
||||
generation modules and does not read the field back. Level setup and the material graph
|
||||
are in [UE-SETUP.md](UE-SETUP.md).
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
|
||||
OUTPUT_DIR="${PLUGIN_ROOT}/Saved/Linux/l164"
|
||||
CXX="${CXX:-g++}"
|
||||
|
||||
command -v "${CXX}" >/dev/null || { echo "C++ compiler not found: ${CXX}" >&2; exit 2; }
|
||||
pkg-config --exists egl gl libpng || {
|
||||
echo "EGL, desktop OpenGL, and libpng development files are required" >&2
|
||||
exit 2
|
||||
}
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
read -r -a GRAPHICS_FLAGS <<<"$(pkg-config --cflags --libs egl gl libpng)"
|
||||
|
||||
taskset -c 0-11 prlimit --as=6442450944 -- \
|
||||
"${CXX}" -std=c++20 -O2 -g -Wall -Wextra -Wpedantic \
|
||||
"${SCRIPT_DIR}/LivingPlane.cpp" -o "${OUTPUT_DIR}/living-plane" \
|
||||
"${GRAPHICS_FLAGS[@]}"
|
||||
|
||||
echo "built ${OUTPUT_DIR}/living-plane"
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare final field checksums across different query and capture settings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
PLUGIN_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def read_checksum(path: pathlib.Path) -> str:
|
||||
try:
|
||||
with path.open(encoding="utf-8") as source:
|
||||
result = json.load(source)
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise RuntimeError(f"could not read {path}: {error}") from error
|
||||
checksum = result.get("field_checksum_fnv1a64")
|
||||
if not isinstance(checksum, str) or len(checksum) != 16:
|
||||
raise RuntimeError(f"{path} has no 16-digit field checksum")
|
||||
return checksum
|
||||
|
||||
|
||||
def compare(left: pathlib.Path, right: pathlib.Path) -> bool:
|
||||
left_checksum = read_checksum(left)
|
||||
right_checksum = read_checksum(right)
|
||||
if left_checksum != right_checksum:
|
||||
print(
|
||||
"FAIL field checksum mismatch: "
|
||||
f"{left}={left_checksum}, {right}={right_checksum}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
print(f"PASS field checksum matches: {left_checksum}")
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--binary",
|
||||
type=pathlib.Path,
|
||||
default=PLUGIN_ROOT / "Saved/Linux/l164/living-plane",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
type=pathlib.Path,
|
||||
default=PLUGIN_ROOT / "Saved/Linux/l164/repeatability",
|
||||
)
|
||||
parser.add_argument("--device", choices=("auto", "cpu", "gpu"), default="cpu")
|
||||
parser.add_argument("--size", type=int, default=512)
|
||||
parser.add_argument("--steps", type=int, default=1800)
|
||||
parser.add_argument(
|
||||
"--compare-only",
|
||||
nargs=2,
|
||||
type=pathlib.Path,
|
||||
metavar=("LEFT_JSON", "RIGHT_JSON"),
|
||||
help="compare existing timing.json files; used to exercise the failure path",
|
||||
)
|
||||
options = parser.parse_args()
|
||||
|
||||
if options.compare_only:
|
||||
return 0 if compare(*options.compare_only) else 1
|
||||
|
||||
if not options.binary.is_file():
|
||||
parser.error(f"binary not found: {options.binary}; run Experiments/LivingPlane/build.sh")
|
||||
|
||||
options.out.mkdir(parents=True, exist_ok=True)
|
||||
off_dir = options.out / "queries-off_capture-off"
|
||||
on_dir = options.out / "queries-on_capture-every-900"
|
||||
for directory in (off_dir, on_dir):
|
||||
if directory.exists():
|
||||
shutil.rmtree(directory)
|
||||
|
||||
common = [
|
||||
f"--device={options.device}",
|
||||
"--scene=alive",
|
||||
f"--size={options.size}",
|
||||
f"--steps={options.steps}",
|
||||
]
|
||||
runs = (
|
||||
("--capture-every=0", "--queries=off", off_dir),
|
||||
("--capture-every=900", "--queries=on", on_dir),
|
||||
)
|
||||
for capture, queries, output in runs:
|
||||
command = [
|
||||
"taskset",
|
||||
"-c",
|
||||
"0-11",
|
||||
"prlimit",
|
||||
"--as=6442450944",
|
||||
"--",
|
||||
"timeout",
|
||||
"1740s",
|
||||
str(options.binary.resolve()),
|
||||
*common,
|
||||
capture,
|
||||
queries,
|
||||
f"--out={output}",
|
||||
]
|
||||
print("RUN " + " ".join(command), flush=True)
|
||||
log_path = options.out / f"{output.name}.log"
|
||||
with log_path.open("w", encoding="utf-8") as log_file:
|
||||
try:
|
||||
subprocess.run(
|
||||
command,
|
||||
cwd=PLUGIN_ROOT,
|
||||
check=True,
|
||||
stdout=log_file,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
print(log_path.read_text(encoding="utf-8"), file=sys.stderr)
|
||||
raise
|
||||
print(log_path.read_text(encoding="utf-8").rstrip())
|
||||
|
||||
left = off_dir / "timing.json"
|
||||
right = on_dir / "timing.json"
|
||||
return 0 if compare(left, right) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (OSError, RuntimeError, subprocess.CalledProcessError) as error:
|
||||
print(f"FAIL repeatability check: {error}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,85 @@
|
||||
Texture2D<float4> SourceState;
|
||||
RWTexture2D<float4> TargetState;
|
||||
|
||||
int2 GridSize;
|
||||
float Dt;
|
||||
float Dx;
|
||||
float PlaneLength;
|
||||
float WaveSpeed;
|
||||
float Damping;
|
||||
float FoundationStiffness;
|
||||
float TimeSeconds;
|
||||
float2 BreathingSettings;
|
||||
float4 ImprintSettings;
|
||||
float4 ObjectPositionRadius[2];
|
||||
float4 ObjectVelocityPush[2];
|
||||
|
||||
[numthreads(8, 8, 1)]
|
||||
void MainCS(uint3 DispatchThreadId : SV_DispatchThreadID)
|
||||
{
|
||||
int2 Pixel = int2(DispatchThreadId.xy);
|
||||
if (Pixel.x >= GridSize.x || Pixel.y >= GridSize.y)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int2 Lo = int2(0, 0);
|
||||
int2 Hi = GridSize - 1;
|
||||
float4 State = SourceState.Load(int3(Pixel, 0));
|
||||
float Height = State.r;
|
||||
float Velocity = State.g;
|
||||
float Imprint = State.b;
|
||||
float Left = SourceState.Load(int3(max(Pixel + int2(-1, 0), Lo), 0)).r;
|
||||
float Right = SourceState.Load(int3(min(Pixel + int2(1, 0), Hi), 0)).r;
|
||||
float Down = SourceState.Load(int3(max(Pixel + int2(0, -1), Lo), 0)).r;
|
||||
float Up = SourceState.Load(int3(min(Pixel + int2(0, 1), Hi), 0)).r;
|
||||
float Laplacian = (Left + Right + Down + Up - 4.0 * Height) / max(Dx * Dx, 1.0e-8);
|
||||
float2 World = (float2(Pixel) + 0.5) * Dx - PlaneLength * 0.5;
|
||||
|
||||
float Interaction = 0.0;
|
||||
float Contact = 0.0;
|
||||
for (int ObjectIndex = 0; ObjectIndex < 2; ++ObjectIndex)
|
||||
{
|
||||
float4 PositionRadius = ObjectPositionRadius[ObjectIndex];
|
||||
if (PositionRadius.w <= 0.0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float4 VelocityPush = ObjectVelocityPush[ObjectIndex];
|
||||
float Radius = PositionRadius.w;
|
||||
float Sigma = max(Radius * (0.58 / 0.43), Dx);
|
||||
float2 Offset = World - PositionRadius.xy;
|
||||
float Weight = exp(-dot(Offset, Offset) / max(2.0 * Sigma * Sigma, 1.0e-8));
|
||||
float HeightAboveContact = max(PositionRadius.z - Radius, 0.0);
|
||||
float ContactHeight = 1.0 - smoothstep(0.0, max(Radius, Dx), HeightAboveContact);
|
||||
float ImpactSpeed = length(VelocityPush.xy) * 0.25 + max(-VelocityPush.z, 0.0) * 0.5;
|
||||
float Push = max(VelocityPush.w, 0.0) + ImpactSpeed;
|
||||
Interaction -= Push * Weight * ContactHeight;
|
||||
if (Push > 0.0)
|
||||
{
|
||||
Contact = max(Contact, Weight * ContactHeight);
|
||||
}
|
||||
}
|
||||
|
||||
float Breathing = 0.0;
|
||||
if (BreathingSettings.x != 0.0)
|
||||
{
|
||||
float Swell = sin(World.x * 0.48 - TimeSeconds * 0.22)
|
||||
* cos(World.y * 0.42 + TimeSeconds * 0.16);
|
||||
Breathing = BreathingSettings.y * Swell * sin(TimeSeconds * 0.48);
|
||||
}
|
||||
|
||||
float NextImprint = Imprint;
|
||||
if (ImprintSettings.x != 0.0)
|
||||
{
|
||||
NextImprint = clamp(Imprint * exp(-Dt / max(ImprintSettings.z, 1.0e-4))
|
||||
- Contact * ImprintSettings.y * Dt, -ImprintSettings.w, 0.0);
|
||||
}
|
||||
float FoundationTarget = ImprintSettings.x != 0.0 ? Imprint : 0.0;
|
||||
float Acceleration = WaveSpeed * WaveSpeed * Laplacian
|
||||
- FoundationStiffness * (Height - FoundationTarget) + Interaction + Breathing;
|
||||
float NextVelocity = (Velocity + Acceleration * Dt) * exp(-Damping * Dt);
|
||||
float NextHeight = Height + NextVelocity * Dt;
|
||||
TargetState[Pixel] = float4(NextHeight, NextVelocity, NextImprint, 0.0);
|
||||
}
|
||||
@@ -322,6 +322,108 @@ bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
|
||||
FirstFailure.IsEmpty() ? TEXT("none") : *FirstFailure));
|
||||
TestEqual(TEXT("all swept desired sets have zero overlapping pairs"), OverlapCases, 0);
|
||||
TestEqual(TEXT("all swept desired sets cover the legacy footprint"), CoverageFailures, 0);
|
||||
|
||||
static const FIntVector DeltaCenters[] = {
|
||||
FIntVector(-5, -2, -1), FIntVector(-3, 1, 2), FIntVector(-1, -3, 0),
|
||||
FIntVector(0, 0, 0), FIntVector(1, 3, -2), FIntVector(3, -1, 1),
|
||||
FIntVector(5, 2, -3),
|
||||
};
|
||||
static const FIntVector DeltaSteps[] = {
|
||||
FIntVector(-1, 0, 0), FIntVector(1, 0, 0),
|
||||
FIntVector(0, -1, 0), FIntVector(0, 1, 0),
|
||||
FIntVector(0, 0, -1), FIntVector(0, 0, 1),
|
||||
};
|
||||
int32 DeltaCases = 0;
|
||||
int32 DeltaFailures = 0;
|
||||
FString FirstDeltaFailure;
|
||||
for (const FIntVector& Center : DeltaCenters)
|
||||
for (const FSweepSettings& Sweep : SweepSettings)
|
||||
for (int32 ClampCase = 0; ClampCase < 3; ++ClampCase)
|
||||
{
|
||||
FParameters P;
|
||||
P.ClipRadius = Sweep.Radius;
|
||||
P.MaxClipLevel = Sweep.MaxLevel;
|
||||
P.RenderDistanceChunks = Sweep.RenderDistance;
|
||||
|
||||
const auto GetVerticalBounds = [&](int32 CenterZ, int32& OutMinZ, int32& OutMaxZ)
|
||||
{
|
||||
OutMinZ = MIN_int32;
|
||||
OutMaxZ = MAX_int32;
|
||||
if (ClampCase == 1)
|
||||
{
|
||||
OutMinZ = CenterZ - 2;
|
||||
OutMaxZ = CenterZ + 3;
|
||||
}
|
||||
else if (ClampCase == 2)
|
||||
{
|
||||
const int32 Band = VoxelClipmapDesiredTiles::FloorDiv(
|
||||
CenterZ, 1 << Sweep.MaxLevel);
|
||||
const int64 BandStart = static_cast<int64>(Band)
|
||||
* (int64{1} << static_cast<uint32>(Sweep.MaxLevel));
|
||||
OutMinZ = static_cast<int32>(BandStart - 1);
|
||||
OutMaxZ = OutMinZ + 6;
|
||||
}
|
||||
};
|
||||
int32 PreviousMinZ = 0;
|
||||
int32 PreviousMaxZ = 0;
|
||||
GetVerticalBounds(Center.Z, PreviousMinZ, PreviousMaxZ);
|
||||
TArray<FVoxelTileKey> PreviousTiles;
|
||||
VoxelClipmapDesiredTiles::Build(Center, PreviousMinZ, PreviousMaxZ, P, PreviousTiles);
|
||||
|
||||
for (const FIntVector& Step : DeltaSteps)
|
||||
{
|
||||
const FIntVector NextCenter = Center + Step;
|
||||
int32 NextMinZ = 0;
|
||||
int32 NextMaxZ = 0;
|
||||
GetVerticalBounds(NextCenter.Z, NextMinZ, NextMaxZ);
|
||||
TArray<FVoxelTileKey> Entering;
|
||||
TArray<FVoxelTileKey> Leaving;
|
||||
VoxelClipmapDesiredTiles::BuildDelta(
|
||||
Center, PreviousMinZ, PreviousMaxZ,
|
||||
NextCenter, NextMinZ, NextMaxZ, P, Entering, Leaving);
|
||||
|
||||
TSet<FVoxelTileKey> Updated;
|
||||
for (const FVoxelTileKey& Tile : PreviousTiles) Updated.Add(Tile);
|
||||
for (const FVoxelTileKey& Tile : Leaving) Updated.Remove(Tile);
|
||||
for (const FVoxelTileKey& Tile : Entering) Updated.Add(Tile);
|
||||
|
||||
TArray<FVoxelTileKey> FullNextTiles;
|
||||
VoxelClipmapDesiredTiles::Build(
|
||||
NextCenter, NextMinZ, NextMaxZ, P, FullNextTiles);
|
||||
TSet<FVoxelTileKey> FullNextSet;
|
||||
for (const FVoxelTileKey& Tile : FullNextTiles) FullNextSet.Add(Tile);
|
||||
bool bMatches = Updated.Num() == FullNextSet.Num();
|
||||
if (bMatches)
|
||||
{
|
||||
for (const FVoxelTileKey& Tile : Updated)
|
||||
{
|
||||
if (!FullNextSet.Contains(Tile))
|
||||
{
|
||||
bMatches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!bMatches)
|
||||
{
|
||||
++DeltaFailures;
|
||||
if (FirstDeltaFailure.IsEmpty())
|
||||
{
|
||||
FirstDeltaFailure = FString::Printf(
|
||||
TEXT("center=(%d,%d,%d) step=(%d,%d,%d) radius=%d level=%d clamp=%d entering=%d leaving=%d full=%d updated=%d"),
|
||||
Center.X, Center.Y, Center.Z, Step.X, Step.Y, Step.Z,
|
||||
Sweep.Radius, Sweep.MaxLevel, ClampCase,
|
||||
Entering.Num(), Leaving.Num(), FullNextSet.Num(), Updated.Num());
|
||||
}
|
||||
}
|
||||
++DeltaCases;
|
||||
}
|
||||
}
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("clipmap delta cases=%d failures=%d first=%s"), DeltaCases, DeltaFailures,
|
||||
FirstDeltaFailure.IsEmpty() ? TEXT("none") : *FirstDeltaFailure));
|
||||
TestEqual(TEXT("clipmap delta update equals a full rebuild on every tested crossing"),
|
||||
DeltaFailures, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -182,4 +182,218 @@ namespace VoxelClipmapDesiredTiles
|
||||
PreviousRingMax = RingMax;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the exact key difference between two adjacent clipmap centers without visiting
|
||||
// unchanged ring cells. A coarser ring can change membership when its finer child ring moves,
|
||||
// even when the coarser ring bounds themselves stay fixed; the cover-box delta below includes
|
||||
// those boundary keys as well.
|
||||
FORCEINLINE void BuildDelta(
|
||||
const FIntVector& PreviousCenter, int32 PreviousMinChunkZ, int32 PreviousMaxChunkZ,
|
||||
const FIntVector& Center, int32 MinChunkZ, int32 MaxChunkZ,
|
||||
const FParameters& Parameters,
|
||||
TArray<FVoxelTileKey>& OutEntering, TArray<FVoxelTileKey>& OutLeaving)
|
||||
{
|
||||
struct FBox
|
||||
{
|
||||
FIntVector Min = FIntVector::ZeroValue;
|
||||
FIntVector Max = FIntVector::ZeroValue;
|
||||
bool bValid = false;
|
||||
};
|
||||
OutEntering.Reset();
|
||||
OutLeaving.Reset();
|
||||
|
||||
const int32 Radius = FMath::Max(1, Parameters.ClipRadius);
|
||||
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
|
||||
const int32 OuterRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
|
||||
FBox PreviousValid[9];
|
||||
FBox CurrentValid[9];
|
||||
FBox PreviousCovered[9];
|
||||
FBox CurrentCovered[9];
|
||||
const auto BuildRings = [&](const FIntVector& RingCenter, int32 RingMinChunkZ,
|
||||
int32 RingMaxChunkZ, FBox* OutValid, FBox* OutCovered)
|
||||
{
|
||||
FIntVector ChildRingMin = FIntVector::ZeroValue;
|
||||
FIntVector ChildRingMax = FIntVector::ZeroValue;
|
||||
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
||||
{
|
||||
const int32 Pow = static_cast<int32>(uint32{1} << static_cast<uint32>(Level));
|
||||
const FIntVector CenterLevel = FloorDiv(RingCenter, Pow);
|
||||
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
||||
const int32 RingAlignment = Level < MaxLevel ? 2 : 1;
|
||||
FIntVector RingMin, RingMax;
|
||||
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
||||
FBox Valid;
|
||||
Valid.Min = RingMin;
|
||||
Valid.Max = RingMax;
|
||||
Valid.Min.Z = FMath::Max(Valid.Min.Z, FloorDiv(RingMinChunkZ, Pow));
|
||||
Valid.Max.Z = FMath::Min(Valid.Max.Z, FloorDiv(RingMaxChunkZ, Pow));
|
||||
Valid.bValid = Valid.Min.X <= Valid.Max.X
|
||||
&& Valid.Min.Y <= Valid.Max.Y && Valid.Min.Z <= Valid.Max.Z;
|
||||
OutValid[Level] = Valid;
|
||||
|
||||
FBox Covered;
|
||||
if (Level > 0)
|
||||
{
|
||||
Covered.Min = FIntVector(
|
||||
CeilDiv(ChildRingMin.X, 2), CeilDiv(ChildRingMin.Y, 2),
|
||||
CeilDiv(ChildRingMin.Z, 2));
|
||||
Covered.Max = FIntVector(
|
||||
ClampInt32(FloorDivWide(static_cast<int64>(ChildRingMax.X) - 1, 2)),
|
||||
ClampInt32(FloorDivWide(static_cast<int64>(ChildRingMax.Y) - 1, 2)),
|
||||
ClampInt32(FloorDivWide(static_cast<int64>(ChildRingMax.Z) - 1, 2)));
|
||||
Covered.bValid = Covered.Min.X <= Covered.Max.X
|
||||
&& Covered.Min.Y <= Covered.Max.Y && Covered.Min.Z <= Covered.Max.Z;
|
||||
}
|
||||
OutCovered[Level] = Covered;
|
||||
ChildRingMin = RingMin;
|
||||
ChildRingMax = RingMax;
|
||||
}
|
||||
};
|
||||
BuildRings(PreviousCenter, PreviousMinChunkZ, PreviousMaxChunkZ,
|
||||
PreviousValid, PreviousCovered);
|
||||
BuildRings(Center, MinChunkZ, MaxChunkZ, CurrentValid, CurrentCovered);
|
||||
|
||||
const auto Intersect = [](const FBox& A, const FBox& B)
|
||||
{
|
||||
FBox Result;
|
||||
if (!A.bValid || !B.bValid) return Result;
|
||||
Result.Min = FIntVector(
|
||||
FMath::Max(A.Min.X, B.Min.X), FMath::Max(A.Min.Y, B.Min.Y),
|
||||
FMath::Max(A.Min.Z, B.Min.Z));
|
||||
Result.Max = FIntVector(
|
||||
FMath::Min(A.Max.X, B.Max.X), FMath::Min(A.Max.Y, B.Max.Y),
|
||||
FMath::Min(A.Max.Z, B.Max.Z));
|
||||
Result.bValid = Result.Min.X <= Result.Max.X
|
||||
&& Result.Min.Y <= Result.Max.Y && Result.Min.Z <= Result.Max.Z;
|
||||
return Result;
|
||||
};
|
||||
const auto AppendDifference = [&Intersect](const FBox& A, const FBox& B, auto& OutBoxes)
|
||||
{
|
||||
if (!A.bValid) return;
|
||||
const FBox Common = Intersect(A, B);
|
||||
if (!Common.bValid)
|
||||
{
|
||||
OutBoxes.Add(A);
|
||||
return;
|
||||
}
|
||||
|
||||
// Six disjoint slabs cover A minus Common. The later slabs are narrowed to the
|
||||
// common extent on each already-partitioned axis, so no key is emitted twice.
|
||||
FBox Slab = A;
|
||||
if (A.Min.X < Common.Min.X)
|
||||
{
|
||||
Slab.Max.X = Common.Min.X - 1;
|
||||
Slab.bValid = Slab.Min.X <= Slab.Max.X;
|
||||
if (Slab.bValid) OutBoxes.Add(Slab);
|
||||
}
|
||||
Slab = A;
|
||||
if (Common.Max.X < A.Max.X)
|
||||
{
|
||||
Slab.Min.X = Common.Max.X + 1;
|
||||
Slab.bValid = Slab.Min.X <= Slab.Max.X;
|
||||
if (Slab.bValid) OutBoxes.Add(Slab);
|
||||
}
|
||||
|
||||
Slab = A;
|
||||
Slab.Min.X = Common.Min.X;
|
||||
Slab.Max.X = Common.Max.X;
|
||||
if (A.Min.Y < Common.Min.Y)
|
||||
{
|
||||
Slab.Max.Y = Common.Min.Y - 1;
|
||||
Slab.bValid = Slab.Min.Y <= Slab.Max.Y;
|
||||
if (Slab.bValid) OutBoxes.Add(Slab);
|
||||
}
|
||||
Slab = A;
|
||||
Slab.Min.X = Common.Min.X;
|
||||
Slab.Max.X = Common.Max.X;
|
||||
if (Common.Max.Y < A.Max.Y)
|
||||
{
|
||||
Slab.Min.Y = Common.Max.Y + 1;
|
||||
Slab.bValid = Slab.Min.Y <= Slab.Max.Y;
|
||||
if (Slab.bValid) OutBoxes.Add(Slab);
|
||||
}
|
||||
|
||||
Slab = A;
|
||||
Slab.Min.X = Common.Min.X;
|
||||
Slab.Max.X = Common.Max.X;
|
||||
Slab.Min.Y = Common.Min.Y;
|
||||
Slab.Max.Y = Common.Max.Y;
|
||||
if (A.Min.Z < Common.Min.Z)
|
||||
{
|
||||
Slab.Max.Z = Common.Min.Z - 1;
|
||||
Slab.bValid = Slab.Min.Z <= Slab.Max.Z;
|
||||
if (Slab.bValid) OutBoxes.Add(Slab);
|
||||
}
|
||||
Slab = A;
|
||||
Slab.Min.X = Common.Min.X;
|
||||
Slab.Max.X = Common.Max.X;
|
||||
Slab.Min.Y = Common.Min.Y;
|
||||
Slab.Max.Y = Common.Max.Y;
|
||||
if (Common.Max.Z < A.Max.Z)
|
||||
{
|
||||
Slab.Min.Z = Common.Max.Z + 1;
|
||||
Slab.bValid = Slab.Min.Z <= Slab.Max.Z;
|
||||
if (Slab.bValid) OutBoxes.Add(Slab);
|
||||
}
|
||||
};
|
||||
const auto Contains = [](const FBox& Box, const FIntVector& Point)
|
||||
{
|
||||
return Box.bValid && Point.X >= Box.Min.X && Point.X <= Box.Max.X
|
||||
&& Point.Y >= Box.Min.Y && Point.Y <= Box.Max.Y
|
||||
&& Point.Z >= Box.Min.Z && Point.Z <= Box.Max.Z;
|
||||
};
|
||||
const auto ForEachBox = [](const FBox& Box, const auto& Callback)
|
||||
{
|
||||
if (!Box.bValid) return;
|
||||
for (int64 Z = Box.Min.Z; Z <= Box.Max.Z; ++Z)
|
||||
for (int64 Y = Box.Min.Y; Y <= Box.Max.Y; ++Y)
|
||||
for (int64 X = Box.Min.X; X <= Box.Max.X; ++X)
|
||||
{
|
||||
Callback(FIntVector(static_cast<int32>(X), static_cast<int32>(Y),
|
||||
static_cast<int32>(Z)));
|
||||
}
|
||||
};
|
||||
|
||||
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
||||
{
|
||||
TArray<FBox, TInlineAllocator<16>> ChangedBoxes;
|
||||
AppendDifference(CurrentValid[Level], PreviousValid[Level], ChangedBoxes);
|
||||
AppendDifference(PreviousValid[Level], CurrentValid[Level], ChangedBoxes);
|
||||
|
||||
if (Level > 0)
|
||||
{
|
||||
const FBox CommonValid = Intersect(CurrentValid[Level], PreviousValid[Level]);
|
||||
if (CommonValid.bValid)
|
||||
{
|
||||
TArray<FBox, TInlineAllocator<16>> CoverDifferences;
|
||||
AppendDifference(CurrentCovered[Level], PreviousCovered[Level], CoverDifferences);
|
||||
AppendDifference(PreviousCovered[Level], CurrentCovered[Level], CoverDifferences);
|
||||
for (const FBox& CoverDifference : CoverDifferences)
|
||||
{
|
||||
const FBox ChangedCover = Intersect(CoverDifference, CommonValid);
|
||||
if (ChangedCover.bValid) ChangedBoxes.Add(ChangedCover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const FBox& ChangedBox : ChangedBoxes)
|
||||
{
|
||||
ForEachBox(ChangedBox, [&](const FIntVector& Coord)
|
||||
{
|
||||
const bool bCurrentMember = Contains(CurrentValid[Level], Coord)
|
||||
&& !Contains(CurrentCovered[Level], Coord);
|
||||
const bool bPreviousMember = Contains(PreviousValid[Level], Coord)
|
||||
&& !Contains(PreviousCovered[Level], Coord);
|
||||
if (bCurrentMember && !bPreviousMember)
|
||||
{
|
||||
OutEntering.Emplace(Coord, Level);
|
||||
}
|
||||
else if (bPreviousMember && !bCurrentMember)
|
||||
{
|
||||
OutLeaving.Emplace(Coord, Level);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace VoxelStreamingAdmissionOrder
|
||||
{
|
||||
template <typename Iterator, typename Less>
|
||||
void StableSortCandidates(Iterator First, Iterator Last, Less Compare)
|
||||
{
|
||||
std::stable_sort(First, Last, Compare);
|
||||
}
|
||||
|
||||
template <typename Range, typename IsAtCapacity, typename SubmitCandidate,
|
||||
typename OnCapacityReached>
|
||||
void AdmitInOrder(const Range& Candidates, IsAtCapacity AtCapacity,
|
||||
SubmitCandidate Submit, OnCapacityReached CapacityReached)
|
||||
{
|
||||
for (const auto& Candidate : Candidates)
|
||||
{
|
||||
if (AtCapacity())
|
||||
{
|
||||
CapacityReached();
|
||||
break;
|
||||
}
|
||||
Submit(Candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1247,6 +1247,8 @@ public:
|
||||
bool bSampleStacksStarted = false;
|
||||
bool bStartupApplyBoostStarted = false;
|
||||
bool bStartupApplyBoostEnded = false;
|
||||
// Fast-travel admission stays disabled until the initial near LOD and collision set is ready.
|
||||
bool bInitialArrivalSetSettled = false;
|
||||
uint64 StartupApplyBoostStartCycles = 0;
|
||||
// Trace the resolved startup budget once per world. / Tracer une fois le budget de demarrage resolu par monde.
|
||||
bool bStartupApplyConfigTraced = false;
|
||||
@@ -1254,6 +1256,21 @@ public:
|
||||
float StartupTraceSpeedCmPerSecond = 0.0f;
|
||||
bool bSpeedAwareTravelMode = false;
|
||||
bool bSpeedAwareFastMode = false;
|
||||
TArray<FVoxelTileKey> CachedCoreDesiredTiles;
|
||||
TSet<FVoxelTileKey> CachedCoreDesiredTileSet;
|
||||
FIntVector CachedCoreDesiredCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
||||
bool bHasCachedCoreDesiredTiles = false;
|
||||
TArray<FVoxelTileKey> CachedDesiredCandidateKeys;
|
||||
TSet<FVoxelTileKey> CachedDesiredCandidateSet;
|
||||
TSet<FVoxelTileKey> CachedDesiredBaseVisualTiles;
|
||||
TSet<FVoxelTileKey> CachedDesiredOverlayTiles;
|
||||
FIntVector CachedDesiredMaterializationCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
||||
bool bHasCachedDesiredMaterialization = false;
|
||||
TArray<FVoxelTileKey> CachedDesiredPriorityOrder;
|
||||
FIntVector CachedDesiredPriorityCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32);
|
||||
bool bHasCachedDesiredPriorityOrder = false;
|
||||
int32 IncrementalDesiredCenterCrossings = 0;
|
||||
int32 IncrementalDesiredMaterializationUpdates = 0;
|
||||
TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
|
||||
TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile
|
||||
// Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this
|
||||
@@ -1602,7 +1619,7 @@ public:
|
||||
// ONE frame → a game-thread teardown spike. Drained by ProcessUnloadQueue (catch-up scaled).
|
||||
TSet<FVoxelTileKey> PendingUnload;
|
||||
|
||||
void ProcessPendingChunks();
|
||||
void ProcessPendingChunks(uint64 TickStartCycles);
|
||||
void ProcessUnloadQueue(); // budgeted teardown drain (see PendingUnload)
|
||||
void QueuePendingUnload(const FVoxelTileKey& Tile);
|
||||
void HideOverlappingDesiredVisuals(const FVoxelTileKey& RetiredTile);
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
#include "VoxelForgeLivingPlaneActor.h"
|
||||
|
||||
#include "Engine/TextureRenderTarget2D.h"
|
||||
#include "Engine/TextureRenderTarget.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Materials/MaterialInstanceDynamic.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "RenderGraphBuilder.h"
|
||||
#include "RenderGraphUtils.h"
|
||||
#include "RHICommandList.h"
|
||||
#include "RHIResources.h"
|
||||
#include "DataDrivenShaderPlatformInfo.h"
|
||||
#include "GlobalShader.h"
|
||||
#include "ShaderParameterStruct.h"
|
||||
#include "UObject/ConstructorHelpers.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int32 LivingPlaneThreadGroupSize = 8;
|
||||
|
||||
struct FLivingPlaneStepInput
|
||||
{
|
||||
FVector4f ObjectPositionRadius[2] = {FVector4f(0, 0, 0, -1), FVector4f(0, 0, 0, -1)};
|
||||
FVector4f ObjectVelocityPush[2] = {FVector4f(0, 0, 0, 0), FVector4f(0, 0, 0, 0)};
|
||||
float TimeSeconds = 0.0f;
|
||||
};
|
||||
|
||||
class FVFLivingPlaneStepCS final : public FGlobalShader
|
||||
{
|
||||
DECLARE_GLOBAL_SHADER(FVFLivingPlaneStepCS);
|
||||
SHADER_USE_PARAMETER_STRUCT(FVFLivingPlaneStepCS, FGlobalShader);
|
||||
|
||||
BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
|
||||
SHADER_PARAMETER(FIntPoint, GridSize)
|
||||
SHADER_PARAMETER(float, Dt)
|
||||
SHADER_PARAMETER(float, Dx)
|
||||
SHADER_PARAMETER(float, PlaneLength)
|
||||
SHADER_PARAMETER(float, WaveSpeed)
|
||||
SHADER_PARAMETER(float, Damping)
|
||||
SHADER_PARAMETER(float, FoundationStiffness)
|
||||
SHADER_PARAMETER(float, TimeSeconds)
|
||||
SHADER_PARAMETER(FVector2f, BreathingSettings)
|
||||
SHADER_PARAMETER(FVector4f, ImprintSettings)
|
||||
SHADER_PARAMETER_ARRAY(FVector4f, ObjectPositionRadius, [2])
|
||||
SHADER_PARAMETER_ARRAY(FVector4f, ObjectVelocityPush, [2])
|
||||
SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D<float4>, SourceState)
|
||||
SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D<float4>, TargetState)
|
||||
END_SHADER_PARAMETER_STRUCT()
|
||||
|
||||
public:
|
||||
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
|
||||
{
|
||||
return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
|
||||
}
|
||||
};
|
||||
|
||||
IMPLEMENT_GLOBAL_SHADER(FVFLivingPlaneStepCS,
|
||||
"/Plugin/VoxelForgeLivingPlane/LivingPlane.usf", "MainCS", SF_Compute)
|
||||
|
||||
UTextureRenderTarget2D* CreateFieldTarget(UObject* Outer, int32 Size, FName Name)
|
||||
{
|
||||
UTextureRenderTarget2D* Target = NewObject<UTextureRenderTarget2D>(Outer, Name, RF_Transient);
|
||||
Target->RenderTargetFormat = RTF_RGBA32f;
|
||||
Target->bCanCreateUAV = true;
|
||||
Target->bAutoGenerateMips = false;
|
||||
Target->ClearColor = FLinearColor::Transparent;
|
||||
Target->Filter = TF_Bilinear;
|
||||
Target->AddressX = TA_Clamp;
|
||||
Target->AddressY = TA_Clamp;
|
||||
Target->InitAutoFormat(Size, Size);
|
||||
Target->UpdateResourceImmediate(true);
|
||||
return Target;
|
||||
}
|
||||
}
|
||||
|
||||
AVoxelForgeLivingPlaneActor::AVoxelForgeLivingPlaneActor()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
PlaneMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("LivingPlaneMesh"));
|
||||
SetRootComponent(PlaneMesh);
|
||||
PlaneMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
PlaneMesh->SetMobility(EComponentMobility::Movable);
|
||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> PlaneAsset(
|
||||
TEXT("/Engine/BasicShapes/Plane.Plane"));
|
||||
if (PlaneAsset.Succeeded())
|
||||
{
|
||||
PlaneMesh->SetStaticMesh(PlaneAsset.Object);
|
||||
}
|
||||
|
||||
ObjectInputs.SetNum(2);
|
||||
}
|
||||
|
||||
void AVoxelForgeLivingPlaneActor::OnConstruction(const FTransform& Transform)
|
||||
{
|
||||
Super::OnConstruction(Transform);
|
||||
if (PlaneMesh)
|
||||
{
|
||||
// /Engine/BasicShapes/Plane is 100 cm square; one Unreal scale unit
|
||||
// therefore corresponds to one metre of authored plane length.
|
||||
PlaneMesh->SetRelativeScale3D(FVector(PlaneLengthMeters, PlaneLengthMeters, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
float AVoxelForgeLivingPlaneActor::GetStableStepSeconds() const
|
||||
{
|
||||
const float Dx = PlaneLengthMeters / static_cast<float>(FMath::Max(GridSize, 16));
|
||||
const float EigenFrequencySquared = 8.0f * WaveSpeedMetersPerSecond * WaveSpeedMetersPerSecond
|
||||
/ FMath::Max(Dx * Dx, 1.0e-8f) + FoundationStiffnessPerSecondSquared;
|
||||
return 1.30f / FMath::Sqrt(FMath::Max(EigenFrequencySquared, 1.0e-8f));
|
||||
}
|
||||
|
||||
void AVoxelForgeLivingPlaneActor::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
GridSize = FMath::Clamp(GridSize, 16, 2048);
|
||||
PlaneLengthMeters = FMath::Max(PlaneLengthMeters, 1.0f);
|
||||
FixedStepSeconds = GetStableStepSeconds();
|
||||
CreateFieldTargets();
|
||||
|
||||
if (PlaneMaterial && PlaneMesh)
|
||||
{
|
||||
DynamicPlaneMaterial = PlaneMesh->CreateDynamicMaterialInstance(0, PlaneMaterial);
|
||||
if (DynamicPlaneMaterial)
|
||||
{
|
||||
DynamicPlaneMaterial->SetScalarParameterValue(TEXT("PlaneLengthMeters"), PlaneLengthMeters);
|
||||
DynamicPlaneMaterial->SetScalarParameterValue(TEXT("FieldSize"), static_cast<float>(GridSize));
|
||||
}
|
||||
}
|
||||
UpdateMaterialField();
|
||||
}
|
||||
|
||||
void AVoxelForgeLivingPlaneActor::CreateFieldTargets()
|
||||
{
|
||||
FieldTargets[0] = CreateFieldTarget(this, GridSize, TEXT("LivingPlaneStateA"));
|
||||
FieldTargets[1] = CreateFieldTarget(this, GridSize, TEXT("LivingPlaneStateB"));
|
||||
CurrentFieldIndex = 0;
|
||||
SimulationStepCount = 0;
|
||||
SimulationAccumulatorSeconds = 0.0;
|
||||
}
|
||||
|
||||
void AVoxelForgeLivingPlaneActor::SetObjectState(int32 ObjectIndex,
|
||||
const FVFLivingPlaneObjectInput& State)
|
||||
{
|
||||
if (!ObjectInputs.IsValidIndex(ObjectIndex) || ObjectIndex >= 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ObjectInputs[ObjectIndex] = State;
|
||||
}
|
||||
|
||||
void AVoxelForgeLivingPlaneActor::ResetSimulation()
|
||||
{
|
||||
if (!IsInGameThread() || !FieldTargets[0] || !FieldTargets[1])
|
||||
{
|
||||
return;
|
||||
}
|
||||
CreateFieldTargets();
|
||||
FixedStepSeconds = GetStableStepSeconds();
|
||||
UpdateMaterialField();
|
||||
}
|
||||
|
||||
void AVoxelForgeLivingPlaneActor::UpdateMaterialField()
|
||||
{
|
||||
if (DynamicPlaneMaterial && FieldTargets[CurrentFieldIndex])
|
||||
{
|
||||
DynamicPlaneMaterial->SetTextureParameterValue(TEXT("LivingField"), FieldTargets[CurrentFieldIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
void AVoxelForgeLivingPlaneActor::EnqueueSimulationSteps(int32 NumSteps)
|
||||
{
|
||||
if (NumSteps <= 0 || !FieldTargets[0] || !FieldTargets[1])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FTextureRenderTargetResource* ResourceA = FieldTargets[0]->GameThread_GetRenderTargetResource();
|
||||
FTextureRenderTargetResource* ResourceB = FieldTargets[1]->GameThread_GetRenderTargetResource();
|
||||
if (!ResourceA || !ResourceB)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<FLivingPlaneStepInput> Steps;
|
||||
Steps.Reserve(NumSteps);
|
||||
const FTransform PlaneTransform = GetActorTransform();
|
||||
for (int32 Step = 0; Step < NumSteps; ++Step)
|
||||
{
|
||||
FLivingPlaneStepInput& Input = Steps.AddDefaulted_GetRef();
|
||||
Input.TimeSeconds = static_cast<float>(SimulationStepCount + Step) * FixedStepSeconds;
|
||||
for (int32 ObjectIndex = 0; ObjectIndex < 2; ++ObjectIndex)
|
||||
{
|
||||
if (!ObjectInputs.IsValidIndex(ObjectIndex) || !ObjectInputs[ObjectIndex].bActive)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FVFLivingPlaneObjectInput& Object = ObjectInputs[ObjectIndex];
|
||||
const FVector LocalPosition = PlaneTransform.InverseTransformPosition(Object.WorldPositionCm);
|
||||
const FVector LocalVelocity = PlaneTransform.InverseTransformVector(Object.WorldVelocityCmPerSecond);
|
||||
const float RadiusMeters = FMath::Max(Object.RadiusCm * 0.01f, 0.001f);
|
||||
Input.ObjectPositionRadius[ObjectIndex] = FVector4f(
|
||||
static_cast<float>(LocalPosition.X * 0.01),
|
||||
static_cast<float>(LocalPosition.Y * 0.01),
|
||||
static_cast<float>(LocalPosition.Z * 0.01),
|
||||
RadiusMeters);
|
||||
Input.ObjectVelocityPush[ObjectIndex] = FVector4f(
|
||||
static_cast<float>(LocalVelocity.X * 0.01),
|
||||
static_cast<float>(LocalVelocity.Y * 0.01),
|
||||
static_cast<float>(LocalVelocity.Z * 0.01),
|
||||
FMath::Max(Object.PushAccelerationMetersPerSecondSquared, 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
const int32 Resolution = GridSize;
|
||||
const int32 InitialFieldIndex = CurrentFieldIndex;
|
||||
const float Dt = static_cast<float>(FixedStepSeconds);
|
||||
const float Dx = PlaneLengthMeters / static_cast<float>(Resolution);
|
||||
const float PlaneLength = PlaneLengthMeters;
|
||||
const float WaveSpeed = WaveSpeedMetersPerSecond;
|
||||
const float Damping = DampingPerSecond;
|
||||
const float Foundation = FoundationStiffnessPerSecondSquared;
|
||||
const float BreathingStrength = bEnableBreathing ? BreathingStrengthMetersPerSecondSquared : 0.0f;
|
||||
const FVector2f BreathingSettings(bEnableBreathing ? 1.0f : 0.0f, BreathingStrength);
|
||||
const FVector4f ImprintSettings(bEnableImprint ? 1.0f : 0.0f,
|
||||
ImprintCaptureMetersPerSecond, FMath::Max(ImprintRecoverySeconds, 1.0e-4f),
|
||||
FMath::Max(ImprintMaxDepthMeters, 0.0f));
|
||||
|
||||
ENQUEUE_RENDER_COMMAND(VoxelForgeLivingPlaneStep)(
|
||||
[ResourceA, ResourceB, Steps = MoveTemp(Steps), InitialFieldIndex, Resolution, Dt, Dx, PlaneLength,
|
||||
WaveSpeed, Damping, Foundation, BreathingSettings, ImprintSettings]
|
||||
(FRHICommandListImmediate& RHICmdList) mutable
|
||||
{
|
||||
FRDGBuilder GraphBuilder(RHICmdList, RDG_EVENT_NAME("VoxelForgeLivingPlane"));
|
||||
FRDGTextureRef Fields[2] = {
|
||||
RegisterExternalTexture(GraphBuilder, ResourceA->GetRenderTargetTexture().GetReference(), TEXT("LivingPlaneStateA")),
|
||||
RegisterExternalTexture(GraphBuilder, ResourceB->GetRenderTargetTexture().GetReference(), TEXT("LivingPlaneStateB"))};
|
||||
TShaderMapRef<FVFLivingPlaneStepCS> ComputeShader(GetGlobalShaderMap(GMaxRHIFeatureLevel));
|
||||
int32 CurrentIndex = InitialFieldIndex;
|
||||
for (const FLivingPlaneStepInput& StepInput : Steps)
|
||||
{
|
||||
const int32 TargetIndex = 1 - CurrentIndex;
|
||||
FVFLivingPlaneStepCS::FParameters* PassParameters =
|
||||
GraphBuilder.AllocParameters<FVFLivingPlaneStepCS::FParameters>();
|
||||
PassParameters->GridSize = FIntPoint(Resolution, Resolution);
|
||||
PassParameters->Dt = Dt;
|
||||
PassParameters->Dx = Dx;
|
||||
PassParameters->PlaneLength = PlaneLength;
|
||||
PassParameters->WaveSpeed = WaveSpeed;
|
||||
PassParameters->Damping = Damping;
|
||||
PassParameters->FoundationStiffness = Foundation;
|
||||
PassParameters->TimeSeconds = StepInput.TimeSeconds;
|
||||
PassParameters->BreathingSettings = BreathingSettings;
|
||||
PassParameters->ImprintSettings = ImprintSettings;
|
||||
for (int32 ObjectIndex = 0; ObjectIndex < 2; ++ObjectIndex)
|
||||
{
|
||||
PassParameters->ObjectPositionRadius[ObjectIndex] = StepInput.ObjectPositionRadius[ObjectIndex];
|
||||
PassParameters->ObjectVelocityPush[ObjectIndex] = StepInput.ObjectVelocityPush[ObjectIndex];
|
||||
}
|
||||
PassParameters->SourceState = GraphBuilder.CreateSRV(Fields[CurrentIndex]);
|
||||
PassParameters->TargetState = GraphBuilder.CreateUAV(Fields[TargetIndex]);
|
||||
FComputeShaderUtils::AddPass(GraphBuilder,
|
||||
RDG_EVENT_NAME("LivingPlaneStep"),
|
||||
ComputeShader, PassParameters,
|
||||
FIntVector(FMath::DivideAndRoundUp(Resolution, LivingPlaneThreadGroupSize),
|
||||
FMath::DivideAndRoundUp(Resolution, LivingPlaneThreadGroupSize), 1));
|
||||
CurrentIndex = TargetIndex;
|
||||
}
|
||||
GraphBuilder.Execute();
|
||||
});
|
||||
|
||||
CurrentFieldIndex = (InitialFieldIndex + NumSteps) & 1;
|
||||
SimulationStepCount += NumSteps;
|
||||
UpdateMaterialField();
|
||||
}
|
||||
|
||||
void AVoxelForgeLivingPlaneActor::Tick(float DeltaSeconds)
|
||||
{
|
||||
Super::Tick(DeltaSeconds);
|
||||
if (FixedStepSeconds <= 0.0 || !FieldTargets[0] || !FieldTargets[1])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SimulationAccumulatorSeconds += FMath::Max(static_cast<double>(DeltaSeconds), 0.0);
|
||||
const int32 StepsToRun = FMath::Min(
|
||||
static_cast<int32>(SimulationAccumulatorSeconds / FixedStepSeconds), 32);
|
||||
if (StepsToRun > 0)
|
||||
{
|
||||
SimulationAccumulatorSeconds -= StepsToRun * FixedStepSeconds;
|
||||
EnqueueSimulationSteps(StepsToRun);
|
||||
}
|
||||
}
|
||||
|
||||
void AVoxelForgeLivingPlaneActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
Super::EndPlay(EndPlayReason);
|
||||
DynamicPlaneMaterial = nullptr;
|
||||
FieldTargets[0] = nullptr;
|
||||
FieldTargets[1] = nullptr;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
#include "Interfaces/IPluginManager.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "ShaderCore.h"
|
||||
|
||||
class FVoxelForgeLivingPlaneModule final : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override
|
||||
{
|
||||
const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("VoxelForge"));
|
||||
if (!Plugin.IsValid())
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("VoxelForgeLivingPlane could not find its owning plugin."));
|
||||
return;
|
||||
}
|
||||
|
||||
AddShaderSourceDirectoryMapping(
|
||||
TEXT("/Plugin/VoxelForgeLivingPlane"),
|
||||
FPaths::Combine(Plugin->GetBaseDir(), TEXT("Shaders")));
|
||||
}
|
||||
};
|
||||
|
||||
IMPLEMENT_MODULE(FVoxelForgeLivingPlaneModule, VoxelForgeLivingPlane)
|
||||
@@ -0,0 +1,114 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "VoxelForgeLivingPlaneActor.generated.h"
|
||||
|
||||
class UMaterialInterface;
|
||||
class UMaterialInstanceDynamic;
|
||||
class UStaticMeshComponent;
|
||||
class UTextureRenderTarget2D;
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FVFLivingPlaneObjectInput
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object")
|
||||
FVector WorldPositionCm = FVector(0.0, 0.0, 43.0);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object")
|
||||
FVector WorldVelocityCmPerSecond = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object", meta=(ClampMin="0.1"))
|
||||
float RadiusCm = 43.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object", meta=(ClampMin="0.0"))
|
||||
float PushAccelerationMetersPerSecondSquared = 36.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object")
|
||||
bool bActive = false;
|
||||
};
|
||||
|
||||
/** A render-only, GPU-stepped heightfield plane. Object state is uploaded; the field is never read back. */
|
||||
UCLASS(BlueprintType, Blueprintable)
|
||||
class VOXELFORGELIVINGPLANE_API AVoxelForgeLivingPlaneActor : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AVoxelForgeLivingPlaneActor();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
virtual void OnConstruction(const FTransform& Transform) override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Living Plane|Object")
|
||||
void SetObjectState(int32 ObjectIndex, const FVFLivingPlaneObjectInput& State);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category="Living Plane")
|
||||
void ResetSimulation();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category="Living Plane")
|
||||
int64 GetSimulationStepCount() const { return SimulationStepCount; }
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Living Plane")
|
||||
TObjectPtr<UStaticMeshComponent> PlaneMesh;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Living Plane|Rendering")
|
||||
TObjectPtr<UMaterialInterface> PlaneMaterial;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="16", ClampMax="2048"))
|
||||
int32 GridSize = 256;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="1.0", ClampMax="100.0"))
|
||||
float PlaneLengthMeters = 12.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="0.0"))
|
||||
float WaveSpeedMetersPerSecond = 4.5f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="0.0"))
|
||||
float DampingPerSecond = 1.35f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="0.0"))
|
||||
float FoundationStiffnessPerSecondSquared = 28.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation")
|
||||
bool bEnableBreathing = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Simulation", meta=(ClampMin="0.0", ClampMax="30.0"))
|
||||
float BreathingStrengthMetersPerSecondSquared = 4.8f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Imprint")
|
||||
bool bEnableImprint = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Imprint", meta=(ClampMin="0.0", ClampMax="1.0"))
|
||||
float ImprintCaptureMetersPerSecond = 0.15f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Imprint", meta=(ClampMin="0.1", ClampMax="60.0"))
|
||||
float ImprintRecoverySeconds = 6.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Imprint", meta=(ClampMin="0.0", ClampMax="1.0"))
|
||||
float ImprintMaxDepthMeters = 0.34f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Living Plane|Object")
|
||||
TArray<FVFLivingPlaneObjectInput> ObjectInputs;
|
||||
|
||||
private:
|
||||
void CreateFieldTargets();
|
||||
void EnqueueSimulationSteps(int32 NumSteps);
|
||||
void UpdateMaterialField();
|
||||
float GetStableStepSeconds() const;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextureRenderTarget2D> FieldTargets[2];
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UMaterialInstanceDynamic> DynamicPlaneMaterial;
|
||||
|
||||
int32 CurrentFieldIndex = 0;
|
||||
int64 SimulationStepCount = 0;
|
||||
double SimulationAccumulatorSeconds = 0.0;
|
||||
double FixedStepSeconds = 0.0;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class VoxelForgeLivingPlane : ModuleRules
|
||||
{
|
||||
public VoxelForgeLivingPlane(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
if (Target.Platform == UnrealTargetPlatform.Linux)
|
||||
{
|
||||
bUseUnity = false;
|
||||
}
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine"
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Projects",
|
||||
"RenderCore",
|
||||
"RHI"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -597,6 +597,62 @@ namespace VoxelForgeCore
|
||||
}
|
||||
}
|
||||
|
||||
inline constexpr std::string_view CoreLiminalFieldRecipeIdV1 = "Core.Liminal.Field";
|
||||
inline constexpr std::uint32_t CoreLiminalFieldRecipeVersionV1 = 1;
|
||||
inline constexpr std::uint32_t CoreLiminalFieldRecipeVersionV2 = 2;
|
||||
inline constexpr std::uint32_t CoreLiminalFieldVariantCountV1 = 4;
|
||||
|
||||
enum class WorldAssemblyLiminalVariantV1 : std::uint32_t
|
||||
{
|
||||
flatFieldMonolith = 0,
|
||||
repeatingHills = 1,
|
||||
roadOverCloudFloor = 2,
|
||||
pillarGrid = 3
|
||||
};
|
||||
|
||||
struct WorldAssemblyLiminalVariantDefinitionV1
|
||||
{
|
||||
WorldAssemblyLiminalVariantV1 variant = WorldAssemblyLiminalVariantV1::flatFieldMonolith;
|
||||
std::string_view name;
|
||||
WorldAssemblySurfaceProfileKindV1 surfaceKind = WorldAssemblySurfaceProfileKindV1::flatField;
|
||||
std::int64_t hillAmplitudeMicrometres = 0;
|
||||
std::int64_t hillWavelengthMicrometres = 0;
|
||||
std::int64_t routeHalfWidthMicrometres = 0;
|
||||
std::int64_t routeHalfThicknessMicrometres = 0;
|
||||
std::int64_t supportSpacingMicrometres = 0;
|
||||
std::int64_t supportRadiusMicrometres = 0;
|
||||
std::int64_t monolithRadiusMicrometres = 0;
|
||||
std::int64_t monolithHeightMicrometres = 0;
|
||||
std::uint32_t pillarRows = 0;
|
||||
std::uint32_t pillarColumns = 0;
|
||||
std::int64_t pillarSpacingMicrometres = 0;
|
||||
std::int64_t pillarRadiusMicrometres = 0;
|
||||
std::int64_t pillarHeightMicrometres = 0;
|
||||
};
|
||||
|
||||
inline constexpr std::array<WorldAssemblyLiminalVariantDefinitionV1,
|
||||
CoreLiminalFieldVariantCountV1> CoreLiminalFieldVariantsV1{{
|
||||
{WorldAssemblyLiminalVariantV1::flatFieldMonolith, "flatField-monolith",
|
||||
WorldAssemblySurfaceProfileKindV1::flatField, 0, 0, 1750000, 500000,
|
||||
36000000, 550000, 1600000, 26000000, 0, 0, 0, 0, 0},
|
||||
{WorldAssemblyLiminalVariantV1::repeatingHills, "periodicHills",
|
||||
WorldAssemblySurfaceProfileKindV1::periodicHills, 2200000, 80000000,
|
||||
1750000, 500000, 36000000, 550000, 0, 0, 0, 0, 0, 0, 0},
|
||||
{WorldAssemblyLiminalVariantV1::roadOverCloudFloor, "road-over-cloud-floor",
|
||||
WorldAssemblySurfaceProfileKindV1::flatField, 0, 0, 2000000, 500000,
|
||||
24000000, 550000, 0, 0, 0, 0, 0, 0, 0},
|
||||
{WorldAssemblyLiminalVariantV1::pillarGrid, "pillar-grid",
|
||||
WorldAssemblySurfaceProfileKindV1::flatField, 0, 0, 1750000, 500000,
|
||||
36000000, 550000, 0, 0, 15, 15, 76000000, 5500000, 40000000}
|
||||
}};
|
||||
|
||||
inline constexpr WorldAssemblyLiminalVariantV1 SelectCoreLiminalFieldVariantV1(
|
||||
std::uint32_t seed) noexcept
|
||||
{
|
||||
return static_cast<WorldAssemblyLiminalVariantV1>(
|
||||
seed % CoreLiminalFieldVariantCountV1);
|
||||
}
|
||||
|
||||
struct WorldAssemblyRecipeV1
|
||||
{
|
||||
std::uint32_t schemaVersion = WorldAssemblySchemaVersion;
|
||||
@@ -654,6 +710,23 @@ namespace VoxelForgeCore
|
||||
std::array<std::int64_t, 4> parametersSubvoxels{};
|
||||
};
|
||||
|
||||
struct WorldAssemblyLiminalFieldPlanV1
|
||||
{
|
||||
std::uint32_t version = 0;
|
||||
WorldAssemblyLiminalVariantV1 variant = WorldAssemblyLiminalVariantV1::flatFieldMonolith;
|
||||
std::uint32_t seed = 0;
|
||||
std::array<std::int64_t, 3> arrivalMouthSubvoxels{};
|
||||
std::array<std::int64_t, 3> departureMouthSubvoxels{};
|
||||
// Fixed distance from each passage player-center anchor to its walk surface.
|
||||
std::int64_t mouthFloorOffsetSubvoxels = 0;
|
||||
std::int64_t floorHeightSubvoxels = 0;
|
||||
std::int64_t routeHalfWidthSubvoxels = 0;
|
||||
std::int64_t routeHalfThicknessSubvoxels = 0;
|
||||
std::int64_t routeOffsetSubvoxels = 0;
|
||||
std::int64_t supportSpacingSubvoxels = 0;
|
||||
std::int64_t supportRadiusSubvoxels = 0;
|
||||
};
|
||||
|
||||
struct WorldAssemblyStratumPlanV1
|
||||
{
|
||||
std::int32_t strateIndex = -1;
|
||||
@@ -678,6 +751,7 @@ namespace VoxelForgeCore
|
||||
std::vector<WorldAssemblyFeatureV1> features;
|
||||
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
||||
std::vector<WorldAssemblyRoutePointV1> route;
|
||||
WorldAssemblyLiminalFieldPlanV1 liminalField;
|
||||
std::uint64_t contentHash = 0;
|
||||
};
|
||||
|
||||
@@ -891,6 +965,10 @@ namespace VoxelForgeCore
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV3 = 3;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV4 = 4;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV5 = 5;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV6 = 6;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV7 = 7;
|
||||
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1;
|
||||
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
||||
@@ -904,7 +982,11 @@ namespace VoxelForgeCore
|
||||
fillPillar = 3,
|
||||
fillDais = 4,
|
||||
fillMonolith = 5,
|
||||
fillPlatform = 6
|
||||
fillPlatform = 6,
|
||||
fillSweptDeckSegment = 7,
|
||||
fillBox = 8,
|
||||
carveBox = 9,
|
||||
fillSteps = 10
|
||||
};
|
||||
|
||||
struct ResolvedLayoutStampRequestV1
|
||||
@@ -914,6 +996,12 @@ namespace VoxelForgeCore
|
||||
ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid;
|
||||
// A preferred center and a bounded deterministic search radius.
|
||||
std::array<std::int64_t, 3> anchorPositionMicrometres{};
|
||||
// Present only for fillSweptDeckSegment; defines its directed centerline.
|
||||
std::array<std::array<std::int64_t, 3>, 2> sweepEndpointsMicrometres{};
|
||||
// V5 permits one continuous swept deck to carry its complete sampled centerline.
|
||||
// V6 also permits a path whose X coordinates are not strictly increasing.
|
||||
// Empty retains the V4 single-segment interpretation.
|
||||
std::vector<std::array<std::int64_t, 3>> sweepPathMicrometres;
|
||||
std::int64_t placementRadiusMicrometres = 0;
|
||||
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||
std::int64_t smoothWidthMicrometres = 0;
|
||||
@@ -921,6 +1009,9 @@ namespace VoxelForgeCore
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::int64_t roundingRadiusMicrometres = 0;
|
||||
std::int64_t minimumSpacingMicrometres = 0;
|
||||
// Vocabulary 7: fillSteps divides the X and Z extents into this many
|
||||
// identical treads and risers. Other stamp kinds keep this at zero.
|
||||
std::uint16_t stepCount = 0;
|
||||
// 0 = free placement, 1 = seated on the authored flat profile floor.
|
||||
std::uint8_t attachmentPolicy = 0;
|
||||
std::int64_t floorMicrometres = 0;
|
||||
@@ -933,6 +1024,8 @@ namespace VoxelForgeCore
|
||||
std::int32_t strateIndex = -1;
|
||||
ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid;
|
||||
std::array<std::int64_t, 3> positionMicrometres{};
|
||||
std::array<std::array<std::int64_t, 3>, 2> sweepEndpointsMicrometres{};
|
||||
std::vector<std::array<std::int64_t, 3>> sweepPathMicrometres;
|
||||
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||
@@ -941,6 +1034,7 @@ namespace VoxelForgeCore
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::int64_t roundingRadiusMicrometres = 0;
|
||||
std::int64_t minimumSpacingMicrometres = 0;
|
||||
std::uint16_t stepCount = 0;
|
||||
std::uint8_t attachmentPolicy = 0;
|
||||
std::int64_t floorMicrometres = 0;
|
||||
std::uint64_t irregularitySeed = 0;
|
||||
@@ -1060,6 +1154,27 @@ namespace VoxelForgeCore
|
||||
bool surfaceToTunnelLandingSupport = false;
|
||||
};
|
||||
|
||||
enum class ResolvedLayoutLandingPadRoleV1 : std::uint32_t
|
||||
{
|
||||
arrival = 0,
|
||||
departure = 1
|
||||
};
|
||||
|
||||
struct ResolvedLayoutLandingPadV1
|
||||
{
|
||||
std::string stableId;
|
||||
ResolvedLayoutLandingPadRoleV1 role = ResolvedLayoutLandingPadRoleV1::arrival;
|
||||
// Capsule-center position. floorHeightSubvoxels is the supported floor below it.
|
||||
std::array<std::int64_t, 3> positionSubvoxels{};
|
||||
std::int64_t floorHeightSubvoxels = 0;
|
||||
// Unit horizontal travel direction, signed Q16, oriented into the layer route.
|
||||
std::array<std::int32_t, 2> approachDirectionQ16{};
|
||||
// Certified horizontal free radius around the capsule center and support below the floor.
|
||||
std::int64_t clearanceSubvoxels = 0;
|
||||
std::int64_t supportDepthSubvoxels = 0;
|
||||
bool certifiedWalkableWithSupport = false;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutStratumV2
|
||||
{
|
||||
std::string stableId;
|
||||
@@ -1089,6 +1204,7 @@ namespace VoxelForgeCore
|
||||
std::int64_t managerChunkWidthMicrometres = 0;
|
||||
std::int64_t dispatchChunkWidthMicrometres = 0;
|
||||
std::vector<ResolvedLayoutAnchorV2> anchors;
|
||||
std::vector<ResolvedLayoutLandingPadV1> landingPads;
|
||||
ResolvedLayoutStratePassageCapabilitiesV1 passageCapabilities;
|
||||
ResolvedLayoutInterfaceCapabilitiesV1 lowerInterfaceCapabilities;
|
||||
};
|
||||
@@ -1228,6 +1344,15 @@ namespace VoxelForgeCore
|
||||
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||
const std::vector<ResolvedLayoutPassageConfigV1>& configs,
|
||||
std::string& error);
|
||||
VFCORE_API bool PlanCoreLiminalFieldV1(
|
||||
WorldAssemblyRecipeV1& recipe, WorldAssemblyStratumPlanV1& stratum,
|
||||
const ResolvedLayoutSnapshotV3& layout, std::string& error);
|
||||
VFCORE_API bool PrepareCoreLiminalLandingPadsV2(
|
||||
WorldAssemblyRecipeV1& recipe, WorldAssemblyStratumPlanV1& stratum,
|
||||
ResolvedLayoutSnapshotV3& layout, std::string& error);
|
||||
VFCORE_API bool PlanCoreLiminalFieldV2(
|
||||
WorldAssemblyRecipeV1& recipe, WorldAssemblyStratumPlanV1& stratum,
|
||||
const ResolvedLayoutSnapshotV3& layout, std::string& error);
|
||||
|
||||
struct ResolvedLayoutStampPlacementAuditV1
|
||||
{
|
||||
|
||||
@@ -176,7 +176,7 @@ namespace
|
||||
{
|
||||
const auto value=static_cast<std::uint8_t>(kind);
|
||||
return value>=static_cast<std::uint8_t>(ResolvedSdfStampKindV1::carveEllipsoid)
|
||||
&&value<=static_cast<std::uint8_t>(ResolvedSdfStampKindV1::fillPlatform);
|
||||
&&value<=static_cast<std::uint8_t>(ResolvedSdfStampKindV1::fillSteps);
|
||||
}
|
||||
bool LayoutStampIsMotif(ResolvedSdfStampKindV1 kind)
|
||||
{return kind==ResolvedSdfStampKindV1::fillMonolith
|
||||
@@ -249,6 +249,71 @@ namespace
|
||||
/WorldAssemblySubvoxelsPerVoxel));
|
||||
return result;
|
||||
};
|
||||
const bool liminalRouteStamp=strate->recipeId==CoreLiminalFieldRecipeIdV1
|
||||
&&stamp.stableId.starts_with("core/liminal-field/route/")
|
||||
&&(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
||stamp.kind==ResolvedSdfStampKindV1::fillPillar);
|
||||
if(liminalRouteStamp)
|
||||
{
|
||||
const auto variant=std::find_if(strate->anchors.begin(),strate->anchors.end(),
|
||||
[](const auto& anchor){return anchor.kind=="liminal_variant"
|
||||
&&anchor.values.size()>=6;});
|
||||
std::vector<const ResolvedLayoutAnchorV2*> points;
|
||||
for(const auto& anchor:strate->anchors)
|
||||
if(anchor.kind=="liminal_route_point"&&anchor.values.size()>=4)
|
||||
points.push_back(&anchor);
|
||||
if(variant==strate->anchors.end()||points.size()<2)
|
||||
{if(reason)*reason="Core.Liminal.Field route stamp has no versioned fixed-mouth route";return false;}
|
||||
const auto routePoint=[&](const ResolvedLayoutAnchorV2& anchor)
|
||||
{
|
||||
std::array<std::int64_t,3> q={anchor.values[0],anchor.values[1],anchor.values[2]};
|
||||
return qToMicrometres(q);
|
||||
};
|
||||
const std::int64_t halfThickness=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(variant->values[5])*coreMicrometres
|
||||
/WorldAssemblySubvoxelsPerVoxel));
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
{
|
||||
const auto first=routePoint(*points.front());
|
||||
const auto last=routePoint(*points.back());
|
||||
if(stamp.sweepPathMicrometres.size()<2
|
||||
||stamp.sweepPathMicrometres.front()[0]!=first[0]
|
||||
||stamp.sweepPathMicrometres.front()[1]!=first[1]
|
||||
||stamp.sweepPathMicrometres.front()[2]!=first[2]-halfThickness
|
||||
||stamp.sweepPathMicrometres.back()[0]!=last[0]
|
||||
||stamp.sweepPathMicrometres.back()[1]!=last[1]
|
||||
||stamp.sweepPathMicrometres.back()[2]!=last[2]-halfThickness)
|
||||
{if(reason)*reason="Core.Liminal.Field deck does not terminate at its fixed passage mouths";return false;}
|
||||
return true;
|
||||
}
|
||||
const auto deck=std::find_if(layout.stamps.begin(),layout.stamps.end(),
|
||||
[&](const auto& existing){return existing.strateIndex==stamp.strateIndex
|
||||
&&existing.stableId=="core/liminal-field/route/deck/000"
|
||||
&&existing.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment;});
|
||||
if(deck==layout.stamps.end()||deck->sweepPathMicrometres.size()<2)
|
||||
{if(reason)*reason="Core.Liminal.Field support has no previously resolved route deck";return false;}
|
||||
double distance=std::numeric_limits<double>::infinity();
|
||||
for(std::size_t i=1;i<deck->sweepPathMicrometres.size();++i)
|
||||
{
|
||||
const auto& a=deck->sweepPathMicrometres[i-1];
|
||||
const auto& b=deck->sweepPathMicrometres[i];
|
||||
const double dx=static_cast<double>(b[0]-a[0]);
|
||||
const double dy=static_cast<double>(b[1]-a[1]);
|
||||
const double lengthSquared=dx*dx+dy*dy;
|
||||
const double t=lengthSquared>0.0?std::clamp(
|
||||
((stamp.positionMicrometres[0]-a[0])*dx
|
||||
+(stamp.positionMicrometres[1]-a[1])*dy)/lengthSquared,
|
||||
0.0,1.0):0.0;
|
||||
const double offsetX=stamp.positionMicrometres[0]-(a[0]+t*dx);
|
||||
const double offsetY=stamp.positionMicrometres[1]-(a[1]+t*dy);
|
||||
distance=std::min(distance,std::hypot(offsetX,offsetY));
|
||||
}
|
||||
const double corridorRadius=deck->halfExtentsMicrometres[1]
|
||||
+stamp.halfExtentsMicrometres[0]+RouteClearanceMicrometres;
|
||||
if(distance>corridorRadius)
|
||||
{if(reason)*reason="Core.Liminal.Field route stamp is outside its versioned route corridor";return false;}
|
||||
return true;
|
||||
}
|
||||
for(const auto& passage:layout.passages)
|
||||
{
|
||||
if(passage.upperStrateIndex!=stamp.strateIndex
|
||||
@@ -548,7 +613,28 @@ namespace
|
||||
for(std::size_t v=0;v<anchor.values.size();++v){if(v)out<<',';out<<anchor.values[v];}
|
||||
out<<"]}";
|
||||
}
|
||||
out<<"]}";
|
||||
out<<']';
|
||||
if(!x.landingPads.empty())
|
||||
{
|
||||
out<<",\"landing_pads\":[";
|
||||
for(std::size_t p=0;p<x.landingPads.size();++p)
|
||||
{
|
||||
if(p)out<<',';const auto& pad=x.landingPads[p];
|
||||
out<<"{\"stable_id\":"<<LayoutEscape(pad.stableId)
|
||||
<<",\"role\":"<<static_cast<std::uint32_t>(pad.role)
|
||||
<<",\"position_q\":["<<pad.positionSubvoxels[0]<<','
|
||||
<<pad.positionSubvoxels[1]<<','<<pad.positionSubvoxels[2]
|
||||
<<"],\"floor_q\":"<<pad.floorHeightSubvoxels
|
||||
<<",\"approach_q16\":["<<pad.approachDirectionQ16[0]<<','
|
||||
<<pad.approachDirectionQ16[1]<<']'
|
||||
<<",\"clearance_q\":"<<pad.clearanceSubvoxels
|
||||
<<",\"support_depth_q\":"<<pad.supportDepthSubvoxels
|
||||
<<",\"certified\":"
|
||||
<<(pad.certifiedWalkableWithSupport?"true":"false")<<'}';
|
||||
}
|
||||
out<<']';
|
||||
}
|
||||
out<<'}';
|
||||
}
|
||||
out<<"],\"passages\":[";
|
||||
for(std::size_t i=0;i<s.passages.size();++i)
|
||||
@@ -598,11 +684,35 @@ namespace
|
||||
<<",\"irregularity_wavelength_um\":"<<stamp.irregularityWavelengthMicrometres
|
||||
<<",\"irregularity_seed\":"<<stamp.irregularitySeed
|
||||
<<",\"yaw_quarter_turns\":"<<static_cast<unsigned>(stamp.yawQuarterTurns);
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
{
|
||||
out<<",\"sweep_endpoints_um\":[["
|
||||
<<stamp.sweepEndpointsMicrometres[0][0]<<','
|
||||
<<stamp.sweepEndpointsMicrometres[0][1]<<','
|
||||
<<stamp.sweepEndpointsMicrometres[0][2]<<"],["
|
||||
<<stamp.sweepEndpointsMicrometres[1][0]<<','
|
||||
<<stamp.sweepEndpointsMicrometres[1][1]<<','
|
||||
<<stamp.sweepEndpointsMicrometres[1][2]<<"]]";
|
||||
if(!stamp.sweepPathMicrometres.empty())
|
||||
{
|
||||
out<<",\"sweep_path_um\":[";
|
||||
for(std::size_t pointIndex=0;pointIndex<stamp.sweepPathMicrometres.size();
|
||||
++pointIndex)
|
||||
{
|
||||
if(pointIndex)out<<',';
|
||||
const auto& point=stamp.sweepPathMicrometres[pointIndex];
|
||||
out<<'['<<point[0]<<','<<point[1]<<','<<point[2]<<']';
|
||||
}
|
||||
out<<']';
|
||||
}
|
||||
}
|
||||
if(s.stampVocabularyVersion>=ResolvedLayoutStampVocabularyVersionV3)
|
||||
out<<",\"rounding_radius_um\":"<<stamp.roundingRadiusMicrometres
|
||||
<<",\"minimum_spacing_um\":"<<stamp.minimumSpacingMicrometres
|
||||
<<",\"attachment_policy\":"<<static_cast<unsigned>(stamp.attachmentPolicy)
|
||||
<<",\"floor_um\":"<<stamp.floorMicrometres;
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSteps)
|
||||
out<<",\"step_count\":"<<stamp.stepCount;
|
||||
out<<'}';
|
||||
}
|
||||
out<<']';
|
||||
@@ -870,6 +980,9 @@ bool ResolvedLayoutSnapshotV3::finalize(std::string& error)
|
||||
for(std::size_t i=0;i<strates.size();++i)
|
||||
{
|
||||
auto& x=strates[i];x.order=static_cast<std::int32_t>(i);
|
||||
std::stable_sort(x.landingPads.begin(),x.landingPads.end(),[](const auto& a,const auto& b)
|
||||
{return a.role!=b.role?static_cast<std::uint32_t>(a.role)
|
||||
<static_cast<std::uint32_t>(b.role):a.stableId<b.stableId;});
|
||||
if(x.stableId.empty())x.stableId=worldIdentity+"/strate/"+std::to_string(x.strateIndex);
|
||||
if(x.content.kind.empty())x.content.kind="content";
|
||||
if(x.ceiling.kind.empty())x.ceiling.kind="ceiling";
|
||||
@@ -930,9 +1043,40 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
const auto add=[&](std::string code,std::string detail)
|
||||
{issues.push_back({std::move(code),std::move(detail)});};
|
||||
const bool strict=mode==ResolvedLayoutValidationModeV3::strict;
|
||||
const std::int64_t playerCenterAboveFloorQ=static_cast<std::int64_t>(std::llround(
|
||||
1.13*WorldAssemblySubvoxelsPerVoxel*1000000.0/coreVoxelSizeMicrometres));
|
||||
for(std::size_t i=0;i<strates.size();++i)
|
||||
{
|
||||
const auto& s=strates[i];
|
||||
std::vector<std::string> landingPadIds;
|
||||
for(const auto& pad:s.landingPads)
|
||||
{
|
||||
if(pad.stableId.empty()
|
||||
||std::find(landingPadIds.begin(),landingPadIds.end(),pad.stableId)
|
||||
!=landingPadIds.end())
|
||||
add("landing_pad_identity",s.stableId+" has an empty or duplicate landing-pad ID");
|
||||
else landingPadIds.push_back(pad.stableId);
|
||||
const auto role=static_cast<std::uint32_t>(pad.role);
|
||||
const std::int64_t directionX=pad.approachDirectionQ16[0];
|
||||
const std::int64_t directionY=pad.approachDirectionQ16[1];
|
||||
const std::int64_t directionLengthSquared=directionX*directionX+directionY*directionY;
|
||||
constexpr std::int64_t UnitQ16Squared=1ll<<32;
|
||||
const bool unitDirection=directionLengthSquared>=UnitQ16Squared*98/100
|
||||
&&directionLengthSquared<=UnitQ16Squared*102/100;
|
||||
const std::int64_t floorMicrometres=pad.floorHeightSubvoxels
|
||||
*static_cast<std::int64_t>(coreVoxelSizeMicrometres)
|
||||
/WorldAssemblySubvoxelsPerVoxel;
|
||||
if(role>static_cast<std::uint32_t>(ResolvedLayoutLandingPadRoleV1::departure)
|
||||
||pad.positionSubvoxels[2]
|
||||
!=pad.floorHeightSubvoxels+playerCenterAboveFloorQ
|
||||
||!s.content.presentInCurrentField
|
||||
||floorMicrometres<s.content.bottomMicrometres
|
||||
||floorMicrometres>=s.content.topMicrometres
|
||||
||!unitDirection||pad.clearanceSubvoxels<=0
|
||||
||pad.supportDepthSubvoxels<=0
|
||||
||!pad.certifiedWalkableWithSupport)
|
||||
add("landing_pad_contract",s.stableId+" publishes an uncertified or malformed landing pad");
|
||||
}
|
||||
const std::int64_t effectiveTransitionWidth=LayoutTransitionLimit(s);
|
||||
if(s.interiorTopMicrometres<=s.interiorBottomMicrometres)
|
||||
add("invalid_interior",s.stableId+" has non-positive content height");
|
||||
@@ -1134,8 +1278,12 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
}
|
||||
else if(stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV1
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV2
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV3)
|
||||
add("stamp_vocabulary_version","resolved stamps require supported vocabulary version 1, 2 or 3");
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV3
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV4
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV5
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV6
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV7)
|
||||
add("stamp_vocabulary_version","resolved stamps require supported vocabulary version 1 through 7");
|
||||
std::vector<std::string> stampIds;
|
||||
for(const auto& stamp:stamps)
|
||||
{
|
||||
@@ -1145,8 +1293,20 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
else stampIds.push_back(stamp.stableId);
|
||||
const bool motif=LayoutStampIsMotif(stamp.kind);
|
||||
if(!LayoutStampKindValid(stamp.kind)||stamp.yawQuarterTurns>3
|
||||
||(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&stampVocabularyVersion<ResolvedLayoutStampVocabularyVersionV4)
|
||||
||((stamp.kind==ResolvedSdfStampKindV1::fillBox
|
||||
||stamp.kind==ResolvedSdfStampKindV1::carveBox
|
||||
||stamp.kind==ResolvedSdfStampKindV1::fillSteps)
|
||||
&&stampVocabularyVersion<ResolvedLayoutStampVocabularyVersionV7)
|
||||
||(motif&&stampVocabularyVersion<ResolvedLayoutStampVocabularyVersionV3))
|
||||
add("stamp_kind","stamp has an unsupported kind or orientation");
|
||||
if((stamp.kind==ResolvedSdfStampKindV1::fillSteps
|
||||
&&(stampVocabularyVersion<ResolvedLayoutStampVocabularyVersionV7
|
||||
||stamp.stepCount<2||stamp.stepCount>128
|
||||
||stamp.roundingRadiusMicrometres!=0))
|
||||
||(stamp.kind!=ResolvedSdfStampKindV1::fillSteps&&stamp.stepCount!=0))
|
||||
add("stamp_steps",stamp.stableId+" has an invalid step count or rounding mode");
|
||||
bool validExtents=true;
|
||||
for(const auto extent:stamp.halfExtentsMicrometres)
|
||||
validExtents=validExtents&&extent>=500000&&extent<=100000000;
|
||||
@@ -1159,6 +1319,49 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
||(stamp.irregularityAmplitudeMicrometres>0
|
||||
&&stamp.irregularityWavelengthMicrometres<=0))
|
||||
add("stamp_parameters",stamp.stableId+" has invalid or unbounded geometry parameters");
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
{
|
||||
double lengthSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const auto a=stamp.sweepEndpointsMicrometres[0][axis];
|
||||
const auto b=stamp.sweepEndpointsMicrometres[1][axis];
|
||||
if(a<-1000000000000ll||a>1000000000000ll
|
||||
||b<-1000000000000ll||b>1000000000000ll)
|
||||
add("stamp_sweep",stamp.stableId+" has an unbounded endpoint");
|
||||
const double delta=static_cast<double>(stamp.sweepEndpointsMicrometres[1][axis])
|
||||
-stamp.sweepEndpointsMicrometres[0][axis];
|
||||
lengthSquared+=delta*delta;
|
||||
}
|
||||
if(stamp.sweepPathMicrometres.empty()&&(!(lengthSquared>0.0)
|
||||
||std::sqrt(lengthSquared)>2.0*stamp.halfExtentsMicrometres[0]+1.0))
|
||||
add("stamp_sweep",stamp.stableId+" has invalid endpoints or longitudinal extent");
|
||||
if(!stamp.sweepPathMicrometres.empty())
|
||||
{
|
||||
bool validPath=stampVocabularyVersion>=ResolvedLayoutStampVocabularyVersionV5
|
||||
&&stamp.sweepPathMicrometres.size()>=2
|
||||
&&stamp.sweepPathMicrometres.size()<=1025
|
||||
&&stamp.sweepPathMicrometres.front()==stamp.sweepEndpointsMicrometres[0]
|
||||
&&stamp.sweepPathMicrometres.back()==stamp.sweepEndpointsMicrometres[1];
|
||||
for(std::size_t pointIndex=0;pointIndex<stamp.sweepPathMicrometres.size();
|
||||
++pointIndex)
|
||||
{
|
||||
const auto& point=stamp.sweepPathMicrometres[pointIndex];
|
||||
for(const auto coordinate:point)
|
||||
validPath=validPath&&coordinate>=-1000000000000ll
|
||||
&&coordinate<=1000000000000ll;
|
||||
if(pointIndex>0)
|
||||
{
|
||||
const auto& previous=stamp.sweepPathMicrometres[pointIndex-1];
|
||||
validPath=validPath&&previous!=point
|
||||
&&(stampVocabularyVersion>=ResolvedLayoutStampVocabularyVersionV6
|
||||
||previous[0]<point[0]);
|
||||
}
|
||||
}
|
||||
if(!validPath)add("stamp_sweep_path",stamp.stableId
|
||||
+" has an invalid swept-deck path for its vocabulary version");
|
||||
}
|
||||
}
|
||||
if(motif)
|
||||
{
|
||||
const auto minimumExtent=*std::min_element(stamp.halfExtentsMicrometres.begin(),
|
||||
@@ -1186,6 +1389,52 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
const std::int64_t pad=motif
|
||||
?(stamp.smoothWidthMicrometres+5)/6+stamp.irregularityAmplitudeMicrometres
|
||||
:stamp.smoothWidthMicrometres+stamp.irregularityAmplitudeMicrometres;
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&stamp.sweepPathMicrometres.empty())
|
||||
{
|
||||
double radiusSquared=0.0;
|
||||
for(const auto extent:stamp.halfExtentsMicrometres)
|
||||
{
|
||||
const double expanded=static_cast<double>(extent+pad);
|
||||
radiusSquared+=expanded*expanded;
|
||||
}
|
||||
const auto radius=static_cast<std::int64_t>(std::ceil(std::sqrt(radiusSquared)));
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const auto minimum=std::min(stamp.sweepEndpointsMicrometres[0][axis],
|
||||
stamp.sweepEndpointsMicrometres[1][axis]);
|
||||
const auto maximum=std::max(stamp.sweepEndpointsMicrometres[0][axis],
|
||||
stamp.sweepEndpointsMicrometres[1][axis]);
|
||||
if(stamp.boundsMinimumMicrometres[axis]!=minimum-radius
|
||||
||stamp.boundsMaximumMicrometres[axis]!=maximum+radius)
|
||||
add("stamp_bounds",stamp.stableId+" conservative sweep bounds do not match its parameters");
|
||||
}
|
||||
}
|
||||
else if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
{
|
||||
double radiusSquared=0.0;
|
||||
for(std::size_t axis=1;axis<3;++axis)
|
||||
{
|
||||
const double expanded=static_cast<double>(stamp.halfExtentsMicrometres[axis]+pad);
|
||||
radiusSquared+=expanded*expanded;
|
||||
}
|
||||
const auto radius=static_cast<std::int64_t>(std::ceil(std::sqrt(radiusSquared)));
|
||||
std::array<std::int64_t,3> minimum=stamp.sweepPathMicrometres.front();
|
||||
std::array<std::int64_t,3> maximum=minimum;
|
||||
for(const auto& point:stamp.sweepPathMicrometres)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
minimum[axis]=std::min(minimum[axis],point[axis]);
|
||||
maximum[axis]=std::max(maximum[axis],point[axis]);
|
||||
}
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(stamp.boundsMinimumMicrometres[axis]!=minimum[axis]-radius
|
||||
||stamp.boundsMaximumMicrometres[axis]!=maximum[axis]+radius)
|
||||
add("stamp_bounds",stamp.stableId
|
||||
+" conservative continuous-sweep bounds do not match its path");
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::array<std::int64_t,3> extents={
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad,
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad,
|
||||
@@ -1196,6 +1445,7 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
||stamp.boundsMaximumMicrometres[axis]
|
||||
!=stamp.positionMicrometres[axis]+extents[axis])
|
||||
add("stamp_bounds",stamp.stableId+" conservative bounds do not match its parameters");
|
||||
}
|
||||
std::string safetyReason;
|
||||
if(!LayoutStampSafe(*this,stamp,&safetyReason))
|
||||
add("stamp_placement",stamp.stableId+" is unsafe: "+safetyReason);
|
||||
@@ -1461,6 +1711,22 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
for(const auto& a:v.Get("anchors").AsArray())
|
||||
{ResolvedLayoutAnchorV2 p;p.kind=LayoutString(a,"kind");p.stableId=LayoutString(a,"stable_id");p.units=LayoutString(a,"units");
|
||||
for(const auto& value:a.Get("values").AsArray())p.values.push_back(value.Integer());x.anchors.push_back(std::move(p));}
|
||||
for(const auto& value:v.Get("landing_pads").AsArray())
|
||||
{
|
||||
ResolvedLayoutLandingPadV1 pad;pad.stableId=LayoutString(value,"stable_id");
|
||||
pad.role=static_cast<ResolvedLayoutLandingPadRoleV1>(LayoutInt(value,"role"));
|
||||
const auto& position=value.Get("position_q").AsArray();
|
||||
for(std::size_t axis=0;axis<3&&axis<position.size();++axis)
|
||||
pad.positionSubvoxels[axis]=position[axis].Integer();
|
||||
pad.floorHeightSubvoxels=LayoutInt(value,"floor_q");
|
||||
const auto& approach=value.Get("approach_q16").AsArray();
|
||||
for(std::size_t axis=0;axis<2&&axis<approach.size();++axis)
|
||||
pad.approachDirectionQ16[axis]=static_cast<std::int32_t>(approach[axis].Integer());
|
||||
pad.clearanceSubvoxels=LayoutInt(value,"clearance_q");
|
||||
pad.supportDepthSubvoxels=LayoutInt(value,"support_depth_q");
|
||||
pad.certifiedWalkableWithSupport=LayoutBool(value,"certified");
|
||||
x.landingPads.push_back(std::move(pad));
|
||||
}
|
||||
s.strates.push_back(std::move(x));
|
||||
}
|
||||
for(const auto& v:root.Get("passages").AsArray())
|
||||
@@ -1497,6 +1763,21 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
target[axis]=values[axis].Integer();
|
||||
};
|
||||
readTriplet("position_um",stamp.positionMicrometres);
|
||||
const auto& sweepEndpoints=v.Get("sweep_endpoints_um").AsArray();
|
||||
for(std::size_t endpoint=0;endpoint<2&&endpoint<sweepEndpoints.size();++endpoint)
|
||||
{
|
||||
const auto& values=sweepEndpoints[endpoint].AsArray();
|
||||
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||
stamp.sweepEndpointsMicrometres[endpoint][axis]=values[axis].Integer();
|
||||
}
|
||||
for(const auto& value:v.Get("sweep_path_um").AsArray())
|
||||
{
|
||||
std::array<std::int64_t,3> point{};
|
||||
const auto& values=value.AsArray();
|
||||
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||
point[axis]=values[axis].Integer();
|
||||
stamp.sweepPathMicrometres.push_back(point);
|
||||
}
|
||||
readTriplet("bounds_min_um",stamp.boundsMinimumMicrometres);
|
||||
readTriplet("bounds_max_um",stamp.boundsMaximumMicrometres);
|
||||
readTriplet("half_extents_um",stamp.halfExtentsMicrometres);
|
||||
@@ -1509,6 +1790,7 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
stamp.floorMicrometres=LayoutInt(v,"floor_um");
|
||||
stamp.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
|
||||
stamp.yawQuarterTurns=static_cast<std::uint8_t>(LayoutInt(v,"yaw_quarter_turns"));
|
||||
stamp.stepCount=static_cast<std::uint16_t>(LayoutInt(v,"step_count"));
|
||||
s.stamps.push_back(std::move(stamp));
|
||||
}
|
||||
for(const auto& v:root.Get("cavern_rooms").AsArray())
|
||||
@@ -1619,7 +1901,31 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
std::sort(ordered.begin(),ordered.end(),[](const auto& a,const auto& b)
|
||||
{return a.stableId<b.stableId;});
|
||||
ResolvedLayoutSnapshotV3 candidate=layout;
|
||||
candidate.stampVocabularyVersion=ResolvedLayoutStampVocabularyVersionV1;
|
||||
const bool hasContinuousSweptDeck=std::any_of(ordered.begin(),ordered.end(),[](const auto& request)
|
||||
{return request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&!request.sweepPathMicrometres.empty();});
|
||||
const bool hasNonMonotoneSweptDeck=std::any_of(ordered.begin(),ordered.end(),[](const auto& request)
|
||||
{
|
||||
if(request.kind!=ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
||request.sweepPathMicrometres.empty())return false;
|
||||
for(std::size_t i=1;i<request.sweepPathMicrometres.size();++i)
|
||||
if(request.sweepPathMicrometres[i-1][0]>=request.sweepPathMicrometres[i][0])
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
const bool hasSweptDeck=std::any_of(ordered.begin(),ordered.end(),[](const auto& request)
|
||||
{return request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment;});
|
||||
const bool hasVocabulary7Stamp=std::any_of(ordered.begin(),ordered.end(),[](const auto& request)
|
||||
{return request.kind==ResolvedSdfStampKindV1::fillBox
|
||||
||request.kind==ResolvedSdfStampKindV1::carveBox
|
||||
||request.kind==ResolvedSdfStampKindV1::fillSteps;});
|
||||
candidate.stampVocabularyVersion=hasVocabulary7Stamp
|
||||
?ResolvedLayoutStampVocabularyVersionV7
|
||||
:hasContinuousSweptDeck
|
||||
?(hasNonMonotoneSweptDeck?ResolvedLayoutStampVocabularyVersionV6
|
||||
:ResolvedLayoutStampVocabularyVersionV5)
|
||||
:hasSweptDeck?ResolvedLayoutStampVocabularyVersionV4
|
||||
:ResolvedLayoutStampVocabularyVersionV1;
|
||||
candidate.stamps.clear();candidate.stamps.reserve(ordered.size());
|
||||
for(std::size_t requestIndex=0;requestIndex<ordered.size();++requestIndex)
|
||||
{
|
||||
@@ -1628,8 +1934,14 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
&&ordered[requestIndex-1].stableId==request.stableId))
|
||||
{error="stamp requests require non-empty unique stable IDs";return false;}
|
||||
if(!LayoutStampKindValid(request.kind)||request.yawQuarterTurns>3
|
||||
||(request.kind==ResolvedSdfStampKindV1::fillSteps
|
||||
?(request.stepCount<2||request.stepCount>128
|
||||
||request.roundingRadiusMicrometres!=0)
|
||||
:request.stepCount!=0)
|
||||
||request.placementRadiusMicrometres<0
|
||||
||request.placementRadiusMicrometres>100000000
|
||||
||(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&request.placementRadiusMicrometres!=0)
|
||||
||request.smoothWidthMicrometres<0||request.smoothWidthMicrometres>10000000
|
||||
||request.irregularityAmplitudeMicrometres<0
|
||||
||request.irregularityAmplitudeMicrometres>10000000
|
||||
@@ -1648,6 +1960,43 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
for(const auto coordinate:request.anchorPositionMicrometres)
|
||||
if(coordinate<-1000000000000ll||coordinate>1000000000000ll)
|
||||
{error="stamp request "+request.stableId+" has an unbounded anchor";return false;}
|
||||
if(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&request.sweepPathMicrometres.empty())
|
||||
{
|
||||
double lengthSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const auto a=request.sweepEndpointsMicrometres[0][axis];
|
||||
const auto b=request.sweepEndpointsMicrometres[1][axis];
|
||||
if(a<-1000000000000ll||a>1000000000000ll
|
||||
||b<-1000000000000ll||b>1000000000000ll)
|
||||
{error="stamp request "+request.stableId+" has an unbounded sweep endpoint";return false;}
|
||||
const double delta=static_cast<double>(b)-a;
|
||||
lengthSquared+=delta*delta;
|
||||
}
|
||||
if(!(lengthSquared>0.0)
|
||||
||std::sqrt(lengthSquared)>2.0*request.halfExtentsMicrometres[0]+1.0)
|
||||
{error="stamp request "+request.stableId+" has invalid swept-segment extents";return false;}
|
||||
}
|
||||
if(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&!request.sweepPathMicrometres.empty())
|
||||
{
|
||||
if(request.sweepPathMicrometres.size()<2
|
||||
||request.sweepPathMicrometres.size()>1025
|
||||
||request.sweepPathMicrometres.front()!=request.sweepEndpointsMicrometres[0]
|
||||
||request.sweepPathMicrometres.back()!=request.sweepEndpointsMicrometres[1])
|
||||
{error="stamp request "+request.stableId+" has an invalid continuous deck path";return false;}
|
||||
for(std::size_t pointIndex=0;pointIndex<request.sweepPathMicrometres.size();
|
||||
++pointIndex)
|
||||
{
|
||||
const auto& point=request.sweepPathMicrometres[pointIndex];
|
||||
for(const auto coordinate:point)
|
||||
if(coordinate<-1000000000000ll||coordinate>1000000000000ll)
|
||||
{error="stamp request "+request.stableId+" has an unbounded path point";return false;}
|
||||
if(pointIndex>0&&request.sweepPathMicrometres[pointIndex-1]==point)
|
||||
{error="stamp request "+request.stableId+" path contains a zero-length segment";return false;}
|
||||
}
|
||||
}
|
||||
|
||||
// The shared JSON value type stores numbers as IEEE-754 doubles; keep the
|
||||
// serialized seed in its exact integer range so the canonical round trip holds.
|
||||
@@ -1662,14 +2011,18 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
return static_cast<std::int64_t>(bits%span)-radius;
|
||||
};
|
||||
bool placed=false;
|
||||
std::string lastPlacementFailure;
|
||||
for(std::uint32_t attempt=0;attempt<(request.placementRadiusMicrometres==0?1u:512u);++attempt)
|
||||
{
|
||||
ResolvedLayoutStampV1 stamp;stamp.stableId=request.stableId;
|
||||
stamp.strateIndex=request.strateIndex;stamp.kind=request.kind;
|
||||
stamp.sweepEndpointsMicrometres=request.sweepEndpointsMicrometres;
|
||||
stamp.sweepPathMicrometres=request.sweepPathMicrometres;
|
||||
stamp.halfExtentsMicrometres=request.halfExtentsMicrometres;
|
||||
stamp.smoothWidthMicrometres=request.smoothWidthMicrometres;
|
||||
stamp.irregularityAmplitudeMicrometres=request.irregularityAmplitudeMicrometres;
|
||||
stamp.irregularityWavelengthMicrometres=request.irregularityWavelengthMicrometres;
|
||||
stamp.stepCount=request.stepCount;
|
||||
stamp.irregularitySeed=baseSeed;stamp.yawQuarterTurns=request.yawQuarterTurns;
|
||||
std::array<std::int64_t,3> offset{};
|
||||
if(request.placementRadiusMicrometres>0)
|
||||
@@ -1687,6 +2040,52 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
stamp.positionMicrometres[axis]=request.anchorPositionMicrometres[axis]+offset[axis];
|
||||
const std::int64_t pad=stamp.smoothWidthMicrometres
|
||||
+stamp.irregularityAmplitudeMicrometres;
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&stamp.sweepPathMicrometres.empty())
|
||||
{
|
||||
double radiusSquared=0.0;
|
||||
for(const auto extent:stamp.halfExtentsMicrometres)
|
||||
{
|
||||
const double expanded=static_cast<double>(extent+pad);
|
||||
radiusSquared+=expanded*expanded;
|
||||
}
|
||||
const std::int64_t radius=static_cast<std::int64_t>(std::ceil(std::sqrt(radiusSquared)));
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const auto minimum=std::min(stamp.sweepEndpointsMicrometres[0][axis],
|
||||
stamp.sweepEndpointsMicrometres[1][axis]);
|
||||
const auto maximum=std::max(stamp.sweepEndpointsMicrometres[0][axis],
|
||||
stamp.sweepEndpointsMicrometres[1][axis]);
|
||||
stamp.boundsMinimumMicrometres[axis]=minimum-radius;
|
||||
stamp.boundsMaximumMicrometres[axis]=maximum+radius;
|
||||
}
|
||||
}
|
||||
else if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
{
|
||||
double radiusSquared=0.0;
|
||||
for(std::size_t axis=1;axis<3;++axis)
|
||||
{
|
||||
const double expanded=static_cast<double>(stamp.halfExtentsMicrometres[axis]+pad);
|
||||
radiusSquared+=expanded*expanded;
|
||||
}
|
||||
const std::int64_t radius=static_cast<std::int64_t>(
|
||||
std::ceil(std::sqrt(radiusSquared)));
|
||||
std::array<std::int64_t,3> minimum=stamp.sweepPathMicrometres.front();
|
||||
std::array<std::int64_t,3> maximum=minimum;
|
||||
for(const auto& point:stamp.sweepPathMicrometres)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
minimum[axis]=std::min(minimum[axis],point[axis]);
|
||||
maximum[axis]=std::max(maximum[axis],point[axis]);
|
||||
}
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
stamp.boundsMinimumMicrometres[axis]=minimum[axis]-radius;
|
||||
stamp.boundsMaximumMicrometres[axis]=maximum[axis]+radius;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::array<std::int64_t,3> extents={
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad,
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad,
|
||||
@@ -1696,11 +2095,15 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
stamp.boundsMinimumMicrometres[axis]=stamp.positionMicrometres[axis]-extents[axis];
|
||||
stamp.boundsMaximumMicrometres[axis]=stamp.positionMicrometres[axis]+extents[axis];
|
||||
}
|
||||
if(!LayoutStampSafe(candidate,stamp))continue;
|
||||
}
|
||||
std::string safetyReason;
|
||||
if(!LayoutStampSafe(candidate,stamp,&safetyReason))
|
||||
{lastPlacementFailure=std::move(safetyReason);continue;}
|
||||
candidate.stamps.push_back(std::move(stamp));placed=true;break;
|
||||
}
|
||||
if(!placed)
|
||||
{error="stamp request "+request.stableId+" has no deterministic passage-safe content placement";return false;}
|
||||
{error="stamp request "+request.stableId+" has no deterministic passage-safe content placement"
|
||||
+(lastPlacementFailure.empty()?std::string{}:": "+lastPlacementFailure);return false;}
|
||||
}
|
||||
if(!candidate.finalize(error))return false;
|
||||
std::vector<ResolvedLayoutIssueV2> issues;std::string strictError;
|
||||
@@ -2259,6 +2662,17 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
{return c.upperStrateIndex==strate;});
|
||||
return it==configs.end()?nullptr:&*it;
|
||||
};
|
||||
const auto landingPadFor=[](const ResolvedLayoutStratumV2& stratum,
|
||||
ResolvedLayoutLandingPadRoleV1 role,std::uint32_t ordinal)
|
||||
->const ResolvedLayoutLandingPadV1*
|
||||
{
|
||||
std::vector<const ResolvedLayoutLandingPadV1*> candidates;
|
||||
for(const auto& pad:stratum.landingPads)
|
||||
if(pad.role==role)candidates.push_back(&pad);
|
||||
std::sort(candidates.begin(),candidates.end(),[](const auto* a,const auto* b)
|
||||
{return a->stableId<b->stableId;});
|
||||
return ordinal<candidates.size()?candidates[ordinal]:nullptr;
|
||||
};
|
||||
const auto routeAnchor=[](const ResolvedLayoutStratumV2& s,bool departure,
|
||||
std::array<std::int64_t,3>& out)
|
||||
{
|
||||
@@ -2355,9 +2769,37 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
passage.upperStrateIndex=upper.strateIndex;passage.lowerStrateIndex=lower.strateIndex;
|
||||
passage.placementRadiusSubvoxels=placementRadius;
|
||||
passage.mouthRadiusSubvoxels=mouthRadius;passage.midRadiusSubvoxels=midRadius;
|
||||
const auto* upperDeparturePad=landingPadFor(upper,
|
||||
ResolvedLayoutLandingPadRoleV1::departure,connection);
|
||||
const auto* lowerArrivalPad=landingPadFor(lower,
|
||||
ResolvedLayoutLandingPadRoleV1::arrival,connection);
|
||||
const auto validateLandingPad=[&](const ResolvedLayoutLandingPadV1* pad,
|
||||
std::string_view side)
|
||||
{
|
||||
if(!pad)return true;
|
||||
const std::int64_t requiredClearance=mouthRadius
|
||||
+static_cast<std::int64_t>(std::llround(0.34*qPerMetre));
|
||||
if(!pad->certifiedWalkableWithSupport||pad->clearanceSubvoxels<requiredClearance
|
||||
||pad->supportDepthSubvoxels<static_cast<std::int64_t>(std::llround(0.50*qPerMetre))
|
||||
||!radialWithin(pad->positionSubvoxels,placementRadius)
|
||||
||pad->positionSubvoxels[2]
|
||||
!=pad->floorHeightSubvoxels+playerCenterAboveFloorQ)
|
||||
{
|
||||
error=passage.stableId+" "+std::string(side)
|
||||
+" landing pad is uncertified, unsupported, too tight, or outside PassageConfig.DistanceMax";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if(!validateLandingPad(upperDeparturePad,"departure")
|
||||
||!validateLandingPad(lowerArrivalPad,"arrival"))return false;
|
||||
std::array<std::int64_t,3> upperMouth{},lowerMouth{};
|
||||
const bool hasUpperRoute=connection==0&&routeAnchor(upper,true,upperMouth);
|
||||
const bool hasLowerRoute=connection==0&&routeAnchor(lower,false,lowerMouth);
|
||||
const bool hasUpperRoute=upperDeparturePad
|
||||
?(upperMouth=upperDeparturePad->positionSubvoxels,true)
|
||||
:connection==0&&routeAnchor(upper,true,upperMouth);
|
||||
const bool hasLowerRoute=lowerArrivalPad
|
||||
?(lowerMouth=lowerArrivalPad->positionSubvoxels,true)
|
||||
:connection==0&&routeAnchor(lower,false,lowerMouth);
|
||||
const bool surfaceGroundDeparture=hasUpperRoute&&upperIsSurfaceWorld;
|
||||
const ResolvedLayoutAnchorV2* passageAnchor=nullptr;
|
||||
if(connection==0)
|
||||
@@ -2406,13 +2848,17 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
|
||||
constexpr std::int64_t LandingExtensionQ=8;
|
||||
std::array<std::int64_t,2> upperTangent{},lowerTangent{};
|
||||
const bool hasUpperTangent=hasUpperRoute
|
||||
&&routeTangent(upper,true,upperTangent);
|
||||
const bool hasUpperTangent=upperDeparturePad
|
||||
?(upperTangent={upperDeparturePad->approachDirectionQ16[0],
|
||||
upperDeparturePad->approachDirectionQ16[1]},true)
|
||||
:hasUpperRoute&&routeTangent(upper,true,upperTangent);
|
||||
const bool hasPassageArrival=std::any_of(lower.anchors.begin(),lower.anchors.end(),
|
||||
[](const auto& anchor){return anchor.kind=="passage_arrival"
|
||||
&&anchor.values.size()>=3;});
|
||||
const bool hasLowerTangent=hasLowerRoute&&!hasPassageArrival
|
||||
&&routeTangent(lower,false,lowerTangent);
|
||||
const bool hasLowerTangent=lowerArrivalPad
|
||||
?(lowerTangent={lowerArrivalPad->approachDirectionQ16[0],
|
||||
lowerArrivalPad->approachDirectionQ16[1]},true)
|
||||
:hasLowerRoute&&!hasPassageArrival&&routeTangent(lower,false,lowerTangent);
|
||||
const bool profiledSurfaceJoin=hasLowerTangent
|
||||
&&std::any_of(lower.anchors.begin(),lower.anchors.end(),
|
||||
[](const auto& anchor){return anchor.kind=="heightfield_profile"
|
||||
|
||||
@@ -1523,6 +1523,8 @@ public:
|
||||
&nativeLatticeFloorSupport);
|
||||
}
|
||||
const auto* bound=FindBoundStratumForManagerZ(Z);
|
||||
const bool liminalRouteComposition=bound&&bound->Resolved
|
||||
&&bound->Resolved->recipeId==VoxelForgeCore::CoreLiminalFieldRecipeIdV1;
|
||||
const bool nativeLattice=bound&&bound->Resolved
|
||||
&&VoxelForgeCore::IsNativeLatticeFieldStratum(*bound->Resolved)
|
||||
&&bound->Resolved->passageCapabilities.hasNativeLatticeField;
|
||||
@@ -1546,7 +1548,9 @@ public:
|
||||
VoxelForgeCore::Linux77Profiler::FScopedPhase l77StampPhase(
|
||||
VoxelForgeCore::Linux77Profiler::EPhase::Stamps);
|
||||
FScopedLegacyFieldTermTimer TermTimer(ELegacyFieldTerm::ComposerStamps);
|
||||
stamped=ApplyResolvedSdfStamps(composed,X,Y,Z);
|
||||
stamped=ApplyResolvedSdfStamps(composed,X,Y,Z,
|
||||
liminalRouteComposition?std::string_view("core/liminal-field/route/")
|
||||
:std::string_view{});
|
||||
}
|
||||
static const bool bL75TraceTnComposition=[]
|
||||
{
|
||||
@@ -1598,6 +1602,12 @@ public:
|
||||
ELegacyFieldTerm::ComposerPassage);
|
||||
final=ApplyResolvedStratePassageCarves(final,X,Y,Z);
|
||||
}
|
||||
// A liminal route is authored to terminate at fixed passage mouths. The
|
||||
// passage carve owns the surrounding bore, while the deck/support stamps
|
||||
// restore its walkable floor so the composed route crosses the interface.
|
||||
if(liminalRouteComposition)
|
||||
final=ApplyResolvedSdfStamps(final,X,Y,Z,{},
|
||||
"core/liminal-field/route/");
|
||||
if(bTraceTnComposition)
|
||||
std::printf("[L75TNCompositionTrace] input=%.9g graph=%.9g boundary=%.9g passage=%.9g stamp=%.9g final=%.9g native=0\n",
|
||||
density,graphDensity,boundary,composed,stamped,final);
|
||||
@@ -3535,6 +3545,8 @@ private:
|
||||
{
|
||||
const VoxelForgeCore::ResolvedLayoutStampV1* record=nullptr;
|
||||
std::array<float,3> positionVoxels{},halfExtentsVoxels{};
|
||||
std::array<std::array<float,3>,2> sweepEndpointsVoxels{};
|
||||
std::vector<std::array<float,3>> sweepPathVoxels;
|
||||
float smoothWidthVoxels=0.0f;
|
||||
float irregularityAmplitudeVoxels=0.0f;
|
||||
float irregularityWavelengthVoxels=0.0f;
|
||||
@@ -3700,9 +3712,24 @@ private:
|
||||
StampSpatialBounds.push_back(bound);
|
||||
FStampRuntime runtime;runtime.record=&stamp;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
runtime.positionVoxels[axis]=static_cast<float>(
|
||||
static_cast<double>(stamp.positionMicrometres[axis])
|
||||
/Owner->CoreVoxelSizeMicrons);
|
||||
for(std::size_t endpoint=0;endpoint<2;++endpoint)
|
||||
runtime.sweepEndpointsVoxels[endpoint][axis]=static_cast<float>(
|
||||
static_cast<double>(stamp.sweepEndpointsMicrometres[endpoint][axis])
|
||||
/Owner->CoreVoxelSizeMicrons);
|
||||
}
|
||||
runtime.sweepPathVoxels.reserve(stamp.sweepPathMicrometres.size());
|
||||
for(const auto& pointMicrometres:stamp.sweepPathMicrometres)
|
||||
{
|
||||
std::array<float,3> point{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
point[axis]=static_cast<float>(static_cast<double>(pointMicrometres[axis])
|
||||
/Owner->CoreVoxelSizeMicrons);
|
||||
runtime.sweepPathVoxels.push_back(point);
|
||||
}
|
||||
const std::size_t xAxis=stamp.yawQuarterTurns%2?1:0;
|
||||
const std::size_t yAxis=stamp.yawQuarterTurns%2?0:1;
|
||||
runtime.halfExtentsVoxels={
|
||||
@@ -5162,6 +5189,20 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
static std::int32_t& CurrentStampSamplingStep() noexcept
|
||||
{
|
||||
static thread_local std::int32_t Step=0;
|
||||
return Step;
|
||||
}
|
||||
struct FScopedStampSamplingStep
|
||||
{
|
||||
explicit FScopedStampSamplingStep(std::int32_t Step)
|
||||
:Previous(CurrentStampSamplingStep())
|
||||
{CurrentStampSamplingStep()=std::max(0,Step);}
|
||||
~FScopedStampSamplingStep(){CurrentStampSamplingStep()=Previous;}
|
||||
std::int32_t Previous=0;
|
||||
};
|
||||
|
||||
bool GetNativeSurfaceHeightsAt(float X,float Y,int32 ChunkZ,
|
||||
float& OutGroundZ,float& OutCeilingZ) const
|
||||
{
|
||||
@@ -7116,6 +7157,134 @@ private:
|
||||
const float top=std::max(StampEllipsoidSdf(x,y,z,halfSize),-z);
|
||||
return SmoothMinDistance(lower,top,round*2.0f);
|
||||
}
|
||||
static float StampStepsSdf(float x,float y,float z,
|
||||
const std::array<float,3>& halfSize,std::uint16_t stepCount)
|
||||
{
|
||||
if(stepCount<2||stepCount>128)return std::numeric_limits<float>::infinity();
|
||||
const float treadWidth=(2.0f*halfSize[0])/stepCount;
|
||||
const float riserHeight=(2.0f*halfSize[2])/stepCount;
|
||||
float distance=std::numeric_limits<float>::infinity();
|
||||
for(std::uint16_t step=0;step<stepCount;++step)
|
||||
{
|
||||
const float stepHeight=(step+1)*riserHeight;
|
||||
const float centerX=-halfSize[0]+(step+0.5f)*treadWidth;
|
||||
const float centerZ=-halfSize[2]+stepHeight*0.5f;
|
||||
const std::array<float,3> extent={treadWidth*0.5f,halfSize[1],stepHeight*0.5f};
|
||||
distance=std::min(distance,StampRoundedBoxSdf(x-centerX,y,z-centerZ,extent,0.0f));
|
||||
}
|
||||
return distance;
|
||||
}
|
||||
static float StampContinuousSweptDeckSdf(const FStampRuntime& stamp,
|
||||
float x,float y,float z)
|
||||
{
|
||||
const auto& path=stamp.sweepPathVoxels;
|
||||
if(path.size()<2)return std::numeric_limits<float>::infinity();
|
||||
const float halfWidth=stamp.halfExtentsVoxels[1];
|
||||
const float halfThickness=stamp.halfExtentsVoxels[2];
|
||||
const float round=std::min(0.35f,std::min(halfWidth,halfThickness)*0.15f);
|
||||
const float searchMargin=halfWidth+halfThickness+2.0f;
|
||||
const bool increasingX=std::adjacent_find(path.begin(),path.end(),
|
||||
[](const auto& a,const auto& b){return a[0]>=b[0];})==path.end();
|
||||
const auto first=increasingX
|
||||
?std::lower_bound(path.begin(),path.end(),x-searchMargin,
|
||||
[](const auto& point,float value){return point[0]<value;})
|
||||
:path.begin();
|
||||
const auto last=increasingX
|
||||
?std::upper_bound(first,path.end(),x+searchMargin,
|
||||
[](float value,const auto& point){return value<point[0];})
|
||||
:path.end();
|
||||
const std::size_t firstIndex=static_cast<std::size_t>(first-path.begin());
|
||||
const std::size_t lastIndex=static_cast<std::size_t>(last-path.begin());
|
||||
const std::size_t segmentBegin=std::min(path.size()-2,
|
||||
firstIndex==0?std::size_t{0}:firstIndex-1);
|
||||
const std::size_t segmentEnd=std::min(path.size()-2,lastIndex);
|
||||
std::size_t nearestSegment=path.size();
|
||||
float nearestProjection=0.0f;
|
||||
float nearestDistanceSquared=std::numeric_limits<float>::infinity();
|
||||
for(std::size_t segment=segmentBegin;segment<=segmentEnd;++segment)
|
||||
{
|
||||
const auto& a=path[segment];const auto& b=path[segment+1];
|
||||
const float dx=b[0]-a[0],dy=b[1]-a[1];
|
||||
const float horizontalSquared=dx*dx+dy*dy;
|
||||
if(!(horizontalSquared>1.0e-10f))continue;
|
||||
const float projection=std::clamp(((x-a[0])*dx+(y-a[1])*dy)
|
||||
/horizontalSquared,0.0f,1.0f);
|
||||
const float nearestX=a[0]+projection*dx;
|
||||
const float nearestY=a[1]+projection*dy;
|
||||
const float offsetX=x-nearestX,offsetY=y-nearestY;
|
||||
const float distanceSquared=offsetX*offsetX+offsetY*offsetY;
|
||||
if(distanceSquared<nearestDistanceSquared
|
||||
||(distanceSquared==nearestDistanceSquared&&segment<nearestSegment))
|
||||
{
|
||||
nearestSegment=segment;
|
||||
nearestProjection=projection;
|
||||
nearestDistanceSquared=distanceSquared;
|
||||
}
|
||||
}
|
||||
if(nearestSegment>=path.size()-1)return std::numeric_limits<float>::infinity();
|
||||
const auto evalCenterline=[&](float parameter,std::array<float,3>& center,
|
||||
std::array<float,3>& tangent)
|
||||
{
|
||||
const auto& p0=path[nearestSegment];
|
||||
const auto& p1=path[nearestSegment+1];
|
||||
const auto& before=path[nearestSegment==0?nearestSegment:nearestSegment-1];
|
||||
const auto& after=path[nearestSegment+2<path.size()
|
||||
?nearestSegment+2:nearestSegment+1];
|
||||
const float t2=parameter*parameter,t3=t2*parameter;
|
||||
const float h00=2.0f*t3-3.0f*t2+1.0f;
|
||||
const float h10=t3-2.0f*t2+parameter;
|
||||
const float h01=-2.0f*t3+3.0f*t2;
|
||||
const float h11=t3-t2;
|
||||
const float d00=6.0f*t2-6.0f*parameter;
|
||||
const float d10=3.0f*t2-4.0f*parameter+1.0f;
|
||||
const float d01=-6.0f*t2+6.0f*parameter;
|
||||
const float d11=3.0f*t2-2.0f*parameter;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const float m0=nearestSegment==0?p1[axis]-p0[axis]
|
||||
:0.5f*(p1[axis]-before[axis]);
|
||||
const float m1=nearestSegment+2>=path.size()?p1[axis]-p0[axis]
|
||||
:0.5f*(after[axis]-p0[axis]);
|
||||
center[axis]=h00*p0[axis]+h10*m0+h01*p1[axis]+h11*m1;
|
||||
tangent[axis]=d00*p0[axis]+d10*m0+d01*p1[axis]+d11*m1;
|
||||
}
|
||||
};
|
||||
std::array<float,3> center{},tangent{};
|
||||
for(int refinement=0;refinement<2;++refinement)
|
||||
{
|
||||
evalCenterline(nearestProjection,center,tangent);
|
||||
const float derivativeSquared=tangent[0]*tangent[0]+tangent[1]*tangent[1];
|
||||
if(!(derivativeSquared>1.0e-10f))return std::numeric_limits<float>::infinity();
|
||||
const float offsetX=center[0]-x,offsetY=center[1]-y;
|
||||
nearestProjection=std::clamp(nearestProjection
|
||||
-(offsetX*tangent[0]+offsetY*tangent[1])/derivativeSquared,0.0f,1.0f);
|
||||
}
|
||||
evalCenterline(nearestProjection,center,tangent);
|
||||
const float tangentLength=std::sqrt(tangent[0]*tangent[0]
|
||||
+tangent[1]*tangent[1]+tangent[2]*tangent[2]);
|
||||
if(!(tangentLength>1.0e-8f))return std::numeric_limits<float>::infinity();
|
||||
for(float& component:tangent)component/=tangentLength;
|
||||
const float horizontalLength=std::sqrt(tangent[0]*tangent[0]
|
||||
+tangent[1]*tangent[1]);
|
||||
if(!(horizontalLength>1.0e-8f))return std::numeric_limits<float>::infinity();
|
||||
const std::array<float,3> side={-tangent[1]/horizontalLength,
|
||||
tangent[0]/horizontalLength,0.0f};
|
||||
const std::array<float,3> up={-tangent[2]*side[1],tangent[2]*side[0],
|
||||
tangent[0]*side[1]-tangent[1]*side[0]};
|
||||
const std::array<float,3> relative={x-center[0],y-center[1],z-center[2]};
|
||||
const auto dot=[&](const std::array<float,3>& axis)
|
||||
{return relative[0]*axis[0]+relative[1]*axis[1]+relative[2]*axis[2];};
|
||||
const float lateral=dot(side),vertical=dot(up);
|
||||
if((nearestSegment==0&&nearestProjection<=0.0f)
|
||||
||(nearestSegment+2==path.size()&&nearestProjection>=1.0f))
|
||||
return StampRoundedBoxSdf(dot(tangent),lateral,vertical,
|
||||
{halfThickness+round,halfWidth,halfThickness},round);
|
||||
const float qSide=std::abs(lateral)-halfWidth+round;
|
||||
const float qUp=std::abs(vertical)-halfThickness+round;
|
||||
const float outside=std::sqrt(std::max(qSide,0.0f)*std::max(qSide,0.0f)
|
||||
+std::max(qUp,0.0f)*std::max(qUp,0.0f));
|
||||
return outside+std::min(std::max(qSide,qUp),0.0f)-round;
|
||||
}
|
||||
float StampSdf(const FStampRuntime& stamp,float x,float y,float z) const
|
||||
{
|
||||
float localX=x-stamp.positionVoxels[0];
|
||||
@@ -7143,6 +7312,13 @@ private:
|
||||
case VoxelForgeCore::ResolvedSdfStampKindV1::fillPlatform:
|
||||
distance=StampRoundedBoxSdf(localX,localY,localZ,
|
||||
stamp.halfExtentsVoxels,stamp.roundingRadiusVoxels);break;
|
||||
case VoxelForgeCore::ResolvedSdfStampKindV1::fillBox:
|
||||
case VoxelForgeCore::ResolvedSdfStampKindV1::carveBox:
|
||||
distance=StampRoundedBoxSdf(localX,localY,localZ,
|
||||
stamp.halfExtentsVoxels,stamp.roundingRadiusVoxels);break;
|
||||
case VoxelForgeCore::ResolvedSdfStampKindV1::fillSteps:
|
||||
distance=StampStepsSdf(localX,localY,localZ,stamp.halfExtentsVoxels,
|
||||
stamp.record->stepCount);break;
|
||||
case VoxelForgeCore::ResolvedSdfStampKindV1::fillDais:
|
||||
if(ResolvedLayout&&ResolvedLayout->stampVocabularyVersion
|
||||
>=VoxelForgeCore::ResolvedLayoutStampVocabularyVersionV2)
|
||||
@@ -7152,6 +7328,40 @@ private:
|
||||
std::min(0.35f,std::min({stamp.halfExtentsVoxels[0],
|
||||
stamp.halfExtentsVoxels[1],stamp.halfExtentsVoxels[2]})*0.15f));
|
||||
break;
|
||||
case VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment:
|
||||
{
|
||||
if(!stamp.sweepPathVoxels.empty())
|
||||
{
|
||||
distance=StampContinuousSweptDeckSdf(stamp,x,y,z);
|
||||
break;
|
||||
}
|
||||
std::array<float,3> tangent{},side{},up{},delta{};
|
||||
float lengthSquared=0.0f,horizontalSquared=0.0f;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
delta[axis]=stamp.sweepEndpointsVoxels[1][axis]
|
||||
-stamp.sweepEndpointsVoxels[0][axis];
|
||||
lengthSquared+=delta[axis]*delta[axis];
|
||||
if(axis<2)horizontalSquared+=delta[axis]*delta[axis];
|
||||
}
|
||||
const float segmentLength=std::sqrt(lengthSquared);
|
||||
const float horizontalLength=std::sqrt(horizontalSquared);
|
||||
if(!(segmentLength>1.0e-5f&&horizontalLength>1.0e-5f))
|
||||
return std::numeric_limits<float>::infinity();
|
||||
for(std::size_t axis=0;axis<3;++axis)tangent[axis]=delta[axis]/segmentLength;
|
||||
side={-delta[1]/horizontalLength,delta[0]/horizontalLength,0.0f};
|
||||
up={-tangent[2]*side[1],tangent[2]*side[0],
|
||||
tangent[0]*side[1]-tangent[1]*side[0]};
|
||||
const std::array<float,3> offset={
|
||||
x-stamp.positionVoxels[0],y-stamp.positionVoxels[1],z-stamp.positionVoxels[2]};
|
||||
const auto dot=[&](const std::array<float,3>& axis)
|
||||
{return offset[0]*axis[0]+offset[1]*axis[1]+offset[2]*axis[2];};
|
||||
const float rounded=std::min(0.35f,
|
||||
std::min(stamp.halfExtentsVoxels[1],stamp.halfExtentsVoxels[2])*0.15f);
|
||||
distance=StampRoundedBoxSdf(dot(tangent),dot(side),dot(up),
|
||||
stamp.halfExtentsVoxels,rounded);
|
||||
break;
|
||||
}
|
||||
default:return std::numeric_limits<float>::infinity();
|
||||
}
|
||||
if(stamp.irregularityAmplitudeVoxels>0.0f)
|
||||
@@ -7163,6 +7373,15 @@ private:
|
||||
|
||||
static double StampAbsMinimum(double minimum,double maximum)
|
||||
{return minimum<=0.0&&maximum>=0.0?0.0:std::min(std::abs(minimum),std::abs(maximum));}
|
||||
static bool UsesCoarsePillarProxy(const FStampRuntime& stamp,std::int32_t sampleStep)
|
||||
{
|
||||
if(!stamp.record||stamp.record->kind
|
||||
!=VoxelForgeCore::ResolvedSdfStampKindV1::fillPillar
|
||||
||sampleStep<=0||stamp.irregularityAmplitudeVoxels>0.0f)return false;
|
||||
return static_cast<float>(sampleStep)>std::min(stamp.halfExtentsVoxels[0],
|
||||
stamp.halfExtentsVoxels[1])
|
||||
||static_cast<float>(sampleStep)>stamp.halfExtentsVoxels[2];
|
||||
}
|
||||
static double StampRoundedBoxSdfDouble(const std::array<double,3>& absolute,
|
||||
const std::array<double,3>& halfSize,double rounding)
|
||||
{
|
||||
@@ -7247,7 +7466,9 @@ private:
|
||||
std::nextafter(maximum+outwardPadding,
|
||||
std::numeric_limits<double>::infinity()),true};
|
||||
}
|
||||
float ApplyResolvedSdfStamps(float density,float X,float Y,float Z) const
|
||||
float ApplyResolvedSdfStamps(float density,float X,float Y,float Z,
|
||||
std::string_view excludedStableIdPrefix={},
|
||||
std::string_view includedStableIdPrefix={}) const
|
||||
{
|
||||
if(StampRuntime.empty()||!Owner||Owner->CoreVoxelSizeMicrons==0)return density;
|
||||
const auto* tileQueries=ActivePassageTileQueries;
|
||||
@@ -7264,12 +7485,18 @@ private:
|
||||
if(index>=StampRuntime.size())return;
|
||||
const auto& runtime=StampRuntime[index];const auto& record=*runtime.record;
|
||||
if(record.strateIndex!=bound->StrateIndex)return;
|
||||
if(!excludedStableIdPrefix.empty()
|
||||
&&record.stableId.starts_with(excludedStableIdPrefix))return;
|
||||
if(!includedStableIdPrefix.empty()
|
||||
&&!record.stableId.starts_with(includedStableIdPrefix))return;
|
||||
if(UsesCoarsePillarProxy(runtime,CurrentStampSamplingStep()))return;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(worldMicrometres[axis]<record.boundsMinimumMicrometres[axis]
|
||||
||worldMicrometres[axis]>record.boundsMaximumMicrometres[axis])return;
|
||||
const float distance=StampSdf(runtime,X,Y,Z);
|
||||
if(record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveEllipsoid
|
||||
||record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveArch)
|
||||
||record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveArch
|
||||
||record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveBox)
|
||||
composed=SmoothMaxDistance(composed,-distance,runtime.smoothWidthVoxels);
|
||||
else
|
||||
composed=SmoothMinDistance(composed,distance,runtime.smoothWidthVoxels);
|
||||
@@ -7281,6 +7508,154 @@ private:
|
||||
apply(static_cast<std::uint32_t>(index));
|
||||
return composed;
|
||||
}
|
||||
|
||||
public:
|
||||
std::uint32_t AppendCoarsePillarProxies(FVoxelMeshData& mesh,
|
||||
const FIntVector& tileOrigin,std::int32_t tileExtent,std::int32_t sampleStep) const
|
||||
{
|
||||
if(!Owner||Owner->CoreVoxelSizeMicrons==0||tileExtent<=0||sampleStep<=0
|
||||
||StampRuntime.empty())return 0;
|
||||
constexpr int32 sideCount=24;
|
||||
constexpr double TwoPi=6.2831853071795864769;
|
||||
const int32 originalVertexCount=mesh.Vertices.Num();
|
||||
const int32 originalSurfaceVertices=std::clamp(mesh.NumSurfaceVertices,0,
|
||||
originalVertexCount);
|
||||
const bool hasNormals=mesh.Normals.Num()==originalVertexCount;
|
||||
const bool hasUvs=mesh.UVs.Num()==originalVertexCount;
|
||||
const bool hasColors=mesh.Colors.Num()==originalVertexCount;
|
||||
const double voxelCentimetres=Owner->CoreVoxelSizeMicrons/10000.0;
|
||||
const std::int64_t tileMinimum[3]={tileOrigin.X,tileOrigin.Y,tileOrigin.Z};
|
||||
const std::int64_t tileMaximum[3]={static_cast<std::int64_t>(tileOrigin.X)+tileExtent,
|
||||
static_cast<std::int64_t>(tileOrigin.Y)+tileExtent,
|
||||
static_cast<std::int64_t>(tileOrigin.Z)+tileExtent};
|
||||
std::vector<int32> proxyIndices;
|
||||
std::uint32_t proxyCount=0;
|
||||
const auto addVertex=[&](const FVector& position,const FVector& normal,
|
||||
const FVector2f& uv)
|
||||
{
|
||||
const int32 index=mesh.Vertices.Add(position);
|
||||
if(hasNormals)mesh.Normals.Add(normal);
|
||||
if(hasUvs)mesh.UVs.Add(uv);
|
||||
if(hasColors)mesh.Colors.Add(FColor::White);
|
||||
return index;
|
||||
};
|
||||
for(const auto& stamp:StampRuntime)
|
||||
{
|
||||
if(!UsesCoarsePillarProxy(stamp,sampleStep))continue;
|
||||
const auto& center=stamp.positionVoxels;
|
||||
if(center[0]<tileMinimum[0]||center[0]>=tileMaximum[0]
|
||||
||center[1]<tileMinimum[1]||center[1]>=tileMaximum[1]
|
||||
||center[2]<tileMinimum[2]||center[2]>=tileMaximum[2])continue;
|
||||
const double radiusX=stamp.halfExtentsVoxels[0];
|
||||
const double radiusY=stamp.halfExtentsVoxels[1];
|
||||
const double bottomVoxel=center[2]-stamp.halfExtentsVoxels[2]-1.0;
|
||||
const double topVoxel=center[2]+stamp.halfExtentsVoxels[2];
|
||||
const auto point=[&](double angle,double z)
|
||||
{
|
||||
return FVector((center[0]+radiusX*std::cos(angle))*voxelCentimetres,
|
||||
(center[1]+radiusY*std::sin(angle))*voxelCentimetres,
|
||||
z*voxelCentimetres);
|
||||
};
|
||||
const int32 capTopCenter=addVertex(FVector(center[0]*voxelCentimetres,
|
||||
center[1]*voxelCentimetres,topVoxel*voxelCentimetres),
|
||||
FVector(0.0,0.0,1.0),FVector2f(0.5f,0.5f));
|
||||
const int32 capBottomCenter=addVertex(FVector(center[0]*voxelCentimetres,
|
||||
center[1]*voxelCentimetres,bottomVoxel*voxelCentimetres),
|
||||
FVector(0.0,0.0,-1.0),FVector2f(0.5f,0.5f));
|
||||
for(int32 sideIndex=0;sideIndex<sideCount;++sideIndex)
|
||||
{
|
||||
const double u0=static_cast<double>(sideIndex)/sideCount;
|
||||
const double u1=static_cast<double>(sideIndex+1)/sideCount;
|
||||
const double a0=TwoPi*u0,a1=TwoPi*u1;
|
||||
const FVector n0=FVector(std::cos(a0)/radiusX,
|
||||
std::sin(a0)/radiusY,0.0).GetSafeNormal();
|
||||
const FVector n1=FVector(std::cos(a1)/radiusX,
|
||||
std::sin(a1)/radiusY,0.0).GetSafeNormal();
|
||||
const int32 bottomA=addVertex(point(a0,bottomVoxel),n0,FVector2f(u0,0.0f));
|
||||
const int32 bottomB=addVertex(point(a1,bottomVoxel),n1,FVector2f(u1,0.0f));
|
||||
const int32 topB=addVertex(point(a1,topVoxel),n1,FVector2f(u1,1.0f));
|
||||
const int32 topA=addVertex(point(a0,topVoxel),n0,FVector2f(u0,1.0f));
|
||||
proxyIndices.insert(proxyIndices.end(),{bottomA,bottomB,topB,
|
||||
bottomA,topB,topA});
|
||||
|
||||
const int32 topAIndex=addVertex(point(a0,topVoxel),FVector(0.0,0.0,1.0),
|
||||
FVector2f(static_cast<float>(0.5+0.5*std::cos(a0)),
|
||||
static_cast<float>(0.5+0.5*std::sin(a0))));
|
||||
const int32 topBIndex=addVertex(point(a1,topVoxel),FVector(0.0,0.0,1.0),
|
||||
FVector2f(static_cast<float>(0.5+0.5*std::cos(a1)),
|
||||
static_cast<float>(0.5+0.5*std::sin(a1))));
|
||||
proxyIndices.insert(proxyIndices.end(),{capTopCenter,topAIndex,topBIndex});
|
||||
|
||||
const int32 bottomAIndex=addVertex(point(a0,bottomVoxel),FVector(0.0,0.0,-1.0),
|
||||
FVector2f(static_cast<float>(0.5+0.5*std::cos(a0)),
|
||||
static_cast<float>(0.5+0.5*std::sin(a0))));
|
||||
const int32 bottomBIndex=addVertex(point(a1,bottomVoxel),FVector(0.0,0.0,-1.0),
|
||||
FVector2f(static_cast<float>(0.5+0.5*std::cos(a1)),
|
||||
static_cast<float>(0.5+0.5*std::sin(a1))));
|
||||
proxyIndices.insert(proxyIndices.end(),
|
||||
{capBottomCenter,bottomBIndex,bottomAIndex});
|
||||
}
|
||||
++proxyCount;
|
||||
}
|
||||
if(proxyIndices.empty())return 0;
|
||||
const int32 proxyVertexCount=mesh.Vertices.Num()-originalVertexCount;
|
||||
const auto insertProxyVertices=[&](auto& values)
|
||||
{
|
||||
if(values.Num()!=originalVertexCount+proxyVertexCount)return;
|
||||
using FArray=std::remove_reference_t<decltype(values)>;
|
||||
FArray reordered;
|
||||
reordered.Reserve(values.Num());
|
||||
for(int32 index=0;index<originalSurfaceVertices;++index)
|
||||
reordered.Add(values[index]);
|
||||
for(int32 index=originalVertexCount;index<values.Num();++index)
|
||||
reordered.Add(values[index]);
|
||||
for(int32 index=originalSurfaceVertices;index<originalVertexCount;++index)
|
||||
reordered.Add(values[index]);
|
||||
values=MoveTemp(reordered);
|
||||
};
|
||||
insertProxyVertices(mesh.Vertices);
|
||||
if(hasNormals)insertProxyVertices(mesh.Normals);
|
||||
if(hasUvs)insertProxyVertices(mesh.UVs);
|
||||
if(hasColors)insertProxyVertices(mesh.Colors);
|
||||
|
||||
const int32 oldTriangleCount=mesh.Triangles.Num()/3;
|
||||
const int32 ceilingStart=std::max(0,mesh.Triangles.Num()-mesh.NumCeilingTriangles*3);
|
||||
const int32 groundTriangleCount=ceilingStart/3;
|
||||
const int32 proxyTriangleCount=static_cast<int32>(proxyIndices.size()/3);
|
||||
TArray<int32> mergedIndices;
|
||||
mergedIndices.Reserve(mesh.Triangles.Num()+static_cast<int32>(proxyIndices.size()));
|
||||
for(int32 index=0;index<ceilingStart;++index)
|
||||
{
|
||||
const int32 oldIndex=mesh.Triangles[index];
|
||||
mergedIndices.Add(oldIndex>=originalSurfaceVertices
|
||||
?oldIndex+proxyVertexCount:oldIndex);
|
||||
}
|
||||
for(const int32 index:proxyIndices)
|
||||
mergedIndices.Add(originalSurfaceVertices+index-originalVertexCount);
|
||||
for(int32 index=ceilingStart;index<mesh.Triangles.Num();++index)
|
||||
{
|
||||
const int32 oldIndex=mesh.Triangles[index];
|
||||
mergedIndices.Add(oldIndex>=originalSurfaceVertices
|
||||
?oldIndex+proxyVertexCount:oldIndex);
|
||||
}
|
||||
mesh.Triangles=MoveTemp(mergedIndices);
|
||||
|
||||
TArray<uint8> mergedMasks;
|
||||
mergedMasks.Reserve(oldTriangleCount+proxyTriangleCount);
|
||||
for(int32 triangle=0;triangle<groundTriangleCount;++triangle)
|
||||
mergedMasks.Add(mesh.TriangleSkirtFaceMasks.IsValidIndex(triangle)
|
||||
?mesh.TriangleSkirtFaceMasks[triangle]:0);
|
||||
for(int32 triangle=0;triangle<proxyTriangleCount;++triangle)mergedMasks.Add(0);
|
||||
if(groundTriangleCount<oldTriangleCount)
|
||||
for(int32 triangle=groundTriangleCount;triangle<oldTriangleCount;++triangle)
|
||||
mergedMasks.Add(mesh.TriangleSkirtFaceMasks.IsValidIndex(triangle)
|
||||
?mesh.TriangleSkirtFaceMasks[triangle]:0);
|
||||
mesh.TriangleSkirtFaceMasks=MoveTemp(mergedMasks);
|
||||
mesh.NumSurfaceVertices=originalSurfaceVertices+proxyVertexCount;
|
||||
return proxyCount;
|
||||
}
|
||||
|
||||
private:
|
||||
float ApplyResolvedCavernGraph(float density,float X,float Y,float Z,
|
||||
float* nativeFloorSupport=nullptr) const
|
||||
{
|
||||
|
||||
@@ -2889,12 +2889,16 @@ namespace VoxelForgeCore
|
||||
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
|
||||
const auto MarchingCubesStart=std::chrono::steady_clock::now();
|
||||
FVoxelUniformBlockProofAuditStats BlockProofAudit;
|
||||
FVoxelMeshData mesh=mesher.GenerateMesh(origin,MeshStep,MeshCells,nullptr,
|
||||
FVoxelMeshData mesh;
|
||||
{
|
||||
FStandaloneShapeWorld::FScopedStampSamplingStep stampSamplingStep(MeshStep);
|
||||
mesh=mesher.GenerateMesh(origin,MeshStep,MeshCells,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr,
|
||||
options.profileMesherStages?&McStageTimings:nullptr,
|
||||
options.auditUniformBlockProofs?&BlockProofAudit:nullptr,
|
||||
options.forceFullDensityCapture);
|
||||
}
|
||||
passageFeatureScope.copySweptTubeRuntimeCounts();
|
||||
if(options.auditUniformBlockProofs)
|
||||
{
|
||||
@@ -3107,6 +3111,13 @@ namespace VoxelForgeCore
|
||||
AppendStratumBoundaryFloors(world,farShape,nativeKey,step,mesh);
|
||||
AppendStratumBoundaryCeilings(world,farShape,nativeKey,step,mesh);
|
||||
}
|
||||
const std::uint32_t coarsePillarProxyCount=tileShape.AppendCoarsePillarProxies(
|
||||
mesh,origin,extent,MeshStep);
|
||||
if(coarsePillarProxyCount>0)
|
||||
std::printf("[CoarsePillarProxy] key=(%d,%d,%d,lod%d) sampling_step_vox=%d "
|
||||
"pillars=%u mode=exact_capped_cylinder status=EMITTED\n",
|
||||
key.coordinate.x,key.coordinate.y,key.coordinate.z,key.lod,MeshStep,
|
||||
coarsePillarProxyCount);
|
||||
l77RefineStage.Restore();
|
||||
|
||||
out.densitySampleCount=static_cast<std::uint64_t>(std::max<int64>(0,sampleCount));
|
||||
|
||||
@@ -539,6 +539,38 @@ namespace world_assembly_detail
|
||||
if (squared == 0)
|
||||
{ error = "world assembly route contains a zero-length segment"; return false; }
|
||||
}
|
||||
const auto& liminal = stratum.liminalField;
|
||||
if (liminal.version != 0)
|
||||
{
|
||||
if ((liminal.version != CoreLiminalFieldRecipeVersionV1
|
||||
&& liminal.version != CoreLiminalFieldRecipeVersionV2)
|
||||
|| static_cast<std::uint32_t>(liminal.variant) >= CoreLiminalFieldVariantCountV1
|
||||
|| liminal.seed != effectiveRecipe.seed
|
||||
|| liminal.variant != SelectCoreLiminalFieldVariantV1(liminal.seed)
|
||||
|| stratum.route.size() < 2
|
||||
|| liminal.mouthFloorOffsetSubvoxels <= 0
|
||||
|| stratum.route.front().positionSubvoxels[0] != liminal.arrivalMouthSubvoxels[0]
|
||||
|| stratum.route.front().positionSubvoxels[1] != liminal.arrivalMouthSubvoxels[1]
|
||||
|| stratum.route.front().positionSubvoxels[2]
|
||||
!= liminal.arrivalMouthSubvoxels[2]-liminal.mouthFloorOffsetSubvoxels
|
||||
|| stratum.route.back().positionSubvoxels[0] != liminal.departureMouthSubvoxels[0]
|
||||
|| stratum.route.back().positionSubvoxels[1] != liminal.departureMouthSubvoxels[1]
|
||||
|| stratum.route.back().positionSubvoxels[2]
|
||||
!= liminal.departureMouthSubvoxels[2]-liminal.mouthFloorOffsetSubvoxels
|
||||
|| liminal.routeHalfWidthSubvoxels < stratum.routeConstraints.capsuleRadiusSubvoxels
|
||||
|| liminal.routeHalfThicknessSubvoxels <= 0
|
||||
|| liminal.routeOffsetSubvoxels <= 0
|
||||
|| liminal.supportSpacingSubvoxels <= 0
|
||||
|| liminal.supportRadiusSubvoxels <= 0
|
||||
|| stratum.surfaceProfile.version != 1)
|
||||
{ error = "Core.Liminal.Field plan has invalid version, seed-selected variant, route mouths or dimensions"; return false; }
|
||||
const std::int64_t bottom = static_cast<std::int64_t>(stratum.bottomChunkZ)
|
||||
* layoutChunkWidthSubvoxels;
|
||||
const std::int64_t top = (static_cast<std::int64_t>(stratum.topChunkZ) + 1)
|
||||
* layoutChunkWidthSubvoxels;
|
||||
if (liminal.floorHeightSubvoxels <= bottom || liminal.floorHeightSubvoxels >= top)
|
||||
{ error = "Core.Liminal.Field floor escapes its finite stratum bounds"; return false; }
|
||||
}
|
||||
const auto& constraints = stratum.routeConstraints;
|
||||
if (constraints.version == 0)
|
||||
{
|
||||
@@ -682,6 +714,24 @@ namespace world_assembly_detail
|
||||
for (const auto value : point.positionSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(point.radiusSubvoxels), 8);
|
||||
}
|
||||
if (stratum.liminalField.version != 0)
|
||||
{
|
||||
const auto& liminal = stratum.liminalField;
|
||||
HashWord(hash, 0x4c494d494e414c31ull, 8);
|
||||
HashWord(hash, liminal.version, 4);
|
||||
HashWord(hash, static_cast<std::uint32_t>(liminal.variant), 4);
|
||||
HashWord(hash, liminal.seed, 4);
|
||||
for (const auto value : liminal.arrivalMouthSubvoxels)
|
||||
HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
for (const auto value : liminal.departureMouthSubvoxels)
|
||||
HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
for (const auto value : {liminal.mouthFloorOffsetSubvoxels,
|
||||
liminal.floorHeightSubvoxels,
|
||||
liminal.routeHalfWidthSubvoxels, liminal.routeHalfThicknessSubvoxels,
|
||||
liminal.routeOffsetSubvoxels, liminal.supportSpacingSubvoxels,
|
||||
liminal.supportRadiusSubvoxels})
|
||||
HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
}
|
||||
if (stratum.surfaceProfile.version != 0)
|
||||
HashSurfaceProfile(hash, stratum.surfaceProfile);
|
||||
return hash;
|
||||
@@ -1059,6 +1109,30 @@ namespace world_assembly_detail
|
||||
{ error = "world assembly route point is missing a fixed-scale field"; return false; }
|
||||
stratum.route.push_back(point);
|
||||
}
|
||||
const VFJson::Value& liminalValue = item.Get("liminal_field");
|
||||
if (!std::holds_alternative<std::nullptr_t>(liminalValue.Data))
|
||||
{
|
||||
auto& liminal = stratum.liminalField;
|
||||
std::uint32_t variant = 0;
|
||||
if (!liminalValue.IsObject()
|
||||
|| !ReadU32(liminalValue.Get("version"), liminal.version)
|
||||
|| !ReadU32(liminalValue.Get("variant"), variant)
|
||||
|| !ReadU32(liminalValue.Get("seed"), liminal.seed)
|
||||
|| !ReadArray(liminalValue.Get("arrival_mouth_q"),
|
||||
liminal.arrivalMouthSubvoxels.data(), 3)
|
||||
|| !ReadArray(liminalValue.Get("departure_mouth_q"),
|
||||
liminal.departureMouthSubvoxels.data(), 3)
|
||||
|| !ReadInteger(liminalValue.Get("mouth_floor_offset_q"),
|
||||
liminal.mouthFloorOffsetSubvoxels)
|
||||
|| !ReadInteger(liminalValue.Get("floor_height_q"), liminal.floorHeightSubvoxels)
|
||||
|| !ReadInteger(liminalValue.Get("route_half_width_q"), liminal.routeHalfWidthSubvoxels)
|
||||
|| !ReadInteger(liminalValue.Get("route_half_thickness_q"), liminal.routeHalfThicknessSubvoxels)
|
||||
|| !ReadInteger(liminalValue.Get("route_offset_q"), liminal.routeOffsetSubvoxels)
|
||||
|| !ReadInteger(liminalValue.Get("support_spacing_q"), liminal.supportSpacingSubvoxels)
|
||||
|| !ReadInteger(liminalValue.Get("support_radius_q"), liminal.supportRadiusSubvoxels))
|
||||
{ error = "Core.Liminal.Field plan is missing a fixed-scale field"; return false; }
|
||||
liminal.variant = static_cast<WorldAssemblyLiminalVariantV1>(variant);
|
||||
}
|
||||
plan.strates.push_back(std::move(stratum));
|
||||
}
|
||||
return true;
|
||||
@@ -2324,6 +2398,724 @@ bool WorldAssemblyManifest::validate(std::string& error) const
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlanCoreLiminalFieldV1(WorldAssemblyRecipeV1& recipe,
|
||||
WorldAssemblyStratumPlanV1& stratum,const ResolvedLayoutSnapshotV3& layout,
|
||||
std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
if(layout.coreVoxelSizeMicrometres==0||stratum.strateIndex<0
|
||||
||stratum.topChunkZ<stratum.bottomChunkZ)
|
||||
{error="Core.Liminal.Field needs a finite stratum and resolved Core voxel size";return false;}
|
||||
const auto resolvedStratum=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[&](const auto& value){return value.strateIndex==stratum.strateIndex
|
||||
&&value.generatorKind=="SurfaceWorld";});
|
||||
if(resolvedStratum==layout.strates.end()||!resolvedStratum->content.presentInCurrentField)
|
||||
{error="Core.Liminal.Field must target a resolved SurfaceWorld content band";return false;}
|
||||
const ResolvedLayoutPassageV1* arrival=nullptr;
|
||||
const ResolvedLayoutPassageV1* departure=nullptr;
|
||||
for(const auto& passage:layout.passages)
|
||||
{
|
||||
if(passage.lowerStrateIndex==stratum.strateIndex)
|
||||
{
|
||||
if(arrival){error="Core.Liminal.Field requires one fixed arrival passage mouth";return false;}
|
||||
arrival=&passage;
|
||||
}
|
||||
if(passage.upperStrateIndex==stratum.strateIndex)
|
||||
{
|
||||
if(departure){error="Core.Liminal.Field requires one fixed departure passage mouth";return false;}
|
||||
departure=&passage;
|
||||
}
|
||||
}
|
||||
if(!arrival||!departure)
|
||||
{error="Core.Liminal.Field requires an adjacent passage on both sides";return false;}
|
||||
|
||||
constexpr std::int64_t Q=WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t coreMicrons=layout.coreVoxelSizeMicrometres;
|
||||
const auto micrometresToQ=[&](std::int64_t micrometres)
|
||||
{
|
||||
const std::int64_t numerator=micrometres*Q;
|
||||
return numerator>=0?(numerator+coreMicrons/2)/coreMicrons
|
||||
:(numerator-coreMicrons/2)/coreMicrons;
|
||||
};
|
||||
const std::int64_t qPerMetre=micrometresToQ(1000000);
|
||||
if(qPerMetre<=0){error="Core.Liminal.Field could not resolve fixed-scale metres";return false;}
|
||||
|
||||
const std::uint32_t seed=recipe.seed;
|
||||
const WorldAssemblyLiminalVariantV1 variant=SelectCoreLiminalFieldVariantV1(seed);
|
||||
const auto& definition=CoreLiminalFieldVariantsV1[static_cast<std::size_t>(variant)];
|
||||
const auto& start=arrival->lowerMouthSubvoxels;
|
||||
const auto& finish=departure->upperMouthSubvoxels;
|
||||
constexpr std::int64_t MaximumMouthCoordinateQ=1000000000ll;
|
||||
if(std::abs(start[0])>MaximumMouthCoordinateQ||std::abs(start[1])>MaximumMouthCoordinateQ
|
||||
||std::abs(start[2])>MaximumMouthCoordinateQ||std::abs(finish[0])>MaximumMouthCoordinateQ
|
||||
||std::abs(finish[1])>MaximumMouthCoordinateQ||std::abs(finish[2])>MaximumMouthCoordinateQ)
|
||||
{error="Core.Liminal.Field passage mouth exceeds the supported fixed-coordinate range";return false;}
|
||||
const std::int64_t mouthPlayerCenterHeight=micrometresToQ(1130000);
|
||||
auto routeStart=start;
|
||||
auto routeFinish=finish;
|
||||
routeStart[2]-=mouthPlayerCenterHeight;
|
||||
routeFinish[2]-=mouthPlayerCenterHeight;
|
||||
const auto findMouthControl=[&](const ResolvedLayoutPassageV1& passage,
|
||||
const std::array<std::int64_t,3>& mouth)
|
||||
{
|
||||
std::size_t found=passage.controlPoints.size();
|
||||
for(std::size_t i=0;i<passage.controlPoints.size();++i)
|
||||
{
|
||||
const auto& point=passage.controlPoints[i];
|
||||
if(point.positionSubvoxels[0]==mouth[0]
|
||||
&&point.positionSubvoxels[1]==mouth[1]
|
||||
&&point.positionSubvoxels[2]-point.radiusSubvoxels
|
||||
+mouthPlayerCenterHeight==mouth[2])
|
||||
{
|
||||
if(found!=passage.controlPoints.size())return passage.controlPoints.size();
|
||||
found=i;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
};
|
||||
const std::size_t arrivalControl=findMouthControl(*arrival,start);
|
||||
const std::size_t departureControl=findMouthControl(*departure,finish);
|
||||
if(arrival->controlPoints.size()<2||departure->controlPoints.size()<2
|
||||
||arrivalControl>=arrival->controlPoints.size()
|
||||
||departureControl>=departure->controlPoints.size())
|
||||
{error="Core.Liminal.Field needs exact passage-control bindings at both fixed mouths";return false;}
|
||||
const auto horizontalLengthQ=[](std::int64_t dx,std::int64_t dy)
|
||||
{
|
||||
const auto absX=static_cast<std::uint64_t>(std::abs(dx));
|
||||
const auto absY=static_cast<std::uint64_t>(std::abs(dy));
|
||||
return world_assembly_detail::IntegerSqrt(absX*absX+absY*absY);
|
||||
};
|
||||
const auto roundRatio=[](std::int64_t numerator,std::uint64_t denominator)
|
||||
{
|
||||
if(denominator==0)return std::int64_t{0};
|
||||
const auto positiveDenominator=static_cast<std::int64_t>(denominator);
|
||||
return numerator>=0
|
||||
?(numerator+positiveDenominator/2)/positiveDenominator
|
||||
:-((-numerator+positiveDenominator/2)/positiveDenominator);
|
||||
};
|
||||
const auto passageSplineControls=[](const ResolvedLayoutPassageV1& passage)
|
||||
{
|
||||
std::vector<PassageSpline::FPoint> controls;
|
||||
controls.reserve(passage.controlPoints.size());
|
||||
for(const auto& point:passage.controlPoints)
|
||||
controls.push_back({static_cast<double>(point.positionSubvoxels[0]),
|
||||
static_cast<double>(point.positionSubvoxels[1]),
|
||||
static_cast<double>(point.positionSubvoxels[2]-point.radiusSubvoxels),
|
||||
static_cast<double>(point.radiusSubvoxels)});
|
||||
return controls;
|
||||
};
|
||||
const auto arrivalSpline=passageSplineControls(*arrival);
|
||||
const auto departureSpline=passageSplineControls(*departure);
|
||||
constexpr double MouthTangentProbeParameter=0.5;
|
||||
const std::size_t arrivalSegment=arrivalControl==0?0:arrivalControl-1;
|
||||
const double arrivalAtT=arrivalControl==0?0.0:1.0;
|
||||
const double arrivalNearT=arrivalControl==0?MouthTangentProbeParameter
|
||||
:1.0-MouthTangentProbeParameter;
|
||||
const auto arrivalNear=PassageSpline::EvaluateRaw(arrivalSpline,arrivalSegment,
|
||||
arrivalNearT,static_cast<double>(qPerMetre));
|
||||
const auto arrivalAt=PassageSpline::EvaluateRaw(arrivalSpline,arrivalSegment,
|
||||
arrivalAtT,static_cast<double>(qPerMetre));
|
||||
const std::size_t departureSegment=departureControl+1==departure->controlPoints.size()
|
||||
?departureControl-1:departureControl;
|
||||
const double departureAtT=departureControl+1==departure->controlPoints.size()?1.0:0.0;
|
||||
const double departureNearT=departureControl+1==departure->controlPoints.size()
|
||||
?1.0-MouthTangentProbeParameter:MouthTangentProbeParameter;
|
||||
const auto departureAt=PassageSpline::EvaluateRaw(departureSpline,departureSegment,
|
||||
departureAtT,static_cast<double>(qPerMetre));
|
||||
const auto departureNear=PassageSpline::EvaluateRaw(departureSpline,departureSegment,
|
||||
departureNearT,static_cast<double>(qPerMetre));
|
||||
const auto roundedDelta=[](double startValue,double endValue)
|
||||
{return static_cast<std::int64_t>(std::llround(endValue-startValue));};
|
||||
const std::int64_t arrivalDx=roundedDelta(arrivalNear[0],arrivalAt[0]);
|
||||
const std::int64_t arrivalDy=roundedDelta(arrivalNear[1],arrivalAt[1]);
|
||||
const std::int64_t arrivalFloorDelta=roundedDelta(arrivalNear[2],arrivalAt[2]);
|
||||
const std::uint64_t arrivalLength=horizontalLengthQ(arrivalDx,arrivalDy);
|
||||
const std::int64_t departureDx=roundedDelta(departureAt[0],departureNear[0]);
|
||||
const std::int64_t departureDy=roundedDelta(departureAt[1],departureNear[1]);
|
||||
const std::int64_t departureFloorDelta=roundedDelta(departureAt[2],departureNear[2]);
|
||||
const std::uint64_t departureLength=horizontalLengthQ(departureDx,departureDy);
|
||||
if(arrivalLength==0||departureLength==0)
|
||||
{error="Core.Liminal.Field passage mouth tangent is too short for a stable connector";return false;}
|
||||
const auto extendMouth=[&](const std::array<std::int64_t,3>& mouth,
|
||||
std::int64_t dx,std::int64_t dy,std::int64_t dz,std::uint64_t horizontalLength,
|
||||
std::int64_t distance,bool forward)
|
||||
{
|
||||
const std::int64_t sign=forward?1:-1;
|
||||
return std::array<std::int64_t,3>{
|
||||
mouth[0]+roundRatio(sign*dx*distance,horizontalLength),
|
||||
mouth[1]+roundRatio(sign*dy*distance,horizontalLength),
|
||||
mouth[2]+roundRatio(sign*dz*distance,horizontalLength)};
|
||||
};
|
||||
constexpr std::int64_t MouthConnectorMidMetres=5;
|
||||
constexpr std::int64_t MouthConnectorFullMetres=10;
|
||||
const auto arrivalConnectorMid=extendMouth(routeStart,arrivalDx,arrivalDy,
|
||||
arrivalFloorDelta,arrivalLength,MouthConnectorMidMetres*qPerMetre,true);
|
||||
const auto arrivalConnector=extendMouth(routeStart,arrivalDx,arrivalDy,
|
||||
arrivalFloorDelta,arrivalLength,MouthConnectorFullMetres*qPerMetre,true);
|
||||
const auto departureConnector=extendMouth(routeFinish,departureDx,departureDy,
|
||||
departureFloorDelta,departureLength,MouthConnectorFullMetres*qPerMetre,false);
|
||||
const auto departureConnectorMid=extendMouth(routeFinish,departureDx,departureDy,
|
||||
departureFloorDelta,departureLength,MouthConnectorMidMetres*qPerMetre,false);
|
||||
const std::int64_t bottom=static_cast<std::int64_t>(stratum.bottomChunkZ)
|
||||
*recipe.layoutChunkWidthSubvoxels;
|
||||
const std::int64_t top=(static_cast<std::int64_t>(stratum.topChunkZ)+1)
|
||||
*recipe.layoutChunkWidthSubvoxels;
|
||||
const std::int64_t minRouteFloor=std::min({routeStart[2],routeFinish[2],
|
||||
arrivalConnectorMid[2],arrivalConnector[2],departureConnector[2],
|
||||
departureConnectorMid[2]});
|
||||
const std::int64_t contentBottom=micrometresToQ(
|
||||
resolvedStratum->content.bottomMicrometres);
|
||||
const std::int64_t profileRelief=variant==WorldAssemblyLiminalVariantV1::repeatingHills
|
||||
?2*micrometresToQ(definition.hillAmplitudeMicrometres):0;
|
||||
const std::int64_t floorHeight=std::max(contentBottom+profileRelief+qPerMetre/4,
|
||||
minRouteFloor-8*qPerMetre);
|
||||
if(floorHeight>=minRouteFloor-2*qPerMetre||floorHeight<=bottom||top<=floorHeight+8*qPerMetre)
|
||||
{error="Core.Liminal.Field has no room for its floor and passage route";return false;}
|
||||
|
||||
stratum.hasRecipeOverride=true;
|
||||
stratum.recipeOverride=recipe;
|
||||
WorldAssemblySurfaceProfileV1 profile;
|
||||
profile.version=1;
|
||||
profile.kind=definition.surfaceKind;
|
||||
profile.h0Subvoxels=floorHeight;
|
||||
if(profile.kind==WorldAssemblySurfaceProfileKindV1::periodicHills)
|
||||
{
|
||||
profile.amplitudeXSubvoxels=micrometresToQ(definition.hillAmplitudeMicrometres);
|
||||
profile.amplitudeYSubvoxels=profile.amplitudeXSubvoxels;
|
||||
profile.wavelengthXSubvoxels=micrometresToQ(definition.hillWavelengthMicrometres);
|
||||
profile.wavelengthYSubvoxels=profile.wavelengthXSubvoxels;
|
||||
ResolveWorldAssemblySurfaceProfilePhasesV1(profile,seed);
|
||||
}
|
||||
stratum.recipeOverride.surfaceProfile=profile;
|
||||
stratum.recipeOverride.contentHash=0;
|
||||
stratum.surfaceProfile=profile;
|
||||
stratum.capHeightSubvoxels=top-4*qPerMetre;
|
||||
stratum.capUndulationSubvoxels=0;
|
||||
stratum.capDetailAmplitudeSubvoxels=0;
|
||||
stratum.capRidgeAmplitudeSubvoxels=0;
|
||||
stratum.waterEnabled=false;
|
||||
stratum.waterSurfaceSubvoxels=0;
|
||||
|
||||
WorldAssemblyLiminalFieldPlanV1 liminal;
|
||||
liminal.version=CoreLiminalFieldRecipeVersionV1;
|
||||
liminal.variant=variant;
|
||||
liminal.seed=seed;
|
||||
liminal.arrivalMouthSubvoxels=start;
|
||||
liminal.departureMouthSubvoxels=finish;
|
||||
liminal.floorHeightSubvoxels=floorHeight;
|
||||
liminal.routeHalfWidthSubvoxels=micrometresToQ(definition.routeHalfWidthMicrometres);
|
||||
liminal.routeHalfThicknessSubvoxels=micrometresToQ(definition.routeHalfThicknessMicrometres);
|
||||
liminal.supportSpacingSubvoxels=micrometresToQ(definition.supportSpacingMicrometres);
|
||||
liminal.supportRadiusSubvoxels=micrometresToQ(definition.supportRadiusMicrometres);
|
||||
liminal.mouthFloorOffsetSubvoxels=mouthPlayerCenterHeight;
|
||||
|
||||
constexpr std::int64_t GradeQ16=17560;
|
||||
constexpr std::int64_t MinimumPathOffsetMetres=150;
|
||||
constexpr std::int64_t ConnectorSpanMetres=16;
|
||||
const std::int64_t dx=departureConnector[0]-arrivalConnector[0];
|
||||
const std::int64_t dy=departureConnector[1]-arrivalConnector[1];
|
||||
const std::uint64_t chord=horizontalLengthQ(dx,dy);
|
||||
std::int64_t directionX=dx,directionY=dy;
|
||||
std::uint64_t directionLength=chord;
|
||||
if(directionLength<static_cast<std::uint64_t>(qPerMetre/4))
|
||||
{directionX=Q;directionY=0;directionLength=static_cast<std::uint64_t>(Q);}
|
||||
const std::int64_t normalX=-directionY;
|
||||
const std::int64_t normalY=directionX;
|
||||
const std::int64_t vertical=std::abs(departureConnector[2]-arrivalConnector[2]);
|
||||
const std::int64_t requiredHorizontal=(vertical*65536+GradeQ16-1)/GradeQ16;
|
||||
std::int64_t routeOffset=std::max(MinimumPathOffsetMetres*qPerMetre,
|
||||
(requiredHorizontal+1)/2+20*qPerMetre);
|
||||
if(chord<ConnectorSpanMetres*static_cast<std::uint64_t>(qPerMetre))
|
||||
routeOffset=std::max(routeOffset,MinimumPathOffsetMetres*qPerMetre);
|
||||
liminal.routeOffsetSubvoxels=routeOffset;
|
||||
|
||||
const auto normalOffset=[&](std::int64_t scale)
|
||||
{
|
||||
return std::array<std::int64_t,2>{
|
||||
normalX*scale/static_cast<std::int64_t>(directionLength),
|
||||
normalY*scale/static_cast<std::int64_t>(directionLength)};
|
||||
};
|
||||
const auto directionOffset=[&](std::int64_t scale)
|
||||
{
|
||||
return std::array<std::int64_t,2>{
|
||||
directionX*scale/static_cast<std::int64_t>(directionLength),
|
||||
directionY*scale/static_cast<std::int64_t>(directionLength)};
|
||||
};
|
||||
const auto normal=normalOffset(routeOffset);
|
||||
const auto connector=chord<ConnectorSpanMetres*static_cast<std::uint64_t>(qPerMetre)
|
||||
?directionOffset(ConnectorSpanMetres*qPerMetre):std::array<std::int64_t,2>{0,0};
|
||||
std::vector<std::array<std::int64_t,3>> horizontalPath;
|
||||
horizontalPath.reserve(8);
|
||||
horizontalPath.push_back(routeStart);
|
||||
horizontalPath.push_back(arrivalConnectorMid);
|
||||
horizontalPath.push_back(arrivalConnector);
|
||||
horizontalPath.push_back({arrivalConnector[0]+normal[0],
|
||||
arrivalConnector[1]+normal[1],0});
|
||||
horizontalPath.push_back({departureConnector[0]+normal[0]+connector[0],
|
||||
departureConnector[1]+normal[1]+connector[1],0});
|
||||
horizontalPath.push_back(departureConnector);
|
||||
horizontalPath.push_back(departureConnectorMid);
|
||||
horizontalPath.push_back(routeFinish);
|
||||
|
||||
std::vector<WorldAssemblyRoutePointV1> route;
|
||||
route.reserve(horizontalPath.size());
|
||||
std::vector<std::uint64_t> cumulative(horizontalPath.size(),0);
|
||||
std::uint64_t totalHorizontal=0;
|
||||
for(std::size_t i=1;i<horizontalPath.size();++i)
|
||||
{
|
||||
const std::int64_t sx=horizontalPath[i][0]-horizontalPath[i-1][0];
|
||||
const std::int64_t sy=horizontalPath[i][1]-horizontalPath[i-1][1];
|
||||
const std::uint64_t segment=horizontalLengthQ(sx,sy);
|
||||
if(segment==0){error="Core.Liminal.Field route contains a zero-horizontal segment";return false;}
|
||||
totalHorizontal+=segment;
|
||||
cumulative[i]=totalHorizontal;
|
||||
}
|
||||
if(totalHorizontal<static_cast<std::uint64_t>(requiredHorizontal))
|
||||
{error="Core.Liminal.Field route cannot meet the 15-degree grade bound";return false;}
|
||||
const std::uint64_t centralFirst=horizontalLengthQ(
|
||||
horizontalPath[3][0]-horizontalPath[2][0],
|
||||
horizontalPath[3][1]-horizontalPath[2][1]);
|
||||
const std::uint64_t centralMiddle=horizontalLengthQ(
|
||||
horizontalPath[4][0]-horizontalPath[3][0],
|
||||
horizontalPath[4][1]-horizontalPath[3][1]);
|
||||
const std::uint64_t centralLast=horizontalLengthQ(
|
||||
horizontalPath[5][0]-horizontalPath[4][0],
|
||||
horizontalPath[5][1]-horizontalPath[4][1]);
|
||||
const std::uint64_t centralTotal=centralFirst+centralMiddle+centralLast;
|
||||
if(centralTotal==0)
|
||||
{error="Core.Liminal.Field central route has no horizontal span";return false;}
|
||||
const std::int64_t centralVertical=departureConnector[2]-arrivalConnector[2];
|
||||
horizontalPath[3][2]=arrivalConnector[2]+roundRatio(
|
||||
centralVertical*static_cast<std::int64_t>(centralFirst),centralTotal);
|
||||
horizontalPath[4][2]=arrivalConnector[2]+roundRatio(
|
||||
centralVertical*static_cast<std::int64_t>(centralFirst+centralMiddle),centralTotal);
|
||||
for(std::size_t i=0;i<horizontalPath.size();++i)
|
||||
{
|
||||
auto point=horizontalPath[i];
|
||||
WorldAssemblyRoutePointV1 routePoint;
|
||||
routePoint.positionSubvoxels=point;
|
||||
routePoint.radiusSubvoxels=std::max<std::int64_t>(liminal.routeHalfWidthSubvoxels,
|
||||
micrometresToQ(500000));
|
||||
std::uint64_t id=14695981039346656037ull;
|
||||
const auto add=[&](std::uint64_t value,std::uint32_t bytes)
|
||||
{for(std::uint32_t byte=0;byte<bytes;++byte){id^=static_cast<std::uint8_t>(value>>(byte*8));id*=1099511628211ull;}};
|
||||
add(seed,4);add(static_cast<std::uint32_t>(stratum.strateIndex),4);add(i,4);
|
||||
routePoint.stableFeatureId=id?id:1;
|
||||
route.push_back(routePoint);
|
||||
}
|
||||
stratum.route=std::move(route);
|
||||
stratum.routeConstraints={1,micrometresToQ(340000),micrometresToQ(1760000),
|
||||
GradeQ16,micrometresToQ(125000),micrometresToQ(250000),
|
||||
micrometresToQ(1000000),micrometresToQ(120000),3};
|
||||
stratum.liminalField=liminal;
|
||||
std::string validationError;
|
||||
if(!world_assembly_detail::ValidateSurfaceProfile(stratum.surfaceProfile,seed,validationError)
|
||||
||!world_assembly_detail::ValidateStratum(stratum,recipe,validationError))
|
||||
{error=validationError.empty()?"Core.Liminal.Field produced an invalid plan":validationError;return false;}
|
||||
std::printf("[CoreLiminalFieldPlan] recipe=%.*s version=%u variant=%.*s seed=%u arrival=%s departure=%s route_points=%zu route_offset_m=%.3f route_grade_limit_deg=15.000 status=PASS\n",
|
||||
static_cast<int>(CoreLiminalFieldRecipeIdV1.size()),CoreLiminalFieldRecipeIdV1.data(),
|
||||
liminal.version,static_cast<int>(definition.name.size()),definition.name.data(),seed,
|
||||
arrival->stableId.c_str(),departure->stableId.c_str(),stratum.route.size(),
|
||||
static_cast<double>(routeOffset)/qPerMetre);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::array<std::array<std::int64_t,2>,6> CoreLiminalRouteXYV2(
|
||||
std::int64_t startX,std::int64_t startY,std::int64_t finishX,
|
||||
std::int64_t finishY,std::int64_t qPerMetre)
|
||||
{
|
||||
return {{{{startX,startY}},{{-40*qPerMetre,40*qPerMetre}},
|
||||
{{-40*qPerMetre,-40*qPerMetre}},{{40*qPerMetre,-40*qPerMetre}},
|
||||
{{40*qPerMetre,40*qPerMetre}},{{finishX,finishY}}}};
|
||||
}
|
||||
|
||||
std::vector<WorldAssemblyRoutePointV1> BuildCoreLiminalRouteV2(
|
||||
const WorldAssemblySurfaceProfileV1& profile,
|
||||
const ResolvedLayoutLandingPadV1& arrivalPad,
|
||||
const ResolvedLayoutLandingPadV1& departurePad,
|
||||
const WorldAssemblyLiminalVariantDefinitionV1& definition,
|
||||
std::uint32_t seed,std::int32_t strateIndex,std::int64_t coreMicrons,
|
||||
std::int64_t qPerMetre,
|
||||
std::uint64_t& totalHorizontal,std::string& error)
|
||||
{
|
||||
error.clear();totalHorizontal=0;
|
||||
const auto groundHeightAt=[&](std::int64_t x,std::int64_t y)
|
||||
{
|
||||
if(profile.kind==WorldAssemblySurfaceProfileKindV1::flatField)
|
||||
return profile.h0Subvoxels;
|
||||
const std::int64_t turnX=world_assembly_detail::SurfaceProfileTurnAtCoordinate(
|
||||
x,profile.wavelengthXSubvoxels,profile.phaseXTurnsQ32);
|
||||
const std::int64_t turnY=world_assembly_detail::SurfaceProfileTurnAtCoordinate(
|
||||
y,profile.wavelengthYSubvoxels,profile.phaseYTurnsQ32);
|
||||
return profile.h0Subvoxels
|
||||
+world_assembly_detail::MulQ30(profile.amplitudeXSubvoxels,
|
||||
world_assembly_detail::CosineQ30(turnX))
|
||||
+world_assembly_detail::MulQ30(profile.amplitudeYSubvoxels,
|
||||
world_assembly_detail::CosineQ30(turnY));
|
||||
};
|
||||
const auto horizontalLengthQ=[](std::int64_t dx,std::int64_t dy)
|
||||
{return world_assembly_detail::IntegerSqrt(static_cast<std::uint64_t>(dx*dx+dy*dy));};
|
||||
const auto micrometresToQ=[&](std::int64_t micrometres)
|
||||
{return (micrometres*WorldAssemblySubvoxelsPerVoxel+coreMicrons/2)/coreMicrons;};
|
||||
const auto routeXY=CoreLiminalRouteXYV2(arrivalPad.positionSubvoxels[0],
|
||||
arrivalPad.positionSubvoxels[1],departurePad.positionSubvoxels[0],
|
||||
departurePad.positionSubvoxels[1],qPerMetre);
|
||||
std::vector<WorldAssemblyRoutePointV1> route;
|
||||
const std::int64_t pointSpacing=12*qPerMetre;
|
||||
const std::int64_t routeRadius=std::max<std::int64_t>(
|
||||
micrometresToQ(definition.routeHalfWidthMicrometres),micrometresToQ(500000));
|
||||
const auto routeId=[&](std::size_t ordinal)
|
||||
{
|
||||
std::uint64_t id=14695981039346656037ull;
|
||||
const auto add=[&](std::uint64_t value,std::uint32_t bytes)
|
||||
{for(std::uint32_t byte=0;byte<bytes;++byte)
|
||||
{id^=static_cast<std::uint8_t>(value>>(byte*8));id*=1099511628211ull;}};
|
||||
add(seed,4);add(static_cast<std::uint32_t>(strateIndex),4);add(ordinal,4);
|
||||
return id?id:1;
|
||||
};
|
||||
const auto appendRoutePoint=[&](std::int64_t x,std::int64_t y)
|
||||
{
|
||||
WorldAssemblyRoutePointV1 point;
|
||||
point.positionSubvoxels={x,y,groundHeightAt(x,y)};
|
||||
point.radiusSubvoxels=routeRadius;
|
||||
point.stableFeatureId=routeId(route.size());
|
||||
route.push_back(point);
|
||||
};
|
||||
const auto interpolate=[](std::int64_t start,std::int64_t delta,
|
||||
std::uint64_t step,std::uint64_t steps)
|
||||
{
|
||||
const std::int64_t numerator=delta*static_cast<std::int64_t>(step);
|
||||
const std::int64_t divisor=static_cast<std::int64_t>(steps);
|
||||
return start+(numerator>=0?(numerator+divisor/2)/divisor
|
||||
:(numerator-divisor/2)/divisor);
|
||||
};
|
||||
appendRoutePoint(routeXY.front()[0],routeXY.front()[1]);
|
||||
for(std::size_t segment=1;segment<routeXY.size();++segment)
|
||||
{
|
||||
const std::int64_t dx=routeXY[segment][0]-routeXY[segment-1][0];
|
||||
const std::int64_t dy=routeXY[segment][1]-routeXY[segment-1][1];
|
||||
const std::uint64_t length=horizontalLengthQ(dx,dy);
|
||||
if(length==0){error="Core.Liminal.Field v2 route contains a zero-horizontal segment";return {};}
|
||||
totalHorizontal+=length;
|
||||
const std::uint64_t steps=std::max<std::uint64_t>(1,
|
||||
(length+static_cast<std::uint64_t>(pointSpacing)-1)
|
||||
/static_cast<std::uint64_t>(pointSpacing));
|
||||
for(std::uint64_t step=1;step<=steps;++step)
|
||||
appendRoutePoint(interpolate(routeXY[segment-1][0],dx,step,steps),
|
||||
interpolate(routeXY[segment-1][1],dy,step,steps));
|
||||
}
|
||||
if(route.size()<2||totalHorizontal<300*static_cast<std::uint64_t>(qPerMetre)
|
||||
||route.front().positionSubvoxels[2]!=arrivalPad.floorHeightSubvoxels
|
||||
||route.back().positionSubvoxels[2]!=departurePad.floorHeightSubvoxels)
|
||||
{error="Core.Liminal.Field v2 pad-to-pad route is incomplete or too short";return {};}
|
||||
return route;
|
||||
}
|
||||
|
||||
bool PrepareCoreLiminalLandingPadsV2(WorldAssemblyRecipeV1& recipe,
|
||||
WorldAssemblyStratumPlanV1& stratum,ResolvedLayoutSnapshotV3& layout,
|
||||
std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
if(layout.coreVoxelSizeMicrometres==0||stratum.strateIndex<0
|
||||
||stratum.topChunkZ<stratum.bottomChunkZ)
|
||||
{error="Core.Liminal.Field v2 needs a finite stratum and resolved Core voxel size";return false;}
|
||||
auto resolved=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[&](const auto& value){return value.strateIndex==stratum.strateIndex
|
||||
&&value.generatorKind=="SurfaceWorld";});
|
||||
if(resolved==layout.strates.end()||!resolved->content.presentInCurrentField)
|
||||
{error="Core.Liminal.Field v2 must target a resolved SurfaceWorld content band";return false;}
|
||||
|
||||
constexpr std::int64_t Q=WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t coreMicrons=layout.coreVoxelSizeMicrometres;
|
||||
const auto micrometresToQ=[&](std::int64_t micrometres)
|
||||
{
|
||||
const std::int64_t numerator=micrometres*Q;
|
||||
return numerator>=0?(numerator+coreMicrons/2)/coreMicrons
|
||||
:(numerator-coreMicrons/2)/coreMicrons;
|
||||
};
|
||||
const std::int64_t qPerMetre=micrometresToQ(1000000);
|
||||
if(qPerMetre<=0){error="Core.Liminal.Field v2 could not resolve fixed-scale metres";return false;}
|
||||
|
||||
const std::uint32_t seed=recipe.seed;
|
||||
const WorldAssemblyLiminalVariantV1 variant=SelectCoreLiminalFieldVariantV1(seed);
|
||||
const auto& definition=CoreLiminalFieldVariantsV1[static_cast<std::size_t>(variant)];
|
||||
const std::int64_t bottom=static_cast<std::int64_t>(stratum.bottomChunkZ)
|
||||
*recipe.layoutChunkWidthSubvoxels;
|
||||
const std::int64_t top=(static_cast<std::int64_t>(stratum.topChunkZ)+1)
|
||||
*recipe.layoutChunkWidthSubvoxels;
|
||||
const std::int64_t contentBottom=micrometresToQ(resolved->content.bottomMicrometres);
|
||||
const std::int64_t profileRelief=variant==WorldAssemblyLiminalVariantV1::repeatingHills
|
||||
?2*micrometresToQ(definition.hillAmplitudeMicrometres):0;
|
||||
const std::int64_t deckSweepRadius=static_cast<std::int64_t>(std::ceil(std::hypot(
|
||||
static_cast<double>(definition.routeHalfWidthMicrometres+250000),
|
||||
static_cast<double>(definition.routeHalfThicknessMicrometres+250000))));
|
||||
const std::int64_t floorClearance=definition.routeHalfThicknessMicrometres
|
||||
+deckSweepRadius+250000;
|
||||
// Keep the field above the lower tunnel's mouth transition so the routed
|
||||
// departure has a clean open section before it reaches the cave wall.
|
||||
constexpr std::int64_t MinimumFieldGroundMarginMicrometres=20000000;
|
||||
const std::int64_t fieldGroundMargin=std::max(floorClearance,
|
||||
MinimumFieldGroundMarginMicrometres);
|
||||
const std::int64_t floorHeight=contentBottom+profileRelief
|
||||
+micrometresToQ(fieldGroundMargin);
|
||||
if(floorHeight<=bottom||top<=floorHeight+8*qPerMetre)
|
||||
{error="Core.Liminal.Field v2 has no room for its supported field and cap";return false;}
|
||||
|
||||
WorldAssemblySurfaceProfileV1 profile;
|
||||
profile.version=1;
|
||||
profile.kind=definition.surfaceKind;
|
||||
profile.h0Subvoxels=floorHeight;
|
||||
if(profile.kind==WorldAssemblySurfaceProfileKindV1::periodicHills)
|
||||
{
|
||||
profile.amplitudeXSubvoxels=micrometresToQ(definition.hillAmplitudeMicrometres);
|
||||
profile.amplitudeYSubvoxels=profile.amplitudeXSubvoxels;
|
||||
profile.wavelengthXSubvoxels=micrometresToQ(definition.hillWavelengthMicrometres);
|
||||
profile.wavelengthYSubvoxels=profile.wavelengthXSubvoxels;
|
||||
ResolveWorldAssemblySurfaceProfilePhasesV1(profile,seed);
|
||||
}
|
||||
if(!world_assembly_detail::ValidateSurfaceProfile(profile,seed,error))return false;
|
||||
|
||||
stratum.hasRecipeOverride=true;
|
||||
stratum.recipeOverride=recipe;
|
||||
stratum.recipeOverride.surfaceProfile=profile;
|
||||
stratum.recipeOverride.contentHash=0;
|
||||
stratum.surfaceProfile=profile;
|
||||
stratum.capHeightSubvoxels=top-4*qPerMetre;
|
||||
stratum.capUndulationSubvoxels=0;
|
||||
stratum.capDetailAmplitudeSubvoxels=0;
|
||||
stratum.capRidgeAmplitudeSubvoxels=0;
|
||||
stratum.waterEnabled=false;
|
||||
stratum.waterSurfaceSubvoxels=0;
|
||||
|
||||
const auto groundHeightAt=[&](std::int64_t x,std::int64_t y)
|
||||
{
|
||||
if(profile.kind==WorldAssemblySurfaceProfileKindV1::flatField)
|
||||
return profile.h0Subvoxels;
|
||||
const std::int64_t turnX=world_assembly_detail::SurfaceProfileTurnAtCoordinate(
|
||||
x,profile.wavelengthXSubvoxels,profile.phaseXTurnsQ32);
|
||||
const std::int64_t turnY=world_assembly_detail::SurfaceProfileTurnAtCoordinate(
|
||||
y,profile.wavelengthYSubvoxels,profile.phaseYTurnsQ32);
|
||||
return profile.h0Subvoxels
|
||||
+world_assembly_detail::MulQ30(profile.amplitudeXSubvoxels,
|
||||
world_assembly_detail::CosineQ30(turnX))
|
||||
+world_assembly_detail::MulQ30(profile.amplitudeYSubvoxels,
|
||||
world_assembly_detail::CosineQ30(turnY));
|
||||
};
|
||||
const auto routeXY=CoreLiminalRouteXYV2(-12*qPerMetre,0,12*qPerMetre,0,qPerMetre);
|
||||
const auto squareRoot=[](std::uint64_t value)
|
||||
{return world_assembly_detail::IntegerSqrt(value);};
|
||||
const auto directionQ16=[&](std::int64_t dx,std::int64_t dy)
|
||||
{
|
||||
const std::uint64_t length=squareRoot(static_cast<std::uint64_t>(dx*dx+dy*dy));
|
||||
if(length==0)return std::array<std::int32_t,2>{{0,0}};
|
||||
const auto rounded=[&](std::int64_t value)
|
||||
{return value>=0?(value+static_cast<std::int64_t>(length/2))
|
||||
/static_cast<std::int64_t>(length)
|
||||
:(value-static_cast<std::int64_t>(length/2))
|
||||
/static_cast<std::int64_t>(length);};
|
||||
return std::array<std::int32_t,2>{{
|
||||
static_cast<std::int32_t>(rounded(dx*65536)),
|
||||
static_cast<std::int32_t>(rounded(dy*65536))}};
|
||||
};
|
||||
const auto arrivalDirection=directionQ16(routeXY[1][0]-routeXY[0][0],
|
||||
routeXY[1][1]-routeXY[0][1]);
|
||||
const auto departureDirection=directionQ16(routeXY.back()[0]-routeXY[routeXY.size()-2][0],
|
||||
routeXY.back()[1]-routeXY[routeXY.size()-2][1]);
|
||||
const std::int64_t playerCenterAboveFloor=micrometresToQ(1130000);
|
||||
const std::int64_t landingClearance=micrometresToQ(6000000);
|
||||
const std::int64_t supportDepth=micrometresToQ(2000000);
|
||||
const auto makePad=[&](std::string id,ResolvedLayoutLandingPadRoleV1 role,
|
||||
const std::array<std::int64_t,2>& xy,const std::array<std::int32_t,2>& direction)
|
||||
{
|
||||
ResolvedLayoutLandingPadV1 pad;
|
||||
pad.stableId=std::move(id);pad.role=role;
|
||||
pad.floorHeightSubvoxels=groundHeightAt(xy[0],xy[1]);
|
||||
pad.positionSubvoxels={xy[0],xy[1],pad.floorHeightSubvoxels+playerCenterAboveFloor};
|
||||
pad.approachDirectionQ16=direction;
|
||||
pad.clearanceSubvoxels=landingClearance;
|
||||
pad.supportDepthSubvoxels=supportDepth;
|
||||
pad.certifiedWalkableWithSupport=true;
|
||||
return pad;
|
||||
};
|
||||
resolved->recipeId=std::string(CoreLiminalFieldRecipeIdV1);
|
||||
resolved->recipeVersion=CoreLiminalFieldRecipeVersionV2;
|
||||
resolved->landingPads.clear();
|
||||
resolved->landingPads.push_back(makePad("core/liminal-field/landing/arrival/000",
|
||||
ResolvedLayoutLandingPadRoleV1::arrival,routeXY.front(),arrivalDirection));
|
||||
resolved->landingPads.push_back(makePad("core/liminal-field/landing/departure/000",
|
||||
ResolvedLayoutLandingPadRoleV1::departure,routeXY.back(),departureDirection));
|
||||
std::uint64_t routeLength=0;
|
||||
stratum.route=BuildCoreLiminalRouteV2(profile,resolved->landingPads[0],
|
||||
resolved->landingPads[1],definition,seed,stratum.strateIndex,coreMicrons,
|
||||
qPerMetre,routeLength,error);
|
||||
if(!error.empty())return false;
|
||||
stratum.routeConstraints={1,micrometresToQ(340000),micrometresToQ(1760000),
|
||||
17560,micrometresToQ(125000),micrometresToQ(250000),
|
||||
micrometresToQ(1000000),micrometresToQ(120000),3};
|
||||
resolved->anchors.erase(std::remove_if(resolved->anchors.begin(),resolved->anchors.end(),
|
||||
[](const auto& anchor){return anchor.kind=="route_point"
|
||||
||anchor.kind=="liminal_route_point"||anchor.kind=="liminal_variant"
|
||||
||anchor.kind=="heightfield_profile";}),resolved->anchors.end());
|
||||
ResolvedLayoutAnchorV2 profileAnchor;
|
||||
profileAnchor.kind="heightfield_profile";
|
||||
profileAnchor.stableId="heightfield-profile/liminal/"
|
||||
+std::to_string(static_cast<std::uint32_t>(profile.kind));
|
||||
profileAnchor.units="profile kind enum";
|
||||
profileAnchor.values={1,static_cast<std::int64_t>(profile.kind)};
|
||||
resolved->anchors.push_back(std::move(profileAnchor));
|
||||
std::uint64_t routeAnchorDistance=0,lastRouteAnchorDistance=0;
|
||||
for(std::size_t pointIndex=0;pointIndex<stratum.route.size();++pointIndex)
|
||||
{
|
||||
if(pointIndex>0)
|
||||
{
|
||||
const auto& previous=stratum.route[pointIndex-1].positionSubvoxels;
|
||||
const auto& current=stratum.route[pointIndex].positionSubvoxels;
|
||||
const auto dx=static_cast<std::uint64_t>(current[0]-previous[0]);
|
||||
const auto dy=static_cast<std::uint64_t>(current[1]-previous[1]);
|
||||
routeAnchorDistance+=world_assembly_detail::IntegerSqrt(dx*dx+dy*dy);
|
||||
}
|
||||
const bool first=pointIndex==0;
|
||||
const bool last=pointIndex+1==stratum.route.size();
|
||||
const bool spaced=routeAnchorDistance-lastRouteAnchorDistance
|
||||
>=static_cast<std::uint64_t>(8*qPerMetre);
|
||||
if(!first&&!last&&!spaced)continue;
|
||||
const auto& point=stratum.route[pointIndex];
|
||||
ResolvedLayoutAnchorV2 routeAnchor;
|
||||
routeAnchor.kind="route_point";
|
||||
routeAnchor.stableId="route/"+std::to_string(pointIndex);
|
||||
routeAnchor.units="1/256 Core voxel";
|
||||
routeAnchor.values={point.positionSubvoxels[0],point.positionSubvoxels[1],
|
||||
point.positionSubvoxels[2]+playerCenterAboveFloor,point.radiusSubvoxels};
|
||||
resolved->anchors.push_back(std::move(routeAnchor));
|
||||
lastRouteAnchorDistance=routeAnchorDistance;
|
||||
}
|
||||
if(!layout.finalize(error))return false;
|
||||
const auto reportPad=[&](const ResolvedLayoutLandingPadV1& pad)
|
||||
{
|
||||
std::printf("[ResolvedLandingPad] strate=%d id=%s role=%s position_m=(%.3f,%.3f,%.3f) "
|
||||
"floor_m=%.3f approach_q16=(%d,%d) clearance_m=%.3f support_m=%.3f "
|
||||
"certified_walkable_with_support=true status=PASS\n",stratum.strateIndex,
|
||||
pad.stableId.c_str(),pad.role==ResolvedLayoutLandingPadRoleV1::arrival?"arrival":"departure",
|
||||
pad.positionSubvoxels[0]/static_cast<double>(qPerMetre),
|
||||
pad.positionSubvoxels[1]/static_cast<double>(qPerMetre),
|
||||
pad.positionSubvoxels[2]/static_cast<double>(qPerMetre),
|
||||
pad.floorHeightSubvoxels/static_cast<double>(qPerMetre),
|
||||
pad.approachDirectionQ16[0],pad.approachDirectionQ16[1],
|
||||
pad.clearanceSubvoxels/static_cast<double>(qPerMetre),
|
||||
pad.supportDepthSubvoxels/static_cast<double>(qPerMetre));
|
||||
};
|
||||
reportPad(resolved->landingPads[0]);reportPad(resolved->landingPads[1]);
|
||||
std::printf("[LiminalPadRoute] points=%zu path_length_m=%.3f spacing_max_m=4.000 "
|
||||
"profile_ground_following=true status=PASS\n",stratum.route.size(),
|
||||
routeLength/static_cast<double>(qPerMetre));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlanCoreLiminalFieldV2(WorldAssemblyRecipeV1& recipe,
|
||||
WorldAssemblyStratumPlanV1& stratum,const ResolvedLayoutSnapshotV3& layout,
|
||||
std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
if(layout.coreVoxelSizeMicrometres==0||stratum.strateIndex<0
|
||||
||stratum.topChunkZ<stratum.bottomChunkZ)
|
||||
{error="Core.Liminal.Field v2 needs a finite stratum and resolved Core voxel size";return false;}
|
||||
const auto resolved=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[&](const auto& value){return value.strateIndex==stratum.strateIndex
|
||||
&&value.recipeId==CoreLiminalFieldRecipeIdV1
|
||||
&&value.recipeVersion==CoreLiminalFieldRecipeVersionV2;});
|
||||
if(resolved==layout.strates.end()||!resolved->content.presentInCurrentField
|
||||
||stratum.surfaceProfile.version!=1)
|
||||
{error="Core.Liminal.Field v2 needs its prepared profile and resolved landing pads";return false;}
|
||||
const ResolvedLayoutLandingPadV1* arrivalPad=nullptr;
|
||||
const ResolvedLayoutLandingPadV1* departurePad=nullptr;
|
||||
for(const auto& pad:resolved->landingPads)
|
||||
{
|
||||
if(pad.role==ResolvedLayoutLandingPadRoleV1::arrival)
|
||||
{if(arrivalPad){error="Core.Liminal.Field v2 has multiple arrival pads";return false;}arrivalPad=&pad;}
|
||||
if(pad.role==ResolvedLayoutLandingPadRoleV1::departure)
|
||||
{if(departurePad){error="Core.Liminal.Field v2 has multiple departure pads";return false;}departurePad=&pad;}
|
||||
}
|
||||
if(!arrivalPad||!departurePad||!arrivalPad->certifiedWalkableWithSupport
|
||||
||!departurePad->certifiedWalkableWithSupport)
|
||||
{error="Core.Liminal.Field v2 requires certified arrival and departure pads";return false;}
|
||||
|
||||
const ResolvedLayoutPassageV1* arrival=nullptr;
|
||||
const ResolvedLayoutPassageV1* departure=nullptr;
|
||||
for(const auto& passage:layout.passages)
|
||||
{
|
||||
if(passage.lowerStrateIndex==stratum.strateIndex)
|
||||
{if(arrival){error="Core.Liminal.Field v2 requires one arrival passage";return false;}arrival=&passage;}
|
||||
if(passage.upperStrateIndex==stratum.strateIndex)
|
||||
{if(departure){error="Core.Liminal.Field v2 requires one departure passage";return false;}departure=&passage;}
|
||||
}
|
||||
if(!arrival||!departure||arrival->lowerMouthSubvoxels!=arrivalPad->positionSubvoxels
|
||||
||departure->upperMouthSubvoxels!=departurePad->positionSubvoxels)
|
||||
{error="Core.Liminal.Field v2 passage mouths are not bound to the certified layer pads";return false;}
|
||||
|
||||
constexpr std::int64_t Q=WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t coreMicrons=layout.coreVoxelSizeMicrometres;
|
||||
const auto micrometresToQ=[&](std::int64_t micrometres)
|
||||
{
|
||||
const std::int64_t numerator=micrometres*Q;
|
||||
return numerator>=0?(numerator+coreMicrons/2)/coreMicrons
|
||||
:(numerator-coreMicrons/2)/coreMicrons;
|
||||
};
|
||||
const std::int64_t qPerMetre=micrometresToQ(1000000);
|
||||
const std::int64_t playerCenterAboveFloor=micrometresToQ(1130000);
|
||||
if(qPerMetre<=0||arrivalPad->positionSubvoxels[2]
|
||||
!=arrivalPad->floorHeightSubvoxels+playerCenterAboveFloor
|
||||
||departurePad->positionSubvoxels[2]
|
||||
!=departurePad->floorHeightSubvoxels+playerCenterAboveFloor)
|
||||
{error="Core.Liminal.Field v2 pad floor and capsule-center heights differ";return false;}
|
||||
|
||||
const std::uint32_t seed=recipe.seed;
|
||||
const WorldAssemblyLiminalVariantV1 variant=SelectCoreLiminalFieldVariantV1(seed);
|
||||
const auto& definition=CoreLiminalFieldVariantsV1[static_cast<std::size_t>(variant)];
|
||||
const auto& profile=stratum.surfaceProfile;
|
||||
std::uint64_t totalHorizontal=0;
|
||||
std::vector<WorldAssemblyRoutePointV1> route=BuildCoreLiminalRouteV2(profile,
|
||||
*arrivalPad,*departurePad,definition,seed,stratum.strateIndex,coreMicrons,
|
||||
qPerMetre,totalHorizontal,error);
|
||||
if(!error.empty())return false;
|
||||
if(route.size()!=stratum.route.size())
|
||||
{error="Core.Liminal.Field v2 route differs from the path used to certify its pads";return false;}
|
||||
for(std::size_t index=0;index<route.size();++index)
|
||||
if(route[index].stableFeatureId!=stratum.route[index].stableFeatureId
|
||||
||route[index].positionSubvoxels!=stratum.route[index].positionSubvoxels
|
||||
||route[index].radiusSubvoxels!=stratum.route[index].radiusSubvoxels)
|
||||
{error="Core.Liminal.Field v2 route differs from the path used to certify its pads";return false;}
|
||||
|
||||
WorldAssemblyLiminalFieldPlanV1 liminal;
|
||||
liminal.version=CoreLiminalFieldRecipeVersionV2;
|
||||
liminal.variant=variant;liminal.seed=seed;
|
||||
liminal.arrivalMouthSubvoxels=arrivalPad->positionSubvoxels;
|
||||
liminal.departureMouthSubvoxels=departurePad->positionSubvoxels;
|
||||
liminal.floorHeightSubvoxels=profile.h0Subvoxels;
|
||||
liminal.routeHalfWidthSubvoxels=micrometresToQ(definition.routeHalfWidthMicrometres);
|
||||
liminal.routeHalfThicknessSubvoxels=micrometresToQ(definition.routeHalfThicknessMicrometres);
|
||||
liminal.routeOffsetSubvoxels=150*qPerMetre;
|
||||
liminal.supportSpacingSubvoxels=micrometresToQ(definition.supportSpacingMicrometres);
|
||||
liminal.supportRadiusSubvoxels=micrometresToQ(definition.supportRadiusMicrometres);
|
||||
liminal.mouthFloorOffsetSubvoxels=playerCenterAboveFloor;
|
||||
stratum.route=std::move(route);
|
||||
stratum.routeConstraints={1,micrometresToQ(340000),micrometresToQ(1760000),
|
||||
17560,micrometresToQ(125000),micrometresToQ(250000),
|
||||
micrometresToQ(1000000),micrometresToQ(120000),3};
|
||||
stratum.liminalField=liminal;
|
||||
std::string validationError;
|
||||
if(!world_assembly_detail::ValidateSurfaceProfile(stratum.surfaceProfile,seed,validationError)
|
||||
||!world_assembly_detail::ValidateStratum(stratum,recipe,validationError))
|
||||
{error=validationError.empty()?"Core.Liminal.Field v2 produced an invalid plan":validationError;return false;}
|
||||
std::printf("[CoreLiminalFieldPlan] recipe=%.*s version=%u variant=%.*s seed=%u "
|
||||
"arrival_pad=%s departure_pad=%s route_points=%zu path_length_m=%.3f "
|
||||
"profile_ground_following=true route_grade_limit_deg=15.000 status=PASS\n",
|
||||
static_cast<int>(CoreLiminalFieldRecipeIdV1.size()),CoreLiminalFieldRecipeIdV1.data(),
|
||||
liminal.version,static_cast<int>(definition.name.size()),definition.name.data(),seed,
|
||||
arrivalPad->stableId.c_str(),departurePad->stableId.c_str(),stratum.route.size(),
|
||||
totalHorizontal/static_cast<double>(qPerMetre));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string WorldAssemblyManifest::ToJson() const
|
||||
{
|
||||
std::string error;
|
||||
@@ -2391,6 +3183,27 @@ std::string WorldAssemblyManifest::ToJson() const
|
||||
<< ",\"minimum_clearance_q\":" << constraints.minimumClearanceSubvoxels
|
||||
<< ",\"minimum_support_probes\":" << constraints.minimumSupportProbes << '}';
|
||||
}
|
||||
if (stratum.liminalField.version != 0)
|
||||
{
|
||||
const auto& liminal = stratum.liminalField;
|
||||
out << ",\"liminal_field\":{\"version\":" << liminal.version
|
||||
<< ",\"variant\":" << static_cast<std::uint32_t>(liminal.variant)
|
||||
<< ",\"seed\":" << liminal.seed << ",\"arrival_mouth_q\":["
|
||||
<< liminal.arrivalMouthSubvoxels[0] << ','
|
||||
<< liminal.arrivalMouthSubvoxels[1] << ','
|
||||
<< liminal.arrivalMouthSubvoxels[2] << "],\"departure_mouth_q\":["
|
||||
<< liminal.departureMouthSubvoxels[0] << ','
|
||||
<< liminal.departureMouthSubvoxels[1] << ','
|
||||
<< liminal.departureMouthSubvoxels[2]
|
||||
<< "],\"mouth_floor_offset_q\":" << liminal.mouthFloorOffsetSubvoxels
|
||||
<< ",\"floor_height_q\":"
|
||||
<< liminal.floorHeightSubvoxels
|
||||
<< ",\"route_half_width_q\":" << liminal.routeHalfWidthSubvoxels
|
||||
<< ",\"route_half_thickness_q\":" << liminal.routeHalfThicknessSubvoxels
|
||||
<< ",\"route_offset_q\":" << liminal.routeOffsetSubvoxels
|
||||
<< ",\"support_spacing_q\":" << liminal.supportSpacingSubvoxels
|
||||
<< ",\"support_radius_q\":" << liminal.supportRadiusSubvoxels << '}';
|
||||
}
|
||||
out << ",\"route\":[";
|
||||
for (std::size_t p = 0; p < stratum.route.size(); ++p)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build three standalone liminal SurfaceWorld fixtures from the owner input."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l141"))
|
||||
|
||||
FLAT_FIELD = {
|
||||
"version": 1,
|
||||
"kind": "flatField",
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures")
|
||||
args = parser.parse_args()
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
base = json.loads(OWNER.read_text(encoding="utf-8"))
|
||||
|
||||
flat = dict(base)
|
||||
flat["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
flat["motif_family_override"] = {
|
||||
"version": 1,
|
||||
"strate_index": 0,
|
||||
"placement_policy": "seed,strate,stable_id",
|
||||
"motifs": [
|
||||
{
|
||||
"stable_id": "liminal/oversized-familiar-monolith-v1",
|
||||
"kind": "cappedCylinder",
|
||||
"radius_m": 1.6,
|
||||
"height_m": 26.0,
|
||||
"route_side_offset_m": 42.0,
|
||||
"placement_radius_m": 0.0,
|
||||
"fillet_radius_m": 0.6,
|
||||
"blend_width_m": 1.2,
|
||||
"minimum_spacing_m": 0.0,
|
||||
"yaw_quarter_turns": 0,
|
||||
"attachment": "seated_floor",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
hills = dict(base)
|
||||
hills["surface_profile_override"] = {
|
||||
"version": 1,
|
||||
"kind": "periodicHills",
|
||||
"amplitude_x_m": 2.2,
|
||||
"amplitude_y_m": 2.2,
|
||||
"wavelength_x_m": 80.0,
|
||||
"wavelength_y_m": 80.0,
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
|
||||
road = dict(base)
|
||||
road["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
road["liminal_road_override"] = {
|
||||
"stable_id": "liminal/raised-road-v1",
|
||||
"strate_index": 0,
|
||||
"position_xy_m": [240.0, 180.0],
|
||||
"half_extents_m": [32.5, 3.0, 0.75],
|
||||
"deck_height_above_field_m": 22.0,
|
||||
"yaw_quarter_turns": 0,
|
||||
}
|
||||
|
||||
for name, fixture in (("flatField-monolith", flat),
|
||||
("periodicHills", hills),
|
||||
("raised-road", road)):
|
||||
path = args.out / f"liminal-{name}.json"
|
||||
path.write_text(json.dumps(fixture, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L141Fixture] name={name} file={path} status=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L141 prototype determinism checks and create eye-level/wide captures."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l141"))
|
||||
CAPTURE_DEFAULT = Path(__file__).resolve().parent / "captures"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
Q_PER_METRE = 1024.0
|
||||
FOG_COLOR = "0.66,0.73,0.77"
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_step(name: str, command: list[object], log: Path, *, env: dict[str, str],
|
||||
stamped: bool = False, timeout_seconds: int = 1740) -> str:
|
||||
expected_stamp = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
binary = Path(str(command[0]))
|
||||
probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
||||
if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected_stamp:
|
||||
raise RuntimeError(f"{name} refused a stale executable: {binary}")
|
||||
started = time.monotonic()
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, f"timeout", f"{timeout_seconds}s",
|
||||
*map(str, command)], cwd=ROOT, env=env,
|
||||
stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=timeout_seconds + 5, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
print(f"[L141Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if stamped and (first_stamp(output) != expected_stamp or current_stamp() != expected_stamp):
|
||||
raise RuntimeError(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"{name} failed; see {log}")
|
||||
return output
|
||||
|
||||
|
||||
def route_frame(manifest_path: Path) -> tuple[tuple[float, float, float],
|
||||
tuple[float, float, float]]:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
strata = [entry for entry in manifest["plan"]["strates"]
|
||||
if entry.get("route") and len(entry["route"]) >= 2]
|
||||
if not strata:
|
||||
raise RuntimeError(f"no planned SurfaceWorld route in {manifest_path}")
|
||||
route = strata[0]["route"]
|
||||
points = [tuple(float(value) / Q_PER_METRE for value in item["position_q"])
|
||||
for item in route]
|
||||
lengths = [math.dist(a, b) for a, b in zip(points, points[1:])]
|
||||
total = sum(lengths)
|
||||
if total <= 1.0e-6:
|
||||
raise RuntimeError(f"degenerate planned route in {manifest_path}")
|
||||
|
||||
def at(distance: float) -> tuple[float, float, float]:
|
||||
remaining = max(0.0, min(total, distance))
|
||||
for index, (a, b, length) in enumerate(zip(points, points[1:], lengths)):
|
||||
if remaining <= length or index == len(lengths) - 1:
|
||||
if length <= 1.0e-9:
|
||||
return b
|
||||
t = remaining / length
|
||||
return tuple(a[axis] + (b[axis] - a[axis]) * t for axis in range(3))
|
||||
remaining -= length
|
||||
return points[-1]
|
||||
|
||||
center = at(total * 0.5)
|
||||
before, after = at(total * 0.5 - 1.0), at(total * 0.5 + 1.0)
|
||||
dx, dy = after[0] - before[0], after[1] - before[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length <= 1.0e-9:
|
||||
raise RuntimeError(f"degenerate route tangent in {manifest_path}")
|
||||
return center, (dx / length, dy / length, 0.0)
|
||||
|
||||
|
||||
def camera_angles(camera, target) -> tuple[float, float]:
|
||||
dx, dy, dz = (target[index] - camera[index] for index in range(3))
|
||||
return math.degrees(math.atan2(dx, dy)), math.degrees(math.atan2(dz, math.hypot(dx, dy)))
|
||||
|
||||
|
||||
def point_toward(camera, target) -> tuple[float, ...]:
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
return (*camera, yaw, pitch)
|
||||
|
||||
|
||||
def stamp(layout: dict, stable_id: str) -> dict:
|
||||
for record in layout.get("stamps", []):
|
||||
if record.get("stable_id") == stable_id:
|
||||
return record
|
||||
raise RuntimeError(f"missing resolved stamp {stable_id}")
|
||||
|
||||
|
||||
def make_views(name: str, manifest_path: Path, layout_path: Path):
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
center, tangent = route_frame(manifest_path)
|
||||
tx, ty, _ = tangent
|
||||
nx, ny = -ty, tx
|
||||
base_height = manifest["recipe"]["surface_profile"]["h0_q"] / Q_PER_METRE
|
||||
if name == "flatField-monolith":
|
||||
monolith = stamp(layout, "liminal/oversized-familiar-monolith-v1")
|
||||
sx, sy, _ = (value / 1.0e6 for value in monolith["position_um"])
|
||||
floor = monolith["floor_um"] / 1.0e6
|
||||
eye_camera = (sx - tx * 36.0, sy - ty * 36.0, floor + 1.6)
|
||||
eye_target = (sx, sy, floor + 10.5)
|
||||
wide_camera = (sx - tx * 42.0 - nx * 18.0,
|
||||
sy - ty * 42.0 - ny * 18.0, floor + 42.0)
|
||||
wide_target = (sx, sy, floor + 8.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 24),
|
||||
"height_center": floor,
|
||||
"height_range": 38.0,
|
||||
}
|
||||
if name == "periodicHills":
|
||||
# Route points are capsule centers: +0.72 m places the view at 1.60 m above ground.
|
||||
eye_camera = (center[0], center[1], center[2] + 0.72)
|
||||
eye_target = (center[0] - tx * 45.0, center[1] - ty * 45.0,
|
||||
center[2] + 0.72)
|
||||
wide_camera = (center[0] - tx * 20.0 - nx * 20.0,
|
||||
center[1] - ty * 20.0 - ny * 20.0, center[2] + 34.0)
|
||||
wide_target = (center[0] - tx * 45.0, center[1] - ty * 45.0,
|
||||
center[2] + 0.72)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 28),
|
||||
"height_center": base_height,
|
||||
"height_range": 5.0,
|
||||
}
|
||||
road = stamp(layout, "liminal/raised-road-v1")
|
||||
rx, ry, rz = (value / 1.0e6 for value in road["position_um"])
|
||||
half_length, half_width, half_thickness = (
|
||||
value / 1.0e6 for value in road["half_extents_um"])
|
||||
deck = rz + half_thickness
|
||||
eye_camera = (rx - half_length + 14.0, ry, deck + 1.6)
|
||||
eye_target = (rx + half_length, ry, base_height)
|
||||
wide_camera = (rx, ry - 28.0, deck + 24.0)
|
||||
wide_target = (rx + 4.0, ry, base_height + 4.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 24),
|
||||
"wide": (wide_camera, wide_target, 26),
|
||||
"height_center": base_height + 11.0,
|
||||
"height_range": 32.0,
|
||||
"road_deck": deck,
|
||||
"road_width": 2.0 * half_width,
|
||||
}
|
||||
|
||||
|
||||
def headless_command(binary: Path, fixture: Path, camera, mesh: Path,
|
||||
manifest: Path, layout: Path, workers: int,
|
||||
clip_radius: int, *, hash_tiles: bool = False,
|
||||
audit_road: bool = False, max_level: int = 0) -> list[object]:
|
||||
command: list[object] = [binary, fixture, f"--workers={workers}",
|
||||
"--seed=0", "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={clip_radius}",
|
||||
f"--max-clip-level={max_level}",
|
||||
f"--max-level={max_level}", "--render-distance-chunks=0",
|
||||
f"--camera={fmt(point_toward(camera, (camera[0], camera[1] + 1.0, camera[2])))}",
|
||||
f"--mesh-export={mesh}", f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
if hash_tiles:
|
||||
command.append("--stream-hash-tiles")
|
||||
if audit_road:
|
||||
command.append("--audit-liminal-road-walk")
|
||||
return command
|
||||
|
||||
|
||||
def determinism_digest(output: str, manifest_path: Path,
|
||||
layout_path: Path) -> tuple[str, str, str]:
|
||||
tile_hashes = sorted(line for line in output.splitlines()
|
||||
if line.startswith("[PipelineTileHash] "))
|
||||
if not manifest_path.is_file() or not layout_path.is_file() or not tile_hashes:
|
||||
raise RuntimeError("determinism run omitted manifest, layout or per-tile hashes")
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
manifest_digest = (manifest["content_hash"],manifest["recipe"]["content_hash"],
|
||||
manifest["plan"]["content_hash"])
|
||||
return ("|".join(manifest_digest),layout["stable_identity"],"\n".join(tile_hashes))
|
||||
|
||||
|
||||
def run_determinism(name: str, fixture: Path, view, *, binaries: dict[str, Path],
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera, _, _ = view["eye"]
|
||||
clip_radius = 1
|
||||
max_clip = 0
|
||||
baseline = None
|
||||
cells = 0
|
||||
for compiler, workers in ((compiler, workers)
|
||||
for compiler in ("clang", "gcc")
|
||||
for workers in (1, 8, 12)):
|
||||
tag = f"{name}-{compiler}-w{workers}"
|
||||
run_dir = out / "determinism" / tag
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
mesh = run_dir / "feature.vfmesh"
|
||||
manifest = run_dir / "manifest.json"
|
||||
layout = run_dir / "layout.json"
|
||||
command = headless_command(binaries[compiler], fixture,camera,mesh,manifest,
|
||||
layout,workers,max_clip,hash_tiles=True)
|
||||
output = run_step(tag,command,out/"logs"/f"determinism-{tag}.log",
|
||||
env=env,stamped=True)
|
||||
digest = determinism_digest(output,manifest,layout)
|
||||
if baseline is None:
|
||||
baseline = digest
|
||||
elif digest != baseline:
|
||||
raise RuntimeError(f"{name} diverged at {compiler} W{workers}; see determinism logs")
|
||||
cells += 1
|
||||
print(f"[L141Determinism] fixture={name} compiler_worker_cells={cells} identical=yes status=PASS",
|
||||
flush=True)
|
||||
|
||||
|
||||
def export_and_capture(name: str, fixture: Path, views: dict, *,
|
||||
headless: Path, renderer: Path, out: Path,
|
||||
artifact_dir: Path, env: dict[str, str]) -> None:
|
||||
for view_name in ("eye", "wide"):
|
||||
camera, target, clip_radius = views[view_name]
|
||||
clip_radius = min(clip_radius,7)
|
||||
stem = f"{name}-{view_name}"
|
||||
mesh = out / "capture-meshes" / f"{stem}.vfmesh"
|
||||
manifest = out / "capture-manifests" / f"{stem}.json"
|
||||
layout = out / "capture-layouts" / f"{stem}.json"
|
||||
png = out / "capture-png" / f"{stem}.png"
|
||||
image = artifact_dir / f"{stem}.jpg"
|
||||
for path in (mesh,manifest,layout,png,image):
|
||||
path.parent.mkdir(parents=True,exist_ok=True)
|
||||
command = headless_command(headless,fixture,camera,mesh,manifest,layout,12,
|
||||
clip_radius,max_level=0)
|
||||
output = run_step(f"capture-{stem}-export",command,
|
||||
out/"logs"/f"capture-{stem}-export.log",env=env,stamped=True)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"{stem} produced an empty mesh")
|
||||
yaw, pitch = camera_angles(camera,target)
|
||||
height_center = views["height_center"]
|
||||
height_range = views["height_range"]
|
||||
renderer_command = [renderer,mesh,png,"--size=1600x900","--fov-deg=72",
|
||||
f"--camera-m={fmt(camera)}",f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}","--tile-skirts=off","--shade=readable",
|
||||
"--headlamp=off","--height-tint=on",f"--height-center-m={height_center:.6f}",
|
||||
f"--height-range-m={height_range:.6f}","--light-direction=-0.42,-0.58,0.70",
|
||||
"--fog=on","--fog-start-m=30","--fog-end-m=105",f"--fog-color={FOG_COLOR}"]
|
||||
run_step(f"capture-{stem}-render",renderer_command,
|
||||
out/"logs"/f"capture-{stem}-render.log",env=env,stamped=True)
|
||||
run_step(f"capture-{stem}-jpeg",["ffmpeg","-nostdin","-hide_banner","-loglevel",
|
||||
"error","-y","-i",png,"-frames:v","1","-q:v","4",image],
|
||||
out/"logs"/f"capture-{stem}-jpeg.log",env=env)
|
||||
quality = run_step(f"capture-{stem}-quality",
|
||||
[sys.executable,ROOT/"Standalone/Tools/verify/capture_quality.py",image],
|
||||
out/"logs"/f"capture-{stem}-quality.log",env=env)
|
||||
if "[CaptureQuality]" not in quality or not image.is_file() \
|
||||
or image.stat().st_size > 300_000:
|
||||
raise RuntimeError(f"{stem} capture failed quality or size checks")
|
||||
eye_height = "1.600m" if view_name == "eye" else "wide"
|
||||
print(f"[L141Capture] fixture={name} view={view_name} eye_height={eye_height} "
|
||||
f"camera_m=({fmt(camera)}) target_m=({fmt(target)}) bytes={image.stat().st_size} "
|
||||
f"sha256={sha256(image)} status=PASS",flush=True)
|
||||
|
||||
|
||||
def run_road_walk_audit(fixture: Path, view, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera = view["eye"][0]
|
||||
run_dir = out / "road-walk-audit"
|
||||
run_dir.mkdir(parents=True,exist_ok=True)
|
||||
output = run_step("road-walk-audit",headless_command(headless,fixture,camera,
|
||||
run_dir/"audit.vfmesh",run_dir/"manifest.json",run_dir/"layout.json",12,1,
|
||||
audit_road=True),out/"logs"/"road-walk-audit.log",env=env,stamped=True)
|
||||
if not re.search(r"\[LiminalRoadWalkAudit\].*status=PASS",output):
|
||||
raise RuntimeError("raised road failed the Core capsule walk audit")
|
||||
|
||||
|
||||
def measure_near_ready(fixture: Path, views: dict, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
uptime = subprocess.run(["uptime"],cwd=ROOT,capture_output=True,text=True,check=True).stdout.strip()
|
||||
load_match = re.search(r"load average:\s*([0-9.]+)",uptime)
|
||||
if not load_match or float(load_match.group(1)) >= 1.5:
|
||||
raise RuntimeError(f"near-ready timing requires a quiet machine: {uptime}")
|
||||
print(f"[L141TimingEnvironment] window=one W12 run after one warmup uptime='{uptime}' "
|
||||
f"status=PASS",flush=True)
|
||||
camera = views["eye"][0]
|
||||
warm_dir = out / "near-ready" / "warmup"
|
||||
timed_dir = out / "near-ready" / "measured"
|
||||
warm_dir.mkdir(parents=True,exist_ok=True)
|
||||
timed_dir.mkdir(parents=True,exist_ok=True)
|
||||
|
||||
def command(directory: Path) -> list[object]:
|
||||
return [headless,fixture,"--workers=12","--world-assembler-prototype",
|
||||
"--seed=0","--stream-benchmark","--stream-pipeline","--force-generate",
|
||||
"--clip-radius=1","--max-clip-level=2","--max-level=2",
|
||||
"--render-distance-chunks=0",
|
||||
f"--camera={fmt(point_toward(camera,(camera[0],camera[1]+1.0,camera[2])))}",
|
||||
f"--world-assembler-manifest-output={directory/'manifest.json'}",
|
||||
f"--resolved-layout-output={directory/'layout.json'}"]
|
||||
|
||||
run_step("near-ready-warmup",command(warm_dir),
|
||||
out/"logs"/"near-ready-warmup.log",env=env,stamped=True)
|
||||
output = run_step("near-ready-measured",command(timed_dir),
|
||||
out/"logs"/"near-ready-measured.log",env=env,stamped=True)
|
||||
match = re.search(r"^\[WorldAssemblerNearLODPipelineReady\] lods=0-2 "
|
||||
r"tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$",output,re.M)
|
||||
if not match or match.group(1)!=match.group(2):
|
||||
raise RuntimeError("near-ready run did not complete the full LOD 0-2 set")
|
||||
print(f"[L141NearReady] window=one W12 run after one warmup clip_radius=1 "
|
||||
f"tiles={match.group(1)}/{match.group(2)} cold_ready_s={match.group(3)} status=PASS",
|
||||
flush=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT)
|
||||
parser.add_argument("--artifact-dir", type=Path, default=CAPTURE_DEFAULT)
|
||||
parser.add_argument("--headless-clang", type=Path, required=True)
|
||||
parser.add_argument("--headless-gcc", type=Path, required=True)
|
||||
parser.add_argument("--renderer", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
out = args.out.resolve()
|
||||
artifact_dir = args.artifact_dir.resolve()
|
||||
fixture_dir = out / "fixtures"
|
||||
for directory in (out,fixture_dir,artifact_dir):
|
||||
directory.mkdir(parents=True,exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(out)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT/"Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(out/"xdg-cache")
|
||||
run_step("build-fixtures",[sys.executable,
|
||||
ROOT/"Standalone/Research/WorldAssemblerM3/Linux141/BuildL141Fixtures.py",
|
||||
f"--out={fixture_dir}"],out/"logs"/"build-fixtures.log",env=env)
|
||||
|
||||
binaries = {"clang":args.headless_clang.resolve(),"gcc":args.headless_gcc.resolve()}
|
||||
fixtures = {
|
||||
"flatField-monolith":fixture_dir/"liminal-flatField-monolith.json",
|
||||
"periodicHills":fixture_dir/"liminal-periodicHills.json",
|
||||
"raised-road":fixture_dir/"liminal-raised-road.json",
|
||||
}
|
||||
probed = {}
|
||||
for name,fixture in fixtures.items():
|
||||
probe_dir=out/"plan-probes"/name
|
||||
probe_dir.mkdir(parents=True,exist_ok=True)
|
||||
camera=(0.0,0.0,-500.0)
|
||||
output=run_step(f"plan-probe-{name}",headless_command(binaries["clang"],fixture,
|
||||
camera,probe_dir/"probe.vfmesh",probe_dir/"manifest.json",
|
||||
probe_dir/"layout.json",12,1),out/"logs"/f"plan-probe-{name}.log",
|
||||
env=env,stamped=True)
|
||||
if "[WorldAssemblerFullManifest]" not in output:
|
||||
raise RuntimeError(f"{name} probe omitted its full plan manifest")
|
||||
probed[name]=make_views(name,probe_dir/"manifest.json",probe_dir/"layout.json")
|
||||
|
||||
for name,fixture in fixtures.items():
|
||||
run_determinism(name,fixture,probed[name],binaries=binaries,out=out,env=env)
|
||||
run_road_walk_audit(fixtures["raised-road"],probed["raised-road"],
|
||||
headless=binaries["clang"],out=out,env=env)
|
||||
for name,fixture in fixtures.items():
|
||||
export_and_capture(name,fixture,probed[name],headless=binaries["clang"],
|
||||
renderer=args.renderer.resolve(),out=out,artifact_dir=artifact_dir,env=env)
|
||||
measure_near_ready(fixtures["raised-road"],probed["raised-road"],
|
||||
headless=binaries["clang"],out=out,env=env)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L141Capture] status=FAIL error={error}",file=sys.stderr,flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,40 @@
|
||||
# L141 — first liminal layer
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
The standalone fixtures and six captures are ready for review. The flat field has one oversized capped-cylinder monolith. The road is a 65 m `fillDais` platform, 6 m wide, with its deck 22 m above a flat field. Fog supplies the road's cloud-sea backdrop in these standalone images.
|
||||
|
||||
The `periodicHills` fixture uses the validated 80 m periods and 2.2 m amplitudes on both axes. Its field repeats exactly, but these captures show one broad rise rather than hills repeating to the horizon. The validated period is longer than the 56 m eye-level capture radius; the wider full-detail capture exceeded Core's 160-million-node interval bound. Smaller periods fail the existing profile validator. A capture extent that trims vertical tiles independently from horizontal tiles is the next useful step; this round leaves the Core profile limits unchanged.
|
||||
|
||||
## Changes
|
||||
|
||||
- Reused Core's `flatField`, `periodicHills`, `fillMonolith`, and `fillDais` support. Added standalone fixture inputs and runners in `BuildL141Fixtures.py` and `CaptureL141.py`.
|
||||
- Added `liminal_road_override` as a standalone adapter to the existing free-placement `fillDais` stamp, plus `--audit-liminal-road-walk`. The walk audit samples the Core field with the existing capsule sampler and checks width and grade.
|
||||
- Added optional distance fog to `LinuxOffscreenCapture` for the screenshots. The fog is capture-only; Unreal cloud rendering remains Unreal-side.
|
||||
- No evaluator change or Unreal `Source/` change. Owner output hashes match their pinned L130/v227 canonical rows in the fast tier.
|
||||
|
||||
## Measurements
|
||||
|
||||
- Determinism: each fixture matched across Clang and GCC at W1, W8, and W12 (six cells per fixture). The matrix used LOD0, clip radius 1; each cell compares recipe/plan hashes, resolved-layout identity, and per-tile hashes. Individual logs are in `Saved/Linux/l141/logs/determinism-*.log`.
|
||||
- Raised-road capsule walk: 127 stations, 8,001 capsule samples, zero blocked samples, zero unsupported stations, 6.000 m width against a 0.680 m capsule diameter, and 0.000° grade. See `Saved/Linux/l141/logs/road-walk-audit.log`.
|
||||
- Near-ready: one W12 LOD0–2 sample after one warm-up, clip radius 1, 139/139 tiles. The measured `cold_ready_s` was **0.173886 s**. The window was one run after one warm-up; `uptime` showed a 1-minute load average of 1.31 and no other build was running. See `Saved/Linux/l141/logs/near-ready-measured.log`.
|
||||
- Each capture is 1600×900 and passed `capture_quality.py`. Eye views place the camera 1.6 m above the field or road deck. Camera coordinates and image hashes are recorded in the capture runner output; export and render logs are under `Saved/Linux/l141/logs/`.
|
||||
|
||||
## Checks
|
||||
|
||||
- Clang 19 and GCC standalone builds passed with FP contraction off, four jobs, and source stamp `0a2828c398aa62870690667901d44f2daf5b1a5547b9640da6dbb01df721af8b`.
|
||||
- `python3 Standalone/Tools/verify/verify.py fast --out Saved/Linux/l141 --overall-timeout 1740` passed in 285.7 s. Owner SurfaceWorld and FloatingIslands W1/W12 canonical hashes matched the L130/v227 baselines.
|
||||
- Fixture determinism, dedicated road walk audit, six standalone captures, and all six image-quality checks passed.
|
||||
- Python bytecode compilation and `git diff --check` passed.
|
||||
- An exploratory broad `--audit-resolved-layout-passages` invocation also ran unrelated passage ripple checks and reported 2/3 passage ripple records passing, with `passage/1-2/0` failing. The focused liminal road audit passes; no legacy threshold was changed.
|
||||
- No Unreal build was run because no Unreal `Source/` file changed. No timing matrix was requested or run.
|
||||
|
||||
## Remaining risk
|
||||
|
||||
The hill screenshots do not yet show multiple full repeats to the horizon. The profile itself is exactly periodic; the standalone capture volume is too short at the current full-detail safety bound. The raised road is geometric and walk-audited, while its clouds are represented by fog in the capture renderer.
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the L142 liminal pictures and walk-audit controls."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l142"))
|
||||
|
||||
FLAT_FIELD = {
|
||||
"version": 1,
|
||||
"kind": "flatField",
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures")
|
||||
args = parser.parse_args()
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
base = json.loads(OWNER.read_text(encoding="utf-8"))
|
||||
|
||||
monolith = dict(base)
|
||||
monolith["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
monolith["motif_family_override"] = {
|
||||
"version": 1,
|
||||
"strate_index": 0,
|
||||
"placement_policy": "seed,strate,stable_id",
|
||||
"motifs": [{
|
||||
"stable_id": "liminal/oversized-familiar-monolith-v1",
|
||||
"kind": "cappedCylinder",
|
||||
"radius_m": 1.6,
|
||||
"height_m": 26.0,
|
||||
"route_side_offset_m": 42.0,
|
||||
"placement_radius_m": 0.0,
|
||||
"fillet_radius_m": 0.6,
|
||||
"blend_width_m": 1.2,
|
||||
"minimum_spacing_m": 0.0,
|
||||
"yaw_quarter_turns": 0,
|
||||
"attachment": "seated_floor",
|
||||
}],
|
||||
}
|
||||
|
||||
hills = dict(base)
|
||||
hills["surface_profile_override"] = {
|
||||
"version": 1,
|
||||
"kind": "periodicHills",
|
||||
"amplitude_x_m": 2.2,
|
||||
"amplitude_y_m": 2.2,
|
||||
"wavelength_x_m": 80.0,
|
||||
"wavelength_y_m": 80.0,
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
|
||||
road_override = {
|
||||
"stable_id": "liminal/road-clouds-v2",
|
||||
"strate_index": 0,
|
||||
"start_xy_m": [120.0, 340.0],
|
||||
"path_length_m": 360.0,
|
||||
"curve_control_offset_m": 84.0,
|
||||
"deck_height_above_field_m": 44.0,
|
||||
"rise_control_m": 22.0,
|
||||
"half_width_m": 4.0,
|
||||
"half_thickness_m": 1.0,
|
||||
"sample_spacing_m": 2.0,
|
||||
"support_spacing_m": 24.0,
|
||||
"support_radius_m": 0.55,
|
||||
}
|
||||
road = dict(base)
|
||||
road["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
road["liminal_road_override"] = dict(road_override)
|
||||
|
||||
short_control = dict(base)
|
||||
short_control["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
short_override = dict(road_override)
|
||||
short_override["path_length_m"] = 180.0
|
||||
short_control["liminal_road_override"] = short_override
|
||||
|
||||
pillars = dict(base)
|
||||
pillars["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
pillars["liminal_pillar_grid_override"] = {
|
||||
"stable_id": "liminal/horizon-pillar-grid-v1",
|
||||
"strate_index": 0,
|
||||
"center_xy_m": [700.0, 800.0],
|
||||
"rows": 15,
|
||||
"columns": 15,
|
||||
"spacing_m": 76.0,
|
||||
"radius_m": 5.5,
|
||||
"height_m": 40.0,
|
||||
}
|
||||
|
||||
fixtures = (
|
||||
("flatField-monolith", monolith),
|
||||
("periodicHills", hills),
|
||||
("road-in-clouds", road),
|
||||
("road-short-control", short_control),
|
||||
("pillar-grid", pillars),
|
||||
)
|
||||
for name, fixture in fixtures:
|
||||
path = args.out / f"liminal-{name}.json"
|
||||
path.write_text(json.dumps(fixture, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L142Fixture] name={name} file={path} status=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,433 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L142 liminal determinism checks, walk audits and multi-LOD captures."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l142"))
|
||||
CAPTURE_DEFAULT = Path(__file__).resolve().parent / "captures"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
Q_PER_METRE = 1024.0
|
||||
FOG_COLOR = "0.66,0.73,0.77"
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_step(name: str, command: list[object], log: Path, *, env: dict[str, str],
|
||||
stamped: bool = False, timeout_seconds: int = 1740,
|
||||
expected_exit: int = 0) -> str:
|
||||
expected_stamp = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
binary = Path(str(command[0]))
|
||||
probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
||||
if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected_stamp:
|
||||
raise RuntimeError(f"{name} refused a stale executable: {binary}")
|
||||
started = time.monotonic()
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, f"timeout", f"{timeout_seconds}s",
|
||||
*map(str, command)], cwd=ROOT, env=env,
|
||||
stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=timeout_seconds + 5, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
print(f"[L142Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if stamped and (first_stamp(output) != expected_stamp or current_stamp() != expected_stamp):
|
||||
raise RuntimeError(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != expected_exit:
|
||||
raise RuntimeError(f"{name} exit={result.returncode}, expected={expected_exit}; see {log}")
|
||||
return output
|
||||
|
||||
|
||||
def route_frame(manifest_path: Path) -> tuple[tuple[float, float, float],
|
||||
tuple[float, float, float]]:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
strata = [entry for entry in manifest["plan"]["strates"]
|
||||
if entry.get("route") and len(entry["route"]) >= 2]
|
||||
if not strata:
|
||||
raise RuntimeError(f"no planned SurfaceWorld route in {manifest_path}")
|
||||
route = strata[0]["route"]
|
||||
points = [tuple(float(value) / Q_PER_METRE for value in item["position_q"])
|
||||
for item in route]
|
||||
lengths = [math.dist(a, b) for a, b in zip(points, points[1:])]
|
||||
total = sum(lengths)
|
||||
if total <= 1.0e-6:
|
||||
raise RuntimeError(f"degenerate planned route in {manifest_path}")
|
||||
|
||||
def at(distance: float) -> tuple[float, float, float]:
|
||||
remaining = max(0.0, min(total, distance))
|
||||
for index, (a, b, length) in enumerate(zip(points, points[1:], lengths)):
|
||||
if remaining <= length or index == len(lengths) - 1:
|
||||
if length <= 1.0e-9:
|
||||
return b
|
||||
t = remaining / length
|
||||
return tuple(a[axis] + (b[axis] - a[axis]) * t for axis in range(3))
|
||||
remaining -= length
|
||||
return points[-1]
|
||||
|
||||
center = at(total * 0.5)
|
||||
before, after = at(total * 0.5 - 1.0), at(total * 0.5 + 1.0)
|
||||
dx, dy = after[0] - before[0], after[1] - before[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length <= 1.0e-9:
|
||||
raise RuntimeError(f"degenerate route tangent in {manifest_path}")
|
||||
return center, (dx / length, dy / length, 0.0)
|
||||
|
||||
|
||||
def camera_angles(camera, target) -> tuple[float, float]:
|
||||
dx, dy, dz = (target[index] - camera[index] for index in range(3))
|
||||
return math.degrees(math.atan2(dx, dy)), math.degrees(math.atan2(dz, math.hypot(dx, dy)))
|
||||
|
||||
|
||||
def point_toward(camera, target) -> tuple[float, ...]:
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
return (*camera, yaw, pitch)
|
||||
|
||||
|
||||
def stamp(layout: dict, stable_id: str) -> dict:
|
||||
for record in layout.get("stamps", []):
|
||||
if record.get("stable_id") == stable_id:
|
||||
return record
|
||||
raise RuntimeError(f"missing resolved stamp {stable_id}")
|
||||
|
||||
|
||||
def make_views(name: str, manifest_path: Path, layout_path: Path):
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
center, tangent = route_frame(manifest_path)
|
||||
tx, ty, _ = tangent
|
||||
nx, ny = -ty, tx
|
||||
base_height = manifest["recipe"]["surface_profile"]["h0_q"] / Q_PER_METRE
|
||||
if name == "flatField-monolith":
|
||||
monolith = stamp(layout, "liminal/oversized-familiar-monolith-v1")
|
||||
sx, sy, _ = (value / 1.0e6 for value in monolith["position_um"])
|
||||
floor = monolith["floor_um"] / 1.0e6
|
||||
eye_camera = (sx - tx * 36.0, sy - ty * 36.0, floor + 1.6)
|
||||
eye_target = (sx, sy, floor + 10.5)
|
||||
wide_camera = (sx - tx * 42.0 - nx * 18.0,
|
||||
sy - ty * 42.0 - ny * 18.0, floor + 42.0)
|
||||
wide_target = (sx, sy, floor + 8.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 24),
|
||||
"height_center": floor,
|
||||
"height_range": 38.0,
|
||||
}
|
||||
if name == "periodicHills":
|
||||
# Route points are capsule centers: +0.72 m places the view at 1.60 m above ground.
|
||||
eye_camera = (center[0], center[1], center[2] + 0.72)
|
||||
eye_target = (center[0] - tx * 360.0, center[1] - ty * 360.0,
|
||||
center[2] + 0.72)
|
||||
wide_camera = (center[0] - tx * 36.0 - nx * 28.0,
|
||||
center[1] - ty * 36.0 - ny * 28.0, center[2] + 30.0)
|
||||
wide_target = (center[0] - tx * 310.0, center[1] - ty * 310.0,
|
||||
center[2] + 0.72)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 28),
|
||||
"height_center": base_height,
|
||||
"height_range": 6.0,
|
||||
}
|
||||
if name == "road-in-clouds":
|
||||
deck = sorted((row for row in layout.get("stamps", [])
|
||||
if row.get("stable_id", "").startswith("liminal/road-clouds-v2/deck/")),
|
||||
key=lambda row: row["stable_id"])
|
||||
if len(deck) < 2:
|
||||
raise RuntimeError("road layout has no resolved swept platform chain")
|
||||
points = [tuple(value / 1.0e6 for value in row["position_um"])
|
||||
for row in deck]
|
||||
tops = [point[2] + row["half_extents_um"][2] / 1.0e6
|
||||
for point, row in zip(points, deck)]
|
||||
index = max(1, len(points) // 12)
|
||||
camera_dx = points[index + 1][0] - points[index - 1][0]
|
||||
camera_dy = points[index + 1][1] - points[index - 1][1]
|
||||
camera_tangent_length = math.hypot(camera_dx, camera_dy)
|
||||
camera_tx, camera_ty = (camera_dx / camera_tangent_length,
|
||||
camera_dy / camera_tangent_length)
|
||||
camera_nx, camera_ny = -camera_ty, camera_tx
|
||||
eye_camera = (points[index][0] + camera_nx * 3.2,
|
||||
points[index][1] + camera_ny * 3.2, tops[index] + 1.6)
|
||||
eye_target = (points[index][0] + camera_tx * 90.0 + camera_nx * 60.0,
|
||||
points[index][1] + camera_ty * 90.0 + camera_ny * 60.0,
|
||||
tops[index] - 44.0)
|
||||
middle = len(points) // 2
|
||||
dx = points[middle + 1][0] - points[middle - 1][0]
|
||||
dy = points[middle + 1][1] - points[middle - 1][1]
|
||||
tangent_length = math.hypot(dx, dy)
|
||||
tx, ty = dx / tangent_length, dy / tangent_length
|
||||
nx, ny = -ty, tx
|
||||
wide_target_index = min(len(points) - 1, middle + 30)
|
||||
wide_camera = (points[middle][0] - tx * 35.0 + nx * 30.0,
|
||||
points[middle][1] - ty * 35.0 + ny * 30.0,
|
||||
tops[middle] + 25.0)
|
||||
wide_target = (points[wide_target_index][0], points[wide_target_index][1],
|
||||
tops[wide_target_index] - 5.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 3),
|
||||
"wide": (wide_camera, wide_target, 3),
|
||||
"height_center": base_height + 23.0,
|
||||
"height_range": 60.0,
|
||||
"road_deck": tops[middle],
|
||||
"road_width": 2.0 * deck[middle]["half_extents_um"][1] / 1.0e6,
|
||||
}
|
||||
if name == "pillar-grid":
|
||||
pillars = [row for row in layout.get("stamps", [])
|
||||
if row.get("stable_id", "").startswith("liminal/horizon-pillar-grid-v1/pillar/")]
|
||||
if len(pillars) < 9:
|
||||
raise RuntimeError("pillar-grid layout has too few resolved supports")
|
||||
xs = [row["position_um"][0] / 1.0e6 for row in pillars]
|
||||
ys = [row["position_um"][1] / 1.0e6 for row in pillars]
|
||||
floor = min((row["position_um"][2] - row["half_extents_um"][2]) / 1.0e6
|
||||
for row in pillars)
|
||||
cx, cy = (min(xs) + max(xs)) * 0.5, (min(ys) + max(ys)) * 0.5
|
||||
eye_camera = (min(xs) - 78.0, cy, floor + 1.6)
|
||||
eye_target = (max(xs) + 90.0, cy, floor + 1.6)
|
||||
wide_camera = (cx - 280.0, cy - 420.0, floor + 230.0)
|
||||
wide_target = (cx, cy, floor + 6.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 3),
|
||||
"wide": (wide_camera, wide_target, 3),
|
||||
"height_center": floor + 12.0,
|
||||
"height_range": 42.0,
|
||||
}
|
||||
raise RuntimeError(f"no capture framing for fixture {name}")
|
||||
|
||||
|
||||
def headless_command(binary: Path, fixture: Path, camera, mesh: Path,
|
||||
manifest: Path, layout: Path, workers: int,
|
||||
clip_radius: int, *, hash_tiles: bool = False,
|
||||
audit_road: bool = False, max_level: int = 0,
|
||||
far_lods: bool = False,
|
||||
max_clip_override: int | None = None) -> list[object]:
|
||||
max_clip_level = (max_clip_override if max_clip_override is not None else
|
||||
8 if far_lods else max_level)
|
||||
command: list[object] = [binary, fixture, f"--workers={workers}",
|
||||
"--seed=0", "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={clip_radius}",
|
||||
f"--max-clip-level={max_clip_level}",
|
||||
f"--max-level={max_level}", "--render-distance-chunks=0",
|
||||
f"--camera={fmt(point_toward(camera, (camera[0], camera[1] + 1.0, camera[2])))}",
|
||||
f"--mesh-export={mesh}", f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
if hash_tiles:
|
||||
command.append("--stream-hash-tiles")
|
||||
if audit_road:
|
||||
command.append("--audit-liminal-road-walk")
|
||||
return command
|
||||
|
||||
|
||||
def determinism_digest(output: str, manifest_path: Path,
|
||||
layout_path: Path) -> tuple[str, str, str]:
|
||||
tile_hashes = sorted(line for line in output.splitlines()
|
||||
if line.startswith("[PipelineTileHash] "))
|
||||
if not manifest_path.is_file() or not layout_path.is_file() or not tile_hashes:
|
||||
raise RuntimeError("determinism run omitted manifest, layout or per-tile hashes")
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
manifest_digest = (manifest["content_hash"],manifest["recipe"]["content_hash"],
|
||||
manifest["plan"]["content_hash"])
|
||||
return ("|".join(manifest_digest),layout["stable_identity"],"\n".join(tile_hashes))
|
||||
|
||||
|
||||
def run_determinism(name: str, fixture: Path, view, *, binaries: dict[str, Path],
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera, _, _ = view["eye"]
|
||||
clip_radius = 1
|
||||
max_clip = 0
|
||||
baseline = None
|
||||
cells = 0
|
||||
for compiler, workers in ((compiler, workers)
|
||||
for compiler in ("clang", "gcc")
|
||||
for workers in (1, 8, 12)):
|
||||
tag = f"{name}-{compiler}-w{workers}"
|
||||
run_dir = out / "determinism" / tag
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
mesh = run_dir / "feature.vfmesh"
|
||||
manifest = run_dir / "manifest.json"
|
||||
layout = run_dir / "layout.json"
|
||||
command = headless_command(binaries[compiler], fixture,camera,mesh,manifest,
|
||||
layout,workers,max_clip,hash_tiles=True)
|
||||
output = run_step(tag,command,out/"logs"/f"determinism-{tag}.log",
|
||||
env=env,stamped=True)
|
||||
digest = determinism_digest(output,manifest,layout)
|
||||
if baseline is None:
|
||||
baseline = digest
|
||||
elif digest != baseline:
|
||||
raise RuntimeError(f"{name} diverged at {compiler} W{workers}; see determinism logs")
|
||||
cells += 1
|
||||
print(f"[L142Determinism] fixture={name} compiler_worker_cells={cells} identical=yes status=PASS",
|
||||
flush=True)
|
||||
|
||||
|
||||
def export_and_capture(name: str, fixture: Path, views: dict, *,
|
||||
headless: Path, renderer: Path, out: Path,
|
||||
artifact_dir: Path, env: dict[str, str]) -> None:
|
||||
for view_name in ("eye", "wide"):
|
||||
camera, target, clip_radius = views[view_name]
|
||||
clip_radius = min(clip_radius,3)
|
||||
stem = f"{name}-{view_name}"
|
||||
mesh = out / "capture-meshes" / f"{stem}.vfmesh"
|
||||
manifest = out / "capture-manifests" / f"{stem}.json"
|
||||
layout = out / "capture-layouts" / f"{stem}.json"
|
||||
png = out / "capture-png" / f"{stem}.png"
|
||||
image = artifact_dir / f"{stem}.jpg"
|
||||
for path in (mesh,manifest,layout,png,image):
|
||||
path.parent.mkdir(parents=True,exist_ok=True)
|
||||
command = headless_command(headless,fixture,camera,mesh,manifest,layout,12,
|
||||
clip_radius,max_level=-1,far_lods=True,
|
||||
max_clip_override=(2 if name == "road-in-clouds" and view_name == "wide"
|
||||
else 3 if name == "road-in-clouds" else None))
|
||||
output = run_step(f"capture-{stem}-export",command,
|
||||
out/"logs"/f"capture-{stem}-export.log",env=env,stamped=True)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"{stem} produced an empty mesh")
|
||||
yaw, pitch = camera_angles(camera,target)
|
||||
height_center = views["height_center"]
|
||||
height_range = views["height_range"]
|
||||
if name == "road-in-clouds":
|
||||
fog_start,fog_end,fog_color=(24,260,"0.88,0.93,0.95")
|
||||
elif name == "pillar-grid":
|
||||
fog_start,fog_end,fog_color=220,1250,"0.82,0.88,0.91"
|
||||
elif name == "periodicHills":
|
||||
fog_start,fog_end,fog_color=210,1000,"0.78,0.84,0.88"
|
||||
else:
|
||||
fog_start,fog_end,fog_color=30,105,FOG_COLOR
|
||||
renderer_command = [renderer,mesh,png,"--size=1600x900","--fov-deg=72",
|
||||
f"--camera-m={fmt(camera)}",f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}","--tile-skirts=on","--shade=readable",
|
||||
"--headlamp=off","--height-tint=on",f"--height-center-m={height_center:.6f}",
|
||||
f"--height-range-m={height_range:.6f}","--light-direction=-0.42,-0.58,0.70",
|
||||
"--fog=on",f"--fog-start-m={fog_start}",f"--fog-end-m={fog_end}",
|
||||
f"--fog-color={fog_color}"]
|
||||
run_step(f"capture-{stem}-render",renderer_command,
|
||||
out/"logs"/f"capture-{stem}-render.log",env=env,stamped=True)
|
||||
run_step(f"capture-{stem}-jpeg",["ffmpeg","-nostdin","-hide_banner","-loglevel",
|
||||
"error","-y","-i",png,"-frames:v","1","-q:v","4",image],
|
||||
out/"logs"/f"capture-{stem}-jpeg.log",env=env)
|
||||
quality = run_step(f"capture-{stem}-quality",
|
||||
[sys.executable,ROOT/"Standalone/Tools/verify/capture_quality.py",image],
|
||||
out/"logs"/f"capture-{stem}-quality.log",env=env)
|
||||
if "[CaptureQuality]" not in quality or not image.is_file() \
|
||||
or image.stat().st_size > 300_000:
|
||||
raise RuntimeError(f"{stem} capture failed quality or size checks")
|
||||
eye_height = "1.600m" if view_name == "eye" else "wide"
|
||||
print(f"[L142Capture] fixture={name} view={view_name} eye_height={eye_height} "
|
||||
f"camera_m=({fmt(camera)}) target_m=({fmt(target)}) bytes={image.stat().st_size} "
|
||||
f"sha256={sha256(image)} status=PASS",flush=True)
|
||||
|
||||
|
||||
def run_road_walk_audit(fixture: Path, view, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera = view["eye"][0]
|
||||
run_dir = out / "road-walk-audit"
|
||||
run_dir.mkdir(parents=True,exist_ok=True)
|
||||
output = run_step("road-walk-audit",headless_command(headless,fixture,camera,
|
||||
run_dir/"audit.vfmesh",run_dir/"manifest.json",run_dir/"layout.json",12,1,
|
||||
audit_road=True),out/"logs"/"road-walk-audit.log",env=env,stamped=True)
|
||||
if not re.search(r"\[LiminalRoadWalkAudit\].*status=PASS",output):
|
||||
raise RuntimeError("raised road failed the Core capsule walk audit")
|
||||
|
||||
|
||||
def run_road_short_negative_control(fixture: Path, view, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera = view["eye"][0]
|
||||
run_dir = out / "road-short-negative-control"
|
||||
run_dir.mkdir(parents=True,exist_ok=True)
|
||||
output = run_step("road-short-negative-control",
|
||||
headless_command(headless,fixture,camera,run_dir/"control.vfmesh",
|
||||
run_dir/"manifest.json",run_dir/"layout.json",12,1,audit_road=True),
|
||||
out/"logs"/"road-short-negative-control.log",env=env,stamped=True,
|
||||
expected_exit=14)
|
||||
match = re.search(r"\[LiminalRoadWalkAudit\].*path_length_m=([0-9.]+).*status=FAIL",output)
|
||||
if not match or float(match.group(1)) >= 300.0:
|
||||
raise RuntimeError("short-road negative control did not falsify the length gate")
|
||||
print(f"[L142RoadNegativeControl] path_length_m={match.group(1)} "
|
||||
"expected=FAIL status=PASS",flush=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT)
|
||||
parser.add_argument("--artifact-dir", type=Path, default=CAPTURE_DEFAULT)
|
||||
parser.add_argument("--headless-clang", type=Path, required=True)
|
||||
parser.add_argument("--headless-gcc", type=Path, required=True)
|
||||
parser.add_argument("--renderer", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
out = args.out.resolve()
|
||||
artifact_dir = args.artifact_dir.resolve()
|
||||
fixture_dir = out / "fixtures"
|
||||
for directory in (out,fixture_dir,artifact_dir):
|
||||
directory.mkdir(parents=True,exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(out)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT/"Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(out/"xdg-cache")
|
||||
run_step("build-fixtures",[sys.executable,
|
||||
ROOT/"Standalone/Research/WorldAssemblerM3/Linux142/BuildL142Fixtures.py",
|
||||
f"--out={fixture_dir}"],out/"logs"/"build-fixtures.log",env=env)
|
||||
|
||||
binaries = {"clang":args.headless_clang.resolve(),"gcc":args.headless_gcc.resolve()}
|
||||
fixtures = {
|
||||
"flatField-monolith":fixture_dir/"liminal-flatField-monolith.json",
|
||||
"periodicHills":fixture_dir/"liminal-periodicHills.json",
|
||||
"road-in-clouds":fixture_dir/"liminal-road-in-clouds.json",
|
||||
"pillar-grid":fixture_dir/"liminal-pillar-grid.json",
|
||||
}
|
||||
probed = {}
|
||||
for name,fixture in fixtures.items():
|
||||
probe_dir=out/"plan-probes"/name
|
||||
probe_dir.mkdir(parents=True,exist_ok=True)
|
||||
camera=(0.0,0.0,-500.0)
|
||||
output=run_step(f"plan-probe-{name}",headless_command(binaries["clang"],fixture,
|
||||
camera,probe_dir/"probe.vfmesh",probe_dir/"manifest.json",
|
||||
probe_dir/"layout.json",12,1),out/"logs"/f"plan-probe-{name}.log",
|
||||
env=env,stamped=True)
|
||||
if "[WorldAssemblerFullManifest]" not in output:
|
||||
raise RuntimeError(f"{name} probe omitted its full plan manifest")
|
||||
probed[name]=make_views(name,probe_dir/"manifest.json",probe_dir/"layout.json")
|
||||
|
||||
for name,fixture in fixtures.items():
|
||||
run_determinism(name,fixture,probed[name],binaries=binaries,out=out,env=env)
|
||||
run_road_walk_audit(fixtures["road-in-clouds"],probed["road-in-clouds"],
|
||||
headless=binaries["clang"],out=out,env=env)
|
||||
run_road_short_negative_control(fixture_dir/"liminal-road-short-control.json",
|
||||
probed["road-in-clouds"],headless=binaries["clang"],out=out,env=env)
|
||||
for name,fixture in fixtures.items():
|
||||
export_and_capture(name,fixture,probed[name],headless=binaries["clang"],
|
||||
renderer=args.renderer.resolve(),out=out,artifact_dir=artifact_dir,env=env)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L142Capture] status=FAIL error={error}",file=sys.stderr,flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,46 @@
|
||||
# L142 — liminal pass 2
|
||||
|
||||
| Hills: eye height | Hills: wide |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
| Road: eye height | Road: wide |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
| Pillar grid: eye height | Pillar grid: wide |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
| Monolith: eye height | Monolith: wide |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
## Result
|
||||
|
||||
The wide hill capture shows the validated 80 m repeating profile continuing across the horizon. The capture now streams LOD 0–8; the profile period and validator are unchanged.
|
||||
|
||||
The road is a cubic swept path made from overlapping solid deck segments, not a chain of platform stamps. The earlier platform chain produced top steps; the swept deck passes the surface-sampled walk audit. Its wide view is capped at LOD 2 to keep the deck legible, so that frame shows a local section; the full route length is measured by the audit.
|
||||
|
||||
The R47 addition is a 15×15 grid of identical pillars, spaced 76 m apart across a 1,064 m span. The monolith eye and wide views are retained as the first liminal slice.
|
||||
|
||||
## Measurements
|
||||
|
||||
- **Road:** 376.827 m horizontal path length, 8.000 m width, and 12.061° maximum sampled surface grade against the 15° limit. The deck rises and falls along the cubic profile. It has 17 supports at about 24 m spacing; the flat cloud floor is 44 m below the deck centerline (about 45 m below the walking surface), with pale fog in both views.
|
||||
- **Walk audit:** 901 surface stations at no more than 0.5 m spacing; 56,763 capsule samples; 0 blocked samples, 0 unsupported samples across 5,871 support probes, 0 grade failures, and 0 missed surface probes. The 180 m short-road control measured 210.341 m and failed the 300 m gate as expected.
|
||||
- **Capture quality:** all eight fixtures/views passed the image-quality gate at 1600×900. Hills and pillars stream through LOD 8; road eye and wide views use caps 3 and 2 respectively.
|
||||
- **Determinism:** the monolith, hills, road, and pillar fixtures each matched across Clang and GCC at W1, W8, and W12 (six compiler/worker cells per fixture), on source stamp `72f239e9534faa27e3796fe29207403b190558403841aeebc87d8e75e98e34c7`.
|
||||
|
||||
## Checks
|
||||
|
||||
- Clang 19 and GCC builds passed with floating-point contraction off, four build jobs, and the L142 source stamp.
|
||||
- `Standalone/Tools/verify/verify.py fast --out Saved/Linux/l142 --overall-timeout 1740` passed. Both current owner fixtures produced all 12 canonical hash rows matching the pinned L130-v227 baseline; W1 and W12 also matched each other.
|
||||
- The fast summary retains non-gating differences against older L46/L53 diagnostic baselines; the current pinned L130 canonical checks pass.
|
||||
- The L142 capture runner passed the walk audit, the short-road negative control, all four determinism comparisons, and all eight image-quality checks. Final logs are in `Saved/Linux/l142/logs/`.
|
||||
- No Unreal build or performance timing matrix was run. No Unreal `Source/` files changed. The inherited single-run near-ready timing step was removed from this picture-and-geometry runner rather than treated as a timing result.
|
||||
|
||||
## Remaining limits
|
||||
|
||||
The road wide capture frames a local section at LOD 2 to retain a continuous deck; the 376.827 m length is established by the full-route walk audit. Captures use the standalone Linux renderer rather than an Unreal viewport.
|
||||
|
||||
Changed Unreal `Source/` files: none.
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 39 KiB |
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the L143 liminal pictures and walk-audit controls."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l143"))
|
||||
|
||||
FLAT_FIELD = {
|
||||
"version": 1,
|
||||
"kind": "flatField",
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures")
|
||||
args = parser.parse_args()
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
base = json.loads(OWNER.read_text(encoding="utf-8"))
|
||||
|
||||
monolith = dict(base)
|
||||
monolith["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
monolith["motif_family_override"] = {
|
||||
"version": 1,
|
||||
"strate_index": 0,
|
||||
"placement_policy": "seed,strate,stable_id",
|
||||
"motifs": [{
|
||||
"stable_id": "liminal/oversized-familiar-monolith-v1",
|
||||
"kind": "cappedCylinder",
|
||||
"radius_m": 1.6,
|
||||
"height_m": 26.0,
|
||||
"route_side_offset_m": 42.0,
|
||||
"placement_radius_m": 0.0,
|
||||
"fillet_radius_m": 0.6,
|
||||
"blend_width_m": 1.2,
|
||||
"minimum_spacing_m": 0.0,
|
||||
"yaw_quarter_turns": 0,
|
||||
"attachment": "seated_floor",
|
||||
}],
|
||||
}
|
||||
|
||||
hills = dict(base)
|
||||
hills["surface_profile_override"] = {
|
||||
"version": 1,
|
||||
"kind": "periodicHills",
|
||||
"amplitude_x_m": 2.2,
|
||||
"amplitude_y_m": 2.2,
|
||||
"wavelength_x_m": 80.0,
|
||||
"wavelength_y_m": 80.0,
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
|
||||
road_override = {
|
||||
"stable_id": "liminal/road-clouds-v2",
|
||||
"strate_index": 0,
|
||||
"start_xy_m": [120.0, 340.0],
|
||||
"path_length_m": 360.0,
|
||||
"curve_control_offset_m": 84.0,
|
||||
"deck_height_above_field_m": 44.0,
|
||||
"rise_control_m": 22.0,
|
||||
"half_width_m": 4.0,
|
||||
"half_thickness_m": 1.0,
|
||||
"sample_spacing_m": 1.0,
|
||||
"support_spacing_m": 24.0,
|
||||
"support_radius_m": 0.55,
|
||||
}
|
||||
road = dict(base)
|
||||
road["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
road["liminal_road_override"] = dict(road_override)
|
||||
|
||||
short_control = dict(base)
|
||||
short_control["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
short_override = dict(road_override)
|
||||
short_override["path_length_m"] = 180.0
|
||||
short_control["liminal_road_override"] = short_override
|
||||
|
||||
pillars = dict(base)
|
||||
pillars["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
pillars["liminal_pillar_grid_override"] = {
|
||||
"stable_id": "liminal/horizon-pillar-grid-v1",
|
||||
"strate_index": 0,
|
||||
"center_xy_m": [700.0, 800.0],
|
||||
"rows": 15,
|
||||
"columns": 15,
|
||||
"spacing_m": 76.0,
|
||||
"radius_m": 5.5,
|
||||
"height_m": 40.0,
|
||||
}
|
||||
|
||||
fixtures = (
|
||||
("flatField-monolith", monolith),
|
||||
("periodicHills", hills),
|
||||
("road-in-clouds", road),
|
||||
("road-short-control", short_control),
|
||||
("pillar-grid", pillars),
|
||||
)
|
||||
for name, fixture in fixtures:
|
||||
path = args.out / f"liminal-{name}.json"
|
||||
path.write_text(json.dumps(fixture, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L143Fixture] name={name} file={path} status=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,463 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L143 liminal determinism checks, walk audits and multi-LOD captures."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l143"))
|
||||
CAPTURE_DEFAULT = Path(__file__).resolve().parent / "captures"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
Q_PER_METRE = 1024.0
|
||||
FOG_COLOR = "0.66,0.73,0.77"
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_step(name: str, command: list[object], log: Path, *, env: dict[str, str],
|
||||
stamped: bool = False, timeout_seconds: int = 1740,
|
||||
expected_exit: int = 0) -> str:
|
||||
expected_stamp = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
binary = Path(str(command[0]))
|
||||
probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
||||
if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected_stamp:
|
||||
raise RuntimeError(f"{name} refused a stale executable: {binary}")
|
||||
started = time.monotonic()
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, f"timeout", f"{timeout_seconds}s",
|
||||
*map(str, command)], cwd=ROOT, env=env,
|
||||
stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=timeout_seconds + 5, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
print(f"[L143Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if stamped and (first_stamp(output) != expected_stamp or current_stamp() != expected_stamp):
|
||||
raise RuntimeError(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != expected_exit:
|
||||
raise RuntimeError(f"{name} exit={result.returncode}, expected={expected_exit}; see {log}")
|
||||
return output
|
||||
|
||||
|
||||
def route_frame(manifest_path: Path) -> tuple[tuple[float, float, float],
|
||||
tuple[float, float, float]]:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
strata = [entry for entry in manifest["plan"]["strates"]
|
||||
if entry.get("route") and len(entry["route"]) >= 2]
|
||||
if not strata:
|
||||
raise RuntimeError(f"no planned SurfaceWorld route in {manifest_path}")
|
||||
route = strata[0]["route"]
|
||||
points = [tuple(float(value) / Q_PER_METRE for value in item["position_q"])
|
||||
for item in route]
|
||||
lengths = [math.dist(a, b) for a, b in zip(points, points[1:])]
|
||||
total = sum(lengths)
|
||||
if total <= 1.0e-6:
|
||||
raise RuntimeError(f"degenerate planned route in {manifest_path}")
|
||||
|
||||
def at(distance: float) -> tuple[float, float, float]:
|
||||
remaining = max(0.0, min(total, distance))
|
||||
for index, (a, b, length) in enumerate(zip(points, points[1:], lengths)):
|
||||
if remaining <= length or index == len(lengths) - 1:
|
||||
if length <= 1.0e-9:
|
||||
return b
|
||||
t = remaining / length
|
||||
return tuple(a[axis] + (b[axis] - a[axis]) * t for axis in range(3))
|
||||
remaining -= length
|
||||
return points[-1]
|
||||
|
||||
center = at(total * 0.5)
|
||||
before, after = at(total * 0.5 - 1.0), at(total * 0.5 + 1.0)
|
||||
dx, dy = after[0] - before[0], after[1] - before[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length <= 1.0e-9:
|
||||
raise RuntimeError(f"degenerate route tangent in {manifest_path}")
|
||||
return center, (dx / length, dy / length, 0.0)
|
||||
|
||||
|
||||
def camera_angles(camera, target) -> tuple[float, float]:
|
||||
dx, dy, dz = (target[index] - camera[index] for index in range(3))
|
||||
return math.degrees(math.atan2(dx, dy)), math.degrees(math.atan2(dz, math.hypot(dx, dy)))
|
||||
|
||||
|
||||
def point_toward(camera, target) -> tuple[float, ...]:
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
return (*camera, yaw, pitch)
|
||||
|
||||
|
||||
def stamp(layout: dict, stable_id: str) -> dict:
|
||||
for record in layout.get("stamps", []):
|
||||
if record.get("stable_id") == stable_id:
|
||||
return record
|
||||
raise RuntimeError(f"missing resolved stamp {stable_id}")
|
||||
|
||||
|
||||
def make_views(name: str, manifest_path: Path, layout_path: Path):
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
center, tangent = route_frame(manifest_path)
|
||||
tx, ty, _ = tangent
|
||||
nx, ny = -ty, tx
|
||||
base_height = manifest["recipe"]["surface_profile"]["h0_q"] / Q_PER_METRE
|
||||
if name == "flatField-monolith":
|
||||
monolith = stamp(layout, "liminal/oversized-familiar-monolith-v1")
|
||||
sx, sy, _ = (value / 1.0e6 for value in monolith["position_um"])
|
||||
floor = monolith["floor_um"] / 1.0e6
|
||||
eye_camera = (sx - tx * 36.0, sy - ty * 36.0, floor + 1.6)
|
||||
eye_target = (sx, sy, floor + 10.5)
|
||||
wide_camera = (sx - tx * 42.0 - nx * 18.0,
|
||||
sy - ty * 42.0 - ny * 18.0, floor + 42.0)
|
||||
wide_target = (sx, sy, floor + 8.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 24),
|
||||
"height_center": floor,
|
||||
"height_range": 38.0,
|
||||
}
|
||||
if name == "periodicHills":
|
||||
# Route points are capsule centers: +0.72 m places the view at 1.60 m above ground.
|
||||
eye_camera = (center[0], center[1], center[2] + 0.72)
|
||||
eye_target = (center[0] - tx * 360.0, center[1] - ty * 360.0,
|
||||
center[2] + 0.72)
|
||||
wide_camera = (center[0] - tx * 36.0 - nx * 28.0,
|
||||
center[1] - ty * 36.0 - ny * 28.0, center[2] + 30.0)
|
||||
wide_target = (center[0] - tx * 310.0, center[1] - ty * 310.0,
|
||||
center[2] + 0.72)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 28),
|
||||
"height_center": base_height,
|
||||
"height_range": 6.0,
|
||||
}
|
||||
if name == "road-in-clouds":
|
||||
deck = stamp(layout,"liminal/road-clouds-v2/deck/000")
|
||||
path_um = deck.get("sweep_path_um", [])
|
||||
if len(path_um) < 2:
|
||||
raise RuntimeError("road layout has no resolved continuous swept path")
|
||||
points = [tuple(value / 1.0e6 for value in point) for point in path_um]
|
||||
half_thickness = deck["half_extents_um"][2] / 1.0e6
|
||||
top = lambda point: point[2] + half_thickness
|
||||
index = max(1, len(points) // 12)
|
||||
camera_dx = points[index + 1][0] - points[index - 1][0]
|
||||
camera_dy = points[index + 1][1] - points[index - 1][1]
|
||||
camera_tangent_length = math.hypot(camera_dx, camera_dy)
|
||||
camera_tx, camera_ty = (camera_dx / camera_tangent_length,
|
||||
camera_dy / camera_tangent_length)
|
||||
camera_nx, camera_ny = -camera_ty, camera_tx
|
||||
half_width = deck["half_extents_um"][1] / 1.0e6
|
||||
eye_target_index = min(len(points) - 1, index + 25)
|
||||
eye_camera = (points[index][0] + camera_nx * (half_width + 2.0),
|
||||
points[index][1] + camera_ny * (half_width + 2.0),
|
||||
top(points[index]) + 1.6)
|
||||
eye_target = (points[eye_target_index][0] + camera_nx * (half_width - 0.4),
|
||||
points[eye_target_index][1] + camera_ny * (half_width - 0.4),
|
||||
top(points[eye_target_index]) + 0.3)
|
||||
middle = len(points) // 2
|
||||
dx = points[middle + 1][0] - points[middle - 1][0]
|
||||
dy = points[middle + 1][1] - points[middle - 1][1]
|
||||
tangent_length = math.hypot(dx, dy)
|
||||
tx, ty = dx / tangent_length, dy / tangent_length
|
||||
nx, ny = -ty, tx
|
||||
wide_target_index = min(len(points) - 1, middle + 30)
|
||||
wide_camera = (points[middle][0] - tx * 35.0 + nx * 30.0,
|
||||
points[middle][1] - ty * 35.0 + ny * 30.0,
|
||||
top(points[middle]) + 25.0)
|
||||
wide_target = (points[wide_target_index][0], points[wide_target_index][1],
|
||||
top(points[wide_target_index]) - 5.0)
|
||||
cloud_camera = (points[middle][0] - tx * 18.0 + nx * 12.0,
|
||||
points[middle][1] - ty * 18.0 + ny * 12.0,
|
||||
base_height + 1.6)
|
||||
cloud_target = (points[middle][0] + tx * 8.0,
|
||||
points[middle][1] + ty * 8.0,
|
||||
points[middle][2] - half_thickness * 0.35)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 3),
|
||||
"wide": (wide_camera, wide_target, 3),
|
||||
"cloud-up": (cloud_camera, cloud_target, 3),
|
||||
"height_center": base_height + 23.0,
|
||||
"height_range": 60.0,
|
||||
"road_deck": top(points[middle]),
|
||||
"road_width": 2.0 * half_width,
|
||||
}
|
||||
if name == "pillar-grid":
|
||||
pillars = [row for row in layout.get("stamps", [])
|
||||
if row.get("stable_id", "").startswith("liminal/horizon-pillar-grid-v1/pillar/")]
|
||||
if len(pillars) < 9:
|
||||
raise RuntimeError("pillar-grid layout has too few resolved supports")
|
||||
xs = [row["position_um"][0] / 1.0e6 for row in pillars]
|
||||
ys = [row["position_um"][1] / 1.0e6 for row in pillars]
|
||||
floor = min((row["position_um"][2] - row["half_extents_um"][2]) / 1.0e6
|
||||
for row in pillars)
|
||||
cx, cy = (min(xs) + max(xs)) * 0.5, (min(ys) + max(ys)) * 0.5
|
||||
eye_camera = (min(xs) - 78.0, cy, floor + 1.6)
|
||||
eye_target = (max(xs) + 90.0, cy, floor + 1.6)
|
||||
wide_camera = (cx - 280.0, cy - 420.0, floor + 230.0)
|
||||
wide_target = (cx, cy, floor + 6.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 3),
|
||||
"wide": (wide_camera, wide_target, 3),
|
||||
"height_center": floor + 12.0,
|
||||
"height_range": 42.0,
|
||||
}
|
||||
raise RuntimeError(f"no capture framing for fixture {name}")
|
||||
|
||||
|
||||
def headless_command(binary: Path, fixture: Path, camera, mesh: Path,
|
||||
manifest: Path, layout: Path, workers: int,
|
||||
clip_radius: int, *, hash_tiles: bool = False,
|
||||
audit_road: bool = False, max_level: int = 0,
|
||||
far_lods: bool = False,
|
||||
max_clip_override: int | None = None) -> list[object]:
|
||||
max_clip_level = (max_clip_override if max_clip_override is not None else
|
||||
8 if far_lods else max_level)
|
||||
command: list[object] = [binary, fixture, f"--workers={workers}",
|
||||
"--seed=0", "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={clip_radius}",
|
||||
f"--max-clip-level={max_clip_level}",
|
||||
f"--max-level={max_level}", "--render-distance-chunks=0",
|
||||
f"--camera={fmt(point_toward(camera, (camera[0], camera[1] + 1.0, camera[2])))}",
|
||||
f"--mesh-export={mesh}", f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
if hash_tiles:
|
||||
command.append("--stream-hash-tiles")
|
||||
if audit_road:
|
||||
command.append("--audit-liminal-road-walk")
|
||||
return command
|
||||
|
||||
|
||||
def determinism_digest(output: str, manifest_path: Path,
|
||||
layout_path: Path) -> tuple[str, str, str]:
|
||||
tile_hashes = sorted(line for line in output.splitlines()
|
||||
if line.startswith("[PipelineTileHash] "))
|
||||
if not manifest_path.is_file() or not layout_path.is_file() or not tile_hashes:
|
||||
raise RuntimeError("determinism run omitted manifest, layout or per-tile hashes")
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
manifest_digest = (manifest["content_hash"],manifest["recipe"]["content_hash"],
|
||||
manifest["plan"]["content_hash"])
|
||||
return ("|".join(manifest_digest),layout["stable_identity"],"\n".join(tile_hashes))
|
||||
|
||||
|
||||
def run_determinism(name: str, fixture: Path, view, *, binaries: dict[str, Path],
|
||||
out: Path, env: dict[str, str], max_level: int = 0,
|
||||
far_lods: bool = False, max_clip_override: int | None = None) -> None:
|
||||
camera, _, _ = view["eye"]
|
||||
clip_radius = 1
|
||||
max_clip = 0
|
||||
baseline = None
|
||||
cells = 0
|
||||
for compiler, workers in ((compiler, workers)
|
||||
for compiler in ("clang", "gcc")
|
||||
for workers in (1, 8, 12)):
|
||||
tag = f"{name}-{compiler}-w{workers}"
|
||||
run_dir = out / "determinism" / tag
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
mesh = run_dir / "feature.vfmesh"
|
||||
manifest = run_dir / "manifest.json"
|
||||
layout = run_dir / "layout.json"
|
||||
command = headless_command(binaries[compiler], fixture,camera,mesh,manifest,
|
||||
layout,workers,max_clip,hash_tiles=True,max_level=max_level,
|
||||
far_lods=far_lods,max_clip_override=max_clip_override)
|
||||
output = run_step(tag,command,out/"logs"/f"determinism-{tag}.log",
|
||||
env=env,stamped=True)
|
||||
digest = determinism_digest(output,manifest,layout)
|
||||
if baseline is None:
|
||||
baseline = digest
|
||||
elif digest != baseline:
|
||||
raise RuntimeError(f"{name} diverged at {compiler} W{workers}; see determinism logs")
|
||||
cells += 1
|
||||
print(f"[L143Determinism] fixture={name} compiler_worker_cells={cells} identical=yes status=PASS",
|
||||
flush=True)
|
||||
|
||||
|
||||
def export_and_capture(name: str, fixture: Path, views: dict, *,
|
||||
headless: Path, renderer: Path, out: Path,
|
||||
artifact_dir: Path, env: dict[str, str]) -> None:
|
||||
view_names = ("eye", "wide", "cloud-up") if name == "road-in-clouds" else ("eye", "wide")
|
||||
for view_name in view_names:
|
||||
camera, target, clip_radius = views[view_name]
|
||||
clip_radius = min(clip_radius,3)
|
||||
stem = f"{name}-{view_name}"
|
||||
mesh = out / "capture-meshes" / f"{stem}.vfmesh"
|
||||
manifest = out / "capture-manifests" / f"{stem}.json"
|
||||
layout = out / "capture-layouts" / f"{stem}.json"
|
||||
png = out / "capture-png" / f"{stem}.png"
|
||||
image = artifact_dir / f"{stem}.jpg"
|
||||
for path in (mesh,manifest,layout,png,image):
|
||||
path.parent.mkdir(parents=True,exist_ok=True)
|
||||
command = headless_command(headless,fixture,camera,mesh,manifest,layout,12,
|
||||
clip_radius,max_level=-1,far_lods=True,
|
||||
max_clip_override=2 if name == "road-in-clouds" else None)
|
||||
output = run_step(f"capture-{stem}-export",command,
|
||||
out/"logs"/f"capture-{stem}-export.log",env=env,stamped=True)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"{stem} produced an empty mesh")
|
||||
yaw, pitch = camera_angles(camera,target)
|
||||
height_center = views["height_center"]
|
||||
height_range = views["height_range"]
|
||||
if name == "road-in-clouds":
|
||||
fog_start,fog_end,fog_color=(24,260,"0.88,0.93,0.95")
|
||||
elif name == "pillar-grid":
|
||||
fog_start,fog_end,fog_color=220,1250,"0.82,0.88,0.91"
|
||||
elif name == "periodicHills":
|
||||
fog_start,fog_end,fog_color=210,1000,"0.78,0.84,0.88"
|
||||
else:
|
||||
fog_start,fog_end,fog_color=30,105,FOG_COLOR
|
||||
renderer_command = [renderer,mesh,png,"--size=1600x900","--fov-deg=72",
|
||||
f"--camera-m={fmt(camera)}",f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}","--tile-skirts=on","--shade=readable",
|
||||
"--headlamp=off","--height-tint=on",f"--height-center-m={height_center:.6f}",
|
||||
f"--height-range-m={height_range:.6f}","--light-direction=-0.42,-0.58,0.70",
|
||||
"--fog=on",f"--fog-start-m={fog_start}",f"--fog-end-m={fog_end}",
|
||||
f"--fog-color={fog_color}"]
|
||||
run_step(f"capture-{stem}-render",renderer_command,
|
||||
out/"logs"/f"capture-{stem}-render.log",env=env,stamped=True)
|
||||
run_step(f"capture-{stem}-jpeg",["ffmpeg","-nostdin","-hide_banner","-loglevel",
|
||||
"error","-y","-i",png,"-frames:v","1","-q:v","4",image],
|
||||
out/"logs"/f"capture-{stem}-jpeg.log",env=env)
|
||||
quality = run_step(f"capture-{stem}-quality",
|
||||
[sys.executable,ROOT/"Standalone/Tools/verify/capture_quality.py",image],
|
||||
out/"logs"/f"capture-{stem}-quality.log",env=env)
|
||||
if "[CaptureQuality]" not in quality or not image.is_file() \
|
||||
or image.stat().st_size > 300_000:
|
||||
raise RuntimeError(f"{stem} capture failed quality or size checks")
|
||||
eye_height = "1.600m" if view_name in ("eye", "cloud-up") else "wide"
|
||||
print(f"[L143Capture] fixture={name} view={view_name} eye_height={eye_height} "
|
||||
f"camera_m=({fmt(camera)}) target_m=({fmt(target)}) bytes={image.stat().st_size} "
|
||||
f"sha256={sha256(image)} status=PASS",flush=True)
|
||||
|
||||
|
||||
def run_road_walk_audit(fixture: Path, view, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera = view["eye"][0]
|
||||
run_dir = out / "road-walk-audit"
|
||||
run_dir.mkdir(parents=True,exist_ok=True)
|
||||
output = run_step("road-walk-audit",headless_command(headless,fixture,camera,
|
||||
run_dir/"audit.vfmesh",run_dir/"manifest.json",run_dir/"layout.json",12,1,
|
||||
audit_road=True),out/"logs"/"road-walk-audit.log",env=env,stamped=True)
|
||||
if not re.search(r"\[LiminalRoadWalkAudit\].*status=PASS",output):
|
||||
raise RuntimeError("raised road failed the Core capsule walk audit")
|
||||
|
||||
|
||||
def run_road_short_negative_control(fixture: Path, view, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera = view["eye"][0]
|
||||
run_dir = out / "road-short-negative-control"
|
||||
run_dir.mkdir(parents=True,exist_ok=True)
|
||||
output = run_step("road-short-negative-control",
|
||||
headless_command(headless,fixture,camera,run_dir/"control.vfmesh",
|
||||
run_dir/"manifest.json",run_dir/"layout.json",12,1,audit_road=True),
|
||||
out/"logs"/"road-short-negative-control.log",env=env,stamped=True,
|
||||
expected_exit=14)
|
||||
match = re.search(r"\[LiminalRoadWalkAudit\].*path_length_m=([0-9.]+).*status=FAIL",output)
|
||||
if not match or float(match.group(1)) >= 300.0:
|
||||
raise RuntimeError("short-road negative control did not falsify the length gate")
|
||||
print(f"[L143RoadNegativeControl] path_length_m={match.group(1)} "
|
||||
"expected=FAIL status=PASS",flush=True)
|
||||
|
||||
|
||||
def retain_unchanged_captures(artifact_dir: Path) -> None:
|
||||
reference_dir = Path(__file__).resolve().parents[1] / "Linux142" / "captures"
|
||||
for name in ("flatField-monolith", "periodicHills"):
|
||||
for view in ("eye", "wide"):
|
||||
filename = f"{name}-{view}.jpg"
|
||||
source, destination = reference_dir / filename, artifact_dir / filename
|
||||
if not source.is_file():
|
||||
raise RuntimeError(f"missing L142 reference capture {source}")
|
||||
shutil.copy2(source,destination)
|
||||
if sha256(source) != sha256(destination):
|
||||
raise RuntimeError(f"unchanged reference capture copy differs: {filename}")
|
||||
print(f"[L143Capture] fixture={name} view={view} retained_from=Linux142 "
|
||||
f"sha256={sha256(destination)} status=PASS",flush=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT)
|
||||
parser.add_argument("--artifact-dir", type=Path, default=CAPTURE_DEFAULT)
|
||||
parser.add_argument("--headless-clang", type=Path, required=True)
|
||||
parser.add_argument("--headless-gcc", type=Path, required=True)
|
||||
parser.add_argument("--renderer", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
out = args.out.resolve()
|
||||
artifact_dir = args.artifact_dir.resolve()
|
||||
fixture_dir = out / "fixtures"
|
||||
for directory in (out,fixture_dir,artifact_dir):
|
||||
directory.mkdir(parents=True,exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(out)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT/"Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(out/"xdg-cache")
|
||||
run_step("build-fixtures",[sys.executable,
|
||||
ROOT/"Standalone/Research/WorldAssemblerM3/Linux143/BuildL143Fixtures.py",
|
||||
f"--out={fixture_dir}"],out/"logs"/"build-fixtures.log",env=env)
|
||||
|
||||
binaries = {"clang":args.headless_clang.resolve(),"gcc":args.headless_gcc.resolve()}
|
||||
fixtures = {
|
||||
"flatField-monolith":fixture_dir/"liminal-flatField-monolith.json",
|
||||
"periodicHills":fixture_dir/"liminal-periodicHills.json",
|
||||
"road-in-clouds":fixture_dir/"liminal-road-in-clouds.json",
|
||||
"pillar-grid":fixture_dir/"liminal-pillar-grid.json",
|
||||
}
|
||||
probed = {}
|
||||
for name,fixture in fixtures.items():
|
||||
probe_dir=out/"plan-probes"/name
|
||||
probe_dir.mkdir(parents=True,exist_ok=True)
|
||||
camera=(0.0,0.0,-500.0)
|
||||
output=run_step(f"plan-probe-{name}",headless_command(binaries["clang"],fixture,
|
||||
camera,probe_dir/"probe.vfmesh",probe_dir/"manifest.json",
|
||||
probe_dir/"layout.json",12,1),out/"logs"/f"plan-probe-{name}.log",
|
||||
env=env,stamped=True)
|
||||
if "[WorldAssemblerFullManifest]" not in output:
|
||||
raise RuntimeError(f"{name} probe omitted its full plan manifest")
|
||||
probed[name]=make_views(name,probe_dir/"manifest.json",probe_dir/"layout.json")
|
||||
|
||||
for name,fixture in fixtures.items():
|
||||
run_determinism(name,fixture,probed[name],binaries=binaries,out=out,env=env)
|
||||
run_determinism("pillar-grid-far-lod",fixtures["pillar-grid"],probed["pillar-grid"],
|
||||
binaries=binaries,out=out,env=env,max_level=-1,far_lods=True,max_clip_override=8)
|
||||
run_road_walk_audit(fixtures["road-in-clouds"],probed["road-in-clouds"],
|
||||
headless=binaries["clang"],out=out,env=env)
|
||||
run_road_short_negative_control(fixture_dir/"liminal-road-short-control.json",
|
||||
probed["road-in-clouds"],headless=binaries["clang"],out=out,env=env)
|
||||
retain_unchanged_captures(artifact_dir)
|
||||
for name in ("road-in-clouds", "pillar-grid"):
|
||||
fixture=fixtures[name]
|
||||
export_and_capture(name,fixture,probed[name],headless=binaries["clang"],
|
||||
renderer=args.renderer.resolve(),out=out,artifact_dir=artifact_dir,env=env)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L143Capture] status=FAIL error={error}",file=sys.stderr,flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,64 @@
|
||||
# L143 — liminal polish
|
||||
|
||||
## Captures
|
||||
|
||||
### Road
|
||||
|
||||
Eye-height:
|
||||
|
||||

|
||||
|
||||
Wide:
|
||||
|
||||

|
||||
|
||||
Looking up from the cloud floor:
|
||||
|
||||

|
||||
|
||||
### Pillar grid
|
||||
|
||||
Eye-height:
|
||||
|
||||

|
||||
|
||||
Wide:
|
||||
|
||||

|
||||
|
||||
### Kept from L142
|
||||
|
||||
These four captures are byte-identical to L142.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Result
|
||||
|
||||
The road is one continuous swept deck, and the distant pillar silhouettes remain complete and consistent. The monolith and periodic hills were retained unchanged.
|
||||
|
||||
## Changes and causes
|
||||
|
||||
- The road edge broke where independently capped deck segments overlapped. The road now resolves to one swept-deck stamp with a shared cubic-Hermite centerline and continuous cross-section; the centerline is sampled at 1 m spacing (361 points). The 8 m deck is 376.829 m long and retains 17 support posts. The sweep path is serialized in stamp-vocabulary V5.
|
||||
- Pillars disappeared or narrowed when the coarse marching-cubes lattice skipped their cross-section. The pillar stamp's interval proof is unsupported, so it conservatively kept tiles but did not protect the mesh from undersampling. At LOD4 the sample spacing is 32 voxels (8 m), already greater than the 5.5 m pillar radius; spacing rises to 64 and 128 voxels at LOD5 and LOD6. The shared meshing path now emits deterministic 24-sided capped-cylinder proxies for regular `fillPillar` stamps once the sample step exceeds their radius or half-height. The same rule applies outside the liminal fixture. The radial approximation error for this fixture is about 4.7 cm, below one 25 cm voxel.
|
||||
|
||||
## Measurements
|
||||
|
||||
- Road walk audit covered the full 376.829 m route at 1,081 surface stations, with 68,103 capsule samples and 7,096 support samples: 0 blocked, 0 unsupported, 0 grade failures, and 0 surface misses. Measured maximum grade was 8.512° against a 15° limit; deck width was 8 m against a 3 m minimum. `road-short-negative-control` failed the walk audit as expected at 210.351 m.
|
||||
- Determinism covered five fixtures—monolith, periodic hills, road, pillar grid, and far-LOD pillar grid—across Clang/GCC and W1/W8/W12 (six compiler/worker cells per fixture). All five reported identical outputs.
|
||||
- Clang and GCC builds passed on source stamp `1baa3a711f7f96df2e70fc93579f41ad0b0211eb42fcb51dabf522e33d888877`. The capture runner and capture-quality checks passed for all requested views.
|
||||
- Fast tier passed in 292.3 s. Its pinned L130-v227 reach-unit owner hashes match exactly: 12/12 rows for each owner fixture, with W1 and W12 identical. Historical L46/L53 hash comparisons remain diagnostic-only and are not the current owner-hash gate.
|
||||
|
||||
## Checks and limits
|
||||
|
||||
- Passed: road audit and short-road negative control; five-fixture Clang/GCC determinism; Clang and GCC builds; capture quality; fast tier; `git diff --check`.
|
||||
- Not run: Unreal build (no Unreal `Source/` files changed) and L80 performance timing (this round made no performance claim).
|
||||
- The coarse proxy is limited to regular `fillPillar` stamps; irregular pillars and other stamp kinds continue through their existing SDF path. The proxy was exercised at LOD4–6 in the pillar-grid captures.
|
||||
- Unreal `Source/` files changed: none. Changed standalone source files: `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h`, `Standalone/Core/src/ShapeWorldInternal.h`, `Standalone/Core/src/VoxelForgeCore.cpp`, `Standalone/Source/StreamBenchmarkHeadless.cpp`, `Standalone/Source/StreamBenchmarkPipeline.h`, and `Standalone/Source/WorldAssemblyPrototype.h`.
|
||||
|
||||
Detailed logs are in `Saved/Linux/l143/`; the capture and audit transcript is `Saved/Linux/l143/logs/capture-runner.log`, the walk audit is `Saved/Linux/l143/logs/road-walk-audit.log`, and the fast-tier summary is `Saved/Linux/l143/fast-summary.json`.
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the 3-strata composed Core.Liminal.Field fixture."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l144"))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures")
|
||||
args = parser.parse_args()
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
world = json.loads(OWNER.read_text(encoding="utf-8"))
|
||||
if len(world.get("strates", [])) < 3:
|
||||
raise SystemExit("owner fixture needs at least three authored strate slots")
|
||||
|
||||
world["strates"] = world["strates"][:3]
|
||||
world["total_strates"] = 3
|
||||
world["settings_asset"]["TotalStrates"] = 3
|
||||
world["settings_asset"]["FixedStrates"] = world["settings_asset"]["FixedStrates"][:3]
|
||||
# Keep the resolved arrival near the departure depth. The owner's 5000-unit
|
||||
# reach clamps to liminal bedrock on most seeds and leaves no field floor.
|
||||
world["strates"][0]["passage_config"]["ReachMin"] = 1536
|
||||
world["strates"][0]["passage_config"]["ReachMax"] = 1792
|
||||
world["surface_profile_override"] = {
|
||||
"version": 1,
|
||||
"kind": "flatField",
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
middle = world["strates"][1]
|
||||
middle["asset"]["GeneratorType"] = 4 # ECaveGeneratorType::SurfaceWorld
|
||||
middle["asset"]["StrateName"] = "Liminal Field"
|
||||
# Keep the departure mouth 24–32 m above liminal bedrock so every variant
|
||||
# has room for its floor, road landing and support footings.
|
||||
middle["passage_config"]["ReachMin"] = 192
|
||||
middle["passage_config"]["ReachMax"] = 256
|
||||
world["core_liminal_field"] = {
|
||||
"version": 1,
|
||||
"strate_index": int(middle["index"]),
|
||||
"variant_policy": "seed_modulo_4",
|
||||
}
|
||||
|
||||
output = args.out / "core-liminal-field-3-strata.json"
|
||||
output.write_text(json.dumps(world, separators=(",", ":")) + "\n", encoding="utf-8")
|
||||
print(f"[L144Fixture] path={output} strates=SurfaceWorld,Core.Liminal.Field,TunnelNetwork status=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture the four seed-selected Core.Liminal.Field v1 variants."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
HERE = Path(__file__).resolve().parent
|
||||
OUT = ROOT / "Saved/Linux/l144"
|
||||
CAPTURES = HERE / "captures"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
Q_PER_METRE = 1024.0
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_step(name: str, command: list[object], log: Path, env: dict[str, str],
|
||||
stamped: bool = False) -> str:
|
||||
expected_stamp = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
binary = Path(str(command[0]))
|
||||
probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
||||
if probe is None or probe.returncode != 0 \
|
||||
or first_stamp(probe.stdout or "") != expected_stamp:
|
||||
raise RuntimeError(f"{name} refused a stale executable: {binary}")
|
||||
started = time.monotonic()
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)],
|
||||
cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=1745, check=False)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L144Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if stamped and (first_stamp(output) != expected_stamp
|
||||
or current_stamp() != expected_stamp):
|
||||
raise RuntimeError(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"{name} exit={result.returncode}; see {log}")
|
||||
return output
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
dx, dy, dz = (target[i] - camera[i] for i in range(3))
|
||||
return math.degrees(math.atan2(dx, dy)), math.degrees(
|
||||
math.atan2(dz, math.hypot(dx, dy)))
|
||||
|
||||
|
||||
def route_views(manifest_path: Path, layout_path: Path, name: str):
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
plan = next(row for row in manifest["plan"]["strates"]
|
||||
if row["strate_index"] == 1 and row.get("liminal_field", {}).get("version") == 1)
|
||||
points = [tuple(value / Q_PER_METRE for value in row["position_q"])
|
||||
for row in plan["route"]]
|
||||
first, second = points[0], points[1]
|
||||
dx, dy = second[0] - first[0], second[1] - first[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length <= 1.0e-6:
|
||||
raise RuntimeError("Core.Liminal.Field arrival tangent is degenerate")
|
||||
tx, ty = dx / length, dy / length
|
||||
eye_camera = (first[0] - tx * 1.5, first[1] - ty * 1.5, first[2] + 1.6)
|
||||
third = points[min(2, len(points) - 1)]
|
||||
dx, dy = third[0] - first[0], third[1] - first[1]
|
||||
tangent_length = math.hypot(dx, dy)
|
||||
tx, ty = dx / tangent_length, dy / tangent_length
|
||||
dz_per_metre = (third[2] - first[2]) / tangent_length
|
||||
eye_target = (first[0] + tx * 40.0, first[1] + ty * 40.0,
|
||||
first[2] + dz_per_metre * 40.0 + 1.6)
|
||||
|
||||
middle = len(points) // 2
|
||||
before, center, after = points[middle - 1], points[middle], points[middle + 1]
|
||||
dx, dy = after[0] - before[0], after[1] - before[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length <= 1.0e-6:
|
||||
raise RuntimeError("Core.Liminal.Field wide-view tangent is degenerate")
|
||||
tx, ty = dx / length, dy / length
|
||||
nx, ny = -ty, tx
|
||||
floor = plan["liminal_field"]["floor_height_q"] / Q_PER_METRE
|
||||
if name == "flatField-monolith":
|
||||
monolith = next(row for row in layout.get("stamps", [])
|
||||
if row.get("stable_id") == "core/liminal-field/monolith/000")
|
||||
monolith_center = tuple(value / 1.0e6 for value in monolith["position_um"])
|
||||
floor = monolith_center[2] - monolith["half_extents_um"][2] / 1.0e6
|
||||
wide_camera = (monolith_center[0] - tx * 30.0 + nx * 18.0,
|
||||
monolith_center[1] - ty * 30.0 + ny * 18.0, floor + 25.0)
|
||||
wide_target = monolith_center
|
||||
elif name == "periodicHills":
|
||||
wide_camera = (center[0] - 20.0, center[1] - 25.0, floor + 10.0)
|
||||
wide_target = (center[0] + 10.0, center[1] + 8.0, floor + 1.0)
|
||||
elif name == "pillar-grid":
|
||||
pillars = [row for row in layout.get("stamps", [])
|
||||
if row.get("stable_id", "").startswith("core/liminal-field/pillars/")]
|
||||
if not pillars:
|
||||
raise RuntimeError("pillar-grid layout has no resolved grid stamps")
|
||||
pairs = []
|
||||
for i, first_pillar in enumerate(pillars):
|
||||
for second_pillar in pillars[i + 1:]:
|
||||
a = first_pillar["position_um"]
|
||||
b = second_pillar["position_um"]
|
||||
dx, dy = (b[0] - a[0]) / 1.0e6, (b[1] - a[1]) / 1.0e6
|
||||
separation = math.hypot(dx, dy)
|
||||
if 60.0 <= separation <= 90.0:
|
||||
mx = (a[0] + b[0]) / 2.0e6
|
||||
my = (a[1] + b[1]) / 2.0e6
|
||||
pairs.append((math.hypot(mx - center[0], my - center[1]),
|
||||
mx, my, dx / separation, dy / separation,
|
||||
(a[2] + b[2]) / 2.0e6,
|
||||
first_pillar["half_extents_um"][2] / 1.0e6))
|
||||
if not pairs:
|
||||
raise RuntimeError("pillar-grid has no neighboring resolved columns")
|
||||
_, mx, my, tx, ty, pillar_z, half_height = min(pairs)
|
||||
floor = pillar_z - half_height
|
||||
nx, ny = -ty, tx
|
||||
wide_camera = (center[0] - tx * 20.0 + nx * 25.0,
|
||||
center[1] - ty * 20.0 + ny * 25.0, center[2] + 8.0)
|
||||
wide_target = (center[0], center[1], floor + 2.0)
|
||||
else:
|
||||
wide_camera = (center[0] - tx * 20.0 + nx * 25.0,
|
||||
center[1] - ty * 20.0 + ny * 25.0, center[2] + 8.0)
|
||||
wide_target = (center[0], center[1], floor + 2.0)
|
||||
return {"eye": (eye_camera, eye_target), "wide": (wide_camera, wide_target)}
|
||||
|
||||
|
||||
def capture_variant(seed: int, name: str, *, headless: Path, renderer: Path,
|
||||
fixture: Path, env: dict[str, str]):
|
||||
stem = f"core-liminal-{name}-seed{seed:02d}"
|
||||
plan_dir = OUT / "capture-plans" / stem
|
||||
plan_dir.mkdir(parents=True, exist_ok=True)
|
||||
manifest = plan_dir / "manifest.json"
|
||||
layout = plan_dir / "layout.json"
|
||||
command = [headless, fixture, "--workers=1", f"--seed={seed}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=1", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--camera=0,0,-540,0,0",
|
||||
f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
output = run_step(f"plan-{stem}", command, OUT / "logs" / f"capture-{stem}-plan.log",
|
||||
env, stamped=True)
|
||||
if "[CoreLiminalFieldDraw]" not in output:
|
||||
raise RuntimeError(f"{stem} plan omitted its Core liminal deck")
|
||||
views = route_views(manifest, layout, name)
|
||||
for view, (eye, target) in views.items():
|
||||
work = OUT / "captures" / f"{stem}-{view}"
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
view_manifest = work / "manifest.json"
|
||||
view_layout = work / "layout.json"
|
||||
mesh = work / "world.vfmesh"
|
||||
yaw, pitch = camera_angles(eye, target)
|
||||
camera = (*eye, yaw, pitch)
|
||||
view_clip_radius = (5 if view == "wide" and name in {
|
||||
"flatField-monolith", "periodicHills"} else
|
||||
3)
|
||||
command = [headless, fixture, "--workers=12", f"--seed={seed}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={view_clip_radius}",
|
||||
"--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--capture-skip-walkability",
|
||||
f"--camera={fmt(camera)}", f"--mesh-export={mesh}",
|
||||
f"--world-assembler-manifest-output={view_manifest}",
|
||||
f"--resolved-layout-output={view_layout}"]
|
||||
output = run_step(f"export-{stem}-{view}", command,
|
||||
OUT / "logs" / f"capture-{stem}-{view}-export.log", env, stamped=True)
|
||||
if "[CoreLiminalFieldDraw]" not in output or not mesh.is_file() \
|
||||
or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"{stem}-{view} export omitted its Core liminal deck or mesh")
|
||||
png = work / f"{view}.png"
|
||||
image = CAPTURES / f"{stem}-{view}.jpg"
|
||||
fog = {
|
||||
"flatField-monolith": (30, 180, "0.66,0.73,0.77", -708.0, 46.0),
|
||||
"periodicHills": (140, 850, "0.78,0.84,0.88", -720.0, 14.0),
|
||||
"road-over-cloud-floor": (24, 420, "0.88,0.93,0.95", -720.0, 65.0),
|
||||
"pillar-grid": (180, 1200, "0.82,0.88,0.91", -720.0, 80.0),
|
||||
}[name]
|
||||
fog_start, fog_end, fog_color, height_center, height_range = fog
|
||||
render = [renderer, mesh, png, "--size=1600x900", "--fov-deg=72",
|
||||
f"--camera-m={fmt(eye)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--tile-skirts=on", "--shade=readable", "--headlamp=off", "--height-tint=on",
|
||||
f"--height-center-m={height_center:.6f}", f"--height-range-m={height_range:.6f}",
|
||||
"--light-direction=-0.42,-0.58,0.70", "--fog=on", f"--fog-start-m={fog_start}",
|
||||
f"--fog-end-m={fog_end}", f"--fog-color={fog_color}"]
|
||||
run_step(f"render-{stem}-{view}", render,
|
||||
OUT / "logs" / f"capture-{stem}-{view}-render.log", env, stamped=True)
|
||||
run_step(f"jpeg-{stem}-{view}", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel",
|
||||
"error", "-y", "-i", png, "-frames:v", "1", "-q:v", "4", image],
|
||||
OUT / "logs" / f"capture-{stem}-{view}-jpeg.log", env)
|
||||
run_step(f"quality-{stem}-{view}", [sys.executable,
|
||||
ROOT / "Standalone/Tools/verify/capture_quality.py", image],
|
||||
OUT / "logs" / f"capture-{stem}-{view}-quality.log", env)
|
||||
print(f"[L144Capture] variant={name} seed={seed} view={view} "
|
||||
f"eye_height={'1.600m' if view == 'eye' else 'wide'} bytes={image.stat().st_size} "
|
||||
f"sha256={sha256(image)} status=PASS", flush=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
headless = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-clang19"
|
||||
renderer = ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-clang19"
|
||||
fixture = OUT / "fixtures/core-liminal-field-3-strata.json"
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(OUT)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT / "Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
CAPTURES.mkdir(parents=True, exist_ok=True)
|
||||
variants = ("flatField-monolith", "periodicHills",
|
||||
"road-over-cloud-floor", "pillar-grid")
|
||||
selected = set(os.environ.get("L144_CAPTURE_VARIANTS", "").split(","))
|
||||
for seed, name in enumerate(variants):
|
||||
if selected and name not in selected:
|
||||
continue
|
||||
capture_variant(seed, name, headless=headless, renderer=renderer,
|
||||
fixture=fixture, env=env)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L144Capture] status=FAIL error={error}", file=sys.stderr, flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,44 @@
|
||||
# Linux144 — Core.Liminal.Field v1
|
||||
|
||||
| Variant (seed) | Arrival at eye height | Wide view |
|
||||
|---|---|---|
|
||||
| Flat field + monolith (0) |  |  |
|
||||
| Repeating hills (1) |  |  |
|
||||
| Road over cloud floor (2) |  |  |
|
||||
| Pillar grid (3) |  |  |
|
||||
|
||||
**Result: stopped at the mouth-join gate; this is not ready to ship.** The new `Core.Liminal.Field` v1 recipe plans and draws all four seed-selected looks between fixed mouths. In the final composed probe, three of four seeds failed one of their two passage interfaces, and all four failed the liminal walk audit. The requested eight-seed-per-variant matrix was not run after the stop condition was reached.
|
||||
|
||||
## What changed
|
||||
|
||||
- Added one data-driven recipe, `Core.Liminal.Field` v1. `seed % 4` selects flat field with monolith, repeating hills, road over cloud floor, or pillar grid.
|
||||
- The liminal plan binds the existing `passage/0-1/0` arrival and `passage/1-2/0` departure as fixed endpoints. It draws an 8-point route deck and structural supports; the road variant uses that deck as its route. The pillar variant resolved 221 grid pillars after route clearance.
|
||||
- Added a three-strata fixture: Surface World → liminal → Tunnel Network. `BuildL144Fixtures.py` and `CaptureL144.py` reproduce the fixture and the eight captures.
|
||||
|
||||
## Composed probe
|
||||
|
||||
The final composed audit window was one run for each seed 0–3 at W1, using Clang 19 source stamp `8acbb7c3dfdb0484100d3d55036db20743ce76d74097b69362bb7d1a1c1d6302`. These are audit counts, not timing comparisons.
|
||||
|
||||
| Variant | Passage interfaces passed | Liminal walk audit | Passage shell / seam audit | Liminal negative control |
|
||||
|---|---:|---|---|---|
|
||||
| Flat field + monolith, seed 0 | 1/2 — FAIL | 222 blocked samples, 7 grade failures — FAIL | PASS | altered departure rejected — PASS |
|
||||
| Repeating hills, seed 1 | 2/2; composed summary FAIL | 1 blocked sample — FAIL | PASS | altered departure rejected — PASS |
|
||||
| Road over cloud floor, seed 2 | 1/2 — FAIL | 3 blocked samples — FAIL | FAIL, 1 floor violation | altered departure rejected — PASS |
|
||||
| Pillar grid, seed 3 | 1/2 — FAIL | 289 blocked samples, 9 grade failures — FAIL | PASS | altered departure rejected — PASS |
|
||||
|
||||
The three partial passage-interface failures reached the requested stop threshold. Every liminal walk probe reported zero unsupported samples and zero structural-support failures; support alone therefore does not explain the blocked routes. The top Surface World route audit passed all four seeds. Passage naturalness and mouth-profile negative controls also detected their injected faults in all four logs.
|
||||
|
||||
## Captures and limits
|
||||
|
||||
All eight 1600×900 images passed `capture_quality.py`. Arrival cameras are 1.6 m above the fixed arrival mouth and look along the route. Capture exports used `--capture-skip-walkability` to preserve render evidence despite the failing composed audits; this flag was not used for the audit runs. The wide pillar view shows the overhead deck and one support rather than a legible field of grid columns, and the other images also expose the current route/deck defects.
|
||||
|
||||
No additional geometry change was attempted after the three-interface-failure threshold. Earlier diagnostics found the short terminal secant exaggerated the mouth grade; widening the tangent probe interval did not make the final composed walk pass. It remains unproven which combination of terminal connector geometry and nearby field solids accounts for the blocked capsule samples.
|
||||
|
||||
## Checks
|
||||
|
||||
- Clang 19 headless build completed; the final audit and capture executables report the source stamp above.
|
||||
- Four composed probes ran at W1, one seed per variant. Planning/drawing and the per-audit results are in `Saved/Linux/l144/logs/probe8-seed00.log` through `probe8-seed03.log`.
|
||||
- Eight capture quality checks passed. `git diff --check` passed.
|
||||
- Not run after the stop gate: eight seeds per variant, GCC or W8/W12 determinism, owner canonical-hash comparison, fast tier, and one near-ready run. No performance timing window was collected.
|
||||
|
||||
Changed Unreal `Source/` files: none. Changed standalone sources include `StreamBenchmarkHeadless.cpp`, `StreamBenchmarkPipeline.h`, and `WorldAssemblyPrototype.h`.
|
||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the 3-strata composed Core.Liminal.Field v2 fixture."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l145"))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures")
|
||||
args = parser.parse_args()
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
world = json.loads(OWNER.read_text(encoding="utf-8"))
|
||||
if len(world.get("strates", [])) < 3:
|
||||
raise SystemExit("owner fixture needs at least three authored strate slots")
|
||||
|
||||
world["strates"] = world["strates"][:3]
|
||||
world["total_strates"] = 3
|
||||
world["settings_asset"]["TotalStrates"] = 3
|
||||
world["settings_asset"]["FixedStrates"] = world["settings_asset"]["FixedStrates"][:3]
|
||||
# Keep the resolved arrival near the departure depth. The owner's 5000-unit
|
||||
# reach clamps to liminal bedrock on most seeds and leaves no field floor.
|
||||
world["strates"][0]["passage_config"]["ReachMin"] = 1536
|
||||
world["strates"][0]["passage_config"]["ReachMax"] = 1792
|
||||
world["surface_profile_override"] = {
|
||||
"version": 1,
|
||||
"kind": "flatField",
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
middle = world["strates"][1]
|
||||
middle["asset"]["GeneratorType"] = 4 # ECaveGeneratorType::SurfaceWorld
|
||||
middle["asset"]["StrateName"] = "Liminal Field"
|
||||
# Keep the departure mouth 24–32 m above liminal bedrock so every variant
|
||||
# has room for its floor, road landing and support footings.
|
||||
middle["passage_config"]["ReachMin"] = 192
|
||||
middle["passage_config"]["ReachMax"] = 256
|
||||
world["core_liminal_field"] = {
|
||||
"version": 2,
|
||||
"strate_index": int(middle["index"]),
|
||||
"variant_policy": "seed_modulo_4",
|
||||
}
|
||||
|
||||
output = args.out / "core-liminal-field-3-strata.json"
|
||||
output.write_text(json.dumps(world, separators=(",", ":")) + "\n", encoding="utf-8")
|
||||
print(f"[L145Fixture] path={output} strates=SurfaceWorld,Core.Liminal.Field,TunnelNetwork status=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture one arrival-mouth eye view per Core.Liminal.Field v2 variant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
HERE = Path(__file__).resolve().parent
|
||||
OUT = ROOT / "Saved/Linux/l145"
|
||||
CAPTURES = HERE / "captures"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
Q_PER_METRE = 1024.0
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_step(name: str, command: list[object], log: Path, env: dict[str, str],
|
||||
stamped: bool = False) -> str:
|
||||
expected_stamp = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
binary = Path(str(command[0]))
|
||||
probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
||||
if probe is None or probe.returncode != 0 \
|
||||
or first_stamp(probe.stdout or "") != expected_stamp:
|
||||
raise RuntimeError(f"{name} refused a stale executable: {binary}")
|
||||
started = time.monotonic()
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)],
|
||||
cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=1745, check=False)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L145Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if stamped and (first_stamp(output) != expected_stamp
|
||||
or current_stamp() != expected_stamp):
|
||||
raise RuntimeError(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"{name} exit={result.returncode}; see {log}")
|
||||
return output
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
dx, dy, dz = (target[i] - camera[i] for i in range(3))
|
||||
return math.degrees(math.atan2(dx, dy)), math.degrees(
|
||||
math.atan2(dz, math.hypot(dx, dy)))
|
||||
|
||||
|
||||
def route_views(manifest_path: Path, layout_path: Path, name: str):
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
plan = next(row for row in manifest["plan"]["strates"]
|
||||
if row["strate_index"] == 1 and row.get("liminal_field", {}).get("version") == 2)
|
||||
points = [tuple(value / Q_PER_METRE for value in row["position_q"])
|
||||
for row in plan["route"]]
|
||||
first, second = points[0], points[1]
|
||||
dx, dy = second[0] - first[0], second[1] - first[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length <= 1.0e-6:
|
||||
raise RuntimeError("Core.Liminal.Field arrival tangent is degenerate")
|
||||
tx, ty = dx / length, dy / length
|
||||
eye_camera = (first[0] - tx * 1.5, first[1] - ty * 1.5, first[2] + 1.6)
|
||||
third = points[min(2, len(points) - 1)]
|
||||
dx, dy = third[0] - first[0], third[1] - first[1]
|
||||
tangent_length = math.hypot(dx, dy)
|
||||
tx, ty = dx / tangent_length, dy / tangent_length
|
||||
dz_per_metre = (third[2] - first[2]) / tangent_length
|
||||
eye_target = (first[0] + tx * 40.0, first[1] + ty * 40.0,
|
||||
first[2] + dz_per_metre * 40.0 + 1.6)
|
||||
|
||||
middle = len(points) // 2
|
||||
before, center, after = points[middle - 1], points[middle], points[middle + 1]
|
||||
dx, dy = after[0] - before[0], after[1] - before[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length <= 1.0e-6:
|
||||
raise RuntimeError("Core.Liminal.Field wide-view tangent is degenerate")
|
||||
tx, ty = dx / length, dy / length
|
||||
nx, ny = -ty, tx
|
||||
floor = plan["liminal_field"]["floor_height_q"] / Q_PER_METRE
|
||||
if name == "flatField-monolith":
|
||||
monolith = next(row for row in layout.get("stamps", [])
|
||||
if row.get("stable_id") == "core/liminal-field/monolith/000")
|
||||
monolith_center = tuple(value / 1.0e6 for value in monolith["position_um"])
|
||||
floor = monolith_center[2] - monolith["half_extents_um"][2] / 1.0e6
|
||||
wide_camera = (monolith_center[0] - tx * 30.0 + nx * 18.0,
|
||||
monolith_center[1] - ty * 30.0 + ny * 18.0, floor + 25.0)
|
||||
wide_target = monolith_center
|
||||
elif name == "periodicHills":
|
||||
wide_camera = (center[0] - 20.0, center[1] - 25.0, floor + 10.0)
|
||||
wide_target = (center[0] + 10.0, center[1] + 8.0, floor + 1.0)
|
||||
elif name == "pillar-grid":
|
||||
pillars = [row for row in layout.get("stamps", [])
|
||||
if row.get("stable_id", "").startswith("core/liminal-field/pillars/")]
|
||||
if not pillars:
|
||||
raise RuntimeError("pillar-grid layout has no resolved grid stamps")
|
||||
pairs = []
|
||||
for i, first_pillar in enumerate(pillars):
|
||||
for second_pillar in pillars[i + 1:]:
|
||||
a = first_pillar["position_um"]
|
||||
b = second_pillar["position_um"]
|
||||
dx, dy = (b[0] - a[0]) / 1.0e6, (b[1] - a[1]) / 1.0e6
|
||||
separation = math.hypot(dx, dy)
|
||||
if 60.0 <= separation <= 90.0:
|
||||
mx = (a[0] + b[0]) / 2.0e6
|
||||
my = (a[1] + b[1]) / 2.0e6
|
||||
pairs.append((math.hypot(mx - center[0], my - center[1]),
|
||||
mx, my, dx / separation, dy / separation,
|
||||
(a[2] + b[2]) / 2.0e6,
|
||||
first_pillar["half_extents_um"][2] / 1.0e6))
|
||||
if not pairs:
|
||||
raise RuntimeError("pillar-grid has no neighboring resolved columns")
|
||||
_, mx, my, tx, ty, pillar_z, half_height = min(pairs)
|
||||
floor = pillar_z - half_height
|
||||
nx, ny = -ty, tx
|
||||
wide_camera = (center[0] - tx * 20.0 + nx * 25.0,
|
||||
center[1] - ty * 20.0 + ny * 25.0, center[2] + 8.0)
|
||||
wide_target = (center[0], center[1], floor + 2.0)
|
||||
else:
|
||||
wide_camera = (center[0] - tx * 20.0 + nx * 25.0,
|
||||
center[1] - ty * 20.0 + ny * 25.0, center[2] + 8.0)
|
||||
wide_target = (center[0], center[1], floor + 2.0)
|
||||
return {"eye": (eye_camera, eye_target), "wide": (wide_camera, wide_target)}
|
||||
|
||||
|
||||
def capture_variant(seed: int, name: str, *, headless: Path, renderer: Path,
|
||||
fixture: Path, env: dict[str, str]):
|
||||
stem = f"core-liminal-{name}-seed{seed:02d}"
|
||||
plan_dir = OUT / "capture-plans" / stem
|
||||
plan_dir.mkdir(parents=True, exist_ok=True)
|
||||
manifest = plan_dir / "manifest.json"
|
||||
layout = plan_dir / "layout.json"
|
||||
command = [headless, fixture, "--workers=1", f"--seed={seed}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=1", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--camera=0,0,-540,0,0",
|
||||
f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
output = run_step(f"plan-{stem}", command, OUT / "logs" / f"capture-{stem}-plan.log",
|
||||
env, stamped=True)
|
||||
if "[CoreLiminalFieldDraw]" not in output:
|
||||
raise RuntimeError(f"{stem} plan omitted its Core liminal deck")
|
||||
views = route_views(manifest, layout, name)
|
||||
for view, (eye, target) in {"eye": views["eye"]}.items():
|
||||
work = OUT / "captures" / f"{stem}-{view}"
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
view_manifest = work / "manifest.json"
|
||||
view_layout = work / "layout.json"
|
||||
mesh = work / "world.vfmesh"
|
||||
yaw, pitch = camera_angles(eye, target)
|
||||
camera = (*eye, yaw, pitch)
|
||||
view_clip_radius = (5 if view == "wide" and name in {
|
||||
"flatField-monolith", "periodicHills"} else
|
||||
3)
|
||||
command = [headless, fixture, "--workers=12", f"--seed={seed}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={view_clip_radius}",
|
||||
"--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--capture-skip-walkability",
|
||||
f"--camera={fmt(camera)}", f"--mesh-export={mesh}",
|
||||
f"--world-assembler-manifest-output={view_manifest}",
|
||||
f"--resolved-layout-output={view_layout}"]
|
||||
output = run_step(f"export-{stem}-{view}", command,
|
||||
OUT / "logs" / f"capture-{stem}-{view}-export.log", env, stamped=True)
|
||||
if "[CoreLiminalFieldDraw]" not in output or not mesh.is_file() \
|
||||
or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"{stem}-{view} export omitted its Core liminal deck or mesh")
|
||||
png = work / f"{view}.png"
|
||||
image = CAPTURES / f"{stem}-{view}.jpg"
|
||||
fog = {
|
||||
"flatField-monolith": (30, 180, "0.66,0.73,0.77", -708.0, 46.0),
|
||||
"periodicHills": (140, 850, "0.78,0.84,0.88", -720.0, 14.0),
|
||||
"road-over-cloud-floor": (24, 420, "0.88,0.93,0.95", -720.0, 65.0),
|
||||
"pillar-grid": (180, 1200, "0.82,0.88,0.91", -720.0, 80.0),
|
||||
}[name]
|
||||
fog_start, fog_end, fog_color, height_center, height_range = fog
|
||||
render = [renderer, mesh, png, "--size=1600x900", "--fov-deg=72",
|
||||
f"--camera-m={fmt(eye)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--tile-skirts=on", "--shade=readable", "--headlamp=off", "--height-tint=on",
|
||||
f"--height-center-m={height_center:.6f}", f"--height-range-m={height_range:.6f}",
|
||||
"--light-direction=-0.42,-0.58,0.70", "--fog=on", f"--fog-start-m={fog_start}",
|
||||
f"--fog-end-m={fog_end}", f"--fog-color={fog_color}"]
|
||||
run_step(f"render-{stem}-{view}", render,
|
||||
OUT / "logs" / f"capture-{stem}-{view}-render.log", env, stamped=True)
|
||||
run_step(f"jpeg-{stem}-{view}", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel",
|
||||
"error", "-y", "-i", png, "-frames:v", "1", "-q:v", "4", image],
|
||||
OUT / "logs" / f"capture-{stem}-{view}-jpeg.log", env)
|
||||
run_step(f"quality-{stem}-{view}", [sys.executable,
|
||||
ROOT / "Standalone/Tools/verify/capture_quality.py", image],
|
||||
OUT / "logs" / f"capture-{stem}-{view}-quality.log", env)
|
||||
print(f"[L145Capture] variant={name} seed={seed} view={view} "
|
||||
f"eye_height={'1.600m' if view == 'eye' else 'wide'} bytes={image.stat().st_size} "
|
||||
f"sha256={sha256(image)} status=PASS", flush=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
headless = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l145-clang"
|
||||
renderer = ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l145-clang"
|
||||
fixture = OUT / "fixtures/core-liminal-field-3-strata.json"
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(OUT)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT / "Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
CAPTURES.mkdir(parents=True, exist_ok=True)
|
||||
variants = ("flatField-monolith", "periodicHills",
|
||||
"road-over-cloud-floor", "pillar-grid")
|
||||
selected = set(os.environ.get("L145_CAPTURE_VARIANTS", "").split(","))
|
||||
for seed, name in enumerate(variants):
|
||||
if selected and name not in selected:
|
||||
continue
|
||||
capture_variant(seed, name, headless=headless, renderer=renderer,
|
||||
fixture=fixture, env=env)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L145Capture] status=FAIL error={error}", file=sys.stderr, flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,51 @@
|
||||
# Linux145 — Core.Liminal.Field v2 landing pads
|
||||
|
||||
**Result: endpoint landing pads alone did not make the liminal join solvable.** The pads bind the passage mouth to certified ground and remove grade failures in the final seed 5 probe, but the passage capsule still intersects native Surface World terrain about 29 m from the departure pad. This matches R48's warning that a pad must cover the approach and tube exclusion envelope; stop here and use a bounded joint plan.
|
||||
|
||||
## What changed
|
||||
|
||||
- Added an opt-in landing-pad record to resolved strata: stable ID, arrival/departure role, position, floor height, Q16 approach direction, clearance, support depth, and a certification bit. The passage planner validates the record and binds the endpoint and tangent to it. Empty pad lists are omitted from canonical serialization, preserving legacy owner layouts and hashes.
|
||||
- Added `Core.Liminal.Field` v2 pads on the field profile: flat ground, hill/valley profile, road endpoints, and pillar-grid clearings. Its layer route connects the pads; the road uses that route as its road centerline.
|
||||
- Added an uncertified-pad negative control. Corrected the ripple audit to retain each valid sample's actual route distance across masked gaps; a 5 m synthetic gap control now confirms that the measurement detects a 5.8 cm ripple and rejects the same smooth cubic control.
|
||||
- Fixed the composed audit's footing placement so it counts actual structural supports. No Unreal `Source/` files changed.
|
||||
|
||||
## Count first: failure cause
|
||||
|
||||
L144's fixed-mouth route already showed both end-connector grade and capsule blockage. For the flat-field seed 0 probe, the liminal route had 222 blocked capsule samples and 7 grade failures, with no unsupported samples or structural-support failures. Pillar-grid seed 3 had 289 blocked samples and 9 grade failures. In the separate seed 5 audit, interface 1–2 had 10 blocked passage-capsule samples and the liminal route had 9 blocked samples, both with zero grade failures. These results say support alone was not the cause; the fixed endpoint geometry sometimes made the approach too steep, while other cases had occupied space at the join. [L144 report](../Linux144/REPORT.md); logs: `Saved/Linux/l144/logs/probe8-seed00.log`, `probe8-seed03.log`, `audit-seed05.log`.
|
||||
|
||||
The pad experiment removed the endpoint-grade part in seed 5, but not the occupied-space part:
|
||||
|
||||
| Probe | Passage 1–2 capsule | Grade / support | Shell | Liminal route |
|
||||
|---|---:|---|---|---|
|
||||
| L144 seed 5, fixed mouths | 10 blocked | 0 grade failures; 0 unsupported | not the failing gate | 9 blocked; 0 grade failures |
|
||||
| L145 seed 5, 45 m ground margin | 140 blocked; 20 interior shell violations | 0 grade failures; 0 unsupported | FAIL | PASS |
|
||||
| L145 seed 5, final 20 m ground margin | 289 blocked | 0 grade failures; 0 unsupported | PASS | PASS |
|
||||
|
||||
The final L145 Clang W1 probe (source stamp `82e3b58c…`) certified both pads. The departure pad was `(12, 0, -716.451) m`, with floor `-717.581 m`, 6 m clearance, and 2 m support depth. Yet the interface-1–2 passage reported 289/50,904 capsule samples blocked, with 0/4,040 unsupported and 0 grade failures. A blocked sample at `(-16.087, 2.915, -722.743) m` had native density `-3.629` and composed density `-0.162`; it is about 29 m from the departure pad. The sample is inside the passage tube, so the layer's field remains solid across the approach even though its pad itself is certified. The lower interface, both shell audits, both ripple audits, and the liminal walk audit passed; the final composed passage summary failed and seam audit did not run for this seed. See `Saved/Linux/l145/logs/audit-seed05-clang-w1.log`.
|
||||
|
||||
For the 45 m attempt, the original seed 0–4 Clang W1 probes passed their composed gates, then seed 5 failed. The seed 5 log was overwritten by the final 20 m rerun; its counts above are from that earlier run. The corrected ripple fitter also removed a false 5.99 cm reading caused by compressing masked samples: at 45 m, seed 0 measured 2.87 cm after retaining route distances. These are probe results, not timing measurements.
|
||||
|
||||
The third repeated seed 5 failure reaches the requested stop condition. This does not show that a full candidate-and-approach contract is impossible; it shows the tested endpoint-only pad contract is too weak. The passage planner needs to choose a candidate while checking the full capsule approach and tube against the layer field, as R48 recommends.
|
||||
|
||||
## Captures
|
||||
|
||||
One 1.6 m eye-height capture was rendered at each arrival pad. All four passed `capture_quality.py`; mesh export used `--capture-skip-walkability`, so these images show appearance and are not walkability evidence.
|
||||
|
||||
| Variant | Capture |
|
||||
|---|---|
|
||||
| Flat field + monolith |  |
|
||||
| Repeating hills |  |
|
||||
| Road over cloud floor |  |
|
||||
| Pillar grid |  |
|
||||
|
||||
## Checks
|
||||
|
||||
- Clang 19 headless and offscreen-capture binaries reported the final source stamp. Four arrival captures passed quality checks.
|
||||
- Fast tier passed on the final source: `Saved/Linux/l145/fast/fast-summary.json` (280.8 s tier duration). The initial default-output attempt refused a stale L42 binary before tests started; rerunning into the isolated L145 output directory with the current L145 binary passed.
|
||||
- Owner canonical hash checks passed: Surface World style 1 and Floating Islands style 2 each matched all 12 pinned L130-v227 rows at W1 and W12, and each recipe's W1/W12 rows matched. These exercise the no-pad owner path.
|
||||
- `git diff --check` passed.
|
||||
- The landing-pad composed matrix stopped at seed 5 after the repeated failure. It did not complete 8 seeds per variant (32 runs). Clang/GCC W1/W8/W12 identity for the liminal fixture was not run. Final-source seeds 0–4 were not rerun after the margin returned to 20 m. No performance timing was collected.
|
||||
|
||||
Changed standalone files: `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h`, `Standalone/Core/src/WorldAssemblerInternal.h`, `Standalone/Source/StreamBenchmarkHeadless.cpp`, `Standalone/Source/StreamBenchmarkPipeline.h`, and `Standalone/Source/WorldAssemblyPrototype.h`.
|
||||
|
||||
Remaining risk: the current pad record certifies a local landing and direction, not an approach corridor. A future joint planner must reserve/check the full capsule and tube against the layer before finalizing pad choice and passage geometry.
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L145's composed liminal audits and the worker/compiler identity matrix."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l145"
|
||||
FIXTURE = OUT / "fixtures/core-liminal-field-3-strata.json"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
AUDIT_FLAGS = ("--audit-resolved-layout-passages", "--audit-liminal-road-walk",
|
||||
"--audit-passage-tile-seams")
|
||||
VARIANTS = ("flatField-monolith", "periodicHills",
|
||||
"road-over-cloud-floor", "pillar-grid")
|
||||
PASSAGE_RE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||
SHELL_RE = re.compile(r"^\[PassageShellAudit\].*status=PASS$", re.M)
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import current_stamp, first_stamp
|
||||
|
||||
|
||||
def parse_seeds(value: str) -> list[int]:
|
||||
result: set[int] = set()
|
||||
for token in value.split(","):
|
||||
token = token.strip()
|
||||
if not token:
|
||||
continue
|
||||
if "-" in token:
|
||||
first, last = (int(part) for part in token.split("-", 1))
|
||||
if first > last:
|
||||
raise argparse.ArgumentTypeError(f"reversed seed range: {token}")
|
||||
result.update(range(first, last + 1))
|
||||
else:
|
||||
result.add(int(token))
|
||||
if not result or min(result) < 0:
|
||||
raise argparse.ArgumentTypeError("seeds must be nonnegative and nonempty")
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def validate_audit(output: str, seed: int, compiler: str, workers: int) -> None:
|
||||
checks = {
|
||||
"landing-pad negative control":
|
||||
"[LandingPadNegativeControl] uncertified_rejected=true status=PASS" in output,
|
||||
"passage walk audits": len(PASSAGE_RE.findall(output)) == 2,
|
||||
"passage shell audits": len(SHELL_RE.findall(output)) == 2,
|
||||
"passage ripple audits": len(re.findall(
|
||||
r"^\[PassageRippleAudit\].*status=PASS$", output, re.M)) == 2,
|
||||
"masked ripple sampling controls": len(re.findall(
|
||||
r"^\[PassageRippleSamplingControl\].*status=PASS$", output, re.M)) == 2,
|
||||
"passage shell summary": bool(re.search(
|
||||
r"^\[PassageShellAuditSummary\].*status=PASS$", output, re.M)),
|
||||
"liminal walk audit": bool(re.search(
|
||||
r"^\[LiminalLayerWalkAudit\].*status=PASS$", output, re.M)),
|
||||
"liminal negative control":
|
||||
"[LiminalLayerNegativeControl] altered_departure_rejected=true status=PASS" in output,
|
||||
"passage summary": bool(re.search(
|
||||
r"^\[ResolvedPassageAuditSummary\].*status=PASS$", output, re.M)),
|
||||
"seam audit": bool(re.search(
|
||||
r"^\[PassageTileSeamAudit\].*status=PASS$", output, re.M)),
|
||||
}
|
||||
failed = [name for name, passed in checks.items() if not passed]
|
||||
if failed:
|
||||
raise RuntimeError(
|
||||
f"seed={seed} compiler={compiler} workers={workers} failed checks: "
|
||||
+ ", ".join(failed))
|
||||
|
||||
|
||||
def run_audit(seed: int, compiler: str, workers: int, *, audit: bool = True) -> dict:
|
||||
binary = ROOT / f"Standalone/bin/linux/VoxelForgeCoreHeadless-l145-{compiler}"
|
||||
if not binary.is_file():
|
||||
raise RuntimeError(f"missing binary: {binary}")
|
||||
expected_stamp = current_stamp()
|
||||
probe = subprocess.run([str(binary), "--print-source-stamp"], cwd=ROOT,
|
||||
capture_output=True, text=True, timeout=15, check=False)
|
||||
if probe.returncode != 0 or first_stamp(probe.stdout) != expected_stamp:
|
||||
raise RuntimeError(f"stale executable: {binary}")
|
||||
|
||||
tag = f"seed{seed:02d}-{compiler}-w{workers}"
|
||||
log = OUT / "logs" / f"audit-{tag}.log"
|
||||
manifest = OUT / "manifests" / f"audit-{tag}.json"
|
||||
layout = OUT / "layouts" / f"audit-{tag}.json"
|
||||
manifest.parent.mkdir(parents=True, exist_ok=True)
|
||||
layout.parent.mkdir(parents=True, exist_ok=True)
|
||||
command = [str(binary), str(FIXTURE), f"--workers={workers}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=1", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--camera=0,0,-540,0,0", f"--seed={seed}",
|
||||
f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
if audit:
|
||||
command.extend(AUDIT_FLAGS)
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(OUT)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT / "Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, "timeout", "1740s", *command],
|
||||
cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=1745, check=False)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"{tag} exit={result.returncode}; see {log}")
|
||||
if first_stamp(output) != expected_stamp or current_stamp() != expected_stamp:
|
||||
raise RuntimeError(f"{tag} source stamp changed during run; see {log}")
|
||||
if audit:
|
||||
validate_audit(output, seed, compiler, workers)
|
||||
if not manifest.is_file() or not layout.is_file():
|
||||
raise RuntimeError(f"{tag} omitted canonical manifest or layout")
|
||||
manifest_value = json.loads(manifest.read_text(encoding="utf-8"))
|
||||
layout_value = json.loads(layout.read_text(encoding="utf-8"))
|
||||
identity = {
|
||||
"seed": seed,
|
||||
"variant": VARIANTS[seed % len(VARIANTS)],
|
||||
"compiler": compiler,
|
||||
"workers": workers,
|
||||
"manifest_hash": manifest_value["content_hash"],
|
||||
"resolved_layout_identity": layout_value["stable_identity"],
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
"elapsed_s": round(time.monotonic() - started, 3),
|
||||
"audited": audit,
|
||||
}
|
||||
print(f"[L145AuditRun] seed={seed} variant={identity['variant']} compiler={compiler} "
|
||||
f"workers={workers} manifest={identity['manifest_hash']} "
|
||||
f"layout={identity['resolved_layout_identity']} "
|
||||
f"elapsed_s={identity['elapsed_s']:.3f} status=PASS", flush=True)
|
||||
return identity
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--seeds", default="0-31", type=parse_seeds,
|
||||
help="audit seeds, e.g. 0-31 or 0,1,2,3")
|
||||
parser.add_argument("--determinism", action="store_true",
|
||||
help="also compare seed 0 on Clang/GCC at W1/W8/W12")
|
||||
parser.add_argument("--only-determinism", action="store_true",
|
||||
help="run only the Clang/GCC seed-0 W1/W8/W12 identity matrix")
|
||||
args = parser.parse_args()
|
||||
if not FIXTURE.is_file():
|
||||
raise SystemExit(f"missing fixture: {FIXTURE}; run BuildL145Fixtures.py first")
|
||||
|
||||
records = []
|
||||
if not args.only_determinism:
|
||||
for seed in args.seeds:
|
||||
records.append(run_audit(seed, "clang", 1))
|
||||
if args.determinism or args.only_determinism:
|
||||
matrix = [run_audit(0, compiler, workers)
|
||||
for compiler in ("clang", "gcc") for workers in (1, 8, 12)]
|
||||
identities = {(row["manifest_hash"], row["resolved_layout_identity"])
|
||||
for row in matrix}
|
||||
if len(identities) != 1:
|
||||
raise RuntimeError(f"Clang/GCC W1/W8/W12 identity mismatch: {matrix}")
|
||||
records.extend(matrix)
|
||||
|
||||
summary = {
|
||||
"audit_seeds": args.seeds if not args.only_determinism else [],
|
||||
"variant_counts": {
|
||||
name: sum(1 for seed in args.seeds if seed % len(VARIANTS) == index)
|
||||
for index, name in enumerate(VARIANTS)
|
||||
} if not args.only_determinism else {},
|
||||
"records": records,
|
||||
"determinism_pass": bool(args.determinism or args.only_determinism),
|
||||
"status": "PASS",
|
||||
}
|
||||
summary_path = OUT / "audit-matrix-summary.json"
|
||||
summary_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
summary_path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L145AuditMatrix] records={len(records)} summary={summary_path} status=PASS",
|
||||
flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L145AuditMatrix] status=FAIL error={error}", file=sys.stderr, flush=True)
|
||||
raise SystemExit(1)
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,46 @@
|
||||
# L146 report
|
||||
|
||||
**Result:** No production fix is retained. The cause is measured: the liminal route fill pass runs after the passage carve and makes part of the bore solid again. The new passage-interior capsule audit catches the seed-5 defect. After three fixes cleared the bore but failed the liminal road support gate, work stopped as required by `AGENTS.md`.
|
||||
|
||||
## Root cause
|
||||
|
||||
Measured at `(-16.087, 2.915, -722.743) m`, seed 5, Clang W1, composed L145 fixture:
|
||||
|
||||
- Before passage composition, density was `-3.62890625`.
|
||||
- The passage SDF reported `union_distance_m=-0.879889786`; carving changed density to `+3.51955914` (air).
|
||||
- The subsequent liminal route stamp changed it to `-0.16169475` (solid).
|
||||
|
||||
The diagnostic directly identifies the post-carve route-fill pass as the cause. The resolved route/deck geometry indicates the fill overlaps the bore there; that specific deck overlap is inferred from the layout. The half-pipe/open-air path, carve reach, and carve blend are not the cause at this sample: the passage carver ran and produced positive air density before the route stamp.
|
||||
|
||||
Trace: `Saved/Linux/l146/logs/diagnostic-seed05-clang-w1.log`.
|
||||
|
||||
## Fix attempts
|
||||
|
||||
All were temporary and have been reverted; production composition and evaluator version remain at L145 / v227.
|
||||
|
||||
1. Move fill stamps before passage carving: passage probes cleared, but the liminal road had 29 unsupported stations.
|
||||
2. Suppress route fills inside the passage interior: same result, 29 unsupported stations.
|
||||
3. Reroute the southern deck around the crossing: passage probes cleared, but 28 road stations remained unsupported.
|
||||
|
||||
The three seed-5 runs are `probe-seed05-clang-w1.log`, `probe-masked-seed05-clang-w1.log`, and `route-seed05-clang-w1.log` under `Saved/Linux/l146/logs/`.
|
||||
|
||||
The shared premise that changing stamp order or locally moving the deck could preserve both bore air and road support failed twice. The route adjustment also failed the support gate, so no fourth fix was attempted.
|
||||
|
||||
## Audit and checks
|
||||
|
||||
`StreamBenchmarkPipeline.h` now classifies each tested player-capsule sample against its passage interior and requires zero solid interior samples. A no-passage negative control uses the same interior classification and must detect solid samples for every passage.
|
||||
|
||||
On the final source, the seed-5 audit measured 50,904 interior capsule samples and 289 solid samples for passage 1-2. Both negative controls detected solid interior samples (165,769 and 49,612). The shell summary passed with zero floor or wall violations, and the liminal road audit passed. The composed passage audit exited 10 as expected because the new interior-air gate detected the defect.
|
||||
|
||||
The final-source evidence is `Saved/Linux/l146/logs/audit-seed05-clang-w1.log`.
|
||||
|
||||
- Clang W1 final-source build: passed; log `Saved/Linux/l146/fast/logs/build-fast-clang-final.log`.
|
||||
- Fast verification tier: passed in 362.5 s; summary `Saved/Linux/l146/fast/fast-summary.json`.
|
||||
- Owner canonical v227 hash rows: passed for both pinned owner worlds; W1 and W12 signatures matched.
|
||||
- `git diff --check`: passed.
|
||||
- The 32-run pad matrix and the liminal Clang/GCC W1/W8/W12 matrix were not run because the three-failed-fix stop condition was reached. The 16-seed cave-planner sweep is not on this branch.
|
||||
- No Unreal `Source/` files changed.
|
||||
|
||||
## Remaining risk
|
||||
|
||||
The seed-5 passage interior remains partly solid in production. A new design must define how route floor support and passage air share their overlap before changing composition again. No passage-composition change or owner hash change is retained.
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L146's composed liminal audits and the worker/compiler identity matrix."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l146"
|
||||
FIXTURE = OUT / "fixtures/core-liminal-field-3-strata.json"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
AUDIT_FLAGS = ("--audit-resolved-layout-passages", "--audit-liminal-road-walk",
|
||||
"--audit-passage-tile-seams")
|
||||
VARIANTS = ("flatField-monolith", "periodicHills",
|
||||
"road-over-cloud-floor", "pillar-grid")
|
||||
PASSAGE_RE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||
SHELL_RE = re.compile(r"^\[PassageShellAudit\].*status=PASS$", re.M)
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import current_stamp, first_stamp
|
||||
|
||||
|
||||
def parse_seeds(value: str) -> list[int]:
|
||||
result: set[int] = set()
|
||||
for token in value.split(","):
|
||||
token = token.strip()
|
||||
if not token:
|
||||
continue
|
||||
if "-" in token:
|
||||
first, last = (int(part) for part in token.split("-", 1))
|
||||
if first > last:
|
||||
raise argparse.ArgumentTypeError(f"reversed seed range: {token}")
|
||||
result.update(range(first, last + 1))
|
||||
else:
|
||||
result.add(int(token))
|
||||
if not result or min(result) < 0:
|
||||
raise argparse.ArgumentTypeError("seeds must be nonnegative and nonempty")
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def validate_audit(output: str, seed: int, compiler: str, workers: int) -> None:
|
||||
checks = {
|
||||
"landing-pad negative control":
|
||||
"[LandingPadNegativeControl] uncertified_rejected=true status=PASS" in output,
|
||||
"passage walk audits": len(PASSAGE_RE.findall(output)) == 2,
|
||||
"passage interior capsule audit": len(re.findall(
|
||||
r"^\[ResolvedPassageAudit\].*passage_interior_capsule_samples=[1-9]\d* "
|
||||
r"passage_interior_solid_samples=0 interior_air=PASS .*walkability=PASS$",
|
||||
output, re.M)) == 2,
|
||||
"passage interior negative controls": len(re.findall(
|
||||
r"^\[ResolvedPassageInteriorNegativeControl\].*interior_solid_samples=[1-9]\d* "
|
||||
r"detected=true status=PASS$", output, re.M)) == 2,
|
||||
"passage interior audit summary": bool(re.search(
|
||||
r"^\[PassageInteriorCapsuleAuditSummary\].*status=PASS$", output, re.M)),
|
||||
"passage shell audits": len(SHELL_RE.findall(output)) == 2,
|
||||
"passage ripple audits": len(re.findall(
|
||||
r"^\[PassageRippleAudit\].*status=PASS$", output, re.M)) == 2,
|
||||
"masked ripple sampling controls": len(re.findall(
|
||||
r"^\[PassageRippleSamplingControl\].*status=PASS$", output, re.M)) == 2,
|
||||
"passage shell summary": bool(re.search(
|
||||
r"^\[PassageShellAuditSummary\].*status=PASS$", output, re.M)),
|
||||
"liminal walk audit": bool(re.search(
|
||||
r"^\[LiminalLayerWalkAudit\].*status=PASS$", output, re.M)),
|
||||
"liminal negative control":
|
||||
"[LiminalLayerNegativeControl] altered_departure_rejected=true status=PASS" in output,
|
||||
"passage summary": bool(re.search(
|
||||
r"^\[ResolvedPassageAuditSummary\].*status=PASS$", output, re.M)),
|
||||
"seam audit": bool(re.search(
|
||||
r"^\[PassageTileSeamAudit\].*status=PASS$", output, re.M)),
|
||||
}
|
||||
failed = [name for name, passed in checks.items() if not passed]
|
||||
if failed:
|
||||
raise RuntimeError(
|
||||
f"seed={seed} compiler={compiler} workers={workers} failed checks: "
|
||||
+ ", ".join(failed))
|
||||
|
||||
|
||||
def run_audit(seed: int, compiler: str, workers: int, *, audit: bool = True) -> dict:
|
||||
binary = OUT / "fast/bin" / f"VoxelForgeCoreHeadless-fast-{compiler}"
|
||||
if not binary.is_file():
|
||||
raise RuntimeError(f"missing binary: {binary}")
|
||||
expected_stamp = current_stamp()
|
||||
probe = subprocess.run([str(binary), "--print-source-stamp"], cwd=ROOT,
|
||||
capture_output=True, text=True, timeout=15, check=False)
|
||||
if probe.returncode != 0 or first_stamp(probe.stdout) != expected_stamp:
|
||||
raise RuntimeError(f"stale executable: {binary}")
|
||||
|
||||
tag = f"seed{seed:02d}-{compiler}-w{workers}"
|
||||
log = OUT / "logs" / f"audit-{tag}.log"
|
||||
manifest = OUT / "manifests" / f"audit-{tag}.json"
|
||||
layout = OUT / "layouts" / f"audit-{tag}.json"
|
||||
manifest.parent.mkdir(parents=True, exist_ok=True)
|
||||
layout.parent.mkdir(parents=True, exist_ok=True)
|
||||
command = [str(binary), str(FIXTURE), f"--workers={workers}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=1", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--camera=0,0,-540,0,0", f"--seed={seed}",
|
||||
f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
if audit:
|
||||
command.extend(AUDIT_FLAGS)
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(OUT)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT / "Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, "timeout", "1740s", *command],
|
||||
cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=1745, check=False)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"{tag} exit={result.returncode}; see {log}")
|
||||
if first_stamp(output) != expected_stamp or current_stamp() != expected_stamp:
|
||||
raise RuntimeError(f"{tag} source stamp changed during run; see {log}")
|
||||
if audit:
|
||||
validate_audit(output, seed, compiler, workers)
|
||||
if not manifest.is_file() or not layout.is_file():
|
||||
raise RuntimeError(f"{tag} omitted canonical manifest or layout")
|
||||
manifest_value = json.loads(manifest.read_text(encoding="utf-8"))
|
||||
layout_value = json.loads(layout.read_text(encoding="utf-8"))
|
||||
identity = {
|
||||
"seed": seed,
|
||||
"variant": VARIANTS[seed % len(VARIANTS)],
|
||||
"compiler": compiler,
|
||||
"workers": workers,
|
||||
"manifest_hash": manifest_value["content_hash"],
|
||||
"resolved_layout_identity": layout_value["stable_identity"],
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
"elapsed_s": round(time.monotonic() - started, 3),
|
||||
"audited": audit,
|
||||
}
|
||||
print(f"[L146AuditRun] seed={seed} variant={identity['variant']} compiler={compiler} "
|
||||
f"workers={workers} manifest={identity['manifest_hash']} "
|
||||
f"layout={identity['resolved_layout_identity']} "
|
||||
f"elapsed_s={identity['elapsed_s']:.3f} status=PASS", flush=True)
|
||||
return identity
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--seeds", default="0-31", type=parse_seeds,
|
||||
help="audit seeds, e.g. 0-31 or 0,1,2,3")
|
||||
parser.add_argument("--determinism", action="store_true",
|
||||
help="also compare seed 0 on Clang/GCC at W1/W8/W12")
|
||||
parser.add_argument("--only-determinism", action="store_true",
|
||||
help="run only the Clang/GCC seed-0 W1/W8/W12 identity matrix")
|
||||
args = parser.parse_args()
|
||||
if not FIXTURE.is_file():
|
||||
raise SystemExit(f"missing fixture: {FIXTURE}; build the L145 liminal fixture first")
|
||||
|
||||
records = []
|
||||
if not args.only_determinism:
|
||||
for seed in args.seeds:
|
||||
records.append(run_audit(seed, "clang", 1))
|
||||
if args.determinism or args.only_determinism:
|
||||
matrix = [run_audit(0, compiler, workers)
|
||||
for compiler in ("clang", "gcc") for workers in (1, 8, 12)]
|
||||
identities = {(row["manifest_hash"], row["resolved_layout_identity"])
|
||||
for row in matrix}
|
||||
if len(identities) != 1:
|
||||
raise RuntimeError(f"Clang/GCC W1/W8/W12 identity mismatch: {matrix}")
|
||||
records.extend(matrix)
|
||||
|
||||
summary = {
|
||||
"audit_seeds": args.seeds if not args.only_determinism else [],
|
||||
"variant_counts": {
|
||||
name: sum(1 for seed in args.seeds if seed % len(VARIANTS) == index)
|
||||
for index, name in enumerate(VARIANTS)
|
||||
} if not args.only_determinism else {},
|
||||
"records": records,
|
||||
"determinism_pass": bool(args.determinism or args.only_determinism),
|
||||
"status": "PASS",
|
||||
}
|
||||
summary_path = OUT / "l146-audit-matrix-summary.json"
|
||||
summary_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
summary_path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L146AuditMatrix] records={len(records)} summary={summary_path} status=PASS",
|
||||
flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L146AuditMatrix] status=FAIL error={error}", file=sys.stderr, flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,38 @@
|
||||
# L157 — near terrain at 30 m/s
|
||||
|
||||
**Result:** Early admission raised moving near coverage, but the W8 experiment did not meet the full latency and worst-tick bar. Keep both experiment controls disabled by default. The UE host was built from `/home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l157-tree/Source`; the VFHost source symlink has since been restored.
|
||||
|
||||
## Measurements
|
||||
|
||||
Five interleaved pairs used the same 30 m/s, 12 s movement plus 5 s dwell, 8 effective tile slots, and four-strate owner plan. Each run had 45–46 moving samples. The uncounted candidate smoke run warmed the same UE binary. The machine was quiet before the matrix (uptime load averages 1.09/1.40/1.22).
|
||||
|
||||
| Moving metric | `assembler/world` median | Early admission + 1 ms cap median | Paired median delta |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| LOD0–2 ready | 36.06% | 48.64% | +10.76 pp |
|
||||
| 55° forward-cone ready | 12.12% | 16.86% | +5.95 pp |
|
||||
| Game-thread tick p99 | 12.51 ms | 10.93 ms | +0.07 ms |
|
||||
| Worst tick per run, median | 14.67 ms | 14.52 ms | +0.18 ms |
|
||||
| Far holes | 4.00% | 5.82% | −0.13 pp |
|
||||
| Initial near-set arrival | 2.80 s | 3.21 s | +0.01 s |
|
||||
|
||||
The p99 median is under 11 ms, but only three candidate runs were under that limit; the other two were 15.75 and 18.17 ms. The worst observed candidate tick was 19.88 ms versus 17.75 ms for baseline. Far-hole results were mixed: the paired median improved slightly, while two candidate pairs regressed and the worst candidate run had 12.48% holes versus a 7.08% baseline worst. Arrival was effectively tied by paired median, with one candidate pair 0.88 s slower. The combined bar therefore fails on repeatability and worst tick; far holes are not robustly no-worse.
|
||||
|
||||
The apply cap lowered median apply p99 from 2.11 to 1.40 ms, but it is not a hard per-frame ceiling: candidate apply p99 reached 6.61 ms and the maximum was 7.58 ms. The loop checks elapsed time after finishing a result. Candidate stream-update p99 reached 15.07 and 13.52 ms in the same two outlier runs. Historical L122 frame joins likewise show that applying extra results adds work on candidate tail frames (about 1.0–1.5 ms), while `UpdateChunks`/desired-set work forms the larger tail; `level_admit` and `submit_tiles` p99 were not consistently higher. The cap cannot make the whole tick fit when update work already exceeds the frame budget.
|
||||
|
||||
## Direction and next lever
|
||||
|
||||
The old metric counts every desired LOD0–2 tile equally, including tiles behind the player. Desired tiles split about 50.44% ahead and 49.56% behind; only 23.20% fell inside the 55° forward cone. Behind readiness was 50.93% baseline / 68.49% candidate, versus 21.71% / 30.60% ahead. The all-around count therefore overstates what is ready in the direction of travel.
|
||||
|
||||
For VR, readiness against the HMD/camera frustum is the useful visibility metric. The velocity-forward cone measured here is a movement proxy, not a head-pose measurement. Report it beside the old all-around residency count. No additional speed fix is validated; a narrow forward-cone admission probe is the next cheap experiment, while desired-set rebuild/update work remains the measured latency target.
|
||||
|
||||
## Checks and limits
|
||||
|
||||
- UBT built `VFHostEditor` successfully, 98/98 actions, using `-NoUBA -MaxParallelActions=4`. It compiled `Source/VoxelForge/Private/VoxelWorld.cpp` from the L157 worktree. Three existing RealtimeMesh deprecation warnings remained.
|
||||
- Fast tier: 268 PASS, 0 FAIL, 5 older-baseline diagnostics. Current canonical owner hashes match L130; W1/W12 hashes match for both pinned owners. Random-camera tile hashes also match across W1/W8/W12.
|
||||
- UE `TileHashDump` comparison was inconclusive: both runs logged `records=0`, despite streamed meshes. No direct UE tile-hash claim is made. The L157 changes are limited to admission/apply controls and trace accounting; no generator or mesher source changed.
|
||||
- W12 movement timing and HMD-pose visibility were not run. W12 was skipped after W8 failed the worst-tick bar; direct UE hash capture emitted no records.
|
||||
- `RunL157Movement.py` passed `py_compile`; `git diff --check` passed.
|
||||
|
||||
Logs and paired data are in `Saved/Linux/l157/`; movement traces and summary are in `Saved/Linux/l157/ue/movement/`.
|
||||
|
||||
Changed Unreal source: `Source/VoxelForge/Private/VoxelWorld.cpp`.
|
||||
@@ -0,0 +1,400 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Interleaved L157 UE movement runs for critical-gate and apply-budget ablations."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
HOST = Path("/home/jahni/VFHost")
|
||||
EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd")
|
||||
OUT = ROOT / "Saved/Linux/l157/ue/movement"
|
||||
HOST_SAVED = HOST / "Saved/Linux/l157"
|
||||
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l157"
|
||||
SPAWN_M = (19.166015625, -14.2001953125, -753.3740234375)
|
||||
MOVE_START_S = 6.0
|
||||
|
||||
CONDITIONS = {
|
||||
"assembler_world": {"wait_for_critical": 1, "apply_budget_ms": 0.0},
|
||||
"early_admission_apply_1ms": {"wait_for_critical": 0, "apply_budget_ms": 1.0},
|
||||
}
|
||||
ORDER = {
|
||||
1: ("assembler_world", "early_admission_apply_1ms"),
|
||||
2: ("early_admission_apply_1ms", "assembler_world"),
|
||||
3: ("assembler_world", "early_admission_apply_1ms"),
|
||||
4: ("early_admission_apply_1ms", "assembler_world"),
|
||||
5: ("assembler_world", "early_admission_apply_1ms"),
|
||||
}
|
||||
|
||||
|
||||
def percentile(values: list[float], fraction: float = 0.99) -> float | None:
|
||||
if not values:
|
||||
return None
|
||||
ordered = sorted(values)
|
||||
return ordered[max(0, min(len(ordered) - 1, math.ceil(len(ordered) * fraction) - 1))]
|
||||
|
||||
|
||||
def read_events(path: Path) -> list[dict]:
|
||||
rows = []
|
||||
if path.is_file():
|
||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
if not line.startswith("{"):
|
||||
continue
|
||||
try:
|
||||
rows.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return rows
|
||||
|
||||
|
||||
def mean(values: list[float]) -> float | None:
|
||||
return statistics.mean(values) if values else None
|
||||
|
||||
|
||||
def percent(numerator: int | float, denominator: int | float) -> float | None:
|
||||
return 100.0 * numerator / denominator if denominator > 0 else None
|
||||
|
||||
|
||||
def run(condition: str, repetition: int, tile_hash_dump: Path | None = None) -> dict:
|
||||
config = CONDITIONS[condition]
|
||||
run_name = f"fast-r{repetition}"
|
||||
out = OUT / condition
|
||||
host_out = HOST_PLUGIN_SAVED / condition / "output"
|
||||
userdir = HOST_SAVED / "user" / condition / run_name
|
||||
zendata = HOST_SAVED / "zen" / condition / run_name
|
||||
ddc = HOST_SAVED / "DDC"
|
||||
trace = out / f"{run_name}.trace.jsonl"
|
||||
abslog = out / f"{run_name}.log"
|
||||
stdout = out / f"{run_name}.stdout.log"
|
||||
export = host_out / run_name
|
||||
for directory in (out, export, userdir, zendata, ddc):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
trace.unlink(missing_ok=True)
|
||||
|
||||
spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWN_M)
|
||||
command = [
|
||||
str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended",
|
||||
"-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch",
|
||||
"-notraceserver", "-nocrashreports", "-DisablePlugins=AndroidFileServer",
|
||||
"-ddc=NoZenLocalFallback", f"-userdir={userdir}", f"-ZenDataPath={zendata}",
|
||||
f"-LocalDataCachePath={ddc}", "-run=VoxelForgeExplore", "-seed=0",
|
||||
"-archetype=TunnelNetwork", "-slot=0", "-modes=export", "-gameconfig=1",
|
||||
"-coreworldtest=1", "-ownerfourstrateplan=1", "-corevoxelsizecm=25",
|
||||
"-coreislandtest=0", "-corefallbacktest=0", "-coreofftest=0",
|
||||
"-coreplantest=0", "-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings",
|
||||
"-corehybridmesher=0", f"-out={export}", "-voxel.StreamingCoarseFirst=1",
|
||||
"-voxel.StreamingDynamicNearReserve=1",
|
||||
"-voxel.StreamingLegacyPendingResultCap=0",
|
||||
"-voxel.CoreRegionConcurrentAdmission=1",
|
||||
"-voxel.StreamingResultBacklogCap=256", "-voxel.StreamingReadyQueueDepth=8",
|
||||
"-voxel.StreamingBackgroundWorkerCap=1", "-voxel.CoreColumnCacheDirectMapped=0",
|
||||
"-voxel.StreamingFarApplySupplement=1",
|
||||
f"-voxel.StreamingFastTravelWaitForAllCriticalTiles={config['wait_for_critical']}",
|
||||
f"-voxel.StreamingFastTravelApplyBudgetMilliseconds={config['apply_budget_ms']:.1f}",
|
||||
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
|
||||
"-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=3000.0",
|
||||
f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}",
|
||||
"-voxel.TestMoveDurationSeconds=12.0", "-voxel.TestExitSeconds=23.0",
|
||||
"-voxel.TestMoveDirection=1,0,0", "-voxel.StreamingSchedulerPolicy=3",
|
||||
"-voxel.TestExitOnSteady=0", "-coreworldstreamtrace=1",
|
||||
f"-voxel.StartupTraceSpawnCm={spawn_cm}",
|
||||
"-voxel.StartupTraceUntilEndPlay=1", "-voxel.StartupTraceThroughCrossing=1",
|
||||
"-voxel.StartupTraceMaxTiles=16384", f"-voxel.StartupTraceFile={trace}",
|
||||
f"-abslog={abslog}",
|
||||
]
|
||||
if tile_hash_dump is not None:
|
||||
tile_hash_dump.parent.mkdir(parents=True, exist_ok=True)
|
||||
tile_hash_dump.unlink(missing_ok=True)
|
||||
command.append(f"-voxel.TileHashDump={tile_hash_dump.resolve()}")
|
||||
wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command]
|
||||
started = time.monotonic()
|
||||
with stdout.open("w", encoding="utf-8") as stream:
|
||||
process = subprocess.run(
|
||||
wrapped, cwd=ROOT, stdout=stream, stderr=subprocess.STDOUT,
|
||||
check=False, timeout=1740)
|
||||
|
||||
events = read_events(trace)
|
||||
samples = [event for event in events if event.get("type") == "streaming_sample"]
|
||||
moving = [event for event in samples if float(event.get("speed_cm_s", 0.0)) >= 50.0]
|
||||
stop_event = next((event for event in events
|
||||
if event.get("type") == "streaming_test_move_stopped"), None)
|
||||
backend = next((event for event in events if event.get("type") == "worker_backend_config"), {})
|
||||
worker_cap = next((event for event in events
|
||||
if event.get("type") == "streaming_worker_cap_readback"), {})
|
||||
if stop_event is None:
|
||||
raise RuntimeError(f"missing move-stop event: {trace}")
|
||||
stop_s = float(stop_event.get("t_s", 0.0))
|
||||
move_start_s = min((float(event.get("t_s", 0.0)) for event in moving), default=float("inf"))
|
||||
dwell = [event for event in samples if float(event.get("t_s", 0.0)) >= stop_s
|
||||
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||
arrival = [event for event in samples if float(event.get("t_s", 0.0)) < move_start_s
|
||||
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||
if process.returncode or len(moving) < 40 or len(dwell) < 16:
|
||||
raise RuntimeError(f"incomplete {condition} run: {trace}; exit={process.returncode}; "
|
||||
f"move={len(moving)} dwell={len(dwell)}")
|
||||
if (int(backend.get("streaming_fast_travel_wait_for_all_critical_tiles", -1))
|
||||
!= config["wait_for_critical"]
|
||||
or abs(float(backend.get("streaming_fast_travel_apply_budget_ms", -1.0))
|
||||
- config["apply_budget_ms"]) > 0.01
|
||||
or int(worker_cap.get("streaming_background_worker_cap", -1)) != 1
|
||||
or int(backend.get("effective_tile_slots", -1)) != 8
|
||||
or int(backend.get("streaming_far_apply_supplement", -1)) != 1):
|
||||
raise RuntimeError(f"CVar/worker readback mismatch: {backend}; {worker_cap}")
|
||||
|
||||
moving_metrics = []
|
||||
for event in moving:
|
||||
desired = event.get("desired_by_lod", [0] * 9)
|
||||
resident = event.get("resident_by_lod", [0] * 9)
|
||||
near_desired = sum(desired[:3])
|
||||
cone_desired = int(event.get("near_forward_cone_desired", 0))
|
||||
weighted_desired = float(event.get("near_forward_weighted_desired", 0.0))
|
||||
moving_metrics.append({
|
||||
"near_ready_pct": percent(sum(resident[:3]), near_desired) or 0.0,
|
||||
"lod0_ready_pct": percent(resident[0], desired[0]) or 0.0,
|
||||
"lod1_ready_pct": percent(resident[1], desired[1]) or 0.0,
|
||||
"lod2_ready_pct": percent(resident[2], desired[2]) or 0.0,
|
||||
"far_hole_pct": percent(
|
||||
sum(desired[3:]) - sum(event.get("covered_by_lod", resident)[3:]),
|
||||
sum(desired[3:])) or 0.0,
|
||||
"ahead_share_pct": percent(int(event.get("near_ahead_desired", 0)), near_desired),
|
||||
"behind_share_pct": percent(int(event.get("near_behind_desired", 0)), near_desired),
|
||||
"side_share_pct": 100.0 - (
|
||||
percent(int(event.get("near_ahead_desired", 0)), near_desired) or 0.0
|
||||
) - (
|
||||
percent(int(event.get("near_behind_desired", 0)), near_desired) or 0.0
|
||||
),
|
||||
"ahead_ready_pct": percent(
|
||||
int(event.get("near_ahead_resident", 0)),
|
||||
int(event.get("near_ahead_desired", 0))),
|
||||
"behind_ready_pct": percent(
|
||||
int(event.get("near_behind_resident", 0)),
|
||||
int(event.get("near_behind_desired", 0))),
|
||||
"forward_cone_ready_pct": percent(
|
||||
int(event.get("near_forward_cone_resident", 0)), cone_desired),
|
||||
"forward_cone_share_pct": percent(cone_desired, near_desired),
|
||||
"forward_weighted_ready_pct": percent(
|
||||
float(event.get("near_forward_weighted_resident", 0.0)), weighted_desired),
|
||||
})
|
||||
|
||||
arrival_near_settled = next((event for event in arrival
|
||||
if sum(event.get("desired_by_lod", [0] * 9)[:3]) > 0
|
||||
and sum(event.get("settled_by_lod", [0] * 9)[:3])
|
||||
>= sum(event.get("desired_by_lod", [0] * 9)[:3])), None)
|
||||
dwell_complete = next((event for event in dwell
|
||||
if sum(event.get("resident_by_lod", [0] * 9)[:3])
|
||||
>= sum(event.get("desired_by_lod", [0] * 9)[:3])
|
||||
and sum(event.get("desired_by_lod", [0] * 9)[3:])
|
||||
<= sum(event.get("covered_by_lod", [0] * 9)[3:])), None)
|
||||
|
||||
work = [event for event in events if event.get("type") == "game_thread_work_frame"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
applies = {event.get("frame"): event for event in events
|
||||
if event.get("type") == "apply_frame"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||
scheduler = {event.get("frame"): event for event in events
|
||||
if event.get("type") == "scheduler_admission"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||
frames = []
|
||||
for event in work:
|
||||
apply = applies.get(event.get("frame"), {})
|
||||
admit = scheduler.get(event.get("frame"), {})
|
||||
frames.append({
|
||||
"frame": event.get("frame"),
|
||||
"tick_ms": float(event.get("tick_ms", 0.0)),
|
||||
"update_chunks_ms": float(event.get("update_chunks_ms", 0.0)),
|
||||
"pending_drain_ms": float(event.get("pending_drain_ms", 0.0)),
|
||||
"apply_ms": float(apply.get("apply_ms", 0.0)),
|
||||
"apply_budget_ms": float(apply.get("apply_budget_ms", 0.0)),
|
||||
"meshes_applied": int(apply.get("meshes_applied", 0)),
|
||||
"time_budget_hit": int(apply.get("time_limit_reached", 0)),
|
||||
"desired_rebuild_ms": float(admit.get("desired_rebuild_ms", 0.0)),
|
||||
"submit_tiles_ms": float(admit.get("submit_tiles_ms", 0.0)),
|
||||
"level_admit_ms": float(admit.get("level_admit_ms", 0.0)),
|
||||
})
|
||||
p99_field = lambda key: percentile([float(row[key]) for row in frames])
|
||||
max_field = lambda key: max((float(row[key]) for row in frames), default=None)
|
||||
dwell_complete_s = (float(dwell_complete.get("t_s", 0.0)) - stop_s
|
||||
if dwell_complete else None)
|
||||
|
||||
row = {
|
||||
"condition": condition,
|
||||
"repetition": repetition,
|
||||
"speed_mps": 30.0,
|
||||
"wait_for_all_critical_tiles": config["wait_for_critical"],
|
||||
"fast_travel_apply_budget_ms": config["apply_budget_ms"],
|
||||
"effective_tile_slots": backend.get("effective_tile_slots"),
|
||||
"exit_code": process.returncode,
|
||||
"wall_s": round(time.monotonic() - started, 3),
|
||||
"moving_samples": len(moving),
|
||||
"dwell_samples": len(dwell),
|
||||
"arrival_samples": len(arrival),
|
||||
"near_ready_moving_mean_pct": mean([m["near_ready_pct"] for m in moving_metrics]),
|
||||
"lod0_ready_moving_mean_pct": mean([m["lod0_ready_pct"] for m in moving_metrics]),
|
||||
"lod1_ready_moving_mean_pct": mean([m["lod1_ready_pct"] for m in moving_metrics]),
|
||||
"lod2_ready_moving_mean_pct": mean([m["lod2_ready_pct"] for m in moving_metrics]),
|
||||
"far_holes_moving_mean_pct": mean([m["far_hole_pct"] for m in moving_metrics]),
|
||||
"near_ahead_desired_share_pct": mean([m["ahead_share_pct"] for m in moving_metrics
|
||||
if m["ahead_share_pct"] is not None]),
|
||||
"near_behind_desired_share_pct": mean([m["behind_share_pct"] for m in moving_metrics
|
||||
if m["behind_share_pct"] is not None]),
|
||||
"near_side_desired_share_pct": mean([m["side_share_pct"] for m in moving_metrics]),
|
||||
"near_ahead_ready_pct": mean([m["ahead_ready_pct"] for m in moving_metrics
|
||||
if m["ahead_ready_pct"] is not None]),
|
||||
"near_behind_ready_pct": mean([m["behind_ready_pct"] for m in moving_metrics
|
||||
if m["behind_ready_pct"] is not None]),
|
||||
"near_forward_cone_half_angle_deg": 55,
|
||||
"near_forward_cone_desired_share_pct": mean([m["forward_cone_share_pct"]
|
||||
for m in moving_metrics
|
||||
if m["forward_cone_share_pct"] is not None]),
|
||||
"near_forward_cone_ready_pct": mean([m["forward_cone_ready_pct"]
|
||||
for m in moving_metrics
|
||||
if m["forward_cone_ready_pct"] is not None]),
|
||||
"near_forward_cosine_weighted_ready_pct": mean(
|
||||
[m["forward_weighted_ready_pct"] for m in moving_metrics
|
||||
if m["forward_weighted_ready_pct"] is not None]),
|
||||
"arrival_lod02_settled_t_s": (
|
||||
float(arrival_near_settled.get("t_s", 0.0)) if arrival_near_settled else None),
|
||||
"near_ready_dwell_end_pct": (100.0 * sum(dwell[-1].get("resident_by_lod", [0] * 9)[:3])
|
||||
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[:3]))),
|
||||
"far_holes_dwell_end_pct": (100.0 * (
|
||||
sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:])
|
||||
- sum(dwell[-1].get("covered_by_lod", [0] * 9)[3:]))
|
||||
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:]))),
|
||||
"dwell_complete_after_stop_s": dwell_complete_s,
|
||||
"game_thread_tick_p99_ms": p99_field("tick_ms"),
|
||||
"game_thread_tick_max_ms": max_field("tick_ms"),
|
||||
"stream_update_p99_ms": p99_field("update_chunks_ms"),
|
||||
"pending_drain_p99_ms": p99_field("pending_drain_ms"),
|
||||
"apply_p99_ms": p99_field("apply_ms"),
|
||||
"apply_max_ms": max_field("apply_ms"),
|
||||
"apply_budget_min_ms": min((f["apply_budget_ms"] for f in frames), default=None),
|
||||
"apply_budget_max_ms": max((f["apply_budget_ms"] for f in frames), default=None),
|
||||
"apply_time_budget_hit_frames": sum(f["time_budget_hit"] for f in frames),
|
||||
"apply_frames": sum(f["meshes_applied"] > 0 for f in frames),
|
||||
"apply_meshes_total": sum(f["meshes_applied"] for f in frames),
|
||||
"desired_rebuild_p99_ms": p99_field("desired_rebuild_ms"),
|
||||
"submit_tiles_p99_ms": p99_field("submit_tiles_ms"),
|
||||
"level_admit_p99_ms": p99_field("level_admit_ms"),
|
||||
"top_tick_frames": sorted(frames, key=lambda f: f["tick_ms"], reverse=True)[:5],
|
||||
"trace": str(trace.relative_to(ROOT)),
|
||||
"log": str(abslog.relative_to(ROOT)),
|
||||
}
|
||||
print("[L157Movement] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--smoke", action="store_true",
|
||||
help="run one candidate trace to validate CVar readback and event fields")
|
||||
parser.add_argument("--geometry-hash-check", action="store_true",
|
||||
help="compare UE per-tile geometry hashes for baseline and early admission")
|
||||
args = parser.parse_args()
|
||||
missing = [path for path in (EDITOR, HOST / "VFHost.uproject") if not path.exists()]
|
||||
if missing:
|
||||
raise SystemExit(f"missing UE runner input: {missing}")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
if args.geometry_hash_check:
|
||||
hash_dir = OUT / "geometry-hash"
|
||||
baseline_path = hash_dir / "assembler-world.json"
|
||||
candidate_path = hash_dir / "early-admission-apply-1ms.json"
|
||||
run("assembler_world", 99, baseline_path)
|
||||
run("early_admission_apply_1ms", 99, candidate_path)
|
||||
baseline_data = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||
candidate_data = json.loads(candidate_path.read_text(encoding="utf-8"))
|
||||
key_fields = ("level", "tile_x", "tile_y", "tile_z")
|
||||
|
||||
def indexed(data: dict) -> dict[tuple[int, ...], tuple]:
|
||||
result = {}
|
||||
for record in data.get("records", []):
|
||||
key = tuple(int(record[field]) for field in key_fields)
|
||||
result[key] = (str(record.get("geometry_hash", "")),
|
||||
int(record.get("triangle_count", -1)),
|
||||
bool(record.get("empty", False)))
|
||||
return result
|
||||
|
||||
baseline_hashes = indexed(baseline_data)
|
||||
candidate_hashes = indexed(candidate_data)
|
||||
common = baseline_hashes.keys() & candidate_hashes.keys()
|
||||
mismatches = [key for key in common
|
||||
if baseline_hashes[key] != candidate_hashes[key]]
|
||||
result = {
|
||||
"baseline_records": len(baseline_hashes),
|
||||
"candidate_records": len(candidate_hashes),
|
||||
"common_records": len(common),
|
||||
"hash_mismatches": len(mismatches),
|
||||
"baseline_only_records": len(baseline_hashes.keys() - candidate_hashes.keys()),
|
||||
"candidate_only_records": len(candidate_hashes.keys() - baseline_hashes.keys()),
|
||||
"baseline_dump": str(baseline_path.relative_to(ROOT)),
|
||||
"candidate_dump": str(candidate_path.relative_to(ROOT)),
|
||||
}
|
||||
(hash_dir / "comparison.json").write_text(
|
||||
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print("[L157GeometryHash] " + json.dumps(result, sort_keys=True), flush=True)
|
||||
return 0 if len(common) >= 100 and not mismatches else 1
|
||||
if args.smoke:
|
||||
run("early_admission_apply_1ms", 0)
|
||||
return 0
|
||||
rows = []
|
||||
partial = OUT / "runs-partial.json"
|
||||
for repetition in range(1, 6):
|
||||
for condition in ORDER[repetition]:
|
||||
row = run(condition, repetition)
|
||||
rows.append(row)
|
||||
partial.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
|
||||
metrics = (
|
||||
"near_ready_moving_mean_pct", "far_holes_moving_mean_pct",
|
||||
"game_thread_tick_p99_ms", "game_thread_tick_max_ms",
|
||||
"arrival_lod02_settled_t_s", "dwell_complete_after_stop_s",
|
||||
"near_ahead_desired_share_pct", "near_behind_desired_share_pct",
|
||||
"near_side_desired_share_pct", "near_forward_cone_desired_share_pct",
|
||||
"near_ahead_ready_pct", "near_behind_ready_pct",
|
||||
"near_forward_cone_ready_pct", "near_forward_cosine_weighted_ready_pct",
|
||||
"apply_p99_ms", "desired_rebuild_p99_ms", "level_admit_p99_ms",
|
||||
)
|
||||
medians = {
|
||||
condition: {metric: statistics.median(
|
||||
row[metric] for row in rows if row["condition"] == condition)
|
||||
for metric in metrics}
|
||||
for condition in CONDITIONS
|
||||
}
|
||||
pairs = []
|
||||
for repetition in range(1, 6):
|
||||
by_condition = {row["condition"]: row for row in rows
|
||||
if row["repetition"] == repetition}
|
||||
for candidate in ("early_admission_apply_1ms",):
|
||||
pairs.append({
|
||||
"repetition": repetition,
|
||||
"candidate": candidate,
|
||||
**{f"{metric}_delta": by_condition[candidate][metric]
|
||||
- by_condition["assembler_world"][metric]
|
||||
for metric in metrics if by_condition[candidate][metric] is not None
|
||||
and by_condition["assembler_world"][metric] is not None},
|
||||
})
|
||||
summary = {
|
||||
"source_commit": subprocess.run(
|
||||
["git", "-c", f"safe.directory={ROOT}", "rev-parse", "HEAD"],
|
||||
cwd=ROOT, check=True, capture_output=True, text=True).stdout.strip(),
|
||||
"protocol": "Five interleaved 30 m/s UE runs per condition; 12 s move plus 5 s dwell; 8 effective tile slots.",
|
||||
"conditions": CONDITIONS,
|
||||
"movement_medians": medians,
|
||||
"paired_deltas_vs_assembler_world": pairs,
|
||||
"runs": rows,
|
||||
}
|
||||
(OUT / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
print("[L157Summary] " + json.dumps({"movement_medians": medians,
|
||||
"runs": len(rows)}, sort_keys=True), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,45 @@
|
||||
# L160 — ahead-first admission and desired rebuild cost
|
||||
|
||||
**Result:** Forward-first admission plus same-center rebuild suppression and Core-key caching raised median 55° travel-cone readiness by 3.75 percentage points. All five candidate game-thread p99s were at or below 11 ms, but the full bar failed: worst ticks regressed in 3/5 pairs, far holes in 3/5, and arrival in 2/5. Median all-around LOD0–2 readiness also fell. Keep the experiment controls off by default.
|
||||
|
||||
## Measurements
|
||||
|
||||
Five interleaved W8 UE pairs ran at 30 m/s for 12 s plus a 5 s dwell, with eight effective tile slots. The pre-run load averages were 0.15 / 1.12 / 1.49. Each run recorded 45 center crossings. The forward-cone value uses travel direction: null-RHI supplied no camera-forward samples. These are desired-residency counts, not measured HMD visibility.
|
||||
|
||||
| Moving readiness | `assembler/world` median | Candidate median | Paired median change |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| All-around LOD0–2 | 39.46% | 36.59% | −3.07 pp |
|
||||
| 55° forward cone | 14.19% | 18.24% | +3.75 pp |
|
||||
|
||||
The cone covered about 23% of desired LOD0–2 tiles in both conditions. The cone improvement is clear, while the all-around average fell; behind readiness was 55.52% baseline versus 50.03% candidate.
|
||||
|
||||
| Gate metric | `assembler/world` median | Candidate median | Candidate result |
|
||||
| --- | ---: | ---: | --- |
|
||||
| Game-thread tick p99 | 10.571 ms | 10.509 ms | All five candidate runs: 10.106–10.560 ms; pass ≤11 ms |
|
||||
| Worst tick per run | 13.615 ms | 14.057 ms | No-worse failed in pairs 1, 2, 4; observed maxima 15.092 / 15.141 ms |
|
||||
| Far holes while moving | 3.238% | 3.260% | No-worse failed in pairs 2, 4, 5 |
|
||||
| First settled LOD0–2 arrival | 2.458 s | 2.460 s | No-worse failed in pairs 1 and 3 |
|
||||
|
||||
The median `UpdateChunks` p99 changed from 9.30 to 9.18 ms. This small difference did not remove the worst-tick regressions.
|
||||
|
||||
### Admission order
|
||||
|
||||
The baseline’s first twelve submitted near tiles were four travel-ahead critical tiles (direction class 1), then three forward-cone critical tiles (class 0), then five forward-cone regular tiles. Candidate order put the three cone critical tiles first, followed by the four travel-ahead critical tiles, then the same cone regular lane. Within cone and ahead classes, entries were ordered by distance with coordinate tie-breaks; side and behind entries retained stable order. Here class 0 is the 55° camera cone when available, or the travel cone in these null-RHI runs; class 1 is travel-ahead outside the cone.
|
||||
|
||||
### Desired-set rebuilds
|
||||
|
||||
The set was not rebuilt only at center crossings. With exactly 45 crossings per run, baseline rebuilt on 313–334 frames (median 324, or 7.20 rebuilds per crossing). The candidate rebuilt on 193–211 frames (median 200, or 4.44 per crossing). Baseline recorded 171–198 same-center Core-region requests per run; candidate recorded 148–166.
|
||||
|
||||
The nonzero `BuildDesiredTiles` p95 was 7.08 ms baseline and 7.52 ms candidate (median across runs); its per-rebuild cost did not fall. The median Core `desiredTiles` query p95 fell from 1.96 to 1.25 ms, with a 77.5% median same-center cache-hit share. Median rebuild p50 was 3.91 / 3.98 ms. The candidate suppresses a forced rebuild after Core region compilation completes and reuses the Core base key list at an unchanged center. `BuildDesiredTiles` still rebuilds the UE desired and overlay sets on each trigger; this is neither an incremental full-set delta nor a time-sliced rebuild, so the remaining rebuild tail persists.
|
||||
|
||||
## Checks and limits
|
||||
|
||||
- Built `VFHostEditor` from the worktree with `taskset -c 0-11`, `-NoUBA`, and `-MaxParallelActions=4`; final build succeeded (3/3 actions). The VFHost `PluginDeps/VoxelForge/Source` symlink was restored to `/home/jahni/VoxelM/Plugins/VoxelForge/Source` afterward.
|
||||
- Fast tier passed: 268 PASS, 0 FAIL, 5 existing diagnostics. Canonical owner hashes match the L130 baseline; W1/W12 owner hashes and randomized W1/W8/W12 worker comparisons match. No generator or mesher source changed.
|
||||
- `RunL160Movement.py` passed `py_compile`; `git diff --check` passed. Final `git diff --check` was also run after writing this report.
|
||||
- HMD/camera-pose readiness and W12 movement timing were not measured. The directional proxy falls back to travel direction because the null-RHI run has no local camera sample.
|
||||
- The candidate combines admission ordering, region-completion rebuild suppression, and same-center Core-key caching, so the five-pair matrix does not isolate each effect. Geometry remained unchanged; no `WorldAssemblyEvaluatorVersion` bump was needed.
|
||||
|
||||
Logs and paired traces are in `Saved/Linux/l160/`; the matrix summary is `Saved/Linux/l160/ue/movement/summary.json`.
|
||||
|
||||
Changed Unreal source: `Source/VoxelForge/Private/VoxelWorld.cpp`, `Source/VoxelForge/Public/VoxelWorld.h`.
|
||||
@@ -0,0 +1,521 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Interleaved L160 UE movement runs for forward admission and Core rebuild ablations."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
HOST = Path("/home/jahni/VFHost")
|
||||
EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd")
|
||||
OUT = ROOT / "Saved/Linux/l160/ue/movement"
|
||||
HOST_SAVED = HOST / "Saved/Linux/l160"
|
||||
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l160"
|
||||
SPAWN_M = (19.166015625, -14.2001953125, -753.3740234375)
|
||||
MOVE_START_S = 6.0
|
||||
|
||||
CONDITIONS = {
|
||||
"assembler_world": {
|
||||
"wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||
"forward_first": 0, "rebuild_on_region_completion": 1,
|
||||
"core_desired_cache": 0,
|
||||
},
|
||||
"forward_first": {
|
||||
"wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||
"forward_first": 1, "rebuild_on_region_completion": 0,
|
||||
"core_desired_cache": 1,
|
||||
},
|
||||
}
|
||||
ORDER = {
|
||||
1: ("assembler_world", "forward_first"),
|
||||
2: ("forward_first", "assembler_world"),
|
||||
3: ("assembler_world", "forward_first"),
|
||||
4: ("forward_first", "assembler_world"),
|
||||
5: ("assembler_world", "forward_first"),
|
||||
}
|
||||
|
||||
|
||||
def percentile(values: list[float], fraction: float = 0.99) -> float | None:
|
||||
if not values:
|
||||
return None
|
||||
ordered = sorted(values)
|
||||
return ordered[max(0, min(len(ordered) - 1, math.ceil(len(ordered) * fraction) - 1))]
|
||||
|
||||
|
||||
def read_events(path: Path) -> list[dict]:
|
||||
rows = []
|
||||
if path.is_file():
|
||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
if not line.startswith("{"):
|
||||
continue
|
||||
try:
|
||||
rows.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return rows
|
||||
|
||||
|
||||
def mean(values: list[float]) -> float | None:
|
||||
return statistics.mean(values) if values else None
|
||||
|
||||
|
||||
def percent(numerator: int | float, denominator: int | float) -> float | None:
|
||||
return 100.0 * numerator / denominator if denominator > 0 else None
|
||||
|
||||
|
||||
def run(condition: str, repetition: int, tile_hash_dump: Path | None = None) -> dict:
|
||||
config = CONDITIONS[condition]
|
||||
run_name = f"fast-r{repetition}"
|
||||
out = OUT / condition
|
||||
host_out = HOST_PLUGIN_SAVED / condition / "output"
|
||||
userdir = HOST_SAVED / "user" / condition / run_name
|
||||
zendata = HOST_SAVED / "zen" / condition / run_name
|
||||
ddc = HOST_SAVED / "DDC"
|
||||
trace = out / f"{run_name}.trace.jsonl"
|
||||
abslog = out / f"{run_name}.log"
|
||||
stdout = out / f"{run_name}.stdout.log"
|
||||
export = host_out / run_name
|
||||
for directory in (out, export, userdir, zendata, ddc):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
trace.unlink(missing_ok=True)
|
||||
|
||||
spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWN_M)
|
||||
command = [
|
||||
str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended",
|
||||
"-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch",
|
||||
"-notraceserver", "-nocrashreports", "-DisablePlugins=AndroidFileServer",
|
||||
"-ddc=NoZenLocalFallback", f"-userdir={userdir}", f"-ZenDataPath={zendata}",
|
||||
f"-LocalDataCachePath={ddc}", "-run=VoxelForgeExplore", "-seed=0",
|
||||
"-archetype=TunnelNetwork", "-slot=0", "-modes=export", "-gameconfig=1",
|
||||
"-coreworldtest=1", "-ownerfourstrateplan=1", "-corevoxelsizecm=25",
|
||||
"-coreislandtest=0", "-corefallbacktest=0", "-coreofftest=0",
|
||||
"-coreplantest=0", "-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings",
|
||||
"-corehybridmesher=0", f"-out={export}", "-voxel.StreamingCoarseFirst=1",
|
||||
"-voxel.StreamingDynamicNearReserve=1",
|
||||
"-voxel.StreamingLegacyPendingResultCap=0",
|
||||
"-voxel.CoreRegionConcurrentAdmission=1",
|
||||
"-voxel.StreamingResultBacklogCap=256", "-voxel.StreamingReadyQueueDepth=8",
|
||||
"-voxel.StreamingBackgroundWorkerCap=1", "-voxel.CoreColumnCacheDirectMapped=0",
|
||||
"-voxel.StreamingFarApplySupplement=1",
|
||||
f"-voxel.StreamingFastTravelWaitForAllCriticalTiles={config['wait_for_critical']}",
|
||||
f"-voxel.StreamingFastTravelApplyBudgetMilliseconds={config['apply_budget_ms']:.1f}",
|
||||
f"-voxel.StreamingFastTravelForwardFirst={config['forward_first']}",
|
||||
f"-voxel.StreamingRebuildOnCoreRegionCompletion={config['rebuild_on_region_completion']}",
|
||||
f"-voxel.StreamingCoreDesiredCacheByCenter={config['core_desired_cache']}",
|
||||
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
|
||||
"-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=3000.0",
|
||||
f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}",
|
||||
"-voxel.TestMoveDurationSeconds=12.0", "-voxel.TestExitSeconds=23.0",
|
||||
"-voxel.TestMoveDirection=1,0,0", "-voxel.StreamingSchedulerPolicy=3",
|
||||
"-voxel.TestExitOnSteady=0", "-coreworldstreamtrace=1",
|
||||
f"-voxel.StartupTraceSpawnCm={spawn_cm}",
|
||||
"-voxel.StartupTraceUntilEndPlay=1", "-voxel.StartupTraceThroughCrossing=1",
|
||||
"-voxel.StartupTraceMaxTiles=16384", f"-voxel.StartupTraceFile={trace}",
|
||||
f"-abslog={abslog}",
|
||||
]
|
||||
if tile_hash_dump is not None:
|
||||
tile_hash_dump.parent.mkdir(parents=True, exist_ok=True)
|
||||
tile_hash_dump.unlink(missing_ok=True)
|
||||
command.append(f"-voxel.TileHashDump={tile_hash_dump.resolve()}")
|
||||
wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command]
|
||||
started = time.monotonic()
|
||||
with stdout.open("w", encoding="utf-8") as stream:
|
||||
process = subprocess.run(
|
||||
wrapped, cwd=ROOT, stdout=stream, stderr=subprocess.STDOUT,
|
||||
check=False, timeout=1740)
|
||||
|
||||
events = read_events(trace)
|
||||
samples = [event for event in events if event.get("type") == "streaming_sample"]
|
||||
moving = [event for event in samples if float(event.get("speed_cm_s", 0.0)) >= 50.0]
|
||||
stop_event = next((event for event in events
|
||||
if event.get("type") == "streaming_test_move_stopped"), None)
|
||||
backend = next((event for event in events if event.get("type") == "worker_backend_config"), {})
|
||||
scheduler_config = next((event for event in events
|
||||
if event.get("type") == "scheduler_admission"), {})
|
||||
worker_cap = next((event for event in events
|
||||
if event.get("type") == "streaming_worker_cap_readback"), {})
|
||||
if stop_event is None:
|
||||
raise RuntimeError(f"missing move-stop event: {trace}")
|
||||
stop_s = float(stop_event.get("t_s", 0.0))
|
||||
move_start_s = min((float(event.get("t_s", 0.0)) for event in moving), default=float("inf"))
|
||||
dwell = [event for event in samples if float(event.get("t_s", 0.0)) >= stop_s
|
||||
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||
arrival = [event for event in samples if float(event.get("t_s", 0.0)) < move_start_s
|
||||
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||
if process.returncode or len(moving) < 40 or len(dwell) < 16:
|
||||
raise RuntimeError(f"incomplete {condition} run: {trace}; exit={process.returncode}; "
|
||||
f"move={len(moving)} dwell={len(dwell)}")
|
||||
if (int(backend.get("streaming_fast_travel_wait_for_all_critical_tiles", -1))
|
||||
!= config["wait_for_critical"]
|
||||
or abs(float(backend.get("streaming_fast_travel_apply_budget_ms", -1.0))
|
||||
- config["apply_budget_ms"]) > 0.01
|
||||
or int(worker_cap.get("streaming_background_worker_cap", -1)) != 1
|
||||
or int(backend.get("effective_tile_slots", -1)) != 8
|
||||
or int(backend.get("streaming_far_apply_supplement", -1)) != 1
|
||||
or int(scheduler_config.get("forward_first", -1)) != config["forward_first"]
|
||||
or int(scheduler_config.get("rebuild_on_core_region_completion", -1))
|
||||
!= config["rebuild_on_region_completion"]
|
||||
or int(scheduler_config.get("core_desired_cache_by_center", -1))
|
||||
!= config["core_desired_cache"]):
|
||||
raise RuntimeError(f"CVar/worker readback mismatch: {backend}; {worker_cap}; {scheduler_config}")
|
||||
|
||||
moving_metrics = []
|
||||
for event in moving:
|
||||
desired = event.get("desired_by_lod", [0] * 9)
|
||||
resident = event.get("resident_by_lod", [0] * 9)
|
||||
near_desired = sum(desired[:3])
|
||||
cone_desired = int(event.get("near_forward_cone_desired", 0))
|
||||
view_cone_desired = int(event.get("near_view_forward_cone_desired", 0))
|
||||
view_valid = bool(event.get("near_view_forward_cone_valid", 0))
|
||||
weighted_desired = float(event.get("near_forward_weighted_desired", 0.0))
|
||||
travel_cone_ready = percent(
|
||||
int(event.get("near_forward_cone_resident", 0)), cone_desired)
|
||||
view_cone_ready = percent(
|
||||
int(event.get("near_view_forward_cone_resident", 0)), view_cone_desired)
|
||||
moving_metrics.append({
|
||||
"near_ready_pct": percent(sum(resident[:3]), near_desired) or 0.0,
|
||||
"lod0_ready_pct": percent(resident[0], desired[0]) or 0.0,
|
||||
"lod1_ready_pct": percent(resident[1], desired[1]) or 0.0,
|
||||
"lod2_ready_pct": percent(resident[2], desired[2]) or 0.0,
|
||||
"far_hole_pct": percent(
|
||||
sum(desired[3:]) - sum(event.get("covered_by_lod", resident)[3:]),
|
||||
sum(desired[3:])) or 0.0,
|
||||
"ahead_share_pct": percent(int(event.get("near_ahead_desired", 0)), near_desired),
|
||||
"behind_share_pct": percent(int(event.get("near_behind_desired", 0)), near_desired),
|
||||
"side_share_pct": 100.0 - (
|
||||
percent(int(event.get("near_ahead_desired", 0)), near_desired) or 0.0
|
||||
) - (
|
||||
percent(int(event.get("near_behind_desired", 0)), near_desired) or 0.0
|
||||
),
|
||||
"ahead_ready_pct": percent(
|
||||
int(event.get("near_ahead_resident", 0)),
|
||||
int(event.get("near_ahead_desired", 0))),
|
||||
"behind_ready_pct": percent(
|
||||
int(event.get("near_behind_resident", 0)),
|
||||
int(event.get("near_behind_desired", 0))),
|
||||
"travel_forward_cone_ready_pct": travel_cone_ready,
|
||||
"view_forward_cone_ready_pct": view_cone_ready,
|
||||
"forward_cone_ready_pct": view_cone_ready if view_valid else travel_cone_ready,
|
||||
"forward_cone_source": "camera" if view_valid else "travel_fallback",
|
||||
"forward_cone_share_pct": percent(cone_desired, near_desired),
|
||||
"forward_weighted_ready_pct": percent(
|
||||
float(event.get("near_forward_weighted_resident", 0.0)), weighted_desired),
|
||||
})
|
||||
|
||||
arrival_near_settled = next((event for event in arrival
|
||||
if sum(event.get("desired_by_lod", [0] * 9)[:3]) > 0
|
||||
and sum(event.get("settled_by_lod", [0] * 9)[:3])
|
||||
>= sum(event.get("desired_by_lod", [0] * 9)[:3])), None)
|
||||
dwell_complete = next((event for event in dwell
|
||||
if sum(event.get("resident_by_lod", [0] * 9)[:3])
|
||||
>= sum(event.get("desired_by_lod", [0] * 9)[:3])
|
||||
and sum(event.get("desired_by_lod", [0] * 9)[3:])
|
||||
<= sum(event.get("covered_by_lod", [0] * 9)[3:])), None)
|
||||
|
||||
work = [event for event in events if event.get("type") == "game_thread_work_frame"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
applies = {event.get("frame"): event for event in events
|
||||
if event.get("type") == "apply_frame"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||
scheduler = {event.get("frame"): event for event in events
|
||||
if event.get("type") == "scheduler_admission"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||
frames = []
|
||||
for event in work:
|
||||
apply = applies.get(event.get("frame"), {})
|
||||
admit = scheduler.get(event.get("frame"), {})
|
||||
frames.append({
|
||||
"frame": event.get("frame"),
|
||||
"tick_ms": float(event.get("tick_ms", 0.0)),
|
||||
"update_chunks_ms": float(event.get("update_chunks_ms", 0.0)),
|
||||
"pending_drain_ms": float(event.get("pending_drain_ms", 0.0)),
|
||||
"apply_ms": float(apply.get("apply_ms", 0.0)),
|
||||
"apply_budget_ms": float(apply.get("apply_budget_ms", 0.0)),
|
||||
"meshes_applied": int(apply.get("meshes_applied", 0)),
|
||||
"time_budget_hit": int(apply.get("time_limit_reached", 0)),
|
||||
"desired_rebuild_ms": float(admit.get("desired_rebuild_ms", 0.0)),
|
||||
"desired_rebuilt": int(admit.get("desired_rebuilt", 0)),
|
||||
"submit_tiles_ms": float(admit.get("submit_tiles_ms", 0.0)),
|
||||
"level_admit_ms": float(admit.get("level_admit_ms", 0.0)),
|
||||
})
|
||||
p99_field = lambda key: percentile([float(row[key]) for row in frames])
|
||||
max_field = lambda key: max((float(row[key]) for row in frames), default=None)
|
||||
rebuild_frames = [row for row in frames if row["desired_rebuilt"]]
|
||||
rebuild_costs = [float(row["desired_rebuild_ms"]) for row in rebuild_frames]
|
||||
moving_scheduler = [event for event in scheduler.values()]
|
||||
center_crossings = 0
|
||||
previous_center = None
|
||||
for event in sorted(moving_scheduler, key=lambda item: int(item.get("frame", 0))):
|
||||
center = tuple(event.get("center", ()))
|
||||
if previous_center is not None and center != previous_center:
|
||||
center_crossings += 1
|
||||
previous_center = center
|
||||
core_requests = [event for event in events
|
||||
if event.get("type") == "core_region_compile_request"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
same_center_core_requests = sum(
|
||||
event.get("reason") == "desired_set_changed_same_center" for event in core_requests)
|
||||
core_desired_queries = [event for event in events
|
||||
if event.get("type") == "core_desired_cache"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
core_desired_query_ms = [float(event.get("query_ms", 0.0))
|
||||
for event in core_desired_queries if float(event.get("query_ms", 0.0)) > 0.0]
|
||||
core_desired_cache_hits = sum(int(event.get("hit", 0)) for event in core_desired_queries)
|
||||
direction_submissions = [event.get("near_submitted_by_direction", [0, 0, 0, 0])
|
||||
for event in moving_scheduler]
|
||||
direction_submissions = [counts for counts in direction_submissions
|
||||
if isinstance(counts, list) and len(counts) == 4]
|
||||
direction_submission_totals = [
|
||||
sum(int(counts[index]) for counts in direction_submissions) for index in range(4)
|
||||
]
|
||||
first_admission_order = next((event.get("near_admission_order_first", [])
|
||||
for event in moving_scheduler
|
||||
if event.get("near_admission_order_first")), [])
|
||||
camera_samples = sum(bool(event.get("near_view_forward_cone_valid", 0)) for event in moving)
|
||||
dwell_complete_s = (float(dwell_complete.get("t_s", 0.0)) - stop_s
|
||||
if dwell_complete else None)
|
||||
|
||||
row = {
|
||||
"condition": condition,
|
||||
"repetition": repetition,
|
||||
"speed_mps": 30.0,
|
||||
"wait_for_all_critical_tiles": config["wait_for_critical"],
|
||||
"fast_travel_apply_budget_ms": config["apply_budget_ms"],
|
||||
"forward_first": config["forward_first"],
|
||||
"rebuild_on_core_region_completion": config["rebuild_on_region_completion"],
|
||||
"core_desired_cache_by_center": config["core_desired_cache"],
|
||||
"effective_tile_slots": backend.get("effective_tile_slots"),
|
||||
"exit_code": process.returncode,
|
||||
"wall_s": round(time.monotonic() - started, 3),
|
||||
"moving_samples": len(moving),
|
||||
"dwell_samples": len(dwell),
|
||||
"arrival_samples": len(arrival),
|
||||
"near_ready_moving_mean_pct": mean([m["near_ready_pct"] for m in moving_metrics]),
|
||||
"lod0_ready_moving_mean_pct": mean([m["lod0_ready_pct"] for m in moving_metrics]),
|
||||
"lod1_ready_moving_mean_pct": mean([m["lod1_ready_pct"] for m in moving_metrics]),
|
||||
"lod2_ready_moving_mean_pct": mean([m["lod2_ready_pct"] for m in moving_metrics]),
|
||||
"far_holes_moving_mean_pct": mean([m["far_hole_pct"] for m in moving_metrics]),
|
||||
"near_ahead_desired_share_pct": mean([m["ahead_share_pct"] for m in moving_metrics
|
||||
if m["ahead_share_pct"] is not None]),
|
||||
"near_behind_desired_share_pct": mean([m["behind_share_pct"] for m in moving_metrics
|
||||
if m["behind_share_pct"] is not None]),
|
||||
"near_side_desired_share_pct": mean([m["side_share_pct"] for m in moving_metrics]),
|
||||
"near_ahead_ready_pct": mean([m["ahead_ready_pct"] for m in moving_metrics
|
||||
if m["ahead_ready_pct"] is not None]),
|
||||
"near_behind_ready_pct": mean([m["behind_ready_pct"] for m in moving_metrics
|
||||
if m["behind_ready_pct"] is not None]),
|
||||
"near_forward_cone_half_angle_deg": 55,
|
||||
"near_forward_cone_desired_share_pct": mean([m["forward_cone_share_pct"]
|
||||
for m in moving_metrics
|
||||
if m["forward_cone_share_pct"] is not None]),
|
||||
"near_forward_cone_ready_pct": mean([m["forward_cone_ready_pct"]
|
||||
for m in moving_metrics
|
||||
if m["forward_cone_ready_pct"] is not None]),
|
||||
"near_travel_forward_cone_ready_pct": mean(
|
||||
[m["travel_forward_cone_ready_pct"] for m in moving_metrics
|
||||
if m["travel_forward_cone_ready_pct"] is not None]),
|
||||
"near_view_forward_cone_ready_pct": mean(
|
||||
[m["view_forward_cone_ready_pct"] for m in moving_metrics
|
||||
if m["view_forward_cone_ready_pct"] is not None]),
|
||||
"camera_forward_samples": camera_samples,
|
||||
"camera_forward_sample_share_pct": percent(camera_samples, len(moving)),
|
||||
"near_forward_cosine_weighted_ready_pct": mean(
|
||||
[m["forward_weighted_ready_pct"] for m in moving_metrics
|
||||
if m["forward_weighted_ready_pct"] is not None]),
|
||||
"arrival_lod02_settled_t_s": (
|
||||
float(arrival_near_settled.get("t_s", 0.0)) if arrival_near_settled else None),
|
||||
"near_ready_dwell_end_pct": (100.0 * sum(dwell[-1].get("resident_by_lod", [0] * 9)[:3])
|
||||
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[:3]))),
|
||||
"far_holes_dwell_end_pct": (100.0 * (
|
||||
sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:])
|
||||
- sum(dwell[-1].get("covered_by_lod", [0] * 9)[3:]))
|
||||
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:]))),
|
||||
"dwell_complete_after_stop_s": dwell_complete_s,
|
||||
"game_thread_tick_p99_ms": p99_field("tick_ms"),
|
||||
"game_thread_tick_max_ms": max_field("tick_ms"),
|
||||
"stream_update_p99_ms": p99_field("update_chunks_ms"),
|
||||
"pending_drain_p99_ms": p99_field("pending_drain_ms"),
|
||||
"apply_p99_ms": p99_field("apply_ms"),
|
||||
"apply_max_ms": max_field("apply_ms"),
|
||||
"apply_budget_min_ms": min((f["apply_budget_ms"] for f in frames), default=None),
|
||||
"apply_budget_max_ms": max((f["apply_budget_ms"] for f in frames), default=None),
|
||||
"apply_time_budget_hit_frames": sum(f["time_budget_hit"] for f in frames),
|
||||
"apply_frames": sum(f["meshes_applied"] > 0 for f in frames),
|
||||
"apply_meshes_total": sum(f["meshes_applied"] for f in frames),
|
||||
"desired_rebuild_p99_ms": p99_field("desired_rebuild_ms"),
|
||||
"desired_rebuild_count": len(rebuild_frames),
|
||||
"desired_rebuilds_per_second": len(rebuild_frames) / 12.0,
|
||||
"center_crossings": center_crossings,
|
||||
"same_center_core_region_requests": same_center_core_requests,
|
||||
"core_desired_cache_hit_count": core_desired_cache_hits,
|
||||
"core_desired_cache_hit_share_pct": percent(
|
||||
core_desired_cache_hits, len(core_desired_queries)),
|
||||
"core_desired_query_nonzero_p50_ms": statistics.median(core_desired_query_ms)
|
||||
if core_desired_query_ms else None,
|
||||
"core_desired_query_nonzero_p95_ms": percentile(core_desired_query_ms, 0.95),
|
||||
"core_desired_query_nonzero_max_ms": max(core_desired_query_ms)
|
||||
if core_desired_query_ms else None,
|
||||
"desired_rebuild_nonzero_p50_ms": statistics.median(rebuild_costs)
|
||||
if rebuild_costs else None,
|
||||
"desired_rebuild_nonzero_p95_ms": percentile(rebuild_costs, 0.95),
|
||||
"desired_rebuild_nonzero_max_ms": max(rebuild_costs) if rebuild_costs else None,
|
||||
"near_submitted_by_direction": direction_submission_totals,
|
||||
"near_admission_first12": first_admission_order,
|
||||
"submit_tiles_p99_ms": p99_field("submit_tiles_ms"),
|
||||
"level_admit_p99_ms": p99_field("level_admit_ms"),
|
||||
"top_tick_frames": sorted(frames, key=lambda f: f["tick_ms"], reverse=True)[:5],
|
||||
"trace": str(trace.relative_to(ROOT)),
|
||||
"log": str(abslog.relative_to(ROOT)),
|
||||
}
|
||||
print("[L160Movement] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--smoke", action="store_true",
|
||||
help="run one forward-first candidate trace to validate CVar readback and event fields")
|
||||
parser.add_argument("--geometry-hash-check", action="store_true",
|
||||
help="compare UE per-tile geometry hashes for baseline and forward-first admission")
|
||||
args = parser.parse_args()
|
||||
missing = [path for path in (EDITOR, HOST / "VFHost.uproject") if not path.exists()]
|
||||
if missing:
|
||||
raise SystemExit(f"missing UE runner input: {missing}")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
if args.geometry_hash_check:
|
||||
hash_dir = OUT / "geometry-hash"
|
||||
baseline_path = hash_dir / "assembler-world.json"
|
||||
candidate_path = hash_dir / "forward-first.json"
|
||||
run("assembler_world", 99, baseline_path)
|
||||
run("forward_first", 99, candidate_path)
|
||||
baseline_data = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||
candidate_data = json.loads(candidate_path.read_text(encoding="utf-8"))
|
||||
key_fields = ("level", "tile_x", "tile_y", "tile_z")
|
||||
|
||||
def indexed(data: dict) -> dict[tuple[int, ...], tuple]:
|
||||
result = {}
|
||||
for record in data.get("records", []):
|
||||
key = tuple(int(record[field]) for field in key_fields)
|
||||
result[key] = (str(record.get("geometry_hash", "")),
|
||||
int(record.get("triangle_count", -1)),
|
||||
bool(record.get("empty", False)))
|
||||
return result
|
||||
|
||||
baseline_hashes = indexed(baseline_data)
|
||||
candidate_hashes = indexed(candidate_data)
|
||||
common = baseline_hashes.keys() & candidate_hashes.keys()
|
||||
mismatches = [key for key in common
|
||||
if baseline_hashes[key] != candidate_hashes[key]]
|
||||
result = {
|
||||
"baseline_records": len(baseline_hashes),
|
||||
"candidate_records": len(candidate_hashes),
|
||||
"common_records": len(common),
|
||||
"hash_mismatches": len(mismatches),
|
||||
"baseline_only_records": len(baseline_hashes.keys() - candidate_hashes.keys()),
|
||||
"candidate_only_records": len(candidate_hashes.keys() - baseline_hashes.keys()),
|
||||
"baseline_dump": str(baseline_path.relative_to(ROOT)),
|
||||
"candidate_dump": str(candidate_path.relative_to(ROOT)),
|
||||
}
|
||||
(hash_dir / "comparison.json").write_text(
|
||||
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print("[L160GeometryHash] " + json.dumps(result, sort_keys=True), flush=True)
|
||||
return 0 if len(common) >= 100 and not mismatches else 1
|
||||
if args.smoke:
|
||||
run("forward_first", 0)
|
||||
return 0
|
||||
rows = []
|
||||
partial = OUT / "runs-partial.json"
|
||||
for repetition in range(1, 6):
|
||||
for condition in ORDER[repetition]:
|
||||
row = run(condition, repetition)
|
||||
rows.append(row)
|
||||
partial.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
|
||||
metrics = (
|
||||
"near_ready_moving_mean_pct", "far_holes_moving_mean_pct",
|
||||
"game_thread_tick_p99_ms", "game_thread_tick_max_ms",
|
||||
"arrival_lod02_settled_t_s", "dwell_complete_after_stop_s",
|
||||
"near_ahead_desired_share_pct", "near_behind_desired_share_pct",
|
||||
"near_side_desired_share_pct", "near_forward_cone_desired_share_pct",
|
||||
"near_ahead_ready_pct", "near_behind_ready_pct",
|
||||
"near_forward_cone_ready_pct", "near_forward_cosine_weighted_ready_pct",
|
||||
"near_travel_forward_cone_ready_pct",
|
||||
"apply_p99_ms", "desired_rebuild_p99_ms", "desired_rebuild_count",
|
||||
"desired_rebuild_nonzero_p95_ms", "center_crossings",
|
||||
"same_center_core_region_requests", "level_admit_p99_ms",
|
||||
"core_desired_cache_hit_count", "core_desired_query_nonzero_p95_ms",
|
||||
)
|
||||
medians = {
|
||||
condition: {metric: statistics.median(
|
||||
row[metric] for row in rows if row["condition"] == condition)
|
||||
for metric in metrics}
|
||||
for condition in CONDITIONS
|
||||
}
|
||||
pairs = []
|
||||
for repetition in range(1, 6):
|
||||
by_condition = {row["condition"]: row for row in rows
|
||||
if row["repetition"] == repetition}
|
||||
candidate = "forward_first"
|
||||
pairs.append({
|
||||
"repetition": repetition,
|
||||
"candidate": candidate,
|
||||
**{f"{metric}_delta": by_condition[candidate][metric]
|
||||
- by_condition["assembler_world"][metric]
|
||||
for metric in metrics if by_condition[candidate][metric] is not None
|
||||
and by_condition["assembler_world"][metric] is not None},
|
||||
})
|
||||
by_pair = {
|
||||
repetition: {row["condition"]: row for row in rows if row["repetition"] == repetition}
|
||||
for repetition in range(1, 6)
|
||||
}
|
||||
candidate_rows = [row for row in rows if row["condition"] == "forward_first"]
|
||||
candidate_gates = {
|
||||
"every_candidate_run_p99_le_11_ms": all(
|
||||
row["game_thread_tick_p99_ms"] <= 11.0 for row in candidate_rows),
|
||||
"paired_worst_tick_no_worse": all(
|
||||
by_pair[rep]["forward_first"]["game_thread_tick_max_ms"]
|
||||
<= by_pair[rep]["assembler_world"]["game_thread_tick_max_ms"]
|
||||
for rep in by_pair),
|
||||
"paired_far_holes_no_worse": all(
|
||||
by_pair[rep]["forward_first"]["far_holes_moving_mean_pct"]
|
||||
<= by_pair[rep]["assembler_world"]["far_holes_moving_mean_pct"]
|
||||
for rep in by_pair),
|
||||
"paired_arrival_no_worse": all(
|
||||
by_pair[rep]["forward_first"]["arrival_lod02_settled_t_s"] is not None
|
||||
and by_pair[rep]["assembler_world"]["arrival_lod02_settled_t_s"] is not None
|
||||
and by_pair[rep]["forward_first"]["arrival_lod02_settled_t_s"]
|
||||
<= by_pair[rep]["assembler_world"]["arrival_lod02_settled_t_s"]
|
||||
for rep in by_pair),
|
||||
"forward_cone_median_delta_pp": statistics.median(
|
||||
by_pair[rep]["forward_first"]["near_forward_cone_ready_pct"]
|
||||
- by_pair[rep]["assembler_world"]["near_forward_cone_ready_pct"]
|
||||
for rep in by_pair),
|
||||
}
|
||||
summary = {
|
||||
"source_commit": subprocess.run(
|
||||
["git", "-c", f"safe.directory={ROOT}", "rev-parse", "HEAD"],
|
||||
cwd=ROOT, check=True, capture_output=True, text=True).stdout.strip(),
|
||||
"protocol": "Five interleaved 30 m/s UE pairs; 12 s move plus 5 s dwell; 8 effective tile slots. L157 critical wait remains on and apply cap remains 0 ms.",
|
||||
"conditions": CONDITIONS,
|
||||
"movement_medians": medians,
|
||||
"paired_deltas_vs_assembler_world": pairs,
|
||||
"candidate_gates": candidate_gates,
|
||||
"runs": rows,
|
||||
}
|
||||
(OUT / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
print("[L160Summary] " + json.dumps({"movement_medians": medians,
|
||||
"candidate_gates": candidate_gates,
|
||||
"runs": len(rows)}, sort_keys=True), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build four standalone liminal geometry fixtures and their capsule-walk specs."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l161"))
|
||||
FLAT_FIELD = {"version": 1, "kind": "flatField", "minimum_arena_clearance_m": 0.0}
|
||||
|
||||
|
||||
def stamp(stamps: list[dict], stable_id: str, kind: str, x: float, y: float,
|
||||
z: float, half: tuple[float, float, float], *, yaw: int = 0,
|
||||
steps: int | None = None) -> None:
|
||||
row = {
|
||||
"stable_id": stable_id,
|
||||
"kind": kind,
|
||||
"anchor_xy_m": [x, y],
|
||||
"z_offset_m": z,
|
||||
"half_extents_m": list(half),
|
||||
"yaw_quarter_turns": yaw,
|
||||
}
|
||||
if steps is not None:
|
||||
row["step_count"] = steps
|
||||
stamps.append(row)
|
||||
|
||||
|
||||
def fixture(name: str, stamps: list[dict], walk_points: list[tuple[float, float, float]],
|
||||
doors: list[dict], *, center: tuple[float, float], wide_camera: tuple[float, float, float],
|
||||
eye_camera: tuple[float, float, float], eye_target_offset: tuple[float, float, float],
|
||||
wide_target_offset: tuple[float, float, float],
|
||||
eye_clip: int, wide_clip: int) -> tuple[str, dict, dict]:
|
||||
root = json.loads(OWNER.read_text(encoding="utf-8"))
|
||||
root["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
root["liminal_stamp_fixture_override"] = {
|
||||
"version": 1,
|
||||
"strate_index": 0,
|
||||
"stamps": stamps,
|
||||
}
|
||||
walk = {
|
||||
"version": 1,
|
||||
"path_points_offset_m": [list(point) for point in walk_points],
|
||||
"door_prefixes": doors,
|
||||
}
|
||||
views = {
|
||||
"center_xy_m": list(center),
|
||||
"eye_camera_offset_m": list(eye_camera),
|
||||
"wide_camera_offset_m": list(wide_camera),
|
||||
"eye_target_offset_m": list(eye_target_offset),
|
||||
"wide_target_offset_m": list(wide_target_offset),
|
||||
"eye_clip_radius": eye_clip,
|
||||
"wide_clip_radius": wide_clip,
|
||||
}
|
||||
return name, root, {"walk": walk, "views": views}
|
||||
|
||||
|
||||
def pool_rooms() -> tuple[str, dict, dict]:
|
||||
stamps: list[dict] = []
|
||||
cx, cy, cell, cells = 520.0, 440.0, 20.0, 7
|
||||
half = cell * cells * 0.5
|
||||
wall_height, wall_half_thickness = 9.0, 1.2
|
||||
for line in range(cells + 1):
|
||||
offset = (line - cells * 0.5) * cell
|
||||
stamp(stamps, f"liminal/pool-rooms-v1/00-wall-x/{line:02d}", "fillBox",
|
||||
cx + offset, cy, wall_height * 0.5,
|
||||
(wall_half_thickness, half, wall_height * 0.5))
|
||||
stamp(stamps, f"liminal/pool-rooms-v1/00-wall-y/{line:02d}", "fillBox",
|
||||
cx, cy + offset, wall_height * 0.5,
|
||||
(half, wall_half_thickness, wall_height * 0.5))
|
||||
for row in range(cells):
|
||||
y = cy + (row - (cells - 1) * 0.5) * cell
|
||||
for line in range(cells + 1):
|
||||
x = cx + (line - cells * 0.5) * cell
|
||||
stamp(stamps, f"liminal/pool-rooms-v1/01-doorway-x/{row:02d}-{line:02d}",
|
||||
"carveArch", x, y + 7.0, 4.0, (3.6, 1.55, 4.0), yaw=1)
|
||||
stamp(stamps, f"liminal/pool-rooms-v1/01-doorway-y/{line:02d}-{row:02d}",
|
||||
"carveArch", cx + (row - (cells - 1) * 0.5) * cell,
|
||||
cy + (line - cells * 0.5) * cell, 4.0,
|
||||
(3.6, 1.55, 4.0))
|
||||
for row in range(cells):
|
||||
y = cy + (row - (cells - 1) * 0.5) * cell
|
||||
for column in range(cells):
|
||||
x = cx + (column - (cells - 1) * 0.5) * cell
|
||||
stamp(stamps, f"liminal/pool-rooms-v1/02-roof/{row:02d}-{column:02d}",
|
||||
"fillBox", x, y, 9.4, (10.0, 10.0, 0.5))
|
||||
stamp(stamps, f"liminal/pool-rooms-v1/03-skylight/{row:02d}-{column:02d}",
|
||||
"carveBox", x, y, 9.4, (5.5, 5.5, 1.0))
|
||||
stamp(stamps, f"liminal/pool-rooms-v1/04-basin/{row:02d}-{column:02d}",
|
||||
"carveBox", x, y, -1.0, (5.5, 5.5, 1.5))
|
||||
path_y = cy + 7.0
|
||||
walk = [(-half - 2.0 + cx, path_y, 0.88), (half + 2.0 + cx, path_y, 0.88)]
|
||||
doors = [
|
||||
{"prefix": "liminal/pool-rooms-v1/01-doorway-x/", "width_axis": 0,
|
||||
"minimum_width_m": 1.5},
|
||||
{"prefix": "liminal/pool-rooms-v1/01-doorway-y/", "width_axis": 0,
|
||||
"minimum_width_m": 1.5},
|
||||
]
|
||||
return fixture("pool-rooms", stamps, walk, doors, center=(cx, cy),
|
||||
wide_camera=(-60.0, 7.0, 3.0), eye_camera=(-40.0, 7.0, 1.6),
|
||||
eye_target_offset=(60.0, 7.0, 0.8),
|
||||
wide_target_offset=(65.0, 7.0, 1.2), eye_clip=4, wide_clip=7)
|
||||
|
||||
|
||||
def backrooms(width: float = 4.0, name: str = "backrooms-grid") -> tuple[str, dict, dict]:
|
||||
stamps: list[dict] = []
|
||||
cx, cy, cell, cells = 820.0, 420.0, 18.0, 7
|
||||
half, wall_half = cell * cells * 0.5, 1.2
|
||||
wall_height = 5.5
|
||||
for line in range(cells + 1):
|
||||
offset = (line - cells * 0.5) * cell
|
||||
stamp(stamps, f"liminal/backrooms-grid-v1/00-wall-x/{line:02d}", "fillBox",
|
||||
cx + offset, cy, wall_height * 0.5,
|
||||
(wall_half, half, wall_height * 0.5))
|
||||
stamp(stamps, f"liminal/backrooms-grid-v1/00-wall-y/{line:02d}", "fillBox",
|
||||
cx, cy + offset, wall_height * 0.5,
|
||||
(half, wall_half, wall_height * 0.5))
|
||||
door_half = width * 0.5
|
||||
for row in range(cells):
|
||||
y = cy + (row - (cells - 1) * 0.5) * cell
|
||||
for line in range(cells + 1):
|
||||
x = cx + (line - cells * 0.5) * cell
|
||||
stamp(stamps, f"liminal/backrooms-grid-v1/01-door-x/{row:02d}-{line:02d}",
|
||||
"carveBox", x, y, 1.25, (1.6, door_half, 1.25))
|
||||
stamp(stamps, f"liminal/backrooms-grid-v1/01-door-y/{line:02d}-{row:02d}",
|
||||
"carveBox", cx + (row - (cells - 1) * 0.5) * cell,
|
||||
cy + (line - cells * 0.5) * cell, 1.25,
|
||||
(door_half, 1.6, 1.25))
|
||||
stamp(stamps, "liminal/backrooms-grid-v1/02-ceiling", "fillBox",
|
||||
cx, cy, 5.9, (half, half, 0.5))
|
||||
walk = [(cx - half - 2.0, cy, 0.88), (cx + half + 2.0, cy, 0.88)]
|
||||
doors = [
|
||||
{"prefix": "liminal/backrooms-grid-v1/01-door-x/", "width_axis": 1,
|
||||
"minimum_width_m": 1.5},
|
||||
{"prefix": "liminal/backrooms-grid-v1/01-door-y/", "width_axis": 0,
|
||||
"minimum_width_m": 1.5},
|
||||
]
|
||||
return fixture(name, stamps, walk, doors, center=(cx, cy),
|
||||
wide_camera=(-42.0, -6.0, 1.6), eye_camera=(-54.0, 0.0, 1.6),
|
||||
eye_target_offset=(102.0, 0.0, 1.6),
|
||||
wide_target_offset=(80.0, 20.0, 0.0), eye_clip=6, wide_clip=6)
|
||||
|
||||
|
||||
def nowhere_stairs() -> tuple[str, dict, dict]:
|
||||
stamps: list[dict] = []
|
||||
cx, cy = 300.0, -760.0
|
||||
stamp(stamps, "liminal/staircase-nowhere-v1/00-stair", "fillSteps",
|
||||
cx, cy, 6.0, (32.0, 4.0, 6.0), steps=64)
|
||||
stamp(stamps, "liminal/staircase-nowhere-v1/04-landing", "fillBox",
|
||||
cx + 40.5, cy, 11.5, (9.0, 4.5, 0.5))
|
||||
stamp(stamps, "liminal/staircase-nowhere-v1/02-arch-wall", "fillBox",
|
||||
cx + 38.0, cy, 7.5, (1.2, 6.5, 7.5))
|
||||
stamp(stamps, "liminal/staircase-nowhere-v1/03-doorway-arch", "carveArch",
|
||||
cx + 38.0, cy, 7.0, (5.0, 5.0, 7.0), yaw=1)
|
||||
# Begin high enough to clear the first riser with the audit capsule, then
|
||||
# walk the same ascending staircase through the arch and onto the landing.
|
||||
points = [(cx - 32.5, cy, 0.88 + 12.0 / 64.0 + 0.05)]
|
||||
tread, rise = 64.0 / 64, 12.0 / 64
|
||||
for index in range(64):
|
||||
points.append((cx - 32.0 + (index + 0.5) * tread,
|
||||
cy, 0.88 + (index + 1) * rise + 0.05))
|
||||
points.extend([(cx + 34.0, cy, 12.93), (cx + 38.0, cy, 12.93),
|
||||
(cx + 44.0, cy, 12.93)])
|
||||
doors = [{"prefix": "liminal/staircase-nowhere-v1/03-doorway-arch", "width_axis": 0,
|
||||
"minimum_width_m": 1.5}]
|
||||
return fixture("staircase-to-nowhere", stamps, points, doors, center=(cx, cy),
|
||||
wide_camera=(-8.0, -32.0, 36.0), eye_camera=(10.0, 0.0, 9.7),
|
||||
eye_target_offset=(38.0, 0.0, 13.6),
|
||||
wide_target_offset=(42.0, 0.0, 11.0), eye_clip=4, wide_clip=6)
|
||||
|
||||
|
||||
def terraced_hills() -> tuple[str, dict, dict]:
|
||||
stamps: list[dict] = []
|
||||
cx, cy, rows, spacing = -520.0, -500.0, 9, 96.0
|
||||
half_run, half_width, half_rise, steps = 48.0, 40.0, 8.0, 80
|
||||
for row in range(rows):
|
||||
y = cy + (row - (rows - 1) * 0.5) * spacing
|
||||
stamp(stamps, f"liminal/terraced-hills-v1/ridge/{row:02d}/rise-west",
|
||||
"fillSteps", cx - half_run, y, half_rise,
|
||||
(half_run, half_width, half_rise), steps=steps)
|
||||
stamp(stamps, f"liminal/terraced-hills-v1/ridge/{row:02d}/rise-east",
|
||||
"fillSteps", cx + half_run, y, half_rise,
|
||||
(half_run, half_width, half_rise), yaw=2, steps=steps)
|
||||
points = [(cx - 100.0, cy, 0.93)]
|
||||
tread, rise = (2.0 * half_run) / steps, (2.0 * half_rise) / steps
|
||||
for index in range(steps):
|
||||
points.append((cx - 2.0 * half_run + (index + 0.5) * tread,
|
||||
cy, 0.93 + (index + 1) * rise))
|
||||
for index in reversed(range(steps)):
|
||||
points.append((cx + 2.0 * half_run - (index + 0.5) * tread,
|
||||
cy, 0.93 + (index + 1) * rise))
|
||||
points.append((cx + 100.0, cy, 0.93))
|
||||
return fixture("terraced-hills", stamps, points, [], center=(cx, cy),
|
||||
wide_camera=(-40.0, 0.0, 20.0), eye_camera=(-60.0, 0.0, 7.8),
|
||||
eye_target_offset=(0.0, 0.0, 14.5),
|
||||
wide_target_offset=(100.0, 0.0, -10.0), eye_clip=4, wide_clip=3)
|
||||
|
||||
|
||||
def write(path: Path, value: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures")
|
||||
args = parser.parse_args()
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
specs = args.out / "walk-specs"
|
||||
fixtures = [pool_rooms(), backrooms(), nowhere_stairs(), terraced_hills()]
|
||||
fixtures.append(backrooms(width=1.2, name="backrooms-narrow-control"))
|
||||
for name, world, metadata in fixtures:
|
||||
write(args.out / f"liminal-{name}.json", world)
|
||||
write(specs / f"{name}.json", metadata["walk"])
|
||||
write(args.out / "views" / f"{name}.json", metadata["views"])
|
||||
print(f"[L161Fixture] name={name} stamps="
|
||||
f"{len(world['liminal_stamp_fixture_override']['stamps'])} status=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L161 stamp-fixture determinism, walk audits and two-view captures."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l161"))
|
||||
CAPTURE_DEFAULT = Path(__file__).resolve().parent / "captures"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
Q_PER_METRE = 1024.0
|
||||
FOG_COLOR = "0.78,0.84,0.87"
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import current_stamp, first_stamp
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_step(name: str, command: list[object], log: Path, *, env: dict[str, str],
|
||||
stamped: bool = False, expected_exit: int = 0,
|
||||
timeout_seconds: int = 1740) -> str:
|
||||
expected_stamp = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
probe = subprocess.run([str(command[0]), "--print-source-stamp"], cwd=ROOT,
|
||||
capture_output=True, text=True, timeout=15, check=False)
|
||||
if probe.returncode != 0 or first_stamp(probe.stdout) != expected_stamp:
|
||||
raise RuntimeError(f"{name} refused a stale executable: {command[0]}")
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, "timeout", f"{timeout_seconds}s",
|
||||
*map(str, command)], cwd=ROOT, env=env, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=timeout_seconds + 5, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
print(f"[L161Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if stamped and (first_stamp(output) != expected_stamp or current_stamp() != expected_stamp):
|
||||
raise RuntimeError(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != expected_exit:
|
||||
raise RuntimeError(f"{name} exit={result.returncode}, expected={expected_exit}; see {log}")
|
||||
return output
|
||||
|
||||
|
||||
def camera_for(view: dict, floor: float, name: str, kind: str) -> tuple[float, float, float]:
|
||||
center_x, center_y = view["center_xy_m"]
|
||||
offset = view[f"{kind}_camera_offset_m"]
|
||||
z = floor + offset[2]
|
||||
if name == "backrooms-grid" or name == "backrooms-narrow-control":
|
||||
z = floor + offset[2]
|
||||
return center_x + offset[0], center_y + offset[1], z
|
||||
|
||||
|
||||
def target_for(view: dict, floor: float, name: str, kind: str) -> tuple[float, float, float]:
|
||||
center_x, center_y = view["center_xy_m"]
|
||||
offset = view[f"{kind}_target_offset_m"]
|
||||
return center_x + offset[0], center_y + offset[1], floor + offset[2]
|
||||
|
||||
|
||||
def headless_command(binary: Path, fixture: Path, camera, mesh: Path,
|
||||
manifest: Path, layout: Path, workers: int, clip_radius: int,
|
||||
*, max_level: int = 0, hash_tiles: bool = False,
|
||||
audit_path: Path | None = None) -> list[object]:
|
||||
command: list[object] = [binary, fixture, f"--workers={workers}", "--seed=0",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={clip_radius}",
|
||||
f"--max-clip-level={max_level}", f"--max-level={max_level}",
|
||||
"--render-distance-chunks=0",
|
||||
f"--camera={fmt((*camera, 0.0, 0.0))}", f"--mesh-export={mesh}",
|
||||
f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
if hash_tiles:
|
||||
command.append("--stream-hash-tiles")
|
||||
if audit_path is not None:
|
||||
command.append(f"--audit-liminal-stamp-walk={audit_path}")
|
||||
return command
|
||||
|
||||
|
||||
def digest(output: str, manifest_path: Path, layout_path: Path) -> tuple[str, str, str]:
|
||||
tile_hashes = sorted(line for line in output.splitlines()
|
||||
if line.startswith("[PipelineTileHash] "))
|
||||
if not manifest_path.is_file() or not layout_path.is_file() or not tile_hashes:
|
||||
raise RuntimeError("determinism run omitted manifest, resolved layout or tile hashes")
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
manifest_digest = "|".join((manifest["content_hash"],
|
||||
manifest["recipe"]["content_hash"], manifest["plan"]["content_hash"]))
|
||||
return manifest_digest, layout["stable_identity"], "\n".join(tile_hashes)
|
||||
|
||||
|
||||
def write_walk_audit(spec_path: Path, manifest_path: Path, audit_path: Path) -> None:
|
||||
spec = json.loads(spec_path.read_text(encoding="utf-8"))
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
surface = manifest["recipe"]["surface_profile"]
|
||||
floor = surface["h0_q"] / Q_PER_METRE
|
||||
lines = ["# P records are world metres; doorway width axes use the resolved stamp's local extents."]
|
||||
for gate in spec["door_prefixes"]:
|
||||
lines.append(f"D {gate['prefix']} {gate['width_axis']} {gate['minimum_width_m']:.6f}")
|
||||
for x, y, z_offset in spec["path_points_offset_m"]:
|
||||
lines.append(f"P {x:.6f} {y:.6f} {floor + z_offset:.6f}")
|
||||
audit_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
audit_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def camera_angles(camera, target) -> tuple[float, float]:
|
||||
dx, dy, dz = (target[index] - camera[index] for index in range(3))
|
||||
return math.degrees(math.atan2(dx, dy)), math.degrees(math.atan2(dz, math.hypot(dx, dy)))
|
||||
|
||||
|
||||
def build_plan(binary: Path, name: str, fixture: Path, view: dict,
|
||||
out: Path, env: dict[str, str]) -> tuple[Path, Path, dict, dict]:
|
||||
run_dir = out / "plan-probes" / name
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
manifest, layout = run_dir / "manifest.json", run_dir / "layout.json"
|
||||
mesh = run_dir / "probe.vfmesh"
|
||||
camera = (view["center_xy_m"][0], view["center_xy_m"][1], -500.0)
|
||||
output = run_step(f"plan-{name}", headless_command(binary, fixture, camera,
|
||||
mesh, manifest, layout, 12, 1), out / "logs" / f"plan-{name}.log",
|
||||
env=env, stamped=True)
|
||||
if "[LiminalStampFixtureResolved] vocabulary=7" not in output:
|
||||
raise RuntimeError(f"{name} did not resolve with stamp vocabulary 7")
|
||||
if not manifest.is_file() or not layout.is_file():
|
||||
raise RuntimeError(f"{name} plan probe omitted its outputs")
|
||||
manifest_value = json.loads(manifest.read_text(encoding="utf-8"))
|
||||
layout_value = json.loads(layout.read_text(encoding="utf-8"))
|
||||
return manifest, layout, manifest_value, layout_value
|
||||
|
||||
|
||||
def run_determinism(name: str, fixture: Path, view: dict, floor: float,
|
||||
binaries: dict[str, Path], out: Path, env: dict[str, str]) -> None:
|
||||
camera = camera_for(view, floor, name, "eye")
|
||||
baseline = None
|
||||
for compiler in ("clang", "gcc"):
|
||||
for workers in (1, 8, 12):
|
||||
tag = f"{name}-{compiler}-w{workers}"
|
||||
run_dir = out / "determinism" / tag
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
mesh, manifest, layout = (run_dir / "fixture.vfmesh",
|
||||
run_dir / "manifest.json", run_dir / "layout.json")
|
||||
output = run_step(tag, headless_command(binaries[compiler], fixture,
|
||||
camera, mesh, manifest, layout, workers, 1,
|
||||
hash_tiles=True), out / "logs" / f"determinism-{tag}.log",
|
||||
env=env, stamped=True)
|
||||
result = digest(output, manifest, layout)
|
||||
if baseline is None:
|
||||
baseline = result
|
||||
elif result != baseline:
|
||||
raise RuntimeError(f"{name} diverged at {compiler} W{workers}; see determinism logs")
|
||||
print(f"[L161Determinism] fixture={name} compiler_worker_cells=6 identical=yes status=PASS",
|
||||
flush=True)
|
||||
|
||||
|
||||
def run_walk_audit(name: str, fixture: Path, view: dict, floor: float,
|
||||
spec: Path, binary: Path, out: Path, env: dict[str, str],
|
||||
*, expected_exit: int = 0) -> str:
|
||||
camera = camera_for(view, floor, name, "eye")
|
||||
run_dir = out / "walk-audits" / name
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
manifest = run_dir / "manifest.json"
|
||||
layout = run_dir / "layout.json"
|
||||
mesh = run_dir / "audit.vfmesh"
|
||||
audit_path = run_dir / "walk.path"
|
||||
# Use a fixture-specific manifest, so the path uses the exact authored floor height.
|
||||
plan = out / "plan-probes" / name / "manifest.json"
|
||||
if not plan.is_file():
|
||||
raise RuntimeError(f"{name} has no resolved manifest for its walk audit")
|
||||
write_walk_audit(spec, plan, audit_path)
|
||||
output = run_step(f"walk-{name}", headless_command(binary, fixture, camera,
|
||||
mesh, manifest, layout, 12, 1, audit_path=audit_path),
|
||||
out / "logs" / f"walk-{name}.log", env=env, stamped=True,
|
||||
expected_exit=expected_exit)
|
||||
expected_status = "FAIL" if expected_exit else "PASS"
|
||||
if not re.search(rf"\[LiminalStampWalkAudit\].*status={expected_status}\b", output):
|
||||
raise RuntimeError(f"{name} walk audit did not report expected {expected_status}")
|
||||
return output
|
||||
|
||||
|
||||
def capture(name: str, fixture: Path, view: dict, floor: float, *, kind: str,
|
||||
binary: Path, renderer: Path, out: Path, artifact_dir: Path,
|
||||
env: dict[str, str]) -> None:
|
||||
camera = camera_for(view, floor, name, kind)
|
||||
target = target_for(view, floor, name, kind)
|
||||
clip_radius = int(view[f"{kind}_clip_radius"])
|
||||
max_level = 1 if name == "terraced-hills" and kind == "wide" else 0
|
||||
stem = f"{name}-{kind}"
|
||||
run_dir = out / "captures" / stem
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
mesh, manifest, layout = (run_dir / "capture.vfmesh",
|
||||
run_dir / "manifest.json", run_dir / "layout.json")
|
||||
png = run_dir / "capture.png"
|
||||
image = artifact_dir / f"{stem}.jpg"
|
||||
output = run_step(f"capture-export-{stem}", headless_command(binary,
|
||||
fixture, camera, mesh, manifest, layout, 12, clip_radius,
|
||||
max_level=max_level), out / "logs" / f"capture-export-{stem}.log",
|
||||
env=env, stamped=True)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"{stem} produced an empty mesh")
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
if name == "backrooms-grid":
|
||||
fog_start, fog_end, fog_color = 90, 500, "0.82,0.83,0.76"
|
||||
elif name == "pool-rooms":
|
||||
fog_start, fog_end, fog_color = 90, 480, "0.76,0.84,0.87"
|
||||
elif name == "staircase-to-nowhere":
|
||||
fog_start, fog_end, fog_color = 110, 700, "0.78,0.84,0.87"
|
||||
else:
|
||||
fog_start, fog_end, fog_color = 160, 1050, "0.78,0.84,0.89"
|
||||
renderer_command = [renderer, mesh, png, "--size=1600x900", "--fov-deg=72",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}", "--tile-skirts=on", "--shade=readable",
|
||||
"--headlamp=off", "--height-tint=on",
|
||||
f"--height-center-m={floor + 3.0:.6f}", "--height-range-m=30.0",
|
||||
"--light-direction=-0.42,-0.58,0.70", "--fog=on",
|
||||
f"--fog-start-m={fog_start}", f"--fog-end-m={fog_end}",
|
||||
f"--fog-color={fog_color}"]
|
||||
run_step(f"capture-render-{stem}", renderer_command,
|
||||
out / "logs" / f"capture-render-{stem}.log", env=env, stamped=True)
|
||||
run_step(f"capture-jpeg-{stem}", ["ffmpeg", "-nostdin", "-hide_banner",
|
||||
"-loglevel", "error", "-y", "-i", png, "-frames:v", "1", "-q:v", "4", image],
|
||||
out / "logs" / f"capture-jpeg-{stem}.log", env=env)
|
||||
quality = run_step(f"capture-quality-{stem}", [sys.executable,
|
||||
ROOT / "Standalone/Tools/verify/capture_quality.py", image],
|
||||
out / "logs" / f"capture-quality-{stem}.log", env=env)
|
||||
if "[CaptureQuality]" not in quality or not image.is_file() or image.stat().st_size > 300_000:
|
||||
raise RuntimeError(f"{stem} failed image quality or size checks")
|
||||
print(f"[L161Capture] fixture={name} view={kind} camera_m=({fmt(camera)}) "
|
||||
f"target_m=({fmt(target)}) bytes={image.stat().st_size} "
|
||||
f"sha256={sha256(image)} status=PASS", flush=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT)
|
||||
parser.add_argument("--artifact-dir", type=Path, default=CAPTURE_DEFAULT)
|
||||
parser.add_argument("--headless-clang", type=Path, required=True)
|
||||
parser.add_argument("--headless-gcc", type=Path, required=True)
|
||||
parser.add_argument("--renderer", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
out, artifact_dir = args.out.resolve(), args.artifact_dir.resolve()
|
||||
fixture_dir = out / "fixtures"
|
||||
for directory in (out, fixture_dir, artifact_dir):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(out)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT / "Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(out / "xdg-cache")
|
||||
run_step("build-fixtures", [sys.executable,
|
||||
Path(__file__).resolve().parent / "BuildL161Fixtures.py",
|
||||
f"--out={fixture_dir}"], out / "logs" / "build-fixtures.log", env=env)
|
||||
|
||||
binaries = {"clang": args.headless_clang.resolve(), "gcc": args.headless_gcc.resolve()}
|
||||
fixtures = {name: fixture_dir / f"liminal-{name}.json" for name in
|
||||
("pool-rooms", "backrooms-grid", "staircase-to-nowhere", "terraced-hills")}
|
||||
views = {}
|
||||
floors = {}
|
||||
for name, fixture_path in fixtures.items():
|
||||
view = json.loads((fixture_dir / "views" / f"{name}.json").read_text(encoding="utf-8"))
|
||||
manifest_path, _, manifest, layout = build_plan(binaries["clang"], name,
|
||||
fixture_path, view, out, env)
|
||||
view["center_xy_m"] = view["center_xy_m"]
|
||||
floors[name] = manifest["recipe"]["surface_profile"]["h0_q"] / Q_PER_METRE
|
||||
if layout["stamp_vocabulary_version"] != 7 or not layout["stamps"]:
|
||||
raise RuntimeError(f"{name} plan lacks resolved vocabulary-7 stamps")
|
||||
write_walk_audit(fixture_dir / "walk-specs" / f"{name}.json",
|
||||
manifest_path, out / "walk-audits" / name / "walk.path")
|
||||
views[name] = view
|
||||
print(f"[L161Plan] fixture={name} stamps={len(layout['stamps'])} "
|
||||
f"floor_m={floors[name]:.3f} layout={layout['stable_identity']} status=PASS")
|
||||
|
||||
for name, fixture_path in fixtures.items():
|
||||
run_walk_audit(name, fixture_path, views[name], floors[name],
|
||||
fixture_dir / "walk-specs" / f"{name}.json", binaries["clang"], out, env)
|
||||
run_determinism(name, fixture_path, views[name], floors[name], binaries, out, env)
|
||||
|
||||
narrow_name = "backrooms-narrow-control"
|
||||
narrow_fixture = fixture_dir / f"liminal-{narrow_name}.json"
|
||||
narrow_view = json.loads((fixture_dir / "views" / f"{narrow_name}.json").read_text(encoding="utf-8"))
|
||||
narrow_manifest, _, narrow_manifest_value, _ = build_plan(binaries["clang"],
|
||||
narrow_name, narrow_fixture, narrow_view, out, env)
|
||||
narrow_floor = narrow_manifest_value["recipe"]["surface_profile"]["h0_q"] / Q_PER_METRE
|
||||
run_walk_audit(narrow_name, narrow_fixture, narrow_view, narrow_floor,
|
||||
fixture_dir / "walk-specs" / f"{narrow_name}.json", binaries["clang"],
|
||||
out, env, expected_exit=14)
|
||||
print("[L161DoorwayNegativeControl] width_m=1.200 minimum_m=1.500 expected=FAIL status=PASS")
|
||||
|
||||
for name, fixture_path in fixtures.items():
|
||||
for kind in ("eye", "wide"):
|
||||
capture(name, fixture_path, views[name], floors[name], kind=kind,
|
||||
binary=binaries["clang"], renderer=args.renderer.resolve(), out=out,
|
||||
artifact_dir=artifact_dir, env=env)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L161Capture] status=FAIL error={error}", file=sys.stderr, flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,31 @@
|
||||
# L161 — More liminal looks
|
||||
|
||||
| Look | Eye height | Wide |
|
||||
|---|---|---|
|
||||
| Pool rooms |  |  |
|
||||
| Backrooms grid |  |  |
|
||||
| Staircase to nowhere |  |  |
|
||||
| Terraced hills |  |  |
|
||||
|
||||
## Result
|
||||
|
||||
Four standalone SurfaceWorld fixtures resolve from the shared stamp vocabulary. They use repeated rooms and basins, a doorway grid, a freestanding stair and arch, and nine stepped ridges. This round authored no inter-strate passages or owner content.
|
||||
|
||||
## Changes
|
||||
|
||||
- Stamp vocabulary v7 adds `fillBox`, `carveBox`, and `fillSteps`; the existing `carveArch` is reused. The new kinds are opt-in.
|
||||
- Added a flat-field liminal stamp-fixture override and a capsule walk audit with doorway-width gates. The audit checks support, collision, grade, and authored opening widths.
|
||||
- Fixtures contain 275 pool-room stamps, 129 backrooms stamps, 4 staircase/arch stamps, and 18 terrace stamps.
|
||||
|
||||
## Verification
|
||||
|
||||
- Clang 19 and GCC 14 builds passed with FP contraction off. Both binaries carry source stamp `8e9e114c1357`.
|
||||
- Each fixture matched across Clang/GCC at W1, W8, and W12: manifest, resolved-layout identity, and tile hashes were identical in all six cells.
|
||||
- Positive capsule audits passed. Pool doorways measured 7.2 m, backrooms doorways 4.0 m, and the arch opening 10.0 m. The narrow 1.2 m backrooms control failed the 1.5 m minimum as expected. The terrace walk had zero blocked samples, unsupported samples, or grade failures.
|
||||
- All eight 1600×900 captures passed image quality checks; files are 25.3–58.4 KB. Capture and audit logs are under `Saved/Linux/l161/logs/`.
|
||||
- Fast tier passed: 273 checks in 346.6 s. Both owner generators matched their pinned L130/v227 canonical baselines exactly (12/12 rows), and W1/W12 signatures matched. Summary: `Saved/Linux/l161/fast-tier/fast-summary.json`.
|
||||
- `git diff --check` passed. No Unreal `Source/` files changed; no Unreal build or medium/full verification tier was run.
|
||||
|
||||
## Remaining risk
|
||||
|
||||
The captures show untextured geometry with simple fog. The terraced wide capture uses clip radius 3 and max LOD 1 to stay within the standalone shape-build bound, so the long-range horizon read still needs an art review.
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 57 KiB |
@@ -0,0 +1,58 @@
|
||||
# L165 — incremental desired-set rebuild
|
||||
|
||||
**Result:** The Core clipmap key update is incremental for ordinary adjacent crossings and cuts median nonzero `BuildDesiredTiles` p95 by 47.7% (7.80 to 4.08 ms). Equality checks pass. Worst tick, p99, near readiness, arrival, and five-pair median far-hole coverage all improved. Far holes regressed by 0.291 percentage points in pair 5, so the strict paired no-regression gate is not clean across all five pairs.
|
||||
|
||||
## Count first
|
||||
|
||||
A count-only trace of the baseline run covered movement from 6.174 to 17.981 s: 316 desired-set updates, including 44 center crossings. At a crossing, 93.813% of prior desired keys were unchanged in aggregate (median crossing 93.405%; range 93.384–97.318%). Same-center updates had 99.955% median overlap.
|
||||
|
||||
| LOD | Prior keys unchanged at crossings |
|
||||
| --- | ---: |
|
||||
| 0 | 86.929% |
|
||||
| 1 | 91.071% |
|
||||
| 2 | 94.981% |
|
||||
| 3 | 97.017% |
|
||||
| 4 | 98.674% |
|
||||
| 5 | 99.053% |
|
||||
| 6 | 99.811% |
|
||||
| 7 | 100.000% |
|
||||
| 8 | 100.000% |
|
||||
|
||||
This trace was used only for key counts. Its commandlet ended before its separate readiness check, so its timings are excluded.
|
||||
|
||||
## Change and movement results
|
||||
|
||||
`BuildDelta` computes entering and leaving box slabs per LOD, including coarser-ring coverage changes. The runtime caches the Core key array and set, applies the delta at one-tile crossings, and updates the transition-descendant index from entering/leaving keys. It falls back to `World::desiredTiles` for larger jumps or when a changed key touches LOD4 or coarser. The UE overlay sets and priority/admission order are still rebuilt on each trigger; this round does not make those materialization steps incremental.
|
||||
|
||||
Five interleaved W8 UE pairs ran at 30 m/s, 45 crossings each, with the L160 `assembler/world` baseline settings and forward-first disabled. The machine was quiet before timing (load averages 0.35 / 1.46 / 1.25). Metrics below are medians of the five runs; rebuild p50/p95 are over nonzero `BuildDesiredTiles` samples in the 12 s movement window.
|
||||
|
||||
| Metric | `assembler/world` | Incremental | Paired result |
|
||||
| --- | ---: | ---: | --- |
|
||||
| `BuildDesiredTiles` p50 | 3.96 ms | 1.28 ms | lower in all five pairs |
|
||||
| `BuildDesiredTiles` p95 | 7.80 ms | 4.08 ms | −3.72 ms median; lower in all five pairs |
|
||||
| Game-thread p99 | 11.46 ms | 7.97 ms | every candidate run was 7.90–8.26 ms |
|
||||
| Worst tick | 13.73 ms | 12.11 ms | lower in all five pairs by 0.86–3.94 ms |
|
||||
| Near LOD0–2 readiness | 41.41% | 42.90% | better in all five pairs |
|
||||
| 55° forward-cone readiness | 14.69% | 15.37% | better in all five pairs |
|
||||
| Far holes while moving | 3.09% | 2.61% | better in four pairs; pair 5 was +0.291 pp |
|
||||
| First settled LOD0–2 arrival | 2.453 s | 2.437 s | earlier in all five pairs |
|
||||
|
||||
The cone uses the travel-direction fallback because null-RHI produced no camera-forward samples. Far-hole coverage measures loaded desired coverage; the desired key set itself matched in every-crossing control checks.
|
||||
|
||||
## Checks and limits
|
||||
|
||||
- Built `VFHostEditor` from this worktree with `taskset -c 0-11`, `-NoUBA`, and `-MaxParallelActions=4`; final build succeeded. Restored `VFHost/PluginDeps/VoxelForge/Source` to `/home/jahni/VoxelM/Plugins/VoxelForge/Source`.
|
||||
- Fast tier passed: 268 PASS, 0 FAIL, 5 diagnostics. Both canonical owner hash sets match the L130 baseline exactly (12/12 rows each); W1/W8/W12 determinism checks passed.
|
||||
- Clipmap automation passed 630 delta cases across six unit steps and three vertical-clamp settings. Its log reports `Result=Success`, failures=0; UE returned exit status 1 when `TestExit=Automation Test Queue Empty` fired.
|
||||
- The runtime equality audit passed all 45 crossings with interval 1, and all 20 sampled audits across the five measured candidate runs (four per run). Injecting an impossible reference key failed at crossing 1 with `missing=1` and triggered the assertion/nonzero UE exit.
|
||||
- `git diff --check` and runner `py_compile` passed. The movement summarizer reports the strict pairwise far-hole gate as failed in pair 5; the five-pair median still improved. The other movement gates passed. HMD/camera-forward readiness and W12 movement were not measured.
|
||||
- The final transition-index range correction only changes configurations whose desired max LOD is below 8; this W8 scene includes LOD8, so the measured code path is unchanged. The forward-first default remains off.
|
||||
|
||||
Changed Unreal `Source/` files:
|
||||
|
||||
- `Source/VoxelForge/Private/VoxelClipmapDesiredTiles.h`
|
||||
- `Source/VoxelForge/Private/VoxelWorld.cpp`
|
||||
- `Source/VoxelForge/Private/Tests/VoxelForgeClipmapNoOverlapTest.cpp`
|
||||
- `Source/VoxelForge/Public/VoxelWorld.h`
|
||||
|
||||
Logs and summaries are in `Saved/Linux/l165/`.
|
||||
@@ -0,0 +1,606 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Interleaved L165 UE movement runs for incremental desired-set rebuilds."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
HOST = Path("/home/jahni/VFHost")
|
||||
EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd")
|
||||
OUT = ROOT / "Saved/Linux/l165/ue/movement"
|
||||
HOST_SAVED = HOST / "Saved/Linux/l165"
|
||||
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l165"
|
||||
SPAWN_M = (19.166015625, -14.2001953125, -753.3740234375)
|
||||
MOVE_START_S = 6.0
|
||||
|
||||
CONDITIONS = {
|
||||
"assembler_world": {
|
||||
"wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||
"forward_first": 0, "rebuild_on_region_completion": 1,
|
||||
"core_desired_cache": 0, "incremental_desired": 0,
|
||||
"audit_every": 0, "inject_mismatch": 0,
|
||||
},
|
||||
"incremental": {
|
||||
"wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||
"forward_first": 0, "rebuild_on_region_completion": 1,
|
||||
"core_desired_cache": 0, "incremental_desired": 1,
|
||||
"audit_every": 10, "inject_mismatch": 0,
|
||||
},
|
||||
}
|
||||
ORDER = {
|
||||
1: ("assembler_world", "incremental"),
|
||||
2: ("incremental", "assembler_world"),
|
||||
3: ("assembler_world", "incremental"),
|
||||
4: ("incremental", "assembler_world"),
|
||||
5: ("assembler_world", "incremental"),
|
||||
}
|
||||
|
||||
|
||||
def percentile(values: list[float], fraction: float = 0.99) -> float | None:
|
||||
if not values:
|
||||
return None
|
||||
ordered = sorted(values)
|
||||
return ordered[max(0, min(len(ordered) - 1, math.ceil(len(ordered) * fraction) - 1))]
|
||||
|
||||
|
||||
def read_events(path: Path) -> list[dict]:
|
||||
rows = []
|
||||
if path.is_file():
|
||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
if not line.startswith("{"):
|
||||
continue
|
||||
try:
|
||||
rows.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return rows
|
||||
|
||||
|
||||
def mean(values: list[float]) -> float | None:
|
||||
return statistics.mean(values) if values else None
|
||||
|
||||
|
||||
def percent(numerator: int | float, denominator: int | float) -> float | None:
|
||||
return 100.0 * numerator / denominator if denominator > 0 else None
|
||||
|
||||
|
||||
def run(condition: str, repetition: int, tile_hash_dump: Path | None = None,
|
||||
expected_mismatch: bool = False) -> dict:
|
||||
config = CONDITIONS[condition]
|
||||
run_name = f"fast-r{repetition}"
|
||||
out = OUT / condition
|
||||
host_out = HOST_PLUGIN_SAVED / condition / "output"
|
||||
userdir = HOST_SAVED / "user" / condition / run_name
|
||||
zendata = HOST_SAVED / "zen" / condition / run_name
|
||||
ddc = HOST_SAVED / "DDC"
|
||||
trace = out / f"{run_name}.trace.jsonl"
|
||||
abslog = out / f"{run_name}.log"
|
||||
stdout = out / f"{run_name}.stdout.log"
|
||||
export = host_out / run_name
|
||||
for directory in (out, export, userdir, zendata, ddc):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
trace.unlink(missing_ok=True)
|
||||
|
||||
spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWN_M)
|
||||
command = [
|
||||
str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended",
|
||||
"-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch",
|
||||
"-notraceserver", "-nocrashreports", "-DisablePlugins=AndroidFileServer",
|
||||
"-ddc=NoZenLocalFallback", f"-userdir={userdir}", f"-ZenDataPath={zendata}",
|
||||
f"-LocalDataCachePath={ddc}", "-run=VoxelForgeExplore", "-seed=0",
|
||||
"-archetype=TunnelNetwork", "-slot=0", "-modes=export", "-gameconfig=1",
|
||||
"-coreworldtest=1", "-ownerfourstrateplan=1", "-corevoxelsizecm=25",
|
||||
"-coreislandtest=0", "-corefallbacktest=0", "-coreofftest=0",
|
||||
"-coreplantest=0", "-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings",
|
||||
"-corehybridmesher=0", f"-out={export}", "-voxel.StreamingCoarseFirst=1",
|
||||
"-voxel.StreamingDynamicNearReserve=1",
|
||||
"-voxel.StreamingLegacyPendingResultCap=0",
|
||||
"-voxel.CoreRegionConcurrentAdmission=1",
|
||||
"-voxel.StreamingResultBacklogCap=256", "-voxel.StreamingReadyQueueDepth=8",
|
||||
"-voxel.StreamingBackgroundWorkerCap=1", "-voxel.CoreColumnCacheDirectMapped=0",
|
||||
"-voxel.StreamingFarApplySupplement=1",
|
||||
f"-voxel.StreamingFastTravelWaitForAllCriticalTiles={config['wait_for_critical']}",
|
||||
f"-voxel.StreamingFastTravelApplyBudgetMilliseconds={config['apply_budget_ms']:.1f}",
|
||||
f"-voxel.StreamingFastTravelForwardFirst={config['forward_first']}",
|
||||
f"-voxel.StreamingRebuildOnCoreRegionCompletion={config['rebuild_on_region_completion']}",
|
||||
f"-voxel.StreamingCoreDesiredCacheByCenter={config['core_desired_cache']}",
|
||||
f"-voxel.StreamingIncrementalDesiredSet={config['incremental_desired']}",
|
||||
f"-voxel.StreamingIncrementalDesiredAuditEvery={config['audit_every']}",
|
||||
f"-voxel.StreamingIncrementalDesiredAuditInjectMismatch={config['inject_mismatch']}",
|
||||
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
|
||||
"-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=3000.0",
|
||||
f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}",
|
||||
"-voxel.TestMoveDurationSeconds=12.0", "-voxel.TestExitSeconds=0.0",
|
||||
"-voxel.TestMoveDirection=1,0,0", "-voxel.StreamingSchedulerPolicy=3",
|
||||
"-voxel.TestExitOnSteady=0", "-coreworldstreamtrace=1",
|
||||
f"-voxel.StartupTraceSpawnCm={spawn_cm}",
|
||||
"-voxel.StartupTraceUntilEndPlay=1", "-voxel.StartupTraceThroughCrossing=1",
|
||||
"-voxel.StartupTraceMaxTiles=16384", f"-voxel.StartupTraceFile={trace}",
|
||||
f"-abslog={abslog}",
|
||||
]
|
||||
if tile_hash_dump is not None:
|
||||
tile_hash_dump.parent.mkdir(parents=True, exist_ok=True)
|
||||
tile_hash_dump.unlink(missing_ok=True)
|
||||
command.append(f"-voxel.TileHashDump={tile_hash_dump.resolve()}")
|
||||
wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command]
|
||||
started = time.monotonic()
|
||||
with stdout.open("w", encoding="utf-8") as stream:
|
||||
process = subprocess.run(
|
||||
wrapped, cwd=ROOT, stdout=stream, stderr=subprocess.STDOUT,
|
||||
check=False, timeout=1740)
|
||||
|
||||
events = read_events(trace)
|
||||
if expected_mismatch:
|
||||
diagnostic = "VoxelForgeIncrementalDesiredAudit"
|
||||
logs = "\n".join(path.read_text(encoding="utf-8", errors="replace")
|
||||
for path in (abslog, stdout) if path.is_file())
|
||||
audit_events = [event for event in events
|
||||
if event.get("type") == "incremental_desired_audit"]
|
||||
failed_audit = any(int(event.get("pass", 1)) == 0 for event in audit_events)
|
||||
if process.returncode == 0 or diagnostic not in logs or "mismatch" not in logs.lower():
|
||||
raise RuntimeError(
|
||||
f"injected mismatch did not fail as expected: exit={process.returncode}; "
|
||||
f"audit_events={len(audit_events)}; log={abslog}")
|
||||
print("[L165MismatchControl] " + json.dumps({
|
||||
"exit_code": process.returncode,
|
||||
"diagnostic_seen": True,
|
||||
"failed_audit_event_seen": failed_audit,
|
||||
"trace": str(trace.relative_to(ROOT)),
|
||||
"log": str(abslog.relative_to(ROOT)),
|
||||
}, sort_keys=True), flush=True)
|
||||
return {"condition": condition, "exit_code": process.returncode,
|
||||
"failed_audit_event_seen": failed_audit}
|
||||
samples = [event for event in events if event.get("type") == "streaming_sample"]
|
||||
moving = [event for event in samples if float(event.get("speed_cm_s", 0.0)) >= 50.0]
|
||||
stop_event = next((event for event in events
|
||||
if event.get("type") == "streaming_test_move_stopped"), None)
|
||||
backend = next((event for event in events if event.get("type") == "worker_backend_config"), {})
|
||||
scheduler_config = next((event for event in events
|
||||
if event.get("type") == "scheduler_admission"), {})
|
||||
worker_cap = next((event for event in events
|
||||
if event.get("type") == "streaming_worker_cap_readback"), {})
|
||||
if stop_event is None:
|
||||
raise RuntimeError(f"missing move-stop event: {trace}")
|
||||
stop_s = float(stop_event.get("t_s", 0.0))
|
||||
move_start_s = min((float(event.get("t_s", 0.0)) for event in moving), default=float("inf"))
|
||||
dwell = [event for event in samples if float(event.get("t_s", 0.0)) >= stop_s
|
||||
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||
arrival = [event for event in samples if float(event.get("t_s", 0.0)) < move_start_s
|
||||
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||
if process.returncode or len(moving) < 40 or len(dwell) < 16:
|
||||
raise RuntimeError(f"incomplete {condition} run: {trace}; exit={process.returncode}; "
|
||||
f"move={len(moving)} dwell={len(dwell)}")
|
||||
if (int(backend.get("streaming_fast_travel_wait_for_all_critical_tiles", -1))
|
||||
!= config["wait_for_critical"]
|
||||
or abs(float(backend.get("streaming_fast_travel_apply_budget_ms", -1.0))
|
||||
- config["apply_budget_ms"]) > 0.01
|
||||
or int(worker_cap.get("streaming_background_worker_cap", -1)) != 1
|
||||
or int(backend.get("effective_tile_slots", -1)) != 8
|
||||
or int(backend.get("streaming_far_apply_supplement", -1)) != 1
|
||||
or int(scheduler_config.get("forward_first", -1)) != config["forward_first"]
|
||||
or int(scheduler_config.get("rebuild_on_core_region_completion", -1))
|
||||
!= config["rebuild_on_region_completion"]
|
||||
or int(scheduler_config.get("core_desired_cache_by_center", -1))
|
||||
!= config["core_desired_cache"]
|
||||
or int(scheduler_config.get("incremental_desired_set", -1))
|
||||
!= config["incremental_desired"]
|
||||
or int(scheduler_config.get("incremental_desired_audit_every", -1))
|
||||
!= config["audit_every"]
|
||||
or int(scheduler_config.get("incremental_desired_audit_inject_mismatch", -1))
|
||||
!= config["inject_mismatch"]):
|
||||
raise RuntimeError(f"CVar/worker readback mismatch: {backend}; {worker_cap}; {scheduler_config}")
|
||||
|
||||
moving_metrics = []
|
||||
for event in moving:
|
||||
desired = event.get("desired_by_lod", [0] * 9)
|
||||
resident = event.get("resident_by_lod", [0] * 9)
|
||||
near_desired = sum(desired[:3])
|
||||
cone_desired = int(event.get("near_forward_cone_desired", 0))
|
||||
view_cone_desired = int(event.get("near_view_forward_cone_desired", 0))
|
||||
view_valid = bool(event.get("near_view_forward_cone_valid", 0))
|
||||
weighted_desired = float(event.get("near_forward_weighted_desired", 0.0))
|
||||
travel_cone_ready = percent(
|
||||
int(event.get("near_forward_cone_resident", 0)), cone_desired)
|
||||
view_cone_ready = percent(
|
||||
int(event.get("near_view_forward_cone_resident", 0)), view_cone_desired)
|
||||
moving_metrics.append({
|
||||
"near_ready_pct": percent(sum(resident[:3]), near_desired) or 0.0,
|
||||
"lod0_ready_pct": percent(resident[0], desired[0]) or 0.0,
|
||||
"lod1_ready_pct": percent(resident[1], desired[1]) or 0.0,
|
||||
"lod2_ready_pct": percent(resident[2], desired[2]) or 0.0,
|
||||
"far_hole_pct": percent(
|
||||
sum(desired[3:]) - sum(event.get("covered_by_lod", resident)[3:]),
|
||||
sum(desired[3:])) or 0.0,
|
||||
"ahead_share_pct": percent(int(event.get("near_ahead_desired", 0)), near_desired),
|
||||
"behind_share_pct": percent(int(event.get("near_behind_desired", 0)), near_desired),
|
||||
"side_share_pct": 100.0 - (
|
||||
percent(int(event.get("near_ahead_desired", 0)), near_desired) or 0.0
|
||||
) - (
|
||||
percent(int(event.get("near_behind_desired", 0)), near_desired) or 0.0
|
||||
),
|
||||
"ahead_ready_pct": percent(
|
||||
int(event.get("near_ahead_resident", 0)),
|
||||
int(event.get("near_ahead_desired", 0))),
|
||||
"behind_ready_pct": percent(
|
||||
int(event.get("near_behind_resident", 0)),
|
||||
int(event.get("near_behind_desired", 0))),
|
||||
"travel_forward_cone_ready_pct": travel_cone_ready,
|
||||
"view_forward_cone_ready_pct": view_cone_ready,
|
||||
"forward_cone_ready_pct": view_cone_ready if view_valid else travel_cone_ready,
|
||||
"forward_cone_source": "camera" if view_valid else "travel_fallback",
|
||||
"forward_cone_share_pct": percent(cone_desired, near_desired),
|
||||
"forward_weighted_ready_pct": percent(
|
||||
float(event.get("near_forward_weighted_resident", 0.0)), weighted_desired),
|
||||
})
|
||||
|
||||
arrival_near_settled = next((event for event in arrival
|
||||
if sum(event.get("desired_by_lod", [0] * 9)[:3]) > 0
|
||||
and sum(event.get("settled_by_lod", [0] * 9)[:3])
|
||||
>= sum(event.get("desired_by_lod", [0] * 9)[:3])), None)
|
||||
dwell_complete = next((event for event in dwell
|
||||
if sum(event.get("resident_by_lod", [0] * 9)[:3])
|
||||
>= sum(event.get("desired_by_lod", [0] * 9)[:3])
|
||||
and sum(event.get("desired_by_lod", [0] * 9)[3:])
|
||||
<= sum(event.get("covered_by_lod", [0] * 9)[3:])), None)
|
||||
|
||||
work = [event for event in events if event.get("type") == "game_thread_work_frame"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
applies = {event.get("frame"): event for event in events
|
||||
if event.get("type") == "apply_frame"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||
scheduler = {event.get("frame"): event for event in events
|
||||
if event.get("type") == "scheduler_admission"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||
frames = []
|
||||
for event in work:
|
||||
apply = applies.get(event.get("frame"), {})
|
||||
admit = scheduler.get(event.get("frame"), {})
|
||||
frames.append({
|
||||
"frame": event.get("frame"),
|
||||
"tick_ms": float(event.get("tick_ms", 0.0)),
|
||||
"update_chunks_ms": float(event.get("update_chunks_ms", 0.0)),
|
||||
"pending_drain_ms": float(event.get("pending_drain_ms", 0.0)),
|
||||
"apply_ms": float(apply.get("apply_ms", 0.0)),
|
||||
"apply_budget_ms": float(apply.get("apply_budget_ms", 0.0)),
|
||||
"meshes_applied": int(apply.get("meshes_applied", 0)),
|
||||
"time_budget_hit": int(apply.get("time_limit_reached", 0)),
|
||||
"desired_rebuild_ms": float(admit.get("desired_rebuild_ms", 0.0)),
|
||||
"desired_rebuilt": int(admit.get("desired_rebuilt", 0)),
|
||||
"submit_tiles_ms": float(admit.get("submit_tiles_ms", 0.0)),
|
||||
"level_admit_ms": float(admit.get("level_admit_ms", 0.0)),
|
||||
})
|
||||
p99_field = lambda key: percentile([float(row[key]) for row in frames])
|
||||
max_field = lambda key: max((float(row[key]) for row in frames), default=None)
|
||||
rebuild_frames = [row for row in frames if row["desired_rebuilt"]]
|
||||
rebuild_costs = [float(row["desired_rebuild_ms"]) for row in rebuild_frames]
|
||||
moving_scheduler = [event for event in scheduler.values()]
|
||||
center_crossings = 0
|
||||
previous_center = None
|
||||
for event in sorted(moving_scheduler, key=lambda item: int(item.get("frame", 0))):
|
||||
center = tuple(event.get("center", ()))
|
||||
if previous_center is not None and center != previous_center:
|
||||
center_crossings += 1
|
||||
previous_center = center
|
||||
core_requests = [event for event in events
|
||||
if event.get("type") == "core_region_compile_request"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
same_center_core_requests = sum(
|
||||
event.get("reason") == "desired_set_changed_same_center" for event in core_requests)
|
||||
core_desired_queries = [event for event in events
|
||||
if event.get("type") == "core_desired_cache"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
core_desired_query_ms = [float(event.get("query_ms", 0.0))
|
||||
for event in core_desired_queries if float(event.get("query_ms", 0.0)) > 0.0]
|
||||
core_desired_cache_hits = sum(int(event.get("hit", 0)) for event in core_desired_queries)
|
||||
incremental_audits = [event for event in events
|
||||
if event.get("type") == "incremental_desired_audit"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
incremental_audit_failures = sum(int(event.get("pass", 1)) == 0
|
||||
for event in incremental_audits)
|
||||
if config["incremental_desired"] and (
|
||||
not incremental_audits or incremental_audit_failures):
|
||||
raise RuntimeError(
|
||||
f"incremental desired equality audit missing/failed: {trace}; "
|
||||
f"audits={len(incremental_audits)} failures={incremental_audit_failures}")
|
||||
direction_submissions = [event.get("near_submitted_by_direction", [0, 0, 0, 0])
|
||||
for event in moving_scheduler]
|
||||
direction_submissions = [counts for counts in direction_submissions
|
||||
if isinstance(counts, list) and len(counts) == 4]
|
||||
direction_submission_totals = [
|
||||
sum(int(counts[index]) for counts in direction_submissions) for index in range(4)
|
||||
]
|
||||
first_admission_order = next((event.get("near_admission_order_first", [])
|
||||
for event in moving_scheduler
|
||||
if event.get("near_admission_order_first")), [])
|
||||
camera_samples = sum(bool(event.get("near_view_forward_cone_valid", 0)) for event in moving)
|
||||
dwell_complete_s = (float(dwell_complete.get("t_s", 0.0)) - stop_s
|
||||
if dwell_complete else None)
|
||||
|
||||
row = {
|
||||
"condition": condition,
|
||||
"repetition": repetition,
|
||||
"speed_mps": 30.0,
|
||||
"wait_for_all_critical_tiles": config["wait_for_critical"],
|
||||
"fast_travel_apply_budget_ms": config["apply_budget_ms"],
|
||||
"forward_first": config["forward_first"],
|
||||
"rebuild_on_core_region_completion": config["rebuild_on_region_completion"],
|
||||
"core_desired_cache_by_center": config["core_desired_cache"],
|
||||
"incremental_desired_set": config["incremental_desired"],
|
||||
"incremental_desired_audit_every": config["audit_every"],
|
||||
"incremental_desired_audit_count": len(incremental_audits),
|
||||
"incremental_desired_audit_failures": incremental_audit_failures,
|
||||
"effective_tile_slots": backend.get("effective_tile_slots"),
|
||||
"exit_code": process.returncode,
|
||||
"wall_s": round(time.monotonic() - started, 3),
|
||||
"moving_samples": len(moving),
|
||||
"dwell_samples": len(dwell),
|
||||
"arrival_samples": len(arrival),
|
||||
"near_ready_moving_mean_pct": mean([m["near_ready_pct"] for m in moving_metrics]),
|
||||
"lod0_ready_moving_mean_pct": mean([m["lod0_ready_pct"] for m in moving_metrics]),
|
||||
"lod1_ready_moving_mean_pct": mean([m["lod1_ready_pct"] for m in moving_metrics]),
|
||||
"lod2_ready_moving_mean_pct": mean([m["lod2_ready_pct"] for m in moving_metrics]),
|
||||
"far_holes_moving_mean_pct": mean([m["far_hole_pct"] for m in moving_metrics]),
|
||||
"near_ahead_desired_share_pct": mean([m["ahead_share_pct"] for m in moving_metrics
|
||||
if m["ahead_share_pct"] is not None]),
|
||||
"near_behind_desired_share_pct": mean([m["behind_share_pct"] for m in moving_metrics
|
||||
if m["behind_share_pct"] is not None]),
|
||||
"near_side_desired_share_pct": mean([m["side_share_pct"] for m in moving_metrics]),
|
||||
"near_ahead_ready_pct": mean([m["ahead_ready_pct"] for m in moving_metrics
|
||||
if m["ahead_ready_pct"] is not None]),
|
||||
"near_behind_ready_pct": mean([m["behind_ready_pct"] for m in moving_metrics
|
||||
if m["behind_ready_pct"] is not None]),
|
||||
"near_forward_cone_half_angle_deg": 55,
|
||||
"near_forward_cone_desired_share_pct": mean([m["forward_cone_share_pct"]
|
||||
for m in moving_metrics
|
||||
if m["forward_cone_share_pct"] is not None]),
|
||||
"near_forward_cone_ready_pct": mean([m["forward_cone_ready_pct"]
|
||||
for m in moving_metrics
|
||||
if m["forward_cone_ready_pct"] is not None]),
|
||||
"near_travel_forward_cone_ready_pct": mean(
|
||||
[m["travel_forward_cone_ready_pct"] for m in moving_metrics
|
||||
if m["travel_forward_cone_ready_pct"] is not None]),
|
||||
"near_view_forward_cone_ready_pct": mean(
|
||||
[m["view_forward_cone_ready_pct"] for m in moving_metrics
|
||||
if m["view_forward_cone_ready_pct"] is not None]),
|
||||
"camera_forward_samples": camera_samples,
|
||||
"camera_forward_sample_share_pct": percent(camera_samples, len(moving)),
|
||||
"near_forward_cosine_weighted_ready_pct": mean(
|
||||
[m["forward_weighted_ready_pct"] for m in moving_metrics
|
||||
if m["forward_weighted_ready_pct"] is not None]),
|
||||
"arrival_lod02_settled_t_s": (
|
||||
float(arrival_near_settled.get("t_s", 0.0)) if arrival_near_settled else None),
|
||||
"near_ready_dwell_end_pct": (100.0 * sum(dwell[-1].get("resident_by_lod", [0] * 9)[:3])
|
||||
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[:3]))),
|
||||
"far_holes_dwell_end_pct": (100.0 * (
|
||||
sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:])
|
||||
- sum(dwell[-1].get("covered_by_lod", [0] * 9)[3:]))
|
||||
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:]))),
|
||||
"dwell_complete_after_stop_s": dwell_complete_s,
|
||||
"game_thread_tick_p99_ms": p99_field("tick_ms"),
|
||||
"game_thread_tick_max_ms": max_field("tick_ms"),
|
||||
"stream_update_p99_ms": p99_field("update_chunks_ms"),
|
||||
"pending_drain_p99_ms": p99_field("pending_drain_ms"),
|
||||
"apply_p99_ms": p99_field("apply_ms"),
|
||||
"apply_max_ms": max_field("apply_ms"),
|
||||
"apply_budget_min_ms": min((f["apply_budget_ms"] for f in frames), default=None),
|
||||
"apply_budget_max_ms": max((f["apply_budget_ms"] for f in frames), default=None),
|
||||
"apply_time_budget_hit_frames": sum(f["time_budget_hit"] for f in frames),
|
||||
"apply_frames": sum(f["meshes_applied"] > 0 for f in frames),
|
||||
"apply_meshes_total": sum(f["meshes_applied"] for f in frames),
|
||||
"desired_rebuild_p99_ms": p99_field("desired_rebuild_ms"),
|
||||
"desired_rebuild_count": len(rebuild_frames),
|
||||
"desired_rebuilds_per_second": len(rebuild_frames) / 12.0,
|
||||
"center_crossings": center_crossings,
|
||||
"same_center_core_region_requests": same_center_core_requests,
|
||||
"core_desired_cache_hit_count": core_desired_cache_hits,
|
||||
"core_desired_cache_hit_share_pct": percent(
|
||||
core_desired_cache_hits, len(core_desired_queries)),
|
||||
"core_desired_query_nonzero_p50_ms": statistics.median(core_desired_query_ms)
|
||||
if core_desired_query_ms else None,
|
||||
"core_desired_query_nonzero_p95_ms": percentile(core_desired_query_ms, 0.95),
|
||||
"core_desired_query_nonzero_max_ms": max(core_desired_query_ms)
|
||||
if core_desired_query_ms else None,
|
||||
"desired_rebuild_nonzero_p50_ms": statistics.median(rebuild_costs)
|
||||
if rebuild_costs else None,
|
||||
"desired_rebuild_nonzero_p95_ms": percentile(rebuild_costs, 0.95),
|
||||
"desired_rebuild_nonzero_max_ms": max(rebuild_costs) if rebuild_costs else None,
|
||||
"near_submitted_by_direction": direction_submission_totals,
|
||||
"near_admission_first12": first_admission_order,
|
||||
"submit_tiles_p99_ms": p99_field("submit_tiles_ms"),
|
||||
"level_admit_p99_ms": p99_field("level_admit_ms"),
|
||||
"top_tick_frames": sorted(frames, key=lambda f: f["tick_ms"], reverse=True)[:5],
|
||||
"trace": str(trace.relative_to(ROOT)),
|
||||
"log": str(abslog.relative_to(ROOT)),
|
||||
}
|
||||
print("[L165Movement] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--smoke", action="store_true",
|
||||
help="run one incremental trace to validate readback and equality audits")
|
||||
parser.add_argument("--geometry-hash-check", action="store_true",
|
||||
help="compare UE per-tile geometry hashes for baseline and incremental desired sets")
|
||||
parser.add_argument("--controls-only", action="store_true",
|
||||
help="run a per-crossing equality audit and injected mismatch negative control")
|
||||
parser.add_argument("--summarize-existing", action="store_true",
|
||||
help="summarize the ten completed rows in runs-partial.json without rerunning UE")
|
||||
args = parser.parse_args()
|
||||
missing = [path for path in (EDITOR, HOST / "VFHost.uproject") if not path.exists()]
|
||||
if missing:
|
||||
raise SystemExit(f"missing UE runner input: {missing}")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
if args.geometry_hash_check:
|
||||
hash_dir = OUT / "geometry-hash"
|
||||
baseline_path = hash_dir / "assembler-world.json"
|
||||
candidate_path = hash_dir / "incremental.json"
|
||||
run("assembler_world", 99, baseline_path)
|
||||
run("incremental", 99, candidate_path)
|
||||
baseline_data = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||
candidate_data = json.loads(candidate_path.read_text(encoding="utf-8"))
|
||||
key_fields = ("level", "tile_x", "tile_y", "tile_z")
|
||||
|
||||
def indexed(data: dict) -> dict[tuple[int, ...], tuple]:
|
||||
result = {}
|
||||
for record in data.get("records", []):
|
||||
key = tuple(int(record[field]) for field in key_fields)
|
||||
result[key] = (str(record.get("geometry_hash", "")),
|
||||
int(record.get("triangle_count", -1)),
|
||||
bool(record.get("empty", False)))
|
||||
return result
|
||||
|
||||
baseline_hashes = indexed(baseline_data)
|
||||
candidate_hashes = indexed(candidate_data)
|
||||
common = baseline_hashes.keys() & candidate_hashes.keys()
|
||||
mismatches = [key for key in common
|
||||
if baseline_hashes[key] != candidate_hashes[key]]
|
||||
result = {
|
||||
"baseline_records": len(baseline_hashes),
|
||||
"candidate_records": len(candidate_hashes),
|
||||
"common_records": len(common),
|
||||
"hash_mismatches": len(mismatches),
|
||||
"baseline_only_records": len(baseline_hashes.keys() - candidate_hashes.keys()),
|
||||
"candidate_only_records": len(candidate_hashes.keys() - baseline_hashes.keys()),
|
||||
"baseline_dump": str(baseline_path.relative_to(ROOT)),
|
||||
"candidate_dump": str(candidate_path.relative_to(ROOT)),
|
||||
}
|
||||
(hash_dir / "comparison.json").write_text(
|
||||
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print("[L165GeometryHash] " + json.dumps(result, sort_keys=True), flush=True)
|
||||
return 0 if len(common) >= 100 and not mismatches else 1
|
||||
if args.smoke:
|
||||
run("incremental", 0)
|
||||
return 0
|
||||
if args.controls_only:
|
||||
control = dict(CONDITIONS["incremental"])
|
||||
control["audit_every"] = 1
|
||||
control["inject_mismatch"] = 0
|
||||
CONDITIONS["audit_every_1_control"] = control
|
||||
run("audit_every_1_control", 0)
|
||||
mismatch = dict(control)
|
||||
mismatch["inject_mismatch"] = 1
|
||||
CONDITIONS["injected_mismatch_control"] = mismatch
|
||||
run("injected_mismatch_control", 0, expected_mismatch=True)
|
||||
return 0
|
||||
partial = OUT / "runs-partial.json"
|
||||
if args.summarize_existing:
|
||||
if not partial.is_file():
|
||||
raise RuntimeError(f"no saved movement rows: {partial}")
|
||||
rows = json.loads(partial.read_text(encoding="utf-8"))
|
||||
if len(rows) != 10:
|
||||
raise RuntimeError(f"expected ten completed movement rows, found {len(rows)}")
|
||||
else:
|
||||
rows = []
|
||||
# Both conditions use this editor binary. Warm each path before measured pairs.
|
||||
run("assembler_world", 0)
|
||||
run("incremental", 0)
|
||||
for repetition in range(1, 6):
|
||||
for condition in ORDER[repetition]:
|
||||
row = run(condition, repetition)
|
||||
rows.append(row)
|
||||
partial.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
|
||||
metrics = (
|
||||
"near_ready_moving_mean_pct", "far_holes_moving_mean_pct",
|
||||
"game_thread_tick_p99_ms", "game_thread_tick_max_ms",
|
||||
"arrival_lod02_settled_t_s", "dwell_complete_after_stop_s",
|
||||
"near_ahead_desired_share_pct", "near_behind_desired_share_pct",
|
||||
"near_side_desired_share_pct", "near_forward_cone_desired_share_pct",
|
||||
"near_ahead_ready_pct", "near_behind_ready_pct",
|
||||
"near_forward_cone_ready_pct", "near_forward_cosine_weighted_ready_pct",
|
||||
"near_travel_forward_cone_ready_pct",
|
||||
"apply_p99_ms", "desired_rebuild_p99_ms", "desired_rebuild_count",
|
||||
"desired_rebuild_nonzero_p95_ms", "center_crossings",
|
||||
"same_center_core_region_requests", "level_admit_p99_ms",
|
||||
"core_desired_cache_hit_count", "core_desired_query_nonzero_p95_ms",
|
||||
)
|
||||
medians = {}
|
||||
for condition in CONDITIONS:
|
||||
medians[condition] = {}
|
||||
for metric in metrics:
|
||||
values = [row[metric] for row in rows
|
||||
if row["condition"] == condition and row[metric] is not None]
|
||||
medians[condition][metric] = statistics.median(values) if values else None
|
||||
pairs = []
|
||||
for repetition in range(1, 6):
|
||||
by_condition = {row["condition"]: row for row in rows
|
||||
if row["repetition"] == repetition}
|
||||
candidate = "incremental"
|
||||
pairs.append({
|
||||
"repetition": repetition,
|
||||
"candidate": candidate,
|
||||
**{f"{metric}_delta": by_condition[candidate][metric]
|
||||
- by_condition["assembler_world"][metric]
|
||||
for metric in metrics if by_condition[candidate][metric] is not None
|
||||
and by_condition["assembler_world"][metric] is not None},
|
||||
})
|
||||
by_pair = {
|
||||
repetition: {row["condition"]: row for row in rows if row["repetition"] == repetition}
|
||||
for repetition in range(1, 6)
|
||||
}
|
||||
candidate_rows = [row for row in rows if row["condition"] == "incremental"]
|
||||
baseline_rows = [row for row in rows if row["condition"] == "assembler_world"]
|
||||
candidate_gates = {
|
||||
"every_candidate_run_p99_le_11_ms": all(
|
||||
row["game_thread_tick_p99_ms"] <= 11.0 for row in candidate_rows),
|
||||
"paired_worst_tick_no_worse": all(
|
||||
by_pair[rep]["incremental"]["game_thread_tick_max_ms"]
|
||||
<= by_pair[rep]["assembler_world"]["game_thread_tick_max_ms"]
|
||||
for rep in by_pair),
|
||||
"paired_near_all_around_no_worse": all(
|
||||
by_pair[rep]["incremental"]["near_ready_moving_mean_pct"]
|
||||
>= by_pair[rep]["assembler_world"]["near_ready_moving_mean_pct"]
|
||||
for rep in by_pair),
|
||||
"paired_forward_cone_no_worse": all(
|
||||
by_pair[rep]["incremental"]["near_forward_cone_ready_pct"]
|
||||
>= by_pair[rep]["assembler_world"]["near_forward_cone_ready_pct"]
|
||||
for rep in by_pair),
|
||||
"paired_far_holes_no_worse": all(
|
||||
by_pair[rep]["incremental"]["far_holes_moving_mean_pct"]
|
||||
<= by_pair[rep]["assembler_world"]["far_holes_moving_mean_pct"]
|
||||
for rep in by_pair),
|
||||
"paired_arrival_no_worse": all(
|
||||
by_pair[rep]["incremental"]["arrival_lod02_settled_t_s"] is not None
|
||||
and by_pair[rep]["assembler_world"]["arrival_lod02_settled_t_s"] is not None
|
||||
and by_pair[rep]["incremental"]["arrival_lod02_settled_t_s"]
|
||||
<= by_pair[rep]["assembler_world"]["arrival_lod02_settled_t_s"]
|
||||
for rep in by_pair),
|
||||
"all_candidate_equality_audits_pass": all(
|
||||
row["incremental_desired_audit_count"] > 0
|
||||
and row["incremental_desired_audit_failures"] == 0
|
||||
for row in candidate_rows),
|
||||
"median_build_desired_p95_down": statistics.median(
|
||||
row["desired_rebuild_nonzero_p95_ms"] for row in candidate_rows)
|
||||
< statistics.median(row["desired_rebuild_nonzero_p95_ms"]
|
||||
for row in baseline_rows),
|
||||
}
|
||||
summary = {
|
||||
"source_commit": subprocess.run(
|
||||
["git", "-c", f"safe.directory={ROOT}", "rev-parse", "HEAD"],
|
||||
cwd=ROOT, check=True, capture_output=True, text=True).stdout.strip(),
|
||||
"protocol": "Five interleaved 30 m/s UE pairs; 12 s move plus 5 s dwell; 8 effective tile slots. L157 critical wait remains on and apply cap remains 0 ms.",
|
||||
"conditions": CONDITIONS,
|
||||
"movement_medians": medians,
|
||||
"paired_deltas_vs_assembler_world": pairs,
|
||||
"candidate_gates": candidate_gates,
|
||||
"runs": rows,
|
||||
}
|
||||
(OUT / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
print("[L165Summary] " + json.dumps({"movement_medians": medians,
|
||||
"candidate_gates": candidate_gates,
|
||||
"runs": len(rows)}, sort_keys=True), flush=True)
|
||||
return 0 if all(candidate_gates.values()) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,58 @@
|
||||
# L166 — Incremental streaming update
|
||||
|
||||
**Result:** Core and overlay candidate materialization now apply entering/leaving deltas, and same-center priority order updates merge only changed keys. Seven 30 m/s W8 pairs show lower median rebuild p95, tick p99, worst tick, near readiness, forward-cone readiness, and far holes. The strict pairwise bar is not fully clean: one worst-tick pair, three far-hole pairs, and four arrival pairs regressed slightly. Set/order audits and the fast tier pass.
|
||||
|
||||
## Count first
|
||||
|
||||
Stage costs below are milliseconds per desired rebuild, reported as the median of the seven runs' p50 and p95 values. Each run's window is its 12-second move at 30 m/s; there were 44–45 center crossings per run.
|
||||
|
||||
| Stage | `assembler/world` p50 / p95 | Incremental p50 / p95 |
|
||||
| --- | ---: | ---: |
|
||||
| Core query/preparation | 0.832 / 2.229 | 0.0002 / 0.142 |
|
||||
| Core crossing delta | — | 0.000 / 0.138 |
|
||||
| UE overlay materialization | 0.350 / 0.723 | 0.103 / 0.356 |
|
||||
| Priority sort/order | 0.306 / 0.656 | 0.038 / 0.437 |
|
||||
| Critical admission order | 0.063 / 0.134 | 0.065 / 0.222 |
|
||||
|
||||
The incremental Core path updated on 98.4% of desired rebuilds. Priority order merged key deltas on 52.2%, reused the cached order unchanged on 33.5%, and fully sorted on the remaining 14.3% (the center-crossing updates). LOD4+ crossings caused five full Core-query fallbacks per 12-second move; their median per-run maximum was 1.19 ms and their median total was 5.25 ms spread across those five events.
|
||||
|
||||
The median worst tick was 11.652 ms versus 12.980 ms for the baseline. The largest candidate worst-tick sample was 12.764 ms at a center crossing: its priority sort took 1.311 ms, overlay materialization 0.915 ms, and Core delta 0.601 ms; no LOD4+ fallback occurred on that tick. Priority sorting remained the largest measured substage in that worst tick. Fallbacks were not the source of the remaining worst ticks.
|
||||
|
||||
## Change and equality checks
|
||||
|
||||
`BuildCoreDesiredTiles` now retains the Core visual set, UE overlay set, candidate set, and priority order. It applies Core and overlay membership deltas to the persistent sets. Live anchor, gate, and support inputs are still walked into a temporary overlay union on each trigger. At an unchanged center, it removes leaving keys and merges sorted entering keys into the cached priority order. Center changes still require a full priority sort because the distance comparator changes. The critical-key promotion list is still rebuilt on each trigger.
|
||||
|
||||
The equality audit independently obtains a full Core desired set, rebuilds the overlay reference, and compares candidate and collision-only sets. A second audit builds the full prioritized and critical order and compares it with the incremental result. Across the seven measured candidate runs, all 28 sampled set audits and all 28 order audits passed. A positive materialization-control run passed 13 additional set and order audits. The injected mismatch control logged `missing=1` and failed with a nonzero UE exit (`-11`), as expected.
|
||||
|
||||
## Seven interleaved pairs
|
||||
|
||||
Each pair compared `assembler/world` at `9c70053` against this worktree, with a 12-second 30 m/s move, 5-second dwell, and eight effective tile slots. Values are medians across seven runs; paired gates are stated separately.
|
||||
|
||||
| Metric | `assembler/world` | Incremental | Paired result |
|
||||
| --- | ---: | ---: | --- |
|
||||
| `BuildDesiredTiles` p95 | 7.281 ms | 3.738 ms | 48.7% lower median |
|
||||
| Game-thread tick p99 | 10.521 ms | 7.420 ms | Lower in all 7 pairs; candidate range 7.031–7.526 ms |
|
||||
| Worst tick | 12.980 ms | 11.652 ms | Lower in 6/7 pairs; pair 5 was 0.347 ms higher |
|
||||
| Near LOD0–2 readiness | 41.995% | 43.668% | Better in all 7 pairs |
|
||||
| 55° forward-cone readiness | 14.779% | 16.116% | Better in all 7 pairs |
|
||||
| Far holes while moving | 3.197% | 2.739% | Lower median; better in 4/7 pairs |
|
||||
| First settled LOD0–2 arrival | 2.432 s | 2.439 s | Paired median delta +0.002 s; later in 4/7 pairs |
|
||||
|
||||
The paired far-hole deltas were −0.072, +0.144, −0.059, +0.301, −1.052, −0.773, and +0.069 percentage points. The L165 +0.291 pp result is within this observed spread; L166 also had one +0.301 pp pair, but the seven-pair median delta was −0.059 pp and four pairs improved. This is consistent with substantial run-to-run variation, not a consistent far-hole regression; it does not establish a strict no-regression guarantee.
|
||||
|
||||
Null-RHI produced no camera-forward samples, so the 55° cone uses the travel-direction fallback. The arrival measure also varied sharply between pairs, including two candidate runs that settled 0.22–0.26 s earlier. Its paired median was effectively flat, but the strict per-pair arrival gate did not pass.
|
||||
|
||||
## Checks and limits
|
||||
|
||||
- VFHost Linux Development build succeeded with `-NoUBA -MaxParallelActions=4`; `VFHost/PluginDeps/VoxelForge/Source` was restored to `/home/jahni/VoxelM/Plugins/VoxelForge/Source`.
|
||||
- `verify.py fast` passed: 268 checks, 0 failures, 5 diagnostics, 281.5 s. Clang/GCC W1/W8/W12 determinism and canonical geometry checks passed.
|
||||
- The live movement `TileHashDump` hook was enabled but wrote zero records for both conditions, so that direct runtime geometry comparison is inconclusive. No meshing or geometry-generation code changed; the fast-tier geometry hashes and sampled full-rebuild order audit passed.
|
||||
- `git diff --check` and runner `py_compile` passed. W12 movement timing, camera-forward cone capture, and separate movement runs on each of the four owner layers were not run.
|
||||
- The strict movement summary fails `paired_worst_tick_no_worse`, `paired_far_holes_no_worse`, and `paired_arrival_no_worse`; p99, near readiness, forward-cone readiness, audits, and median rebuild-p95 gates pass.
|
||||
|
||||
Changed Unreal `Source/` files:
|
||||
|
||||
- `Source/VoxelForge/Private/VoxelWorld.cpp`
|
||||
- `Source/VoxelForge/Public/VoxelWorld.h`
|
||||
|
||||
Logs, traces, summary, and verifier artifacts are under `Saved/Linux/l166/`.
|
||||
@@ -0,0 +1,685 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Interleaved L166 UE movement runs for incremental desired-set rebuilds."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
HOST = Path("/home/jahni/VFHost")
|
||||
EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd")
|
||||
OUT = ROOT / "Saved/Linux/l166/ue/movement"
|
||||
HOST_SAVED = HOST / "Saved/Linux/l166"
|
||||
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l166"
|
||||
SPAWN_M = (19.166015625, -14.2001953125, -753.3740234375)
|
||||
MOVE_START_S = 6.0
|
||||
|
||||
CONDITIONS = {
|
||||
"assembler_world": {
|
||||
"wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||
"forward_first": 0, "rebuild_on_region_completion": 1,
|
||||
"core_desired_cache": 0, "incremental_desired": 0,
|
||||
"audit_every": 0, "materialization_audit_every": 0, "inject_mismatch": 0,
|
||||
},
|
||||
"incremental": {
|
||||
"wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||
"forward_first": 0, "rebuild_on_region_completion": 1,
|
||||
"core_desired_cache": 0, "incremental_desired": 1,
|
||||
"audit_every": 10, "materialization_audit_every": 0, "inject_mismatch": 0,
|
||||
},
|
||||
}
|
||||
ORDER = {
|
||||
1: ("assembler_world", "incremental"),
|
||||
2: ("incremental", "assembler_world"),
|
||||
3: ("assembler_world", "incremental"),
|
||||
4: ("incremental", "assembler_world"),
|
||||
5: ("assembler_world", "incremental"),
|
||||
6: ("incremental", "assembler_world"),
|
||||
7: ("assembler_world", "incremental"),
|
||||
}
|
||||
|
||||
|
||||
def percentile(values: list[float], fraction: float = 0.99) -> float | None:
|
||||
if not values:
|
||||
return None
|
||||
ordered = sorted(values)
|
||||
return ordered[max(0, min(len(ordered) - 1, math.ceil(len(ordered) * fraction) - 1))]
|
||||
|
||||
|
||||
def read_events(path: Path) -> list[dict]:
|
||||
rows = []
|
||||
if path.is_file():
|
||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
if not line.startswith("{"):
|
||||
continue
|
||||
try:
|
||||
rows.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return rows
|
||||
|
||||
|
||||
def mean(values: list[float]) -> float | None:
|
||||
return statistics.mean(values) if values else None
|
||||
|
||||
|
||||
def desired_breakdown(events: list[dict], move_start_s: float, stop_s: float) -> dict:
|
||||
rows = [event for event in events
|
||||
if event.get("type") == "desired_rebuild_breakdown"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
keys = ("total_ms", "core_prepare_ms", "core_delta_ms", "fallback_ms",
|
||||
"overlay_materialize_ms", "priority_sort_ms", "critical_order_ms")
|
||||
result = {"rebuild_count": len(rows)}
|
||||
result["incremental_materialization_update_count"] = sum(
|
||||
int(event.get("materialization_update", 0)) != 0 for event in rows)
|
||||
result["priority_order_reuse_count"] = sum(
|
||||
int(event.get("priority_order_reused", 0)) != 0 for event in rows)
|
||||
result["priority_order_update_count"] = sum(
|
||||
int(event.get("priority_order_updated", 0)) != 0 for event in rows)
|
||||
result["incremental_materialization_update_share_pct"] = percent(
|
||||
result["incremental_materialization_update_count"], len(rows))
|
||||
result["priority_order_reuse_share_pct"] = percent(
|
||||
result["priority_order_reuse_count"], len(rows))
|
||||
result["priority_order_update_share_pct"] = percent(
|
||||
result["priority_order_update_count"], len(rows))
|
||||
for key in keys:
|
||||
values = [float(event.get(key, 0.0)) for event in rows]
|
||||
result[f"{key}_p50"] = statistics.median(values) if values else None
|
||||
result[f"{key}_p95"] = percentile(values, 0.95)
|
||||
result[f"{key}_max"] = max(values) if values else None
|
||||
fallback_rows = [event for event in events
|
||||
if event.get("type") == "core_desired_cache"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s
|
||||
and int(event.get("full_fallback", 0)) != 0]
|
||||
result["fallback_count"] = len(fallback_rows)
|
||||
result["lod4plus_fallback_count"] = sum(
|
||||
int(event.get("fallback_reason", 0)) == 1 for event in fallback_rows)
|
||||
result["other_fallback_count"] = sum(
|
||||
int(event.get("fallback_reason", 0)) == 2 for event in fallback_rows)
|
||||
result["fallback_cost_sum_ms"] = sum(
|
||||
float(event.get("fallback_ms", 0.0)) for event in fallback_rows)
|
||||
result["fallback_cost_max_ms"] = max(
|
||||
(float(event.get("fallback_ms", 0.0)) for event in fallback_rows), default=None)
|
||||
return result
|
||||
|
||||
|
||||
def percent(numerator: int | float, denominator: int | float) -> float | None:
|
||||
return 100.0 * numerator / denominator if denominator > 0 else None
|
||||
|
||||
|
||||
def run(condition: str, repetition: int, tile_hash_dump: Path | None = None,
|
||||
expected_mismatch: bool = False) -> dict:
|
||||
config = CONDITIONS[condition]
|
||||
run_name = f"fast-r{repetition}"
|
||||
out = OUT / condition
|
||||
host_out = HOST_PLUGIN_SAVED / condition / "output"
|
||||
userdir = HOST_SAVED / "user" / condition / run_name
|
||||
zendata = HOST_SAVED / "zen" / condition / run_name
|
||||
ddc = HOST_SAVED / "DDC"
|
||||
trace = out / f"{run_name}.trace.jsonl"
|
||||
abslog = out / f"{run_name}.log"
|
||||
stdout = out / f"{run_name}.stdout.log"
|
||||
export = host_out / run_name
|
||||
for directory in (out, export, userdir, zendata, ddc):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
trace.unlink(missing_ok=True)
|
||||
|
||||
spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWN_M)
|
||||
command = [
|
||||
str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended",
|
||||
"-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch",
|
||||
"-notraceserver", "-nocrashreports", "-DisablePlugins=AndroidFileServer",
|
||||
"-ddc=NoZenLocalFallback", f"-userdir={userdir}", f"-ZenDataPath={zendata}",
|
||||
f"-LocalDataCachePath={ddc}", "-run=VoxelForgeExplore", "-seed=0",
|
||||
"-archetype=TunnelNetwork", "-slot=0", "-modes=export", "-gameconfig=1",
|
||||
"-coreworldtest=1", "-ownerfourstrateplan=1", "-corevoxelsizecm=25",
|
||||
"-coreislandtest=0", "-corefallbacktest=0", "-coreofftest=0",
|
||||
"-coreplantest=0", "-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings",
|
||||
"-corehybridmesher=0", f"-out={export}", "-voxel.StreamingCoarseFirst=1",
|
||||
"-voxel.StreamingDynamicNearReserve=1",
|
||||
"-voxel.StreamingLegacyPendingResultCap=0",
|
||||
"-voxel.CoreRegionConcurrentAdmission=1",
|
||||
"-voxel.StreamingResultBacklogCap=256", "-voxel.StreamingReadyQueueDepth=8",
|
||||
"-voxel.StreamingBackgroundWorkerCap=1", "-voxel.CoreColumnCacheDirectMapped=0",
|
||||
"-voxel.StreamingFarApplySupplement=1",
|
||||
f"-voxel.StreamingFastTravelWaitForAllCriticalTiles={config['wait_for_critical']}",
|
||||
f"-voxel.StreamingFastTravelApplyBudgetMilliseconds={config['apply_budget_ms']:.1f}",
|
||||
f"-voxel.StreamingFastTravelForwardFirst={config['forward_first']}",
|
||||
f"-voxel.StreamingRebuildOnCoreRegionCompletion={config['rebuild_on_region_completion']}",
|
||||
f"-voxel.StreamingCoreDesiredCacheByCenter={config['core_desired_cache']}",
|
||||
f"-voxel.StreamingIncrementalDesiredSet={config['incremental_desired']}",
|
||||
f"-voxel.StreamingIncrementalDesiredAuditEvery={config['audit_every']}",
|
||||
f"-voxel.StreamingIncrementalDesiredMaterializationAuditEvery={config['materialization_audit_every']}",
|
||||
f"-voxel.StreamingIncrementalDesiredAuditInjectMismatch={config['inject_mismatch']}",
|
||||
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
|
||||
"-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=3000.0",
|
||||
f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}",
|
||||
"-voxel.TestMoveDurationSeconds=12.0", "-voxel.TestExitSeconds=0.0",
|
||||
"-voxel.TestMoveDirection=1,0,0", "-voxel.StreamingSchedulerPolicy=3",
|
||||
"-voxel.TestExitOnSteady=0", "-coreworldstreamtrace=1",
|
||||
f"-voxel.StartupTraceSpawnCm={spawn_cm}",
|
||||
"-voxel.StartupTraceUntilEndPlay=1", "-voxel.StartupTraceThroughCrossing=1",
|
||||
"-voxel.StartupTraceMaxTiles=16384", f"-voxel.StartupTraceFile={trace}",
|
||||
f"-abslog={abslog}",
|
||||
]
|
||||
if tile_hash_dump is not None:
|
||||
tile_hash_dump.parent.mkdir(parents=True, exist_ok=True)
|
||||
tile_hash_dump.unlink(missing_ok=True)
|
||||
command.append(f"-voxel.TileHashDump={tile_hash_dump.resolve()}")
|
||||
wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command]
|
||||
started = time.monotonic()
|
||||
with stdout.open("w", encoding="utf-8") as stream:
|
||||
process = subprocess.run(
|
||||
wrapped, cwd=ROOT, stdout=stream, stderr=subprocess.STDOUT,
|
||||
check=False, timeout=1740)
|
||||
|
||||
events = read_events(trace)
|
||||
if expected_mismatch:
|
||||
diagnostic = "VoxelForgeIncrementalDesiredAudit"
|
||||
logs = "\n".join(path.read_text(encoding="utf-8", errors="replace")
|
||||
for path in (abslog, stdout) if path.is_file())
|
||||
audit_events = [event for event in events
|
||||
if event.get("type") in {"incremental_desired_audit",
|
||||
"incremental_desired_order_audit"}]
|
||||
failed_audit = any(int(event.get("pass", 1)) == 0 for event in audit_events)
|
||||
failed_audit = failed_audit or (
|
||||
diagnostic in logs and "mismatch" in logs.lower())
|
||||
if process.returncode == 0 or diagnostic not in logs or "mismatch" not in logs.lower():
|
||||
raise RuntimeError(
|
||||
f"injected mismatch did not fail as expected: exit={process.returncode}; "
|
||||
f"audit_events={len(audit_events)}; log={abslog}")
|
||||
print("[L166MismatchControl] " + json.dumps({
|
||||
"exit_code": process.returncode,
|
||||
"diagnostic_seen": True,
|
||||
"failed_audit_event_seen": failed_audit,
|
||||
"trace": str(trace.relative_to(ROOT)),
|
||||
"log": str(abslog.relative_to(ROOT)),
|
||||
}, sort_keys=True), flush=True)
|
||||
return {"condition": condition, "exit_code": process.returncode,
|
||||
"failed_audit_event_seen": failed_audit}
|
||||
samples = [event for event in events if event.get("type") == "streaming_sample"]
|
||||
moving = [event for event in samples if float(event.get("speed_cm_s", 0.0)) >= 50.0]
|
||||
stop_event = next((event for event in events
|
||||
if event.get("type") == "streaming_test_move_stopped"), None)
|
||||
backend = next((event for event in events if event.get("type") == "worker_backend_config"), {})
|
||||
scheduler_config = next((event for event in events
|
||||
if event.get("type") == "scheduler_admission"), {})
|
||||
worker_cap = next((event for event in events
|
||||
if event.get("type") == "streaming_worker_cap_readback"), {})
|
||||
if stop_event is None:
|
||||
raise RuntimeError(f"missing move-stop event: {trace}")
|
||||
stop_s = float(stop_event.get("t_s", 0.0))
|
||||
move_start_s = min((float(event.get("t_s", 0.0)) for event in moving), default=float("inf"))
|
||||
dwell = [event for event in samples if float(event.get("t_s", 0.0)) >= stop_s
|
||||
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||
arrival = [event for event in samples if float(event.get("t_s", 0.0)) < move_start_s
|
||||
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||
desired_stage_breakdown = desired_breakdown(events, move_start_s, stop_s)
|
||||
if process.returncode or len(moving) < 40 or len(dwell) < 16:
|
||||
raise RuntimeError(f"incomplete {condition} run: {trace}; exit={process.returncode}; "
|
||||
f"move={len(moving)} dwell={len(dwell)}")
|
||||
if (int(backend.get("streaming_fast_travel_wait_for_all_critical_tiles", -1))
|
||||
!= config["wait_for_critical"]
|
||||
or abs(float(backend.get("streaming_fast_travel_apply_budget_ms", -1.0))
|
||||
- config["apply_budget_ms"]) > 0.01
|
||||
or int(worker_cap.get("streaming_background_worker_cap", -1)) != 1
|
||||
or int(backend.get("effective_tile_slots", -1)) != 8
|
||||
or int(backend.get("streaming_far_apply_supplement", -1)) != 1
|
||||
or int(scheduler_config.get("forward_first", -1)) != config["forward_first"]
|
||||
or int(scheduler_config.get("rebuild_on_core_region_completion", -1))
|
||||
!= config["rebuild_on_region_completion"]
|
||||
or int(scheduler_config.get("core_desired_cache_by_center", -1))
|
||||
!= config["core_desired_cache"]
|
||||
or int(scheduler_config.get("incremental_desired_set", -1))
|
||||
!= config["incremental_desired"]
|
||||
or int(scheduler_config.get("incremental_desired_audit_every", -1))
|
||||
!= config["audit_every"]
|
||||
or int(scheduler_config.get("incremental_desired_materialization_audit_every", -1))
|
||||
!= config["materialization_audit_every"]
|
||||
or int(scheduler_config.get("incremental_desired_audit_inject_mismatch", -1))
|
||||
!= config["inject_mismatch"]):
|
||||
raise RuntimeError(f"CVar/worker readback mismatch: {backend}; {worker_cap}; {scheduler_config}")
|
||||
|
||||
moving_metrics = []
|
||||
for event in moving:
|
||||
desired = event.get("desired_by_lod", [0] * 9)
|
||||
resident = event.get("resident_by_lod", [0] * 9)
|
||||
near_desired = sum(desired[:3])
|
||||
cone_desired = int(event.get("near_forward_cone_desired", 0))
|
||||
view_cone_desired = int(event.get("near_view_forward_cone_desired", 0))
|
||||
view_valid = bool(event.get("near_view_forward_cone_valid", 0))
|
||||
weighted_desired = float(event.get("near_forward_weighted_desired", 0.0))
|
||||
travel_cone_ready = percent(
|
||||
int(event.get("near_forward_cone_resident", 0)), cone_desired)
|
||||
view_cone_ready = percent(
|
||||
int(event.get("near_view_forward_cone_resident", 0)), view_cone_desired)
|
||||
moving_metrics.append({
|
||||
"near_ready_pct": percent(sum(resident[:3]), near_desired) or 0.0,
|
||||
"lod0_ready_pct": percent(resident[0], desired[0]) or 0.0,
|
||||
"lod1_ready_pct": percent(resident[1], desired[1]) or 0.0,
|
||||
"lod2_ready_pct": percent(resident[2], desired[2]) or 0.0,
|
||||
"far_hole_pct": percent(
|
||||
sum(desired[3:]) - sum(event.get("covered_by_lod", resident)[3:]),
|
||||
sum(desired[3:])) or 0.0,
|
||||
"ahead_share_pct": percent(int(event.get("near_ahead_desired", 0)), near_desired),
|
||||
"behind_share_pct": percent(int(event.get("near_behind_desired", 0)), near_desired),
|
||||
"side_share_pct": 100.0 - (
|
||||
percent(int(event.get("near_ahead_desired", 0)), near_desired) or 0.0
|
||||
) - (
|
||||
percent(int(event.get("near_behind_desired", 0)), near_desired) or 0.0
|
||||
),
|
||||
"ahead_ready_pct": percent(
|
||||
int(event.get("near_ahead_resident", 0)),
|
||||
int(event.get("near_ahead_desired", 0))),
|
||||
"behind_ready_pct": percent(
|
||||
int(event.get("near_behind_resident", 0)),
|
||||
int(event.get("near_behind_desired", 0))),
|
||||
"travel_forward_cone_ready_pct": travel_cone_ready,
|
||||
"view_forward_cone_ready_pct": view_cone_ready,
|
||||
"forward_cone_ready_pct": view_cone_ready if view_valid else travel_cone_ready,
|
||||
"forward_cone_source": "camera" if view_valid else "travel_fallback",
|
||||
"forward_cone_share_pct": percent(cone_desired, near_desired),
|
||||
"forward_weighted_ready_pct": percent(
|
||||
float(event.get("near_forward_weighted_resident", 0.0)), weighted_desired),
|
||||
})
|
||||
|
||||
arrival_near_settled = next((event for event in arrival
|
||||
if sum(event.get("desired_by_lod", [0] * 9)[:3]) > 0
|
||||
and sum(event.get("settled_by_lod", [0] * 9)[:3])
|
||||
>= sum(event.get("desired_by_lod", [0] * 9)[:3])), None)
|
||||
dwell_complete = next((event for event in dwell
|
||||
if sum(event.get("resident_by_lod", [0] * 9)[:3])
|
||||
>= sum(event.get("desired_by_lod", [0] * 9)[:3])
|
||||
and sum(event.get("desired_by_lod", [0] * 9)[3:])
|
||||
<= sum(event.get("covered_by_lod", [0] * 9)[3:])), None)
|
||||
|
||||
work = [event for event in events if event.get("type") == "game_thread_work_frame"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
applies = {event.get("frame"): event for event in events
|
||||
if event.get("type") == "apply_frame"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||
scheduler = {event.get("frame"): event for event in events
|
||||
if event.get("type") == "scheduler_admission"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||
frames = []
|
||||
for event in work:
|
||||
apply = applies.get(event.get("frame"), {})
|
||||
admit = scheduler.get(event.get("frame"), {})
|
||||
frames.append({
|
||||
"frame": event.get("frame"),
|
||||
"tick_ms": float(event.get("tick_ms", 0.0)),
|
||||
"update_chunks_ms": float(event.get("update_chunks_ms", 0.0)),
|
||||
"pending_drain_ms": float(event.get("pending_drain_ms", 0.0)),
|
||||
"apply_ms": float(apply.get("apply_ms", 0.0)),
|
||||
"apply_budget_ms": float(apply.get("apply_budget_ms", 0.0)),
|
||||
"meshes_applied": int(apply.get("meshes_applied", 0)),
|
||||
"time_budget_hit": int(apply.get("time_limit_reached", 0)),
|
||||
"desired_rebuild_ms": float(admit.get("desired_rebuild_ms", 0.0)),
|
||||
"desired_rebuilt": int(admit.get("desired_rebuilt", 0)),
|
||||
"submit_tiles_ms": float(admit.get("submit_tiles_ms", 0.0)),
|
||||
"level_admit_ms": float(admit.get("level_admit_ms", 0.0)),
|
||||
})
|
||||
p99_field = lambda key: percentile([float(row[key]) for row in frames])
|
||||
max_field = lambda key: max((float(row[key]) for row in frames), default=None)
|
||||
rebuild_frames = [row for row in frames if row["desired_rebuilt"]]
|
||||
rebuild_costs = [float(row["desired_rebuild_ms"]) for row in rebuild_frames]
|
||||
moving_scheduler = [event for event in scheduler.values()]
|
||||
center_crossings = 0
|
||||
previous_center = None
|
||||
for event in sorted(moving_scheduler, key=lambda item: int(item.get("frame", 0))):
|
||||
center = tuple(event.get("center", ()))
|
||||
if previous_center is not None and center != previous_center:
|
||||
center_crossings += 1
|
||||
previous_center = center
|
||||
core_requests = [event for event in events
|
||||
if event.get("type") == "core_region_compile_request"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
same_center_core_requests = sum(
|
||||
event.get("reason") == "desired_set_changed_same_center" for event in core_requests)
|
||||
core_desired_queries = [event for event in events
|
||||
if event.get("type") == "core_desired_cache"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
core_desired_query_ms = [float(event.get("query_ms", 0.0))
|
||||
for event in core_desired_queries if float(event.get("query_ms", 0.0)) > 0.0]
|
||||
core_desired_cache_hits = sum(int(event.get("hit", 0)) for event in core_desired_queries)
|
||||
incremental_audits = [event for event in events
|
||||
if event.get("type") == "incremental_desired_audit"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
incremental_audit_failures = sum(int(event.get("pass", 1)) == 0
|
||||
for event in incremental_audits)
|
||||
order_audits = [event for event in events
|
||||
if event.get("type") == "incremental_desired_order_audit"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
order_audit_failures = sum(int(event.get("pass", 1)) == 0 for event in order_audits)
|
||||
materialization_audits = [event for event in incremental_audits
|
||||
if int(event.get("materialization_audit", 0)) != 0]
|
||||
if config["incremental_desired"] and (
|
||||
not incremental_audits or incremental_audit_failures
|
||||
or not order_audits or order_audit_failures
|
||||
or (config["materialization_audit_every"] > 0
|
||||
and not materialization_audits)):
|
||||
raise RuntimeError(
|
||||
f"incremental desired equality audit missing/failed: {trace}; "
|
||||
f"set={len(incremental_audits)}/{incremental_audit_failures} "
|
||||
f"order={len(order_audits)}/{order_audit_failures} "
|
||||
f"materialization={len(materialization_audits)}")
|
||||
direction_submissions = [event.get("near_submitted_by_direction", [0, 0, 0, 0])
|
||||
for event in moving_scheduler]
|
||||
direction_submissions = [counts for counts in direction_submissions
|
||||
if isinstance(counts, list) and len(counts) == 4]
|
||||
direction_submission_totals = [
|
||||
sum(int(counts[index]) for counts in direction_submissions) for index in range(4)
|
||||
]
|
||||
first_admission_order = next((event.get("near_admission_order_first", [])
|
||||
for event in moving_scheduler
|
||||
if event.get("near_admission_order_first")), [])
|
||||
camera_samples = sum(bool(event.get("near_view_forward_cone_valid", 0)) for event in moving)
|
||||
dwell_complete_s = (float(dwell_complete.get("t_s", 0.0)) - stop_s
|
||||
if dwell_complete else None)
|
||||
|
||||
row = {
|
||||
"condition": condition,
|
||||
"repetition": repetition,
|
||||
"speed_mps": 30.0,
|
||||
"wait_for_all_critical_tiles": config["wait_for_critical"],
|
||||
"fast_travel_apply_budget_ms": config["apply_budget_ms"],
|
||||
"forward_first": config["forward_first"],
|
||||
"rebuild_on_core_region_completion": config["rebuild_on_region_completion"],
|
||||
"core_desired_cache_by_center": config["core_desired_cache"],
|
||||
"incremental_desired_set": config["incremental_desired"],
|
||||
"incremental_desired_audit_every": config["audit_every"],
|
||||
"incremental_desired_materialization_audit_every": config["materialization_audit_every"],
|
||||
"incremental_desired_audit_count": len(incremental_audits),
|
||||
"incremental_desired_audit_failures": incremental_audit_failures,
|
||||
"incremental_desired_order_audit_count": len(order_audits),
|
||||
"incremental_desired_order_audit_failures": order_audit_failures,
|
||||
"incremental_desired_materialization_audit_count": len(materialization_audits),
|
||||
"effective_tile_slots": backend.get("effective_tile_slots"),
|
||||
"exit_code": process.returncode,
|
||||
"wall_s": round(time.monotonic() - started, 3),
|
||||
"moving_samples": len(moving),
|
||||
"dwell_samples": len(dwell),
|
||||
"arrival_samples": len(arrival),
|
||||
"near_ready_moving_mean_pct": mean([m["near_ready_pct"] for m in moving_metrics]),
|
||||
"lod0_ready_moving_mean_pct": mean([m["lod0_ready_pct"] for m in moving_metrics]),
|
||||
"lod1_ready_moving_mean_pct": mean([m["lod1_ready_pct"] for m in moving_metrics]),
|
||||
"lod2_ready_moving_mean_pct": mean([m["lod2_ready_pct"] for m in moving_metrics]),
|
||||
"far_holes_moving_mean_pct": mean([m["far_hole_pct"] for m in moving_metrics]),
|
||||
"near_ahead_desired_share_pct": mean([m["ahead_share_pct"] for m in moving_metrics
|
||||
if m["ahead_share_pct"] is not None]),
|
||||
"near_behind_desired_share_pct": mean([m["behind_share_pct"] for m in moving_metrics
|
||||
if m["behind_share_pct"] is not None]),
|
||||
"near_side_desired_share_pct": mean([m["side_share_pct"] for m in moving_metrics]),
|
||||
"near_ahead_ready_pct": mean([m["ahead_ready_pct"] for m in moving_metrics
|
||||
if m["ahead_ready_pct"] is not None]),
|
||||
"near_behind_ready_pct": mean([m["behind_ready_pct"] for m in moving_metrics
|
||||
if m["behind_ready_pct"] is not None]),
|
||||
"near_forward_cone_half_angle_deg": 55,
|
||||
"near_forward_cone_desired_share_pct": mean([m["forward_cone_share_pct"]
|
||||
for m in moving_metrics
|
||||
if m["forward_cone_share_pct"] is not None]),
|
||||
"near_forward_cone_ready_pct": mean([m["forward_cone_ready_pct"]
|
||||
for m in moving_metrics
|
||||
if m["forward_cone_ready_pct"] is not None]),
|
||||
"near_travel_forward_cone_ready_pct": mean(
|
||||
[m["travel_forward_cone_ready_pct"] for m in moving_metrics
|
||||
if m["travel_forward_cone_ready_pct"] is not None]),
|
||||
"near_view_forward_cone_ready_pct": mean(
|
||||
[m["view_forward_cone_ready_pct"] for m in moving_metrics
|
||||
if m["view_forward_cone_ready_pct"] is not None]),
|
||||
"camera_forward_samples": camera_samples,
|
||||
"camera_forward_sample_share_pct": percent(camera_samples, len(moving)),
|
||||
"near_forward_cosine_weighted_ready_pct": mean(
|
||||
[m["forward_weighted_ready_pct"] for m in moving_metrics
|
||||
if m["forward_weighted_ready_pct"] is not None]),
|
||||
"arrival_lod02_settled_t_s": (
|
||||
float(arrival_near_settled.get("t_s", 0.0)) if arrival_near_settled else None),
|
||||
"near_ready_dwell_end_pct": (100.0 * sum(dwell[-1].get("resident_by_lod", [0] * 9)[:3])
|
||||
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[:3]))),
|
||||
"far_holes_dwell_end_pct": (100.0 * (
|
||||
sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:])
|
||||
- sum(dwell[-1].get("covered_by_lod", [0] * 9)[3:]))
|
||||
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:]))),
|
||||
"dwell_complete_after_stop_s": dwell_complete_s,
|
||||
"game_thread_tick_p99_ms": p99_field("tick_ms"),
|
||||
"game_thread_tick_max_ms": max_field("tick_ms"),
|
||||
"stream_update_p99_ms": p99_field("update_chunks_ms"),
|
||||
"pending_drain_p99_ms": p99_field("pending_drain_ms"),
|
||||
"apply_p99_ms": p99_field("apply_ms"),
|
||||
"apply_max_ms": max_field("apply_ms"),
|
||||
"apply_budget_min_ms": min((f["apply_budget_ms"] for f in frames), default=None),
|
||||
"apply_budget_max_ms": max((f["apply_budget_ms"] for f in frames), default=None),
|
||||
"apply_time_budget_hit_frames": sum(f["time_budget_hit"] for f in frames),
|
||||
"apply_frames": sum(f["meshes_applied"] > 0 for f in frames),
|
||||
"apply_meshes_total": sum(f["meshes_applied"] for f in frames),
|
||||
"desired_rebuild_p99_ms": p99_field("desired_rebuild_ms"),
|
||||
"desired_rebuild_count": len(rebuild_frames),
|
||||
"desired_rebuilds_per_second": len(rebuild_frames) / 12.0,
|
||||
"center_crossings": center_crossings,
|
||||
"same_center_core_region_requests": same_center_core_requests,
|
||||
"core_desired_cache_hit_count": core_desired_cache_hits,
|
||||
"core_desired_cache_hit_share_pct": percent(
|
||||
core_desired_cache_hits, len(core_desired_queries)),
|
||||
"core_desired_query_nonzero_p50_ms": statistics.median(core_desired_query_ms)
|
||||
if core_desired_query_ms else None,
|
||||
"core_desired_query_nonzero_p95_ms": percentile(core_desired_query_ms, 0.95),
|
||||
"core_desired_query_nonzero_max_ms": max(core_desired_query_ms)
|
||||
if core_desired_query_ms else None,
|
||||
"desired_rebuild_nonzero_p50_ms": statistics.median(rebuild_costs)
|
||||
if rebuild_costs else None,
|
||||
"desired_rebuild_nonzero_p95_ms": percentile(rebuild_costs, 0.95),
|
||||
"desired_rebuild_nonzero_max_ms": max(rebuild_costs) if rebuild_costs else None,
|
||||
"desired_breakdown": desired_stage_breakdown,
|
||||
"near_submitted_by_direction": direction_submission_totals,
|
||||
"near_admission_first12": first_admission_order,
|
||||
"submit_tiles_p99_ms": p99_field("submit_tiles_ms"),
|
||||
"level_admit_p99_ms": p99_field("level_admit_ms"),
|
||||
"top_tick_frames": sorted(frames, key=lambda f: f["tick_ms"], reverse=True)[:5],
|
||||
"trace": str(trace.relative_to(ROOT)),
|
||||
"log": str(abslog.relative_to(ROOT)),
|
||||
}
|
||||
print("[L166Movement] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--smoke", action="store_true",
|
||||
help="run one incremental trace to validate readback and equality audits")
|
||||
parser.add_argument("--count-only", action="store_true",
|
||||
help="run one baseline/candidate pair for desired-stage cost attribution")
|
||||
parser.add_argument("--geometry-hash-check", action="store_true",
|
||||
help="compare UE per-tile geometry hashes for baseline and incremental desired sets")
|
||||
parser.add_argument("--controls-only", action="store_true",
|
||||
help="run a per-crossing equality audit and injected mismatch negative control")
|
||||
parser.add_argument("--summarize-existing", action="store_true",
|
||||
help="summarize the fourteen completed rows in runs-partial.json without rerunning UE")
|
||||
args = parser.parse_args()
|
||||
missing = [path for path in (EDITOR, HOST / "VFHost.uproject") if not path.exists()]
|
||||
if missing:
|
||||
raise SystemExit(f"missing UE runner input: {missing}")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
if args.geometry_hash_check:
|
||||
hash_dir = OUT / "geometry-hash"
|
||||
baseline_path = hash_dir / "assembler-world.json"
|
||||
candidate_path = hash_dir / "incremental.json"
|
||||
run("assembler_world", 99, baseline_path)
|
||||
run("incremental", 99, candidate_path)
|
||||
baseline_data = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||
candidate_data = json.loads(candidate_path.read_text(encoding="utf-8"))
|
||||
key_fields = ("level", "tile_x", "tile_y", "tile_z")
|
||||
|
||||
def indexed(data: dict) -> dict[tuple[int, ...], tuple]:
|
||||
result = {}
|
||||
for record in data.get("records", []):
|
||||
key = tuple(int(record[field]) for field in key_fields)
|
||||
result[key] = (str(record.get("geometry_hash", "")),
|
||||
int(record.get("triangle_count", -1)),
|
||||
bool(record.get("empty", False)))
|
||||
return result
|
||||
|
||||
baseline_hashes = indexed(baseline_data)
|
||||
candidate_hashes = indexed(candidate_data)
|
||||
common = baseline_hashes.keys() & candidate_hashes.keys()
|
||||
mismatches = [key for key in common
|
||||
if baseline_hashes[key] != candidate_hashes[key]]
|
||||
result = {
|
||||
"baseline_records": len(baseline_hashes),
|
||||
"candidate_records": len(candidate_hashes),
|
||||
"common_records": len(common),
|
||||
"hash_mismatches": len(mismatches),
|
||||
"baseline_only_records": len(baseline_hashes.keys() - candidate_hashes.keys()),
|
||||
"candidate_only_records": len(candidate_hashes.keys() - baseline_hashes.keys()),
|
||||
"baseline_dump": str(baseline_path.relative_to(ROOT)),
|
||||
"candidate_dump": str(candidate_path.relative_to(ROOT)),
|
||||
}
|
||||
(hash_dir / "comparison.json").write_text(
|
||||
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print("[L166GeometryHash] " + json.dumps(result, sort_keys=True), flush=True)
|
||||
return 0 if len(common) >= 100 and not mismatches else 1
|
||||
if args.count_only:
|
||||
run("assembler_world", 99)
|
||||
run("incremental", 99)
|
||||
return 0
|
||||
if args.smoke:
|
||||
run("incremental", 0)
|
||||
return 0
|
||||
if args.controls_only:
|
||||
control = dict(CONDITIONS["incremental"])
|
||||
control["audit_every"] = 0
|
||||
control["materialization_audit_every"] = 25
|
||||
control["inject_mismatch"] = 0
|
||||
CONDITIONS["materialization_audit_control"] = control
|
||||
run("materialization_audit_control", 0)
|
||||
mismatch = dict(control)
|
||||
mismatch["materialization_audit_every"] = 1
|
||||
mismatch["inject_mismatch"] = 1
|
||||
CONDITIONS["injected_materialization_mismatch_control"] = mismatch
|
||||
run("injected_materialization_mismatch_control", 0, expected_mismatch=True)
|
||||
return 0
|
||||
partial = OUT / "runs-partial.json"
|
||||
if args.summarize_existing:
|
||||
if not partial.is_file():
|
||||
raise RuntimeError(f"no saved movement rows: {partial}")
|
||||
rows = json.loads(partial.read_text(encoding="utf-8"))
|
||||
if len(rows) != 14:
|
||||
raise RuntimeError(f"expected fourteen completed movement rows, found {len(rows)}")
|
||||
else:
|
||||
rows = []
|
||||
# Both conditions use this editor binary. Warm each path before measured pairs.
|
||||
run("assembler_world", 0)
|
||||
run("incremental", 0)
|
||||
for repetition in range(1, 8):
|
||||
for condition in ORDER[repetition]:
|
||||
row = run(condition, repetition)
|
||||
rows.append(row)
|
||||
partial.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
|
||||
metrics = (
|
||||
"near_ready_moving_mean_pct", "far_holes_moving_mean_pct",
|
||||
"game_thread_tick_p99_ms", "game_thread_tick_max_ms",
|
||||
"arrival_lod02_settled_t_s", "dwell_complete_after_stop_s",
|
||||
"near_ahead_desired_share_pct", "near_behind_desired_share_pct",
|
||||
"near_side_desired_share_pct", "near_forward_cone_desired_share_pct",
|
||||
"near_ahead_ready_pct", "near_behind_ready_pct",
|
||||
"near_forward_cone_ready_pct", "near_forward_cosine_weighted_ready_pct",
|
||||
"near_travel_forward_cone_ready_pct",
|
||||
"apply_p99_ms", "desired_rebuild_p99_ms", "desired_rebuild_count",
|
||||
"desired_rebuild_nonzero_p95_ms", "center_crossings",
|
||||
"same_center_core_region_requests", "level_admit_p99_ms",
|
||||
"core_desired_cache_hit_count", "core_desired_query_nonzero_p95_ms",
|
||||
)
|
||||
medians = {}
|
||||
for condition in CONDITIONS:
|
||||
medians[condition] = {}
|
||||
for metric in metrics:
|
||||
values = [row[metric] for row in rows
|
||||
if row["condition"] == condition and row[metric] is not None]
|
||||
medians[condition][metric] = statistics.median(values) if values else None
|
||||
pairs = []
|
||||
for repetition in range(1, 8):
|
||||
by_condition = {row["condition"]: row for row in rows
|
||||
if row["repetition"] == repetition}
|
||||
candidate = "incremental"
|
||||
pairs.append({
|
||||
"repetition": repetition,
|
||||
"candidate": candidate,
|
||||
**{f"{metric}_delta": by_condition[candidate][metric]
|
||||
- by_condition["assembler_world"][metric]
|
||||
for metric in metrics if by_condition[candidate][metric] is not None
|
||||
and by_condition["assembler_world"][metric] is not None},
|
||||
})
|
||||
by_pair = {
|
||||
repetition: {row["condition"]: row for row in rows if row["repetition"] == repetition}
|
||||
for repetition in range(1, 8)
|
||||
}
|
||||
candidate_rows = [row for row in rows if row["condition"] == "incremental"]
|
||||
baseline_rows = [row for row in rows if row["condition"] == "assembler_world"]
|
||||
candidate_gates = {
|
||||
"every_candidate_run_p99_le_11_ms": all(
|
||||
row["game_thread_tick_p99_ms"] <= 11.0 for row in candidate_rows),
|
||||
"paired_p99_no_worse": all(
|
||||
by_pair[rep]["incremental"]["game_thread_tick_p99_ms"]
|
||||
<= by_pair[rep]["assembler_world"]["game_thread_tick_p99_ms"]
|
||||
for rep in by_pair),
|
||||
"paired_worst_tick_no_worse": all(
|
||||
by_pair[rep]["incremental"]["game_thread_tick_max_ms"]
|
||||
<= by_pair[rep]["assembler_world"]["game_thread_tick_max_ms"]
|
||||
for rep in by_pair),
|
||||
"paired_near_all_around_no_worse": all(
|
||||
by_pair[rep]["incremental"]["near_ready_moving_mean_pct"]
|
||||
>= by_pair[rep]["assembler_world"]["near_ready_moving_mean_pct"]
|
||||
for rep in by_pair),
|
||||
"paired_forward_cone_no_worse": all(
|
||||
by_pair[rep]["incremental"]["near_forward_cone_ready_pct"]
|
||||
>= by_pair[rep]["assembler_world"]["near_forward_cone_ready_pct"]
|
||||
for rep in by_pair),
|
||||
"paired_far_holes_no_worse": all(
|
||||
by_pair[rep]["incremental"]["far_holes_moving_mean_pct"]
|
||||
<= by_pair[rep]["assembler_world"]["far_holes_moving_mean_pct"]
|
||||
for rep in by_pair),
|
||||
"paired_arrival_no_worse": all(
|
||||
by_pair[rep]["incremental"]["arrival_lod02_settled_t_s"] is not None
|
||||
and by_pair[rep]["assembler_world"]["arrival_lod02_settled_t_s"] is not None
|
||||
and by_pair[rep]["incremental"]["arrival_lod02_settled_t_s"]
|
||||
<= by_pair[rep]["assembler_world"]["arrival_lod02_settled_t_s"]
|
||||
for rep in by_pair),
|
||||
"all_candidate_equality_audits_pass": all(
|
||||
row["incremental_desired_audit_count"] > 0
|
||||
and row["incremental_desired_audit_failures"] == 0
|
||||
and row["incremental_desired_order_audit_count"] > 0
|
||||
and row["incremental_desired_order_audit_failures"] == 0
|
||||
for row in candidate_rows),
|
||||
"median_build_desired_p95_down": statistics.median(
|
||||
row["desired_rebuild_nonzero_p95_ms"] for row in candidate_rows)
|
||||
< statistics.median(row["desired_rebuild_nonzero_p95_ms"]
|
||||
for row in baseline_rows),
|
||||
}
|
||||
summary = {
|
||||
"source_commit": subprocess.run(
|
||||
["git", "-c", f"safe.directory={ROOT}", "rev-parse", "HEAD"],
|
||||
cwd=ROOT, check=True, capture_output=True, text=True).stdout.strip(),
|
||||
"protocol": "Seven interleaved 30 m/s UE pairs; 12 s move plus 5 s dwell; 8 effective tile slots. L157 critical wait remains on and apply cap remains 0 ms.",
|
||||
"conditions": CONDITIONS,
|
||||
"movement_medians": medians,
|
||||
"paired_deltas_vs_assembler_world": pairs,
|
||||
"candidate_gates": candidate_gates,
|
||||
"runs": rows,
|
||||
}
|
||||
(OUT / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
print("[L166Summary] " + json.dumps({"movement_medians": medians,
|
||||
"candidate_gates": candidate_gates,
|
||||
"runs": len(rows)}, sort_keys=True), flush=True)
|
||||
return 0 if all(candidate_gates.values()) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,39 @@
|
||||
# L167 — Re-test L157 and L160 on the faster base
|
||||
|
||||
**Result:** No candidate meets the full default bar. Leave L157 and L160 disabled. L160 is tick-safe in this W8 run but lowers all-around near readiness and delays arrival. L157 improves readiness but still spikes; combining them improves readiness most, but worsens worst ticks and delays arrival.
|
||||
|
||||
## Measurement
|
||||
|
||||
Ran base, L157, L160, and both in seven interleaved repetitions each at 30 m/s. Each measured run covered a 12-second move and 5-second dwell, with eight effective tile slots and the four-strata owner plan. One base warm-up was excluded. Timing began with 1/5/15-minute loads at 0.77/0.96/1.46 and no other build or editor process running. Values below are medians across seven runs; p99 range and maximum worst tick show the full run spread.
|
||||
|
||||
The common base had `StreamingIncrementalDesiredSet=1` with audits off, matching the faster L165/L166 path. The checked-in CVar default remains off; this was a controlled runtime setting for the comparison. Null-RHI provided no camera-forward samples, so the 55-degree forward cone uses travel direction.
|
||||
|
||||
| Condition | Tick p99 median (run range), ms | Worst tick median / run max, ms | Near readiness, % | Forward cone, % | Far holes, % | LOD0–2 arrival, s |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| Base | 7.289 (6.786–7.513) | 12.061 / 12.465 | 43.39 | 15.77 | 3.00 | 2.230 |
|
||||
| L157 | 8.582 (8.046–12.462) | 12.462 / 16.689 | 55.44 | 18.61 | 2.64 | 2.469 |
|
||||
| L160 | 7.187 (6.962–8.009) | 11.747 / 12.503 | 38.03 | 20.32 | 2.59 | 2.447 |
|
||||
| Both | 8.932 (8.064–10.973) | 12.594 / 14.278 | 45.55 | 25.24 | 2.85 | 2.461 |
|
||||
|
||||
Near readiness, cone readiness, and far holes improved for L157 and the combination; L160 lowered all-around near readiness. All three candidates delayed median arrival by 0.216–0.239 seconds. L157 had one run above the 11 ms p99 limit and a 16.689 ms worst tick. The combination stayed within the p99 limit in all seven runs, but its median worst tick was 0.533 ms worse than base and one run reached 14.278 ms. L160 passed the tick limits but failed readiness and arrival gates. Therefore none qualifies as default.
|
||||
|
||||
## Spike count and cause
|
||||
|
||||
Trace data attributes the L157 spikes to main-thread desired-set and scheduler work, not the 1 ms apply budget:
|
||||
|
||||
- At the 16.689 ms worst tick, `UpdateChunks` took 15.500 ms. `SubmitTiles` took 8.807 ms, including 8.336 ms in level sorting; desired rebuild took 5.353 ms. Apply took only 0.374 ms. The scheduler trace counted 912 reserve-declined tiles and three near tasks submitted.
|
||||
- A second 16.198 ms tick had 13.852 ms in `UpdateChunks` and an 11.777 ms desired rebuild, including 6.679 ms of overlay materialization. `SubmitTiles` took 0.813 ms and apply took 1.087 ms.
|
||||
- The combined run's 14.278 ms maximum had 10.279 ms in `UpdateChunks`, 2.859 ms draining pending results, and one applied result taking 2.799 ms against the 1 ms budget (`time_limit_reached=1`). Its level sort was only 0.095 ms. The budget is checked after a result completes, so it cannot bound one expensive apply.
|
||||
|
||||
These traces show more than one source: level sorting under L157 admission, expensive desired-set rebuild/materialization, and individual result-apply cost in the combined mode. No tuning change was attempted because the measured work does not support a single cap adjustment that would address all three; all candidates also fail another promotion gate.
|
||||
|
||||
## Checks and limits
|
||||
|
||||
- VFHost Linux Development build succeeded with `-NoUBA -MaxParallelActions=4`; the temporary VFHost plugin source link was restored afterward.
|
||||
- `verify.py fast` passed in 298.0 seconds: 268 pass, 0 fail, 5 diagnostics. Its 84 binary runs had 82 pass and 2 expected failures. Geometry checks and Clang/GCC W1/W8/W12 determinism passed. The L167 source stamp matched the L166 verifier binary (`ba79279c16018d2996e724df0f7c0e8a210929baebc1aa14bef6a76fbda96faa`).
|
||||
- Movement timings used W8, the effective slot count read back by this VFHost command. W12 movement was not run; the command's 12-core affinity exposed eight TaskGraph background workers and eight effective streaming slots. Fast-tier W12 determinism did run. The live movement `TileHashDump` hook was not enabled; no geometry-generation source changed.
|
||||
- Runner `py_compile` and `git diff --check` passed on the final files. No Unreal `Source/` files changed.
|
||||
|
||||
Changed Unreal `Source/` files: none.
|
||||
|
||||
Artifacts are under `Saved/Linux/l167/`; the interleaved movement runner is `RunL167Movement.py` alongside this report.
|
||||
@@ -0,0 +1,639 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Interleaved L167 UE movement runs for the L157/L160 levers on the L165/L166 base."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
HOST = Path("/home/jahni/VFHost")
|
||||
EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd")
|
||||
OUT = ROOT / "Saved/Linux/l167/ue/movement"
|
||||
HOST_SAVED = HOST / "Saved/Linux/l167"
|
||||
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l167"
|
||||
SPAWN_M = (19.166015625, -14.2001953125, -753.3740234375)
|
||||
MOVE_START_S = 6.0
|
||||
|
||||
_COMMON = {
|
||||
# Keep the faster L165/L166 path active for every condition; omit its audit
|
||||
# during timing so the audit itself cannot create a frame spike.
|
||||
"incremental_desired": 1, "audit_every": 0,
|
||||
"materialization_audit_every": 0, "inject_mismatch": 0,
|
||||
}
|
||||
CONDITIONS = {
|
||||
"base": {
|
||||
**_COMMON, "wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||
"forward_first": 0, "rebuild_on_region_completion": 1,
|
||||
"core_desired_cache": 0,
|
||||
},
|
||||
"l157": {
|
||||
**_COMMON, "wait_for_critical": 0, "apply_budget_ms": 1.0,
|
||||
"forward_first": 0, "rebuild_on_region_completion": 1,
|
||||
"core_desired_cache": 0,
|
||||
},
|
||||
"l160": {
|
||||
**_COMMON, "wait_for_critical": 1, "apply_budget_ms": 0.0,
|
||||
"forward_first": 1, "rebuild_on_region_completion": 0,
|
||||
"core_desired_cache": 1,
|
||||
},
|
||||
"both": {
|
||||
**_COMMON, "wait_for_critical": 0, "apply_budget_ms": 1.0,
|
||||
"forward_first": 1, "rebuild_on_region_completion": 0,
|
||||
"core_desired_cache": 1,
|
||||
},
|
||||
}
|
||||
ORDER = {
|
||||
1: ("base", "l157", "l160", "both"),
|
||||
2: ("l157", "l160", "both", "base"),
|
||||
3: ("l160", "both", "base", "l157"),
|
||||
4: ("both", "base", "l157", "l160"),
|
||||
5: ("l160", "l157", "base", "both"),
|
||||
6: ("both", "l160", "l157", "base"),
|
||||
7: ("base", "both", "l160", "l157"),
|
||||
}
|
||||
|
||||
|
||||
def percentile(values: list[float], fraction: float = 0.99) -> float | None:
|
||||
if not values:
|
||||
return None
|
||||
ordered = sorted(values)
|
||||
return ordered[max(0, min(len(ordered) - 1, math.ceil(len(ordered) * fraction) - 1))]
|
||||
|
||||
|
||||
def read_events(path: Path) -> list[dict]:
|
||||
rows = []
|
||||
if path.is_file():
|
||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
if not line.startswith("{"):
|
||||
continue
|
||||
try:
|
||||
rows.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return rows
|
||||
|
||||
|
||||
def mean(values: list[float]) -> float | None:
|
||||
return statistics.mean(values) if values else None
|
||||
|
||||
|
||||
def desired_breakdown(events: list[dict], move_start_s: float, stop_s: float) -> dict:
|
||||
rows = [event for event in events
|
||||
if event.get("type") == "desired_rebuild_breakdown"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
keys = ("total_ms", "core_prepare_ms", "core_delta_ms", "fallback_ms",
|
||||
"overlay_materialize_ms", "priority_sort_ms", "critical_order_ms")
|
||||
result = {"rebuild_count": len(rows)}
|
||||
result["incremental_materialization_update_count"] = sum(
|
||||
int(event.get("materialization_update", 0)) != 0 for event in rows)
|
||||
result["priority_order_reuse_count"] = sum(
|
||||
int(event.get("priority_order_reused", 0)) != 0 for event in rows)
|
||||
result["priority_order_update_count"] = sum(
|
||||
int(event.get("priority_order_updated", 0)) != 0 for event in rows)
|
||||
result["incremental_materialization_update_share_pct"] = percent(
|
||||
result["incremental_materialization_update_count"], len(rows))
|
||||
result["priority_order_reuse_share_pct"] = percent(
|
||||
result["priority_order_reuse_count"], len(rows))
|
||||
result["priority_order_update_share_pct"] = percent(
|
||||
result["priority_order_update_count"], len(rows))
|
||||
for key in keys:
|
||||
values = [float(event.get(key, 0.0)) for event in rows]
|
||||
result[f"{key}_p50"] = statistics.median(values) if values else None
|
||||
result[f"{key}_p95"] = percentile(values, 0.95)
|
||||
result[f"{key}_max"] = max(values) if values else None
|
||||
fallback_rows = [event for event in events
|
||||
if event.get("type") == "core_desired_cache"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s
|
||||
and int(event.get("full_fallback", 0)) != 0]
|
||||
result["fallback_count"] = len(fallback_rows)
|
||||
result["lod4plus_fallback_count"] = sum(
|
||||
int(event.get("fallback_reason", 0)) == 1 for event in fallback_rows)
|
||||
result["other_fallback_count"] = sum(
|
||||
int(event.get("fallback_reason", 0)) == 2 for event in fallback_rows)
|
||||
result["fallback_cost_sum_ms"] = sum(
|
||||
float(event.get("fallback_ms", 0.0)) for event in fallback_rows)
|
||||
result["fallback_cost_max_ms"] = max(
|
||||
(float(event.get("fallback_ms", 0.0)) for event in fallback_rows), default=None)
|
||||
return result
|
||||
|
||||
|
||||
def percent(numerator: int | float, denominator: int | float) -> float | None:
|
||||
return 100.0 * numerator / denominator if denominator > 0 else None
|
||||
|
||||
|
||||
def run(condition: str, repetition: int, tile_hash_dump: Path | None = None,
|
||||
expected_mismatch: bool = False) -> dict:
|
||||
config = CONDITIONS[condition]
|
||||
run_name = f"fast-r{repetition}"
|
||||
out = OUT / condition
|
||||
host_out = HOST_PLUGIN_SAVED / condition / "output"
|
||||
userdir = HOST_SAVED / "user" / condition / run_name
|
||||
zendata = HOST_SAVED / "zen" / condition / run_name
|
||||
ddc = HOST_SAVED / "DDC"
|
||||
trace = out / f"{run_name}.trace.jsonl"
|
||||
abslog = out / f"{run_name}.log"
|
||||
stdout = out / f"{run_name}.stdout.log"
|
||||
export = host_out / run_name
|
||||
for directory in (out, export, userdir, zendata, ddc):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
trace.unlink(missing_ok=True)
|
||||
|
||||
spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWN_M)
|
||||
command = [
|
||||
str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended",
|
||||
"-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch",
|
||||
"-notraceserver", "-nocrashreports", "-DisablePlugins=AndroidFileServer",
|
||||
"-ddc=NoZenLocalFallback", f"-userdir={userdir}", f"-ZenDataPath={zendata}",
|
||||
f"-LocalDataCachePath={ddc}", "-run=VoxelForgeExplore", "-seed=0",
|
||||
"-archetype=TunnelNetwork", "-slot=0", "-modes=export", "-gameconfig=1",
|
||||
"-coreworldtest=1", "-ownerfourstrateplan=1", "-corevoxelsizecm=25",
|
||||
"-coreislandtest=0", "-corefallbacktest=0", "-coreofftest=0",
|
||||
"-coreplantest=0", "-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings",
|
||||
"-corehybridmesher=0", f"-out={export}", "-voxel.StreamingCoarseFirst=1",
|
||||
"-voxel.StreamingDynamicNearReserve=1",
|
||||
"-voxel.StreamingLegacyPendingResultCap=0",
|
||||
"-voxel.CoreRegionConcurrentAdmission=1",
|
||||
"-voxel.StreamingResultBacklogCap=256", "-voxel.StreamingReadyQueueDepth=8",
|
||||
"-voxel.StreamingBackgroundWorkerCap=1", "-voxel.CoreColumnCacheDirectMapped=0",
|
||||
"-voxel.StreamingFarApplySupplement=1",
|
||||
f"-voxel.StreamingFastTravelWaitForAllCriticalTiles={config['wait_for_critical']}",
|
||||
f"-voxel.StreamingFastTravelApplyBudgetMilliseconds={config['apply_budget_ms']:.1f}",
|
||||
f"-voxel.StreamingFastTravelForwardFirst={config['forward_first']}",
|
||||
f"-voxel.StreamingRebuildOnCoreRegionCompletion={config['rebuild_on_region_completion']}",
|
||||
f"-voxel.StreamingCoreDesiredCacheByCenter={config['core_desired_cache']}",
|
||||
f"-voxel.StreamingIncrementalDesiredSet={config['incremental_desired']}",
|
||||
f"-voxel.StreamingIncrementalDesiredAuditEvery={config['audit_every']}",
|
||||
f"-voxel.StreamingIncrementalDesiredMaterializationAuditEvery={config['materialization_audit_every']}",
|
||||
f"-voxel.StreamingIncrementalDesiredAuditInjectMismatch={config['inject_mismatch']}",
|
||||
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
|
||||
"-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=3000.0",
|
||||
f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}",
|
||||
"-voxel.TestMoveDurationSeconds=12.0", "-voxel.TestExitSeconds=0.0",
|
||||
"-voxel.TestMoveDirection=1,0,0", "-voxel.StreamingSchedulerPolicy=3",
|
||||
"-voxel.TestExitOnSteady=0", "-coreworldstreamtrace=1",
|
||||
f"-voxel.StartupTraceSpawnCm={spawn_cm}",
|
||||
"-voxel.StartupTraceUntilEndPlay=1", "-voxel.StartupTraceThroughCrossing=1",
|
||||
"-voxel.StartupTraceMaxTiles=16384", f"-voxel.StartupTraceFile={trace}",
|
||||
f"-abslog={abslog}",
|
||||
]
|
||||
if tile_hash_dump is not None:
|
||||
tile_hash_dump.parent.mkdir(parents=True, exist_ok=True)
|
||||
tile_hash_dump.unlink(missing_ok=True)
|
||||
command.append(f"-voxel.TileHashDump={tile_hash_dump.resolve()}")
|
||||
wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command]
|
||||
started = time.monotonic()
|
||||
with stdout.open("w", encoding="utf-8") as stream:
|
||||
process = subprocess.run(
|
||||
wrapped, cwd=ROOT, stdout=stream, stderr=subprocess.STDOUT,
|
||||
check=False, timeout=1740)
|
||||
|
||||
events = read_events(trace)
|
||||
if expected_mismatch:
|
||||
diagnostic = "VoxelForgeIncrementalDesiredAudit"
|
||||
logs = "\n".join(path.read_text(encoding="utf-8", errors="replace")
|
||||
for path in (abslog, stdout) if path.is_file())
|
||||
audit_events = [event for event in events
|
||||
if event.get("type") in {"incremental_desired_audit",
|
||||
"incremental_desired_order_audit"}]
|
||||
failed_audit = any(int(event.get("pass", 1)) == 0 for event in audit_events)
|
||||
failed_audit = failed_audit or (
|
||||
diagnostic in logs and "mismatch" in logs.lower())
|
||||
if process.returncode == 0 or diagnostic not in logs or "mismatch" not in logs.lower():
|
||||
raise RuntimeError(
|
||||
f"injected mismatch did not fail as expected: exit={process.returncode}; "
|
||||
f"audit_events={len(audit_events)}; log={abslog}")
|
||||
print("[L166MismatchControl] " + json.dumps({
|
||||
"exit_code": process.returncode,
|
||||
"diagnostic_seen": True,
|
||||
"failed_audit_event_seen": failed_audit,
|
||||
"trace": str(trace.relative_to(ROOT)),
|
||||
"log": str(abslog.relative_to(ROOT)),
|
||||
}, sort_keys=True), flush=True)
|
||||
return {"condition": condition, "exit_code": process.returncode,
|
||||
"failed_audit_event_seen": failed_audit}
|
||||
samples = [event for event in events if event.get("type") == "streaming_sample"]
|
||||
moving = [event for event in samples if float(event.get("speed_cm_s", 0.0)) >= 50.0]
|
||||
stop_event = next((event for event in events
|
||||
if event.get("type") == "streaming_test_move_stopped"), None)
|
||||
backend = next((event for event in events if event.get("type") == "worker_backend_config"), {})
|
||||
scheduler_config = next((event for event in events
|
||||
if event.get("type") == "scheduler_admission"), {})
|
||||
worker_cap = next((event for event in events
|
||||
if event.get("type") == "streaming_worker_cap_readback"), {})
|
||||
if stop_event is None:
|
||||
raise RuntimeError(f"missing move-stop event: {trace}")
|
||||
stop_s = float(stop_event.get("t_s", 0.0))
|
||||
move_start_s = min((float(event.get("t_s", 0.0)) for event in moving), default=float("inf"))
|
||||
dwell = [event for event in samples if float(event.get("t_s", 0.0)) >= stop_s
|
||||
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||
arrival = [event for event in samples if float(event.get("t_s", 0.0)) < move_start_s
|
||||
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||
desired_stage_breakdown = desired_breakdown(events, move_start_s, stop_s)
|
||||
if process.returncode or len(moving) < 40 or len(dwell) < 16:
|
||||
raise RuntimeError(f"incomplete {condition} run: {trace}; exit={process.returncode}; "
|
||||
f"move={len(moving)} dwell={len(dwell)}")
|
||||
if (int(backend.get("streaming_fast_travel_wait_for_all_critical_tiles", -1))
|
||||
!= config["wait_for_critical"]
|
||||
or abs(float(backend.get("streaming_fast_travel_apply_budget_ms", -1.0))
|
||||
- config["apply_budget_ms"]) > 0.01
|
||||
or int(worker_cap.get("streaming_background_worker_cap", -1)) != 1
|
||||
or int(backend.get("effective_tile_slots", -1)) != 8
|
||||
or int(backend.get("streaming_far_apply_supplement", -1)) != 1
|
||||
or int(scheduler_config.get("forward_first", -1)) != config["forward_first"]
|
||||
or int(scheduler_config.get("rebuild_on_core_region_completion", -1))
|
||||
!= config["rebuild_on_region_completion"]
|
||||
or int(scheduler_config.get("core_desired_cache_by_center", -1))
|
||||
!= config["core_desired_cache"]
|
||||
or int(scheduler_config.get("incremental_desired_set", -1))
|
||||
!= config["incremental_desired"]
|
||||
or int(scheduler_config.get("incremental_desired_audit_every", -1))
|
||||
!= config["audit_every"]
|
||||
or int(scheduler_config.get("incremental_desired_materialization_audit_every", -1))
|
||||
!= config["materialization_audit_every"]
|
||||
or int(scheduler_config.get("incremental_desired_audit_inject_mismatch", -1))
|
||||
!= config["inject_mismatch"]):
|
||||
raise RuntimeError(f"CVar/worker readback mismatch: {backend}; {worker_cap}; {scheduler_config}")
|
||||
|
||||
moving_metrics = []
|
||||
for event in moving:
|
||||
desired = event.get("desired_by_lod", [0] * 9)
|
||||
resident = event.get("resident_by_lod", [0] * 9)
|
||||
near_desired = sum(desired[:3])
|
||||
cone_desired = int(event.get("near_forward_cone_desired", 0))
|
||||
view_cone_desired = int(event.get("near_view_forward_cone_desired", 0))
|
||||
view_valid = bool(event.get("near_view_forward_cone_valid", 0))
|
||||
weighted_desired = float(event.get("near_forward_weighted_desired", 0.0))
|
||||
travel_cone_ready = percent(
|
||||
int(event.get("near_forward_cone_resident", 0)), cone_desired)
|
||||
view_cone_ready = percent(
|
||||
int(event.get("near_view_forward_cone_resident", 0)), view_cone_desired)
|
||||
moving_metrics.append({
|
||||
"near_ready_pct": percent(sum(resident[:3]), near_desired) or 0.0,
|
||||
"lod0_ready_pct": percent(resident[0], desired[0]) or 0.0,
|
||||
"lod1_ready_pct": percent(resident[1], desired[1]) or 0.0,
|
||||
"lod2_ready_pct": percent(resident[2], desired[2]) or 0.0,
|
||||
"far_hole_pct": percent(
|
||||
sum(desired[3:]) - sum(event.get("covered_by_lod", resident)[3:]),
|
||||
sum(desired[3:])) or 0.0,
|
||||
"ahead_share_pct": percent(int(event.get("near_ahead_desired", 0)), near_desired),
|
||||
"behind_share_pct": percent(int(event.get("near_behind_desired", 0)), near_desired),
|
||||
"side_share_pct": 100.0 - (
|
||||
percent(int(event.get("near_ahead_desired", 0)), near_desired) or 0.0
|
||||
) - (
|
||||
percent(int(event.get("near_behind_desired", 0)), near_desired) or 0.0
|
||||
),
|
||||
"ahead_ready_pct": percent(
|
||||
int(event.get("near_ahead_resident", 0)),
|
||||
int(event.get("near_ahead_desired", 0))),
|
||||
"behind_ready_pct": percent(
|
||||
int(event.get("near_behind_resident", 0)),
|
||||
int(event.get("near_behind_desired", 0))),
|
||||
"travel_forward_cone_ready_pct": travel_cone_ready,
|
||||
"view_forward_cone_ready_pct": view_cone_ready,
|
||||
"forward_cone_ready_pct": view_cone_ready if view_valid else travel_cone_ready,
|
||||
"forward_cone_source": "camera" if view_valid else "travel_fallback",
|
||||
"forward_cone_share_pct": percent(cone_desired, near_desired),
|
||||
"forward_weighted_ready_pct": percent(
|
||||
float(event.get("near_forward_weighted_resident", 0.0)), weighted_desired),
|
||||
})
|
||||
|
||||
arrival_near_settled = next((event for event in arrival
|
||||
if sum(event.get("desired_by_lod", [0] * 9)[:3]) > 0
|
||||
and sum(event.get("settled_by_lod", [0] * 9)[:3])
|
||||
>= sum(event.get("desired_by_lod", [0] * 9)[:3])), None)
|
||||
dwell_complete = next((event for event in dwell
|
||||
if sum(event.get("resident_by_lod", [0] * 9)[:3])
|
||||
>= sum(event.get("desired_by_lod", [0] * 9)[:3])
|
||||
and sum(event.get("desired_by_lod", [0] * 9)[3:])
|
||||
<= sum(event.get("covered_by_lod", [0] * 9)[3:])), None)
|
||||
|
||||
work = [event for event in events if event.get("type") == "game_thread_work_frame"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
applies = {event.get("frame"): event for event in events
|
||||
if event.get("type") == "apply_frame"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||
scheduler = {event.get("frame"): event for event in events
|
||||
if event.get("type") == "scheduler_admission"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||
frames = []
|
||||
for event in work:
|
||||
apply = applies.get(event.get("frame"), {})
|
||||
admit = scheduler.get(event.get("frame"), {})
|
||||
frames.append({
|
||||
"frame": event.get("frame"),
|
||||
"tick_ms": float(event.get("tick_ms", 0.0)),
|
||||
"update_chunks_ms": float(event.get("update_chunks_ms", 0.0)),
|
||||
"pending_drain_ms": float(event.get("pending_drain_ms", 0.0)),
|
||||
"apply_ms": float(apply.get("apply_ms", 0.0)),
|
||||
"apply_budget_ms": float(apply.get("apply_budget_ms", 0.0)),
|
||||
"meshes_applied": int(apply.get("meshes_applied", 0)),
|
||||
"time_budget_hit": int(apply.get("time_limit_reached", 0)),
|
||||
"desired_rebuild_ms": float(admit.get("desired_rebuild_ms", 0.0)),
|
||||
"desired_rebuilt": int(admit.get("desired_rebuilt", 0)),
|
||||
"submit_tiles_ms": float(admit.get("submit_tiles_ms", 0.0)),
|
||||
"level_admit_ms": float(admit.get("level_admit_ms", 0.0)),
|
||||
})
|
||||
p99_field = lambda key: percentile([float(row[key]) for row in frames])
|
||||
max_field = lambda key: max((float(row[key]) for row in frames), default=None)
|
||||
rebuild_frames = [row for row in frames if row["desired_rebuilt"]]
|
||||
rebuild_costs = [float(row["desired_rebuild_ms"]) for row in rebuild_frames]
|
||||
moving_scheduler = [event for event in scheduler.values()]
|
||||
center_crossings = 0
|
||||
previous_center = None
|
||||
for event in sorted(moving_scheduler, key=lambda item: int(item.get("frame", 0))):
|
||||
center = tuple(event.get("center", ()))
|
||||
if previous_center is not None and center != previous_center:
|
||||
center_crossings += 1
|
||||
previous_center = center
|
||||
core_requests = [event for event in events
|
||||
if event.get("type") == "core_region_compile_request"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
same_center_core_requests = sum(
|
||||
event.get("reason") == "desired_set_changed_same_center" for event in core_requests)
|
||||
core_desired_queries = [event for event in events
|
||||
if event.get("type") == "core_desired_cache"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
core_desired_query_ms = [float(event.get("query_ms", 0.0))
|
||||
for event in core_desired_queries if float(event.get("query_ms", 0.0)) > 0.0]
|
||||
core_desired_cache_hits = sum(int(event.get("hit", 0)) for event in core_desired_queries)
|
||||
incremental_audits = [event for event in events
|
||||
if event.get("type") == "incremental_desired_audit"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
incremental_audit_failures = sum(int(event.get("pass", 1)) == 0
|
||||
for event in incremental_audits)
|
||||
order_audits = [event for event in events
|
||||
if event.get("type") == "incremental_desired_order_audit"
|
||||
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||
order_audit_failures = sum(int(event.get("pass", 1)) == 0 for event in order_audits)
|
||||
materialization_audits = [event for event in incremental_audits
|
||||
if int(event.get("materialization_audit", 0)) != 0]
|
||||
if config["audit_every"] > 0 and (
|
||||
not incremental_audits or incremental_audit_failures
|
||||
or not order_audits or order_audit_failures
|
||||
or (config["materialization_audit_every"] > 0
|
||||
and not materialization_audits)):
|
||||
raise RuntimeError(
|
||||
f"incremental desired equality audit missing/failed: {trace}; "
|
||||
f"set={len(incremental_audits)}/{incremental_audit_failures} "
|
||||
f"order={len(order_audits)}/{order_audit_failures} "
|
||||
f"materialization={len(materialization_audits)}")
|
||||
direction_submissions = [event.get("near_submitted_by_direction", [0, 0, 0, 0])
|
||||
for event in moving_scheduler]
|
||||
direction_submissions = [counts for counts in direction_submissions
|
||||
if isinstance(counts, list) and len(counts) == 4]
|
||||
direction_submission_totals = [
|
||||
sum(int(counts[index]) for counts in direction_submissions) for index in range(4)
|
||||
]
|
||||
first_admission_order = next((event.get("near_admission_order_first", [])
|
||||
for event in moving_scheduler
|
||||
if event.get("near_admission_order_first")), [])
|
||||
camera_samples = sum(bool(event.get("near_view_forward_cone_valid", 0)) for event in moving)
|
||||
dwell_complete_s = (float(dwell_complete.get("t_s", 0.0)) - stop_s
|
||||
if dwell_complete else None)
|
||||
|
||||
row = {
|
||||
"condition": condition,
|
||||
"repetition": repetition,
|
||||
"speed_mps": 30.0,
|
||||
"wait_for_all_critical_tiles": config["wait_for_critical"],
|
||||
"fast_travel_apply_budget_ms": config["apply_budget_ms"],
|
||||
"forward_first": config["forward_first"],
|
||||
"rebuild_on_core_region_completion": config["rebuild_on_region_completion"],
|
||||
"core_desired_cache_by_center": config["core_desired_cache"],
|
||||
"incremental_desired_set": config["incremental_desired"],
|
||||
"incremental_desired_audit_every": config["audit_every"],
|
||||
"incremental_desired_materialization_audit_every": config["materialization_audit_every"],
|
||||
"incremental_desired_audit_count": len(incremental_audits),
|
||||
"incremental_desired_audit_failures": incremental_audit_failures,
|
||||
"incremental_desired_order_audit_count": len(order_audits),
|
||||
"incremental_desired_order_audit_failures": order_audit_failures,
|
||||
"incremental_desired_materialization_audit_count": len(materialization_audits),
|
||||
"effective_tile_slots": backend.get("effective_tile_slots"),
|
||||
"exit_code": process.returncode,
|
||||
"wall_s": round(time.monotonic() - started, 3),
|
||||
"moving_samples": len(moving),
|
||||
"dwell_samples": len(dwell),
|
||||
"arrival_samples": len(arrival),
|
||||
"near_ready_moving_mean_pct": mean([m["near_ready_pct"] for m in moving_metrics]),
|
||||
"lod0_ready_moving_mean_pct": mean([m["lod0_ready_pct"] for m in moving_metrics]),
|
||||
"lod1_ready_moving_mean_pct": mean([m["lod1_ready_pct"] for m in moving_metrics]),
|
||||
"lod2_ready_moving_mean_pct": mean([m["lod2_ready_pct"] for m in moving_metrics]),
|
||||
"far_holes_moving_mean_pct": mean([m["far_hole_pct"] for m in moving_metrics]),
|
||||
"near_ahead_desired_share_pct": mean([m["ahead_share_pct"] for m in moving_metrics
|
||||
if m["ahead_share_pct"] is not None]),
|
||||
"near_behind_desired_share_pct": mean([m["behind_share_pct"] for m in moving_metrics
|
||||
if m["behind_share_pct"] is not None]),
|
||||
"near_side_desired_share_pct": mean([m["side_share_pct"] for m in moving_metrics]),
|
||||
"near_ahead_ready_pct": mean([m["ahead_ready_pct"] for m in moving_metrics
|
||||
if m["ahead_ready_pct"] is not None]),
|
||||
"near_behind_ready_pct": mean([m["behind_ready_pct"] for m in moving_metrics
|
||||
if m["behind_ready_pct"] is not None]),
|
||||
"near_forward_cone_half_angle_deg": 55,
|
||||
"near_forward_cone_desired_share_pct": mean([m["forward_cone_share_pct"]
|
||||
for m in moving_metrics
|
||||
if m["forward_cone_share_pct"] is not None]),
|
||||
"near_forward_cone_ready_pct": mean([m["forward_cone_ready_pct"]
|
||||
for m in moving_metrics
|
||||
if m["forward_cone_ready_pct"] is not None]),
|
||||
"near_travel_forward_cone_ready_pct": mean(
|
||||
[m["travel_forward_cone_ready_pct"] for m in moving_metrics
|
||||
if m["travel_forward_cone_ready_pct"] is not None]),
|
||||
"near_view_forward_cone_ready_pct": mean(
|
||||
[m["view_forward_cone_ready_pct"] for m in moving_metrics
|
||||
if m["view_forward_cone_ready_pct"] is not None]),
|
||||
"camera_forward_samples": camera_samples,
|
||||
"camera_forward_sample_share_pct": percent(camera_samples, len(moving)),
|
||||
"near_forward_cosine_weighted_ready_pct": mean(
|
||||
[m["forward_weighted_ready_pct"] for m in moving_metrics
|
||||
if m["forward_weighted_ready_pct"] is not None]),
|
||||
"arrival_lod02_settled_t_s": (
|
||||
float(arrival_near_settled.get("t_s", 0.0)) if arrival_near_settled else None),
|
||||
"near_ready_dwell_end_pct": (100.0 * sum(dwell[-1].get("resident_by_lod", [0] * 9)[:3])
|
||||
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[:3]))),
|
||||
"far_holes_dwell_end_pct": (100.0 * (
|
||||
sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:])
|
||||
- sum(dwell[-1].get("covered_by_lod", [0] * 9)[3:]))
|
||||
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:]))),
|
||||
"dwell_complete_after_stop_s": dwell_complete_s,
|
||||
"game_thread_tick_p99_ms": p99_field("tick_ms"),
|
||||
"game_thread_tick_max_ms": max_field("tick_ms"),
|
||||
"stream_update_p99_ms": p99_field("update_chunks_ms"),
|
||||
"pending_drain_p99_ms": p99_field("pending_drain_ms"),
|
||||
"apply_p99_ms": p99_field("apply_ms"),
|
||||
"apply_max_ms": max_field("apply_ms"),
|
||||
"apply_budget_min_ms": min((f["apply_budget_ms"] for f in frames), default=None),
|
||||
"apply_budget_max_ms": max((f["apply_budget_ms"] for f in frames), default=None),
|
||||
"apply_time_budget_hit_frames": sum(f["time_budget_hit"] for f in frames),
|
||||
"apply_frames": sum(f["meshes_applied"] > 0 for f in frames),
|
||||
"apply_meshes_total": sum(f["meshes_applied"] for f in frames),
|
||||
"desired_rebuild_p99_ms": p99_field("desired_rebuild_ms"),
|
||||
"desired_rebuild_count": len(rebuild_frames),
|
||||
"desired_rebuilds_per_second": len(rebuild_frames) / 12.0,
|
||||
"center_crossings": center_crossings,
|
||||
"same_center_core_region_requests": same_center_core_requests,
|
||||
"core_desired_cache_hit_count": core_desired_cache_hits,
|
||||
"core_desired_cache_hit_share_pct": percent(
|
||||
core_desired_cache_hits, len(core_desired_queries)),
|
||||
"core_desired_query_nonzero_p50_ms": statistics.median(core_desired_query_ms)
|
||||
if core_desired_query_ms else None,
|
||||
"core_desired_query_nonzero_p95_ms": percentile(core_desired_query_ms, 0.95),
|
||||
"core_desired_query_nonzero_max_ms": max(core_desired_query_ms)
|
||||
if core_desired_query_ms else None,
|
||||
"desired_rebuild_nonzero_p50_ms": statistics.median(rebuild_costs)
|
||||
if rebuild_costs else None,
|
||||
"desired_rebuild_nonzero_p95_ms": percentile(rebuild_costs, 0.95),
|
||||
"desired_rebuild_nonzero_max_ms": max(rebuild_costs) if rebuild_costs else None,
|
||||
"desired_breakdown": desired_stage_breakdown,
|
||||
"near_submitted_by_direction": direction_submission_totals,
|
||||
"near_admission_first12": first_admission_order,
|
||||
"submit_tiles_p99_ms": p99_field("submit_tiles_ms"),
|
||||
"level_admit_p99_ms": p99_field("level_admit_ms"),
|
||||
"top_tick_frames": sorted(frames, key=lambda f: f["tick_ms"], reverse=True)[:5],
|
||||
"trace": str(trace.relative_to(ROOT)),
|
||||
"log": str(abslog.relative_to(ROOT)),
|
||||
}
|
||||
print("[L167Movement] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--smoke", action="store_true",
|
||||
help="run one unmeasured base warm-up and validate CVar readback")
|
||||
parser.add_argument("--resume", action="store_true",
|
||||
help="resume an incomplete matrix from runs-partial.json")
|
||||
parser.add_argument("--summarize-existing", action="store_true",
|
||||
help="summarize 28 completed matrix rows without rerunning UE")
|
||||
args = parser.parse_args()
|
||||
missing = [path for path in (EDITOR, HOST / "VFHost.uproject") if not path.exists()]
|
||||
if missing:
|
||||
raise SystemExit(f"missing UE runner input: {missing}")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
partial = OUT / "runs-partial.json"
|
||||
if args.smoke:
|
||||
run("base", 0)
|
||||
return 0
|
||||
if args.summarize_existing:
|
||||
if not partial.is_file():
|
||||
raise RuntimeError(f"no saved movement rows: {partial}")
|
||||
rows = json.loads(partial.read_text(encoding="utf-8"))
|
||||
else:
|
||||
if partial.is_file() and not args.resume:
|
||||
raise RuntimeError(f"partial matrix exists; use --resume or move it first: {partial}")
|
||||
rows = json.loads(partial.read_text(encoding="utf-8")) if partial.is_file() else []
|
||||
if not rows:
|
||||
# One editor binary is used by every condition, so one warm-up is enough.
|
||||
run("base", 0)
|
||||
completed = {(row["repetition"], row["condition"]) for row in rows}
|
||||
for repetition in range(1, 8):
|
||||
for condition in ORDER[repetition]:
|
||||
if (repetition, condition) in completed:
|
||||
continue
|
||||
row = run(condition, repetition)
|
||||
rows.append(row)
|
||||
partial.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
|
||||
metrics = (
|
||||
"near_ready_moving_mean_pct", "far_holes_moving_mean_pct",
|
||||
"game_thread_tick_p99_ms", "game_thread_tick_max_ms",
|
||||
"arrival_lod02_settled_t_s", "dwell_complete_after_stop_s",
|
||||
"near_ahead_ready_pct", "near_behind_ready_pct",
|
||||
"near_forward_cone_ready_pct", "near_forward_cosine_weighted_ready_pct",
|
||||
"apply_p99_ms", "apply_max_ms", "desired_rebuild_nonzero_p95_ms",
|
||||
"stream_update_p99_ms", "pending_drain_p99_ms", "submit_tiles_p99_ms",
|
||||
"level_admit_p99_ms", "desired_rebuild_count", "center_crossings",
|
||||
)
|
||||
|
||||
def median_for(condition: str, metric: str) -> float | None:
|
||||
values = [row[metric] for row in rows
|
||||
if row["condition"] == condition and row.get(metric) is not None]
|
||||
return statistics.median(values) if values else None
|
||||
|
||||
medians = {
|
||||
condition: {metric: median_for(condition, metric) for metric in metrics}
|
||||
for condition in CONDITIONS
|
||||
}
|
||||
gates = {}
|
||||
paired_deltas = {}
|
||||
for candidate in ("l157", "l160", "both"):
|
||||
candidate_rows = [row for row in rows if row["condition"] == candidate]
|
||||
candidate_median_worst = median_for(candidate, "game_thread_tick_max_ms")
|
||||
base_median_worst = median_for("base", "game_thread_tick_max_ms")
|
||||
candidate_near = median_for(candidate, "near_ready_moving_mean_pct")
|
||||
base_near = median_for("base", "near_ready_moving_mean_pct")
|
||||
candidate_cone = median_for(candidate, "near_forward_cone_ready_pct")
|
||||
base_cone = median_for("base", "near_forward_cone_ready_pct")
|
||||
candidate_far = median_for(candidate, "far_holes_moving_mean_pct")
|
||||
base_far = median_for("base", "far_holes_moving_mean_pct")
|
||||
candidate_arrival = median_for(candidate, "arrival_lod02_settled_t_s")
|
||||
base_arrival = median_for("base", "arrival_lod02_settled_t_s")
|
||||
candidate_gates = {
|
||||
"seven_runs_complete": len(candidate_rows) == 7,
|
||||
"every_run_p99_le_11_ms": len(candidate_rows) == 7 and all(
|
||||
row["game_thread_tick_p99_ms"] <= 11.0 for row in candidate_rows),
|
||||
"median_worst_tick_no_worse": (
|
||||
candidate_median_worst is not None and base_median_worst is not None
|
||||
and candidate_median_worst <= base_median_worst),
|
||||
"no_run_worst_tick_above_14_ms": len(candidate_rows) == 7 and all(
|
||||
row["game_thread_tick_max_ms"] <= 14.0 for row in candidate_rows),
|
||||
"median_near_readiness_up": (
|
||||
candidate_near is not None and base_near is not None
|
||||
and candidate_near > base_near),
|
||||
"median_forward_cone_readiness_up": (
|
||||
candidate_cone is not None and base_cone is not None
|
||||
and candidate_cone > base_cone),
|
||||
"median_far_holes_no_worse": (
|
||||
candidate_far is not None and base_far is not None
|
||||
and candidate_far <= base_far),
|
||||
"median_arrival_no_worse": (
|
||||
candidate_arrival is not None and base_arrival is not None
|
||||
and candidate_arrival <= base_arrival),
|
||||
}
|
||||
gates[candidate] = {
|
||||
**candidate_gates,
|
||||
"passes_default_bar": all(candidate_gates.values()),
|
||||
}
|
||||
paired_deltas[candidate] = []
|
||||
for repetition in range(1, 8):
|
||||
by_condition = {row["condition"]: row for row in rows
|
||||
if row["repetition"] == repetition}
|
||||
if candidate not in by_condition or "base" not in by_condition:
|
||||
continue
|
||||
paired_deltas[candidate].append({
|
||||
"repetition": repetition,
|
||||
**{f"{metric}_delta": by_condition[candidate][metric]
|
||||
- by_condition["base"][metric]
|
||||
for metric in metrics if by_condition[candidate].get(metric) is not None
|
||||
and by_condition["base"].get(metric) is not None},
|
||||
})
|
||||
|
||||
summary = {
|
||||
"source_commit": subprocess.run(
|
||||
["git", "-c", f"safe.directory={ROOT}", "rev-parse", "HEAD"],
|
||||
cwd=ROOT, check=True, capture_output=True, text=True).stdout.strip(),
|
||||
"protocol": "Seven interleaved 30 m/s W8 UE runs per condition; 12 s move plus 5 s dwell; 8 effective tile slots; one base warm-up.",
|
||||
"conditions": CONDITIONS,
|
||||
"movement_medians": medians,
|
||||
"candidate_gates": gates,
|
||||
"paired_deltas_vs_base": paired_deltas,
|
||||
"runs": rows,
|
||||
}
|
||||
(OUT / "summary.json").write_text(
|
||||
json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print("[L167Summary] " + json.dumps({
|
||||
"movement_medians": medians,
|
||||
"candidate_gates": gates,
|
||||
"runs": len(rows),
|
||||
}, sort_keys=True), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,41 @@
|
||||
# L168 — Default the incremental desired set
|
||||
|
||||
**Result:** `voxel.StreamingIncrementalDesiredSet` now defaults to `1`. In five interleaved 30 m/s UE pairs against `assembler/world` `c6357b5`, candidate tick p99 fell from **10.07 to 7.01 ms** and median worst tick from **13.24 to 11.63 ms**. P99 was lower in all five pairs; worst tick was lower in four, with one +0.06 ms pair.
|
||||
|
||||
## Defaults and trace
|
||||
|
||||
The main switch gates the Core clipmap delta, overlay membership updates, priority-order merge/reuse, and the incremental path's same-center Core-key cache. Overlay and priority updates have no separate switches. Defaults after the change:
|
||||
|
||||
| CVar | Default | Meaning |
|
||||
|---|---:|---|
|
||||
| `StreamingIncrementalDesiredSet` | 1 | Core, overlay, and priority updates are incremental. |
|
||||
| `StreamingCoreDesiredCacheByCenter` | 0 | Separate L160 cache stays off; the incremental path has its own cache. |
|
||||
| `StreamingRebuildOnCoreRegionCompletion` | 1 | L160 same-center rebuild suppression stays off. |
|
||||
| `StreamingFastTravelForwardFirst` | 0 | L160 ahead-first stays off. |
|
||||
| `StreamingFastTravelWaitForAllCriticalTiles` | 1 | L157 early admission stays off. |
|
||||
| `StreamingFastTravelApplyBudgetMilliseconds` | 0 | L157 fast-travel apply cap stays off. |
|
||||
| `StreamingIncrementalDesiredAuditEvery` | 0 | Set/order audit stays off. |
|
||||
| `StreamingIncrementalDesiredMaterializationAuditEvery` | 0 | Materialization audit stays off. |
|
||||
| `StreamingIncrementalDesiredAuditInjectMismatch` | 0 | Negative-control injection stays off. |
|
||||
|
||||
The candidate warm-up was a default 30 m/s run with no streaming-configuration CVar overrides. Readback showed `incremental_desired_set=1`; its 12-second movement trace recorded 40 Core deltas, 313 incremental materialization updates, 168 priority-order updates, and 105 priority-order reuses. Baseline readback was `incremental_desired_set=0`. Every timing run also read back the L157/L160 and audit defaults in the table; only the VFHost movement and trace harness arguments were supplied.
|
||||
|
||||
## Measurement and checks
|
||||
|
||||
Five interleaved W8 pairs used the four-strata owner plan, a 12-second move plus 5-second dwell, and eight effective streaming slots. Tick metrics below are medians of per-run values over the 12-second movement window. One warm-up per binary was excluded. I discarded an initial provisional matrix because the longer load averages exceeded the quiet gate; the reported matrix began at **0.45 / 0.69 / 1.47** (1/5/15-minute averages), with no build running.
|
||||
|
||||
| Metric | `c6357b5` | Default-on | Paired result |
|
||||
|---|---:|---:|---|
|
||||
| Tick p99 | 10.07 ms | 7.01 ms | Lower in all 5 pairs |
|
||||
| Worst tick | 13.24 ms | 11.63 ms | Lower in 4/5; one +0.06 ms pair |
|
||||
| Near LOD0–2 readiness | 42.33% | 43.73% | +1.40 pp median |
|
||||
| Moving far holes | 2.04% | 1.85% | −0.19 pp median |
|
||||
| First settled LOD0–2 | 1.677 s | 1.672 s | 5 ms earlier median |
|
||||
|
||||
The separate audit run enabled both audit intervals at 1: **307** desired-set/collision-only comparisons, **307** priority-order comparisons, and **302** materialization comparisons passed; there were zero mismatches. Audit-run metrics are excluded from timing.
|
||||
|
||||
`verify.py fast` passed in **301.7 s** (268 pass, 0 fail, 5 diagnostics; 84 binary runs). VFHost Linux Development builds succeeded for the baseline and candidate with `-NoUBA -MaxParallelActions=4`. Runner `py_compile` and `git diff --check` passed. Fast-tier determinism covers W1/W8/W12. UE W12 movement was not run because the same 12-core VFHost affinity exposes eight TaskGraph background workers/effective streaming slots; movement used W8.
|
||||
|
||||
Changed Unreal `Source/` files:
|
||||
|
||||
- `Source/VoxelForge/Private/VoxelWorld.cpp`
|
||||