Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d654ff861 | ||
|
|
a45ba9bb10 | ||
|
|
a4e214561f | ||
|
|
6cd9e4da76 | ||
|
|
9c6d261d01 | ||
|
|
eb47d72258 | ||
|
|
95bf654fbc | ||
|
|
0065ee68e6 | ||
|
|
ebae71b142 | ||
|
|
81071969fc | ||
|
|
cbbfd46bd3 | ||
|
|
044cd6d9c5 |
No files matched your search
@@ -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.
|
||||
@@ -1522,11 +1522,12 @@ collision ring became walkable at 0.632 s. `voxel.StreamingCoarseFirst=1` with
|
||||
residency by about one third on the staged plan, but null-RHI residency does not prove rendered
|
||||
enclosure. Keep coarse-first opt-in until that view is checked in a rendered owner-plan run.
|
||||
|
||||
The separate `voxel.StreamingStartupApplyBoost` switch stays default-off. At `1`, it temporarily allows
|
||||
12 visible mesh submissions / 14 ms while the initial desired set and walkable ring are incomplete,
|
||||
with a 30 second ceiling; at `0`, the authored budget is unchanged. On the matched legacy-order test,
|
||||
the boost changed full-set readiness from 17.412 s to 17.346 s and removed count-cap frames, but did
|
||||
not improve end-to-end residency. Keep the normal 4 / 2 ms guard for play.
|
||||
The separate `voxel.StreamingStartupApplyBoost` switch is default-on in the current code. At `1`, it
|
||||
temporarily allows up to 24 visible mesh submissions / 14 ms while the initial desired set and
|
||||
walkable ring are incomplete, with a 30 second ceiling; at `0`, the authored budget is unchanged.
|
||||
The historical matched S6 run changed full-set readiness from 17.412 s to 17.346 s and removed
|
||||
count-cap frames, but did not improve end-to-end residency. These current defaults are defined in
|
||||
`Source/VoxelForge/Private/VoxelWorld.cpp`; the S6 measurement remains historical context.
|
||||
|
||||
The S6 trace found the shared pool was not saturated: its 10-slot idle estimate was about 132
|
||||
slot-seconds on legacy order and 73 on gated coarse-first, while game-thread tile applies totaled only
|
||||
@@ -1556,8 +1557,8 @@ by its RAII guard when the worker lambda exits. This still counts queued and run
|
||||
results do not consume an admission slot. `voxel.StreamingLegacyPendingResultCap=1` restores the old
|
||||
`PendingTiles` accounting; the test harness's `NearReserveMode=Legacy` also sets
|
||||
`voxel.StreamingDynamicNearReserve=0`, reproducing the S6 path. `StreamingDynamicNearReserve` now
|
||||
defaults to `1`; `StreamingCoarseFirst` remains `0`, and the apply boost remains `0` in the matched
|
||||
measurement.
|
||||
defaults to `1`; `StreamingCoarseFirst` remains `0`. That historical S6 matched measurement disabled
|
||||
the startup apply boost (`voxel.StreamingStartupApplyBoost=0`); the current default is on (§8.27).
|
||||
|
||||
On the 2,578-key owner SurfaceWorld trace, the old-path control reached the full set in 17.527 s,
|
||||
walkable ring in 0.650 s, and full LOD0 in 2.281 s. Dynamic admission reached them in 7.780 s,
|
||||
|
||||
@@ -10,6 +10,7 @@ then wait for his results / pasted errors. Same for in-editor checks — ask for
|
||||
try to run anything.
|
||||
|
||||
## Read first (in order, load only what the task needs)
|
||||
- **[CURRENT-STATE.md](CURRENT-STATE.md) — current shipping snapshot and verification entry point.**
|
||||
- **[CODEMAP.md](CODEMAP.md) — ALWAYS.** Navigation: what it is, data-flow (§2), symbol→`file:line`
|
||||
index (§3), "I want to change X → go here" (§5), conventions & gotchas (§6). Trust **symbol names
|
||||
over line numbers** (lines drift).
|
||||
|
||||
@@ -388,7 +388,7 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
|
||||
| `AdvanceHeadlessDigMotionTest` / `RecordHeadlessDigMotionFrame` | 3060 / 3217 | Explicit `-voxel.TestDigMotion=1` repro: optional near digs, then repeats underfoot digs and samples `Generator->GetDensityAt` immediately after each edit, on its first subsequent simulated frame, and on every following observation frame. `-voxel.TestDigMotionFieldOverride=1` is a measurement-only prototype that clears `CurrentFloor` and switches CMC to falling when the sampled post-edit density is air; it is inactive by default. The test counts false support, false air and downward velocity beyond one gravity step. `-voxel.TestDigMotionUnderfootDigCount` is clamped to 1–100; `-voxel.TestDigMotionUnderfootIntervalSeconds=0` performs per-frame rapid edits. The accompanying gap audit is scoped to the dig session. |
|
||||
| `MaybeFinishHeadlessStreamingTest` | — | The existing `voxel.TestExitSeconds` diagnostic emits `VoxelForgeStreamingExitDiag` at most every 30 elapsed seconds, including threshold and early-return test flags; no output when the timer is unconfigured. |
|
||||
| `UpdateVisualStalenessAudit` | 5034 | Test-only camera-frustum coverage audit: tracks up to 24 real pre-skirt isosurface points per visible component, then counts frames where no visible tile component covers a previously visible, still-desired point (excluding edited bounds). `voxel.TestVisualGapInjectEarlyHide=1` hides a valid component for three frames and must be caught. `voxel.TestGapAuditLegacyHandoff=1` restores old handoff culling for A/B baselines. |
|
||||
| `ProcessPendingChunks` | 7435 | Drains `CriticalProcessQueue`, `EditedProcessQueue`, `NearProcessQueue`, then `ProcessQueue`; applies under the authored 4-mesh/2 ms budget during normal play and the bounded shared startup budget (12 meshes/14 ms) until the initial desired set and walkable ring are ready. `voxel.StreamingStartupApplyBoost=0` restores the authored budget. `startup_apply_config` traces the actor-read CVar and both budgets; `startup_apply_budget` records activation/end. Reads per-request cancellation before removing its map entry, traces stale/obsolete/aborted discards with epoch and ordinal, then applies via `ApplyTileResult`. Step 1 prequeue still releases here and emits `VFStep1UploadCorpusReleased`. |
|
||||
| `ProcessPendingChunks` | 7435 | Drains `CriticalProcessQueue`, `EditedProcessQueue`, `NearProcessQueue`, then `ProcessQueue`; applies under the authored 4-mesh/2 ms budget during normal play and the default-on bounded startup budget (24 meshes/14 ms, at most 30 s) until the initial desired set and walkable ring are ready. `voxel.StreamingStartupApplyBoost=0` restores the authored budget. `startup_apply_config` traces the actor-read CVar and both budgets; `startup_apply_budget` records activation/end. Reads per-request cancellation before removing its map entry, traces stale/obsolete/aborted discards with epoch and ordinal, then applies via `ApplyTileResult`. Step 1 prequeue still releases here and emits `VFStep1UploadCorpusReleased`. |
|
||||
| `ApplyTileResult` | — | **Shared game-thread apply** for one `FChunkResult` (async drain + sync carve): discards stale generation epochs and keys no longer desired, marks loaded, ingests capture, EMPTY releases the tile's existing component (a re-gen can flip content→empty on band change — old geometry must not linger), else `ApplyMeshToTile`. Returns true iff a visible mesh uploaded (counts the budget). Doesn't touch `PendingTiles` (caller's). |
|
||||
| `FVoxelStackSampler` / `FScopedVoxelStackRegistration` (`VoxelStackSampler.h/.cpp`) | — | **Diagnostic raw stack sampler, off by default.** A sampler thread captures registered generation workers' stacks (`CaptureThreadStackBackTrace`) every N µs and tags each with the tile's LOD. It writes bounded raw PCs to CSV plus a symbolization-free summary under `Saved/`; symbol resolution is deferred until after process exit so the measured process never initializes/calls DbgHelp for this diagnostic. Lifecycle teardown is serialized and registration TLS is session-tagged. Registration is per tile TASK in `LoadTile` (and the export's tasks), never per sample; disabled costs one relaxed atomic load. Game `-voxel.SampleStacks=<us>`, commandlet `-samplestacks[=us]`. |
|
||||
| Room-cache performance (2026-09-13): `VF_FindPlayerFitPointForRoomMemoized` / `GPlayerFitMemo` (`VoxelCaveMorphology.cpp`), `voxel.TileCacheWindow` + `VoxelGenLOD::TileOriginVoxels/TileCellsPerAxis` (`VoxelGenerator.h`), `FChunkSDFSpatialIndex` | — | **Player-fit memo:** exact 97-word key, process-wide, `voxel.PlayerFitMemo`. **Tile cache window:** the room cache covers the requesting tile's footprint plus a fixed collect margin. It is set via `TGuardValue` in `GenerateMesh`/classify, and is window-invariant by construction (the margin does not grow with the tile). **LOD-gated:** fused path at LOD ≥ `voxel.TileCacheWindowMinLOD` (3), op-stack at LOD ≥ `voxel.TileCacheWindowOpStackMinLOD` (4); lower LODs keep the 4-chunk worker window, because the tile window slowed LOD0 evaluation ~6%. Strates whose authored terrain ops exceed the proven 256-voxel extent (or are malformed/unloaded) fall back to the legacy window (`VF_ComputeTerrainOpEnvelope`). `voxel.SpatialIndex` is -1 auto (off at fused LOD0, on elsewhere), 0 or 1. **Spatial index:** ordered XY buckets over cache primitives, exact rejection. The `static_assert` in `VoxelStrateTypes.h` ties `VF_STRATE_PARAM_FIELDS` to `FStrateGenerationParams` (83 fields + 8 B padding). |
|
||||
@@ -396,7 +396,7 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
|
||||
| `RecordTileHash` / `WriteTileHashDump` | — | Headless `-voxel.TileHashDump=<absolute path>` parity instrument. Records the exact `FVoxelMeshData` used by the streaming result, including empty/classified tiles, then writes sorted absolute tile keys, LOD step/cells, content-band limits, settings digest context, and the shared `VoxelGeometryHash` at `EndPlay`. |
|
||||
| `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread. Uses `PreEditDensityCache` when the tile is resident in the bounded local cache, replays the current DiffLayer over that exact procedural lattice, then meshes/applies inline so a carve is visible THIS frame. A cache miss uses normal generation; it captures the unedited lattice if the tile needs meshing, while uniform-tile proofs remain enabled. |
|
||||
| `PreEditDensityCache` / `VoxelForgePreEditDensityCache::BuildEditedGrid` | — | Per-world cache of the pawn's current L0 tile: exact 35³ pre-edit samples, max **171,500 bytes**. It is pruned when the pawn crosses chunk boundaries and cleared on `RegenerateAllChunks`. `BuildEditedGrid` snapshots touched diff chunks once and reproduces `GetDensityAt`'s final `pre-edit - EvaluateMods` operation before the per-call shared-grid mesher path. |
|
||||
| `UpdateChunksAroundPosition` / `GetGenerationAdmissionCount` | 10990 / 10971 | Shared Core/legacy scheduler. Builds the desired set and delta-culls leavers; admits the occupied/support tile plus collision-required LOD0 tiles in the pawn's 3×3 XY ring as `CollisionCritical`. S5 coarse-first admission stays opt-in (`voxel.StreamingCoarseFirst=1`). `voxel.StreamingDynamicNearReserve=1` is now the default: it keeps the authored near reserve while moving or until the stationary walkable ring is ready, then makes those slots available to coarse work. Admission counts submitted queued/running tasks, not completed results waiting in the MPSC result queues; `voxel.StreamingLegacyPendingResultCap=1` restores the old `PendingTiles`-based cap for the negative control. `PendingTiles` still lasts through game-thread apply for duplicate suppression and result bookkeeping. The clipmap stays non-overlapping and load-before-unload / `TransitionHold` reconciliation remains authoritative. Standalone has no mirror of this actor scheduler. §8.10 + §8.26–8.28. |
|
||||
| `UpdateChunksAroundPosition` / `GetGenerationAdmissionCount` | 10990 / 10971 | Shared Core/legacy scheduler. Builds the desired set and delta-culls leavers; admits the occupied/support tile plus collision-required LOD0 tiles in the pawn's 3×3 XY ring as `CollisionCritical`. S5 coarse-first admission stays opt-in (`voxel.StreamingCoarseFirst=1`). `voxel.StreamingDynamicNearReserve=1` is the default: it keeps the authored near reserve while moving or until the stationary walkable ring is ready, then makes those slots available to coarse work. Admission counts submitted queued/running tasks, not completed results waiting in the MPSC result queues; `voxel.StreamingLegacyPendingResultCap=1` restores the old `PendingTiles`-based cap for the negative control. `voxel.StreamingFarApplySupplement=1` is the default L121 lane: after the visible-apply cap, at most one far result is admitted when under half the apply-time budget and startup boost is inactive. `PendingTiles` still lasts through game-thread apply for duplicate suppression and result bookkeeping. The clipmap stays non-overlapping and load-before-unload / `TransitionHold` reconciliation remains authoritative. Standalone has no mirror of this actor scheduler. §8.10 + §8.26–8.28. |
|
||||
| `AdvanceHeadlessCollisionGateStressTest` / `ObserveHeadlessCollisionGateStressTest` / `MaybeFinishHeadlessCollisionGateStressTest` / `MeasureHeadlessCollisionGateStressTerrain` | — | Explicit `-voxel.TestCollisionGateStress=1` harness only. The normal spawn-height cvar offsets from the measured density surface; `voxel.TestCollisionGateStressDeepDrop=1` carves a process-local box shaft from above the top to just above the floor tile, extending along the post-landing route so the pawn does not run into the shaft sidewall, temporarily raises only the transient test DiffLayer brush-radius allowance for that fixture, removes only the local pawn's redundant streaming anchor for this isolated repro, proves every sample in two complete outside-ring level-0 tiles is air before seeding their known-empty status, prewarms the known-empty in-ring column, and asserts that the support walk reaches the in-layout floor outside the aligned level-0 render ring before the pawn drops. The log reports whether the fixed gate retains that otherwise out-of-ring target. Other streaming anchors remain active. The fixture rejects a target or floor outside the layout, a non-air empty-tile proof, or a floor inside the aligned level-0 ring. The `GetDensityAt` column at capsule feet defines the local air/solid surface; the deep-drop column crossing is cached after its first exact scan. Each frame records actor position, velocity, direct foot tile and gate support readiness, gate state, signed feet clearance, and a ≤0.5-voxel sweep between positions. PASS requires no measured clearance below −5 cm; deep-drop gate-on also requires horizontal travel during a hold. Result logs include the terminal density column, first crossing below spawn height, hold count/time, and gated horizontal progress. |
|
||||
| `BuildDesiredTiles` | 6482 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells), then adds streaming anchors and the pending player's collision-only support column before the leaver sweep. It orders from the true local pawn position and builds `CriticalDesiredTiles` for the unresolved support target, occupied/support and heading tiles. Also rebuilds `DesiredTransitionDescendants` (ancestor → desired descendants, including face/edge-touching replacements) for the reconcile pass. |
|
||||
| `CollisionGateDesiredTiles` / `RequestCollisionGateSupportTile` | 5062 | Retains an unresolved support target and up to three level-0 tiles below it across desired-set rebuilds. The additional keys are hidden collision-only tiles, queued ahead of ordinary streaming, and pruned once resolved or outside the active column. |
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# VoxelForge current state
|
||||
|
||||
As of 2026-10-31. L131 is on `assembler/world` as `8107196`; this L132 worktree is on `wip/l132-dispatch-truth`, starting at `ebae71b`. Current-state claims below point to their code or measured report.
|
||||
|
||||
## What ships today
|
||||
|
||||
- **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).
|
||||
- **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
|
||||
|
||||
Headless medians in seconds, five interleaved L80-style measurements per version in each cell. Near-ready means the complete LOD0–2 set; whole-view is the full requested view. Values below are the edited L131 binary; the paired pre-cleanup comparison is in the [L131 report](Standalone/Research/WorldAssemblerM3/Linux131/REPORT.md) and [timing summary](Saved/Linux/l131/timing/summary.json).
|
||||
|
||||
| Stratum | Near-ready W8 / W12 | Whole-view W8 / W12 |
|
||||
|---|---:|---:|
|
||||
| S0 | 0.804 / 0.848 | 1.811 / 1.832 |
|
||||
| S1 | 1.014 / 1.002 | 1.993 / 1.886 |
|
||||
| S2 | 1.060 / 1.012 | 1.960 / 1.865 |
|
||||
| S3 | 0.917 / 0.884 | 1.887 / 1.782 |
|
||||
|
||||
Across 40 paired runs per metric, the mean relative change from pre-cleanup L130 was −0.22% near-ready (95% CI −1.02% to +0.57%) and −0.29% whole-view (−1.04% to +0.46%); neither indicates a measurable slowdown. The machine gate was load 0.81 and 99.2% idle. [L80 protocol](Standalone/Research/WorldAssemblerM3/Linux80/REPORT.md), [runner and raw runs](Standalone/Research/WorldAssemblerM3/Linux131/RunL131Timing.py).
|
||||
|
||||
The latest recorded Linux Unreal start-to-LOD0–2 values are L127's one run per stratum: S0 2.456 s, S1 2.231 s, S2 2.514 s, S3 2.221 s. L130 did not rerun UE startup or movement. Five L127 runs at 30 m/s, 12 s moving plus 5 s dwell, had median moving near coverage **40.22%**, far holes **3.46%**, tick p99 **10.80 ms**, and worst tick **13.27 ms**. After stopping, near coverage reached 100% and far holes reached 0%. These are the latest UE references, not v227 UE measurements. [L127 report](Standalone/Research/WorldAssemblerM3/Linux127/REPORT.md), [L127 movement summary](Saved/Linux/l127/ue/movement-pairs-summary.json), [L127 start summaries](Saved/Linux/l127/ue/merge/start-summary.json).
|
||||
|
||||
## Open gaps
|
||||
|
||||
- Multiplayer world lifecycle is unfinished: seed/layout replication, authoritative modification RPCs, and serialized diff replay for late join are still open. See [ARCHITECTURE §9.5 and §9.7](ARCHITECTURE.md).
|
||||
- Determinism has not been verified on Windows. Linux Clang/GCC agreement does not establish MSVC agreement.
|
||||
- NullRHI measures residency and apply timing, not the first presented pixel or rendered enclosure. L127 also found four low-contrast views among 23 otherwise verified captures. [L121](Standalone/Research/WorldAssemblerM3/Linux121/REPORT.md), [L127](Standalone/Research/WorldAssemblerM3/Linux127/REPORT.md).
|
||||
- UE startup/movement numbers above predate L130's v227 reach change; there is no L130/L131 UE movement comparison.
|
||||
|
||||
## How to verify
|
||||
|
||||
- Build standalone Clang or GCC with [`Standalone/BuildLinux.sh`](Standalone/BuildLinux.sh), for example `bash Standalone/BuildLinux.sh --compiler=clang++-19 --variant=l132-clang --fp-contract=off --jobs=4`. The script pins CPUs 0–11 and applies `prlimit --as=6442450944` itself.
|
||||
- Fast tier: `python3 Standalone/Tools/verify/verify.py fast --out Saved/Linux/l132 --overall-timeout 1740`. It includes the L132 four-pose owner density-dispatch report; per-run logs and the summary stay under `Saved/Linux/l132`.
|
||||
- Canonical output baselines are the v227 files in `Standalone/Tools/verify/baselines/owner-hash/canonical/`. L131 also provides reproducible pre-cleanup identity matrices for owner4 tile hashes and owner4/owner2 canonical hashes under `Standalone/Research/WorldAssemblerM3/Linux131/`.
|
||||
- Timing follows L80: quiet machine, no overlapping build, one warm-up per version, five interleaved runs per stratum and worker count, W8/W12. Each headless run uses `taskset -c 0-11`, `prlimit --as=6442450944`, and a 1,740 s timeout. See [`RunL131Timing.py`](Standalone/Research/WorldAssemblerM3/Linux131/RunL131Timing.py).
|
||||
- Linux host build: `/home/jahni/UnrealEngine/Engine/Build/BatchFiles/Linux/Build.sh VFHostEditor Linux Development -Project=/home/jahni/VFHost/VFHost.uproject -NoUBA -MaxParallelActions=4 -Log=/home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l131/ubt.log`. Run Unreal without `prlimit`.
|
||||
|
||||
## Closed experiments
|
||||
|
||||
These rejected speed/streaming proposals stay closed unless new evidence changes their measured failure. Reports and surviving patches are linked in the [L131 inventory](Standalone/Research/WorldAssemblerM3/Linux131/REPORT.md).
|
||||
|
||||
| Line | Result |
|
||||
|---|---|
|
||||
| L116 tunnel chord bound and z cache | Pair count fell 20.8%, but stayed 1.96× L109 and readiness was 15–30% slower. Patch: [`chord-bound-zcolumn-not-merged.patch`](Standalone/Research/WorldAssemblerM3/Linux116/patches/chord-bound-zcolumn-not-merged.patch), commit `89504ec`; see [report](Standalone/Research/WorldAssemblerM3/Linux116/REPORT.md). |
|
||||
| L118 pair pruning / deferred floor work | Pruned 17–18% of candidate pairs but remained 17.2–25.7% behind L109 near-ready. [Report](Standalone/Research/WorldAssemblerM3/Linux118/REPORT.md); patch: [`l118-designs.patch`](Standalone/Research/WorldAssemblerM3/Linux118/l118-designs.patch). |
|
||||
| L126 floor-call filters | Cut floor calls 20–23% without reducing pair counts; all eight cells missed L109. Candidate source was restored and no standalone patch was kept; see [report](Standalone/Research/WorldAssemblerM3/Linux126/REPORT.md) and commit `01a5094`. |
|
||||
| L128 half-open slab ownership | Ownership audit passed, but readiness was 14.8–28.0% behind L109 and moving coverage dropped. Report and patch `l128-slab-ownership.patch` are in archived commit `7022d02`. |
|
||||
| L129 2 m physical reach | v225's metre/Core-voxel bounds mismatch made the correctly bounded candidate do more work; it was slower and failed UE movement gates. Report and patch `l129-reach-2m.patch` are in archived commit `0d1140c`. |
|
||||
| L120 priority split / extra worker slots | Neither met coverage and tick gates; 10 slots reached a 16.67 ms worst tick against 15.43 ms baseline. [Report](Standalone/Research/WorldAssemblerM3/Linux120/REPORT.md); no production patch retained. |
|
||||
| L122 admission relaxation | Early admission missed p99 or far-hole gates; no behavior retained. [Report](Standalone/Research/WorldAssemblerM3/Linux122/REPORT.md); patch: [`l122-trace.patch`](Standalone/Research/WorldAssemblerM3/Linux122/l122-trace.patch). |
|
||||
| L123 lookahead / completion gate | Lookahead failed p99 and worsened near/far coverage; queue-only also failed p99. [Report](Standalone/Research/WorldAssemblerM3/Linux123/REPORT.md); patch: [`l123-queue-lookahead.patch`](Standalone/Research/WorldAssemblerM3/Linux123/l123-queue-lookahead.patch). |
|
||||
| L124 early LOD0 pages | Optimistic gain stayed below 100 ms except negative cases; no publication/scheduling implementation was made. [Report](Standalone/Research/WorldAssemblerM3/Linux124/REPORT.md). |
|
||||
@@ -494,7 +494,10 @@ namespace VoxelForgeCore
|
||||
// Version 223 projects the floor onto one continuous arc-length profile and groups
|
||||
// dense passage samples into coarse tile candidates. Version 224 keeps the same
|
||||
// continuous floor while relaxing the spline chord tolerance and bounding shell work.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 225;
|
||||
// Version 226 unifies the passage influence reach in metres and converts it to core
|
||||
// voxels for every broadphase and proof bound. Version 227 uses the exact smooth-max
|
||||
// support width/6 when deriving that reach.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 227;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
@@ -594,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;
|
||||
@@ -651,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;
|
||||
@@ -675,6 +751,7 @@ namespace VoxelForgeCore
|
||||
std::vector<WorldAssemblyFeatureV1> features;
|
||||
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
||||
std::vector<WorldAssemblyRoutePointV1> route;
|
||||
WorldAssemblyLiminalFieldPlanV1 liminalField;
|
||||
std::uint64_t contentHash = 0;
|
||||
};
|
||||
|
||||
@@ -888,6 +965,9 @@ 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 ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1;
|
||||
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
||||
@@ -901,7 +981,8 @@ namespace VoxelForgeCore
|
||||
fillPillar = 3,
|
||||
fillDais = 4,
|
||||
fillMonolith = 5,
|
||||
fillPlatform = 6
|
||||
fillPlatform = 6,
|
||||
fillSweptDeckSegment = 7
|
||||
};
|
||||
|
||||
struct ResolvedLayoutStampRequestV1
|
||||
@@ -911,6 +992,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;
|
||||
@@ -930,6 +1017,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{};
|
||||
@@ -1057,6 +1146,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;
|
||||
@@ -1086,6 +1196,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;
|
||||
};
|
||||
@@ -1225,6 +1336,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::fillSweptDeckSegment);
|
||||
}
|
||||
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,6 +684,28 @@ 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
|
||||
@@ -870,6 +978,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 +1041,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 +1276,11 @@ 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)
|
||||
add("stamp_vocabulary_version","resolved stamps require supported vocabulary version 1 through 6");
|
||||
std::vector<std::string> stampIds;
|
||||
for(const auto& stamp:stamps)
|
||||
{
|
||||
@@ -1145,6 +1290,8 @@ 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)
|
||||
||(motif&&stampVocabularyVersion<ResolvedLayoutStampVocabularyVersionV3))
|
||||
add("stamp_kind","stamp has an unsupported kind or orientation");
|
||||
bool validExtents=true;
|
||||
@@ -1159,6 +1306,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 +1376,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 +1432,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 +1698,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 +1750,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);
|
||||
@@ -1619,7 +1887,25 @@ 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;});
|
||||
candidate.stampVocabularyVersion=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)
|
||||
{
|
||||
@@ -1630,6 +1916,8 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
if(!LayoutStampKindValid(request.kind)||request.yawQuarterTurns>3
|
||||
||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 +1936,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,10 +1987,13 @@ 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;
|
||||
@@ -1687,6 +2015,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 +2070,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 +2637,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 +2744,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 +2823,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"
|
||||
|
||||
@@ -33,50 +33,41 @@
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
// L88-only composition split. These switches let the timing harness remove passage work and
|
||||
// resolved boundary-band work independently. They are diagnostic only, never world settings.
|
||||
static bool VF_IsL88DiagnosticSwitchEnabled(const char* Name)
|
||||
static bool VF_IsDiagnosticEnvSwitchEnabled(const char* Name)
|
||||
{
|
||||
const char* Value=std::getenv(Name);
|
||||
return Value&&Value[0]=='1'&&Value[1]=='\0';
|
||||
}
|
||||
|
||||
static bool VF_IsL88PassageCompositionDisabled()
|
||||
{
|
||||
static const bool bDisabled=VF_IsL88DiagnosticSwitchEnabled(
|
||||
"VOXELFORGE_L88_DISABLE_PASSAGES")
|
||||
||VF_IsL88DiagnosticSwitchEnabled("VOXELFORGE_L86_DISABLE_PASSAGE_COMPOSITION");
|
||||
return bDisabled;
|
||||
}
|
||||
|
||||
static bool VF_IsL88BoundaryBandsDisabled()
|
||||
{
|
||||
static const bool bDisabled=VF_IsL88DiagnosticSwitchEnabled(
|
||||
"VOXELFORGE_L88_DISABLE_BOUNDARY_BANDS")
|
||||
||VF_IsL88DiagnosticSwitchEnabled("VOXELFORGE_L86_DISABLE_PASSAGE_COMPOSITION");
|
||||
return bDisabled;
|
||||
}
|
||||
|
||||
static bool VF_IsL88AllowPassageAblationWalkabilityFailure()
|
||||
{
|
||||
static const bool bEnabled=VF_IsL88DiagnosticSwitchEnabled(
|
||||
"VOXELFORGE_L88_ALLOW_PASSAGE_ABLATION_WALKABILITY_FAILURE");
|
||||
return bEnabled;
|
||||
}
|
||||
|
||||
|
||||
static bool VF_IsL98PassagePrepareTraceEnabled()
|
||||
{
|
||||
static const bool bEnabled=VF_IsL88DiagnosticSwitchEnabled(
|
||||
static const bool bEnabled=VF_IsDiagnosticEnvSwitchEnabled(
|
||||
"VOXELFORGE_L98_PASSAGE_PREP_TRACE");
|
||||
return bEnabled;
|
||||
}
|
||||
|
||||
static double VF_L93PassageCarveReachMetres()
|
||||
static bool VF_IsL130PassageReachProofAuditEnabled()
|
||||
{
|
||||
// The generated world uses a fixed bounded tube influence. An explicit diagnostic
|
||||
// option can still select the legacy point-evaluation path for comparisons.
|
||||
return 4.0;
|
||||
static const bool bEnabled=VF_IsDiagnosticEnvSwitchEnabled(
|
||||
"VOXELFORGE_L130_PASSAGE_REACH_PROOF_AUDIT");
|
||||
return bEnabled;
|
||||
}
|
||||
|
||||
static constexpr double VF_PassageShellThicknessMetres=0.75;
|
||||
static constexpr double VF_PassageWallReliefMetres=0.21;
|
||||
static constexpr double VF_PassageCornerBlendMetres=0.50;
|
||||
static constexpr double VF_PassageReachMarginMetres=0.01;
|
||||
// Radial support in metres: shell + maximum relief + the smooth-max support
|
||||
// (corner-blend width / 6) + a 1 cm guard for float and bound rounding.
|
||||
static constexpr double VF_PassageInfluenceReachMetres=
|
||||
VF_PassageShellThicknessMetres+VF_PassageWallReliefMetres
|
||||
+VF_PassageCornerBlendMetres/6.0+VF_PassageReachMarginMetres;
|
||||
|
||||
static double VF_PassageInfluenceReachCoreVoxels(
|
||||
std::uint32_t coreVoxelSizeMicrometres)
|
||||
{
|
||||
return VF_PassageInfluenceReachMetres*1000000.0
|
||||
/std::max<std::uint32_t>(1,coreVoxelSizeMicrometres);
|
||||
}
|
||||
|
||||
enum class EPassageDensityQueryCaller : std::size_t
|
||||
@@ -306,22 +297,18 @@ public:
|
||||
profileStats_(profileStats),profileLod_(profileLod),
|
||||
passageCarveReachMetres_(std::isfinite(passageCarveReachMetres)
|
||||
&&passageCarveReachMetres>=0.0?passageCarveReachMetres
|
||||
:VF_L93PassageCarveReachMetres())
|
||||
:VF_PassageInfluenceReachMetres)
|
||||
{
|
||||
mayContainResolvedBoundaryBand_=shape.ResolvedBoundaryBandsIntersectSubvoxelRange(
|
||||
minimumSubvoxels_[2],maximumSubvoxels_[2]);
|
||||
const bool disablePassageComposition=VF_IsL88PassageCompositionDisabled();
|
||||
if(VF_IsL88BoundaryBandsDisabled())mayContainResolvedBoundaryBand_=false;
|
||||
mayContainTransitionPassageFade_=!disablePassageComposition
|
||||
&&shape.ResolvedTransitionPassageFadeIntersectsTileXY(
|
||||
mayContainTransitionPassageFade_=shape.ResolvedTransitionPassageFadeIntersectsTileXY(
|
||||
minimumSubvoxels_,maximumSubvoxels_);
|
||||
if(!disablePassageComposition)
|
||||
featureIndices_=shape.BuildPassageFeatureTileList(
|
||||
minimumSubvoxels_,maximumSubvoxels_);
|
||||
stampIndices_=shape.BuildStampTileList(minimumSubvoxels_,maximumSubvoxels_);
|
||||
cavernFeatureIndices_=shape.BuildCavernFeatureTileList(
|
||||
minimumSubvoxels_,maximumSubvoxels_);
|
||||
passageRoutes_.assign(shape.ResolvedLayout&&!disablePassageComposition
|
||||
passageRoutes_.assign(shape.ResolvedLayout
|
||||
?shape.ResolvedLayout->passages.size():0,0);
|
||||
passageRouteSegments_.resize(passageRoutes_.size());
|
||||
for(const std::uint32_t featureIndex:featureIndices_)
|
||||
@@ -1204,8 +1191,8 @@ public:
|
||||
if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X)
|
||||
||!std::isfinite(Y)||!std::isfinite(Z))return density;
|
||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
constexpr double FloorShellThicknessMetres=0.75;
|
||||
constexpr double WallReliefMetres=0.21;
|
||||
constexpr double FloorShellThicknessMetres=VF_PassageShellThicknessMetres;
|
||||
constexpr double WallReliefMetres=VF_PassageWallReliefMetres;
|
||||
constexpr double OpenAirSideWallHeightMetres=1.0;
|
||||
constexpr double MouthShellBlendMetres=5.0;
|
||||
const double qPerMetre=Q*1000000.0/
|
||||
@@ -1536,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;
|
||||
@@ -1544,12 +1533,8 @@ public:
|
||||
// the generic stratum transition distance creates a discontinuity at a
|
||||
// mouth that crosses a wide transition band. Keep that opt-in field whole;
|
||||
// its host-rock value already supplies the ceiling and bedrock.
|
||||
const bool disablePassageComposition=!nativeLattice
|
||||
&&VF_IsL88PassageCompositionDisabled();
|
||||
const bool disableBoundaryBands=!nativeLattice
|
||||
&&VF_IsL88BoundaryBandsDisabled();
|
||||
float boundary=graphDensity;
|
||||
if(!nativeLattice&&!disableBoundaryBands)
|
||||
if(!nativeLattice)
|
||||
{
|
||||
VoxelForgeCore::Linux77Profiler::FScopedPhase l77BandsPhase(
|
||||
VoxelForgeCore::Linux77Profiler::EPhase::Bands);
|
||||
@@ -1563,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=[]
|
||||
{
|
||||
@@ -1578,7 +1565,6 @@ public:
|
||||
?bound->Resolved->passageCapabilities.hasNativeCaveField
|
||||
:bound->Resolved->passageCapabilities.hasNativeLatticeField))
|
||||
{
|
||||
if(!disablePassageComposition)
|
||||
{
|
||||
VoxelForgeCore::Linux77Profiler::FScopedPhase l77PassagePhase(
|
||||
VoxelForgeCore::Linux77Profiler::EPhase::Passage);
|
||||
@@ -1609,7 +1595,6 @@ public:
|
||||
ELegacyFieldTerm::ComposerPassage);
|
||||
final=ApplyResolvedCavernFloorCaps(stamped,X,Y,Z);
|
||||
}
|
||||
if(!disablePassageComposition)
|
||||
{
|
||||
VoxelForgeCore::Linux77Profiler::FScopedPhase l77PassagePhase(
|
||||
VoxelForgeCore::Linux77Profiler::EPhase::Passage);
|
||||
@@ -1617,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);
|
||||
@@ -1630,8 +1621,8 @@ public:
|
||||
{
|
||||
constexpr double Q=static_cast<double>(
|
||||
VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
const double qPerMetre=Q*1000000.0/
|
||||
std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
const double reachCoreVoxels=VF_PassageInfluenceReachCoreVoxels(
|
||||
ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
const auto overlaps=[&](const double low[3],const double high[3])
|
||||
{
|
||||
for(int axis=0;axis<3;++axis)
|
||||
@@ -1648,7 +1639,8 @@ public:
|
||||
const auto& p3=passage.controlPoints[
|
||||
std::min(segment+2,passage.controlPoints.size()-1)];
|
||||
const double radius=std::max(static_cast<double>(a.radiusSubvoxels),
|
||||
static_cast<double>(b.radiusSubvoxels))/Q+1.0;
|
||||
static_cast<double>(b.radiusSubvoxels))/Q
|
||||
+reachCoreVoxels+0.01;
|
||||
double low[3]{},high[3]{};
|
||||
for(int axis=0;axis<3;++axis)
|
||||
{
|
||||
@@ -2313,20 +2305,10 @@ public:
|
||||
Substage=Clock::now();
|
||||
const bool bPassageRoutesValid=!bValidatePassageRoutes||ValidatePassageRoutes();
|
||||
if(!bPassageRoutesValid)
|
||||
{
|
||||
if(VF_IsL88PassageCompositionDisabled()
|
||||
&&!VF_IsL88BoundaryBandsDisabled()
|
||||
&&VF_IsL88AllowPassageAblationWalkabilityFailure())
|
||||
{
|
||||
std::printf("[L88PassageAblation] route walkability failed as expected; "
|
||||
"continuing only to measure passage-off/bands-on readiness\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
Error="compiled shape world contains a solid passage centreline sample";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Stats.PassageValidationSeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
|
||||
Built=true;
|
||||
if(bIncludeFarWorld)BuildFarWorld();
|
||||
@@ -3499,7 +3481,7 @@ private:
|
||||
double centerZSubvoxels,double queryZSubvoxels,double floorDepthSubvoxels,
|
||||
double qPerMetre)
|
||||
{
|
||||
constexpr double CornerBlendMetres=0.50;
|
||||
constexpr double CornerBlendMetres=VF_PassageCornerBlendMetres;
|
||||
const float floorDistance=static_cast<float>(centerZSubvoxels
|
||||
-floorDepthSubvoxels-queryZSubvoxels);
|
||||
return SmoothMaxDistance(roundDistanceSubvoxels,floorDistance,
|
||||
@@ -3563,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;
|
||||
@@ -3728,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={
|
||||
@@ -4811,13 +4810,9 @@ private:
|
||||
PassageNearestNodes.resize(ResolvedLayout?ResolvedLayout->passages.size():0);
|
||||
if(!ResolvedLayout||ResolvedLayout->passages.empty())return;
|
||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
constexpr double PassageShellMetres=0.75;
|
||||
constexpr double ReliefMetres=0.21;
|
||||
constexpr double SweepReachMetres=4.0;
|
||||
constexpr double CornerBlendMetres=0.50;
|
||||
constexpr std::size_t CandidateFineSamplesPerSpan=5;
|
||||
const double requiredReach=std::max(SweepReachMetres+ReliefMetres
|
||||
+CornerBlendMetres*0.25,PassageShellMetres+ReliefMetres);
|
||||
const double requiredReachCoreVoxels=VF_PassageInfluenceReachCoreVoxels(
|
||||
ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();
|
||||
++passageIndex)
|
||||
{
|
||||
@@ -4854,7 +4849,7 @@ private:
|
||||
routeMinimum[axis]=std::min(routeMinimum[axis],localMinimum[axis]);
|
||||
routeMaximum[axis]=std::max(routeMaximum[axis],localMaximum[axis]);
|
||||
}
|
||||
const double localExtent=requiredReach
|
||||
const double localExtent=requiredReachCoreVoxels
|
||||
+std::max(prepared.radiusA,prepared.radiusB)/Q+0.01;
|
||||
FPassageProofBound proof;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
@@ -4872,7 +4867,8 @@ private:
|
||||
std::ceil((localMaximum[axis]+localExtent)*Q));
|
||||
}
|
||||
}
|
||||
const double routeExtent=maximumRadiusSubvoxels/Q+requiredReach+0.01;
|
||||
const double routeExtent=maximumRadiusSubvoxels/Q
|
||||
+requiredReachCoreVoxels+0.01;
|
||||
const double tileExtent=routeExtent;
|
||||
std::array<std::int64_t,3> featureMinimum{},featureMaximum{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
@@ -4899,6 +4895,113 @@ private:
|
||||
BuildPassageFeatureNode(0,PassageFeatureOrder.size());
|
||||
if(!PassageProofBounds.empty())
|
||||
BuildPassageProofNode(0,PassageProofBounds.size());
|
||||
|
||||
if(VF_IsL130PassageReachProofAuditEnabled())
|
||||
{
|
||||
constexpr double Q=static_cast<double>(
|
||||
VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
constexpr std::array<std::array<double,2>,8> RadialDirections={{
|
||||
{1.0,0.0},{0.7071067811865475,0.7071067811865475},
|
||||
{0.0,1.0},{-0.7071067811865475,0.7071067811865475},
|
||||
{-1.0,0.0},{-0.7071067811865475,-0.7071067811865475},
|
||||
{0.0,-1.0},{0.7071067811865475,-0.7071067811865475}}};
|
||||
constexpr std::array<double,2> CrestHeightsMetres={0.625,0.875};
|
||||
const double qPerMetre=Q*1000000.0/
|
||||
std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
const std::array<double,6> shellOffsetsMetres={0.70,0.78,0.86,0.94,
|
||||
VF_PassageInfluenceReachMetres-0.005,
|
||||
VF_PassageInfluenceReachMetres+0.005};
|
||||
std::uint64_t sampled=0,signRelevant=0,proofMisses=0,featureMisses=0;
|
||||
for(std::size_t passageIndex=0;
|
||||
passageIndex<PassageSegmentData.size();++passageIndex)
|
||||
{
|
||||
const auto& segments=PassageSegmentData[passageIndex];
|
||||
if(segments.empty())continue;
|
||||
const auto& constants=PassageComposerConstants[passageIndex];
|
||||
const std::size_t stride=std::max<std::size_t>(1,
|
||||
(segments.size()+127)/128);
|
||||
for(std::size_t segmentIndex=0;segmentIndex<segments.size();
|
||||
segmentIndex+=stride)
|
||||
{
|
||||
const auto& segment=segments[segmentIndex];
|
||||
if(segment.lengthSq<=1.0e-12)continue;
|
||||
constexpr double T=0.5;
|
||||
const double centerX=segment.a[0]+T*segment.delta[0];
|
||||
const double centerY=segment.a[1]+T*segment.delta[1];
|
||||
const double centerZ=segment.a[2]+T*segment.delta[2];
|
||||
const double radius=segment.radiusA+T*segment.radiusDelta;
|
||||
const double floorDepth=PassageFlatFloorDepthSubvoxels(
|
||||
constants,segmentIndex,T,radius,qPerMetre);
|
||||
const double floorCenterZ=PassageFlatFloorCenterZSubvoxels(
|
||||
constants,segments,segmentIndex,T,segment,centerX,centerY,
|
||||
floorDepth,qPerMetre);
|
||||
for(const double crestHeightMetres:CrestHeightsMetres)
|
||||
{
|
||||
const double sampleZ=floorCenterZ-floorDepth
|
||||
+crestHeightMetres*qPerMetre;
|
||||
const double verticalDelta=sampleZ-centerZ;
|
||||
for(const double shellOffsetMetres:shellOffsetsMetres)
|
||||
{
|
||||
const double radialDistance=radius
|
||||
+shellOffsetMetres*qPerMetre;
|
||||
if(radialDistance*radialDistance
|
||||
<=verticalDelta*verticalDelta)continue;
|
||||
const double horizontalDistance=std::sqrt(
|
||||
radialDistance*radialDistance
|
||||
-verticalDelta*verticalDelta);
|
||||
for(const auto& direction:RadialDirections)
|
||||
{
|
||||
const double sampleQ[3]={
|
||||
centerX+direction[0]*horizontalDistance,
|
||||
centerY+direction[1]*horizontalDistance,sampleZ};
|
||||
const double sampleVoxel[3]={sampleQ[0]/Q,
|
||||
sampleQ[1]/Q,sampleQ[2]/Q};
|
||||
const float contribution=ApplyResolvedStratePassageCarves(
|
||||
0.0f,static_cast<float>(sampleVoxel[0]),
|
||||
static_cast<float>(sampleVoxel[1]),
|
||||
static_cast<float>(sampleVoxel[2]));
|
||||
++sampled;
|
||||
if(std::abs(contribution)<=1.0e-6f)continue;
|
||||
++signRelevant;
|
||||
if(PassageProofNodes.empty()
|
||||
||!PassageProofBoundsOverlapNode(0,sampleVoxel,
|
||||
sampleVoxel))
|
||||
++proofMisses;
|
||||
std::array<std::int64_t,3> sampleMinimum{},sampleMaximum{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
sampleMinimum[axis]=static_cast<std::int64_t>(
|
||||
std::floor(sampleQ[axis]));
|
||||
sampleMaximum[axis]=static_cast<std::int64_t>(
|
||||
std::ceil(sampleQ[axis]));
|
||||
}
|
||||
const auto candidates=BuildPassageFeatureTileList(
|
||||
sampleMinimum,sampleMaximum);
|
||||
const bool featureContainsSample=std::any_of(
|
||||
candidates.begin(),candidates.end(),
|
||||
[&](std::uint32_t featureIndex)
|
||||
{
|
||||
return featureIndex<PassageTileFeatures.size()
|
||||
&&PassageTileFeatures[featureIndex].passageIndex
|
||||
==passageIndex;
|
||||
});
|
||||
if(!featureContainsSample)++featureMisses;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool auditPassed=sampled>0&&signRelevant>0
|
||||
&&proofMisses==0&&featureMisses==0;
|
||||
std::printf("[L130PassageReachProofAudit] reach_m=%.3f sampled=%llu sign_relevant=%llu proof_misses=%llu feature_misses=%llu status=%s\n",
|
||||
VF_PassageInfluenceReachMetres,
|
||||
static_cast<unsigned long long>(sampled),
|
||||
static_cast<unsigned long long>(signRelevant),
|
||||
static_cast<unsigned long long>(proofMisses),
|
||||
static_cast<unsigned long long>(featureMisses),
|
||||
auditPassed?"PASS":"FAIL");
|
||||
if(!auditPassed)std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
bool PassageProofBoundsOverlapNode(int nodeIndex,const double minimum[3],
|
||||
@@ -5086,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
|
||||
{
|
||||
@@ -7040,6 +7157,117 @@ private:
|
||||
const float top=std::max(StampEllipsoidSdf(x,y,z,halfSize),-z);
|
||||
return SmoothMinDistance(lower,top,round*2.0f);
|
||||
}
|
||||
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];
|
||||
@@ -7076,6 +7304,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)
|
||||
@@ -7087,6 +7349,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)
|
||||
{
|
||||
@@ -7171,7 +7442,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;
|
||||
@@ -7188,6 +7461,11 @@ 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;
|
||||
@@ -7205,6 +7483,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
|
||||
{
|
||||
@@ -11442,10 +11868,9 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||||
const FIntVector& origin,std::int32_t step,std::int32_t cells,std::uint32_t seed)
|
||||
{
|
||||
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
constexpr double InfluenceMetres=4.0;
|
||||
constexpr double OpenAirSideWallHeightMetres=1.0;
|
||||
if(!shape_||step<=0||cells<=0||!std::isfinite(passageCarveReachMetres_)
|
||||
||std::abs(passageCarveReachMetres_-InfluenceMetres)>1.0e-9)return;
|
||||
||std::abs(passageCarveReachMetres_-VF_PassageInfluenceReachMetres)>1.0e-9)return;
|
||||
const bool profile=profileStats_!=nullptr;
|
||||
const auto prepareStart=profile?std::chrono::steady_clock::now()
|
||||
:std::chrono::steady_clock::time_point{};
|
||||
@@ -11459,7 +11884,7 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||||
sweep->dimension=cells+3;
|
||||
const std::size_t gridSampleCount=static_cast<std::size_t>(sweep->dimension)
|
||||
*static_cast<std::size_t>(sweep->dimension)*static_cast<std::size_t>(sweep->dimension);
|
||||
sweep->influenceSubvoxels=InfluenceMetres*static_cast<double>(Q)*1000000.0
|
||||
sweep->influenceSubvoxels=passageCarveReachMetres_*static_cast<double>(Q)*1000000.0
|
||||
/std::max<std::uint32_t>(1,shape_->ResolvedLayout
|
||||
?shape_->ResolvedLayout->coreVoxelSizeMicrometres:250000);
|
||||
sweep->passageToSweep.assign(passageRouteSegments_.size(),-1);
|
||||
@@ -11473,7 +11898,7 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||||
const double qPerMetre=static_cast<double>(Q)*1000000.0/
|
||||
std::max<std::uint32_t>(1,shape_->ResolvedLayout
|
||||
?shape_->ResolvedLayout->coreVoxelSizeMicrometres:250000);
|
||||
const double shellThickness=0.75*qPerMetre;
|
||||
const double shellThickness=VF_PassageShellThicknessMetres*qPerMetre;
|
||||
const double stampedOutsideDistance=sweep->influenceSubvoxels+1.0;
|
||||
constexpr double MouthShellBlendMetres=5.0;
|
||||
const double wallReliefShell=0.41*qPerMetre;
|
||||
@@ -11633,7 +12058,7 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||||
// more than cornerWidth/6. Skip its work for pairs outside this exact
|
||||
// support, with a small subvoxel guard for float rounding. The final
|
||||
// shell union only consumes negative distances.
|
||||
constexpr double CornerBlendMetres=0.50;
|
||||
constexpr double CornerBlendMetres=VF_PassageCornerBlendMetres;
|
||||
const float roundDistanceFloat=static_cast<float>(signedDistance);
|
||||
const float floorDistanceFloat=static_cast<float>(floorCenterZ-floorDepth-qz);
|
||||
const double flatDistanceLowerBound=std::max(
|
||||
|
||||
@@ -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,37 @@
|
||||
# VoxelForge — open read-only review: what should be done and focused on now?
|
||||
|
||||
You are an outside reviewer. **Do not edit, create or delete any file, and do not build or run anything.** Read the
|
||||
repository and its reports and reason. Be thorough; mark each claim as [CODE] (with file:line), [MEASURED] (from a
|
||||
report), or [INFERENCE].
|
||||
|
||||
## The project, in the owner's words and goals
|
||||
|
||||
- VoxelForge is a UE5 density-field voxel plugin for a **multiplayer** game. The world is made of stacked "strates"
|
||||
(layers) linked by tunnels. The standalone C++ generator is "Core" (`Standalone/Core`), and it is also compiled into
|
||||
Unreal. Read `CLAUDE.md`, `CODEMAP.md`, `ARCHITECTURE.md`, `GDD.md`, `COMPOSER-NOTES.md` and `fable-idea.md` at the
|
||||
plugin root for the game and design intent. The research reports are in
|
||||
`Standalone/Research/WorldAssemblerM3/Linux*/REPORT.md` and the `Astra*/` reviews.
|
||||
- Owner goals stated over time:
|
||||
- the near terrain (LOD0–2) ready in ~1 s on arrival, then far terrain keeps filling in at low priority;
|
||||
- same seed gives the same world on every machine (byte-identity with older versions is NOT required);
|
||||
- smooth for VR and high-speed travel;
|
||||
- all generators (surface, floating islands, caves, dreamcore/liminal, etc.) working in Core;
|
||||
- the system should eventually *invent* strates and worlds from tagged authored content plus a seed;
|
||||
- keep things simple and avoid over-complication.
|
||||
- The current main line is branch `assembler/world`.
|
||||
|
||||
## Your task
|
||||
|
||||
Look at the whole: code, reports, design docs, and the direction of the last few dozen rounds. Tell the owner what
|
||||
should be done and focused on now, whatever it is. That may be performance, features, generation quality, gameplay
|
||||
foundations, architecture, technical debt, tooling, process, or stopping something. Don't assume the recent focus is
|
||||
the right one; question it if the evidence says so.
|
||||
|
||||
Answer:
|
||||
1. **Where the project actually is**: what works, what's fragile, what's missing for the game to exist.
|
||||
2. **The top 3–5 things to focus on now**, ranked, each with why, a rough size, what "done" looks like, and how to
|
||||
tell it worked.
|
||||
3. **What to stop or deprioritise**, and why.
|
||||
4. **Risks or debts** that will bite later if ignored.
|
||||
|
||||
Write plain Markdown, at most ~1,500 words, readable by a non-specialist owner (explain jargon briefly).
|
||||
@@ -0,0 +1,72 @@
|
||||
[INFERENCE] **Focus now on a small, persistent multiplayer descent, supported by end-to-end Unreal performance work.** VoxelForge has substantial terrain technology. The larger uncertainty is whether it delivers a reliable, interesting shared expedition. Another sequence of tunnel optimizations will not resolve that.
|
||||
|
||||
Read-only review of `assembler/world` at `044cd6d`; no files changed and no builds, tests, or project execution. [CODE] identifies repository evidence; design-document references establish intent, not implementation. [MEASURED] means reported results, not independently reproduced measurements.
|
||||
|
||||
**1. Where the project actually is**
|
||||
|
||||
[CODE] **Core is real and integrated, but generator implementation remains mixed.** Unreal compiles the canonical standalone sources through validated wrappers. Core dispatches to surface, floating-island and native graph evaluators, with a legacy-generator fallback when appropriate. That is useful coverage, but does not mean every family has a complete native implementation. See [VoxelForgeCore.Build.cs:15](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForgeCore/VoxelForgeCore.Build.cs:15) and [ShapeWorldInternal.h:600](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:600).
|
||||
|
||||
[MEASURED] **Passage correctness and speed have improved substantially.** L109 simplified tunnels and removed support-restoration machinery, reducing floating-island and tunnel-network near-generation times by approximately 31–38%. L125 reports 21,425/21,425 open route samples, a maximum sampled floor offset of 7.884 mm, and matching cross-compiler/worker hashes. These are meaningful engineering achievements. [L109](Standalone/Research/WorldAssemblerM3/Linux109/REPORT.md), [L125](Standalone/Research/WorldAssemblerM3/Linux125/REPORT.md).
|
||||
|
||||
[MEASURED] **The one-second player experience is not achieved.** L125’s standalone near-ready medians were approximately 0.78–1.03 seconds. L127’s Unreal startup took 2.22–2.51 seconds; at 30 m/s, median near coverage fell from 43.36% to 40.22% after the smooth-tunnel merge. Both versions recovered after stopping. L130 found no measurable timing improvement and skipped Unreal movement. These are different workloads; subtracting their times would not isolate a particular overhead. [L125](Standalone/Research/WorldAssemblerM3/Linux125/REPORT.md), [L127](Standalone/Research/WorldAssemblerM3/Linux127/REPORT.md), [L130](Standalone/Research/WorldAssemblerM3/Linux130/REPORT.md).
|
||||
|
||||
[CODE] **Multiplayer foundations exist; the multiplayer lifecycle remains unfinished here.** There are season hashes, local terrain-edit entry points, and streaming-anchor infrastructure. The architecture explicitly leaves join replication, authoritative edit messages, late-join snapshots and networked season changes open. The edit entry point directly dispatches to local modification paths. [VoxelWorld.h:574](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelWorld.h:574), [VoxelWorld.cpp:15771](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:15771), [ARCHITECTURE.md:2408](/home/jahni/VoxelM/Plugins/VoxelForge/ARCHITECTURE.md:2408).
|
||||
|
||||
[CODE] **The composer is more than a proposal, but less than the full content vision.** It rolls candidates, measures them and rejects failures. The intended system additionally combines tagged geometry, materials, decorations, creatures and audio; theme coherence remains an open design item. [VoxelSeasonManifest.cpp:2523](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelSeasonManifest.cpp:2523), [COMPOSER-NOTES.md:1512](/home/jahni/VoxelM/Plugins/VoxelForge/COMPOSER-NOTES.md:1512).
|
||||
|
||||
[INFERENCE] **This repository demonstrates a capable terrain platform, not yet a validated game loop.** I cannot establish what gameplay exists outside the plugin. The GDD itself identifies “Is the descent fun?” as its first unresolved question. [GDD.md:441](/home/jahni/VoxelM/Plugins/VoxelForge/GDD.md:441).
|
||||
|
||||
**2. Ranked priorities**
|
||||
|
||||
All sizes below are [INFERENCE]: rough focused engineering estimates, excluding substantial art production.
|
||||
|
||||
**1 — Deliver one multiplayer expedition that survives reconnecting. Size: 3–6 weeks.**
|
||||
|
||||
[INFERENCE] Use two contrasting strates and their connecting tunnel. Two players descend, find one destination, uncover or collect something, return, and retain their changes after restart. Include a third player joining after terrain has changed. Avoid expanding immediately into the city economy or community backend.
|
||||
|
||||
[INFERENCE] The necessary foundation is a server-approved world identity, ordered authoritative edits, a saved snapshot plus subsequent edits, and collision around separated players. Give edits stable identifiers so retransmission cannot apply them twice. Reuse the existing local edit and anchor systems.
|
||||
|
||||
[INFERENCE] **Done:** host/client edits agree; a late joiner reconstructs the same terrain; restart preserves it; incompatible world versions fail clearly; remote players have ground when away from the host.
|
||||
|
||||
[INFERENCE] **Success:** a complete shared expedition without terrain disagreement, lost progress or collision failures—and players can explain what made them want to descend again. This tests the project’s central assumptions much earlier than a complete RPG would.
|
||||
|
||||
**2 — Close the Unreal arrival and movement gap. Size: 1–3 weeks initially.**
|
||||
|
||||
[MEASURED] L122 found idle generation capacity while admission was blocked; L123 measured approximately 19 ms from region request to game-thread observation despite sub-millisecond median compilation. Simple lookahead and admission relaxations failed their combined coverage/frame-time gates. L121’s modest extra far-apply lane did improve far coverage. [L121](Standalone/Research/WorldAssemblerM3/Linux121/REPORT.md), [L122](Standalone/Research/WorldAssemblerM3/Linux122/REPORT.md), [L123](Standalone/Research/WorldAssemblerM3/Linux123/REPORT.md).
|
||||
|
||||
[INFERENCE] First establish one current-build, rendered Unreal baseline on intended hardware. Measure arrival through **visible terrain and usable collision**, then walking, 30 m/s travel, turns, teleporting and edits. Record near coverage, far gaps, frame-time spikes and memory separately. NullRHI measurements, which omit rendering, cannot certify VR comfort.
|
||||
|
||||
[INFERENCE] Investigate region completion, admission and publication before another broad density rewrite. A game-thread continuation is a candidate experiment, not a proven solution; keep world mutation on its owning thread.
|
||||
|
||||
[INFERENCE] **Done:** approximately one-second LOD0–2 readiness on the agreed hardware, safe collision, steadily filling far terrain, and an explicit movement-coverage target without worsening frame spikes. **Success:** repeated rendered runs meet those limits, including with remote-player collision work. Do not redefine “ready” as only the nearest handful of tiles.
|
||||
|
||||
**3 — Turn generator breadth into playable variety. Size: 2–4 weeks for the first content slice.**
|
||||
|
||||
[INFERENCE] Establish a compact capability matrix for each family: Core dispatch, entrances/exits, interior connectivity, edits, materials/content, far terrain and performance. Include dreamcore/liminal explicitly; motif generation alone does not establish a convincing anomalous place.
|
||||
|
||||
[MEASURED] L58 illustrates the distinction: its lattice route passed reachability checks, but the review described a corkscrew tunnel rather than the intended open vertical shaft. Technical validity and identity are separate acceptance criteria. [L58](Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md).
|
||||
|
||||
[INFERENCE] Extend the existing offline composer with a small tagged library: a grounded cave, an island environment and one liminal identity, each with coherent materials, sound and a destination. Preserve owner review before publishing a season.
|
||||
|
||||
[INFERENCE] **Done:** several seeds produce recognizably different, coherent places with findable progression and usable encounter spaces. **Success:** players navigate them without developer guidance and distinguish their identities without reading labels. Expand families after this works.
|
||||
|
||||
**4 — Consolidate the evidence and remove obsolete complexity incrementally. Size: 2–4 days, then ongoing.**
|
||||
|
||||
[INFERENCE] Maintain one short current-state page: shipped behavior, open defects, active baseline, hardware and next acceptance test. Keep historical reports as history. Remove rejected experimental paths when they no longer serve a specific comparison; extract stable responsibilities when touching oversized files.
|
||||
|
||||
[INFERENCE] **Done:** a contributor can identify the current evaluator, runtime path, multiplayer gaps and performance limits without reconstructing 130 rounds. **Success:** new work no longer repeats rejected experiments or treats old measurements as current.
|
||||
|
||||
**3. Stop or deprioritise**
|
||||
|
||||
- [INFERENCE] **Pause open-ended tunnel micro-optimization.** L125/L126 missed the older speed bar; L130’s reach correction produced no measurable speedup. Keep correctness fixes, but require a current cost account and a bounded experiment before further optimization. [MEASURED: L126](Standalone/Research/WorldAssemblerM3/Linux126/REPORT.md), [L130](Standalone/Research/WorldAssemblerM3/Linux130/REPORT.md).
|
||||
- [INFERENCE] **Do not implement Astra recommendations mechanically.** Astra1’s support-heavy model predates L109’s simplification. Astra2/3 offer hypotheses, not measured wins for today’s code. Re-profile before adopting them.
|
||||
- [INFERENCE] **Defer GPU generation, a mesher replacement, elaborate discovery systems and large-scale backend work.** None currently resolves the missing shared expedition.
|
||||
- [INFERENCE] **Stop requiring old-version byte identity for deliberate generation changes.** Preserve peer agreement within a version and explicit versioned saves; permit reviewed geometry improvements.
|
||||
|
||||
**4. Debts that will bite later**
|
||||
|
||||
[INFERENCE] **Determinism needs a deployment contract.** Linux Clang/GCC and worker-count agreement is strong evidence, not proof across every intended machine. Include Windows/compiler/CPU combinations actually supported. World identity must cover evaluator, content, layout, scale and edit state—not merely seed. [MEASURED: L130](Standalone/Research/WorldAssemblerM3/Linux130/REPORT.md).
|
||||
|
||||
[CODE] **Scale and fallback complexity remain active integration risks.** Core is opt-in, and runtime queries bridge different coordinate conventions. [VoxelWorld.h:300](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelWorld.h:300), [VoxelWorld.cpp:15612](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:15612). [INFERENCE] Test edits, content placement and collision across those boundaries before broadening authoring.
|
||||
|
||||
[INFERENCE] **A passing route audit does not prove a whole strate is enjoyable, connected or suitable for six-player combat.** Retain geometric checks, but add actual traversal, destination discovery and encounter-space evaluation. Those are now more valuable than another polished tunnel benchmark.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Linux130 — passage reach
|
||||
|
||||
On `wip/l130-reach-units` from `b96dd73`, passage influence is now **1.0533 m**: 0.75 m shell + 0.21 m relief + 0.50 m smooth-max support (`width/6`) + 0.01 m margin. One metres-to-core-voxels conversion feeds proof, feature, fallback and off-grid bounds; the sweep cutoff uses the same metre value. The dead 4 m cutoff is removed. Evaluator version is 227.
|
||||
|
||||
The debug shell/crest audit sampled 25,728 points, including 5 mm inside and outside the reach; 18,993 had sign-relevant passage contribution. Proof and feature bounds missed none. The nine-station floor probe still peaks at **7.884 mm**, matching L125. The 23 L125 views and six L129 mouth close-ups are byte-identical. Fast verification, walkability and shell audits pass; the seam audit found 0 mismatches across 4 pairs / 4,900 shared samples.
|
||||
|
||||
Clang/GCC W1/W8/W12 hashes agree across all 18 runs. Versus v225, hashes changed for owner4 full-detail, whole-view and LOD0–3; owner2 surface-world and floating-islands whole-view and LOD2–3. Exact-query hashes stayed unchanged. The v227 canonical baselines are recorded; changed hashes reflect the shorter density influence halo, while sampled sign-relevant bounds and captured surfaces remain covered.
|
||||
|
||||
L80 timing used 10 interleaved pairs per stratum/worker count. Across the eight case medians, the weighted deltas were +0.33% for LOD0–2 and +0.31% whole-view. Across 80 paired runs, mean deltas were +0.26% (95% interval −0.44% to +0.96%) and +0.47% (−0.06% to +1.00%); both intervals include zero. No measurable timing change; UE movement was skipped. The Linux UE host build succeeded in 76.30 s with `-NoUBA -MaxParallelActions=4`, without `prlimit`. `ARCHITECTURE.md` and `CODEMAP.md` had no note for this optimized influence reach, so they were unchanged.
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run Clang/GCC owner hashes at W1/W8/W12 and compare with b96dd73."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l130/hashes"
|
||||
BASELINE = json.loads((ROOT / "Saved/Linux/l125/hashes/summary.json").read_text())
|
||||
DATASETS = {
|
||||
"owner4": ROOT / "Standalone/data/owner_world.json",
|
||||
"owner2": ROOT / "Standalone/data/regression/owner_world_2strate_l61.json",
|
||||
}
|
||||
RECIPES = {"owner4": ("surface-world",),
|
||||
"owner2": ("surface-world", "floating-islands")}
|
||||
BINARIES = {
|
||||
"clang": ROOT / "Saved/Linux/l130/bin/VoxelForgeCoreHeadless-l130-clang",
|
||||
"gcc": ROOT / "Saved/Linux/l130/bin/VoxelForgeCoreHeadless-l130-gcc",
|
||||
}
|
||||
HASH_RE = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=(\S+)$", re.M)
|
||||
|
||||
|
||||
def run(dataset: str, recipe: str, workers: int, compiler: str) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
label = f"{dataset}-{recipe}-w{workers}-{compiler}"
|
||||
log = OUT / f"{label}.log"
|
||||
layout = OUT / f"{label}.layout.json"
|
||||
command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARIES[compiler]), str(DATASETS[dataset]),
|
||||
f"--workers={workers}", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=8", f"--recipe={recipe}",
|
||||
"--stream-hash", f"--resolved-layout-output={layout}"]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
hashes = HASH_RE.findall(output)
|
||||
if result.returncode or len(hashes) != 12:
|
||||
raise SystemExit(f"hash run failed: {label}; exit={result.returncode} "
|
||||
f"hashes={len(hashes)} log={log}")
|
||||
stamp = STAMP_RE.search(output)
|
||||
row = {"dataset": dataset, "recipe": recipe, "workers": workers,
|
||||
"compiler": compiler, "exit_code": result.returncode, "hashes": hashes,
|
||||
"source_stamp": stamp.group(1) if stamp else None,
|
||||
"elapsed_s": round(time.monotonic() - started, 3),
|
||||
"log": str(log.relative_to(ROOT))}
|
||||
print("[L130HashRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if any(not path.is_file() for path in (*DATASETS.values(), *BINARIES.values())):
|
||||
raise SystemExit("missing L130 hash input")
|
||||
old = {(row["dataset"], row["recipe"], row["workers"], row["compiler"]): row
|
||||
for row in BASELINE["runs"]}
|
||||
rows = []
|
||||
for dataset, recipes in RECIPES.items():
|
||||
for recipe in recipes:
|
||||
for workers in (1, 8, 12):
|
||||
for compiler in ("clang", "gcc"):
|
||||
rows.append(run(dataset, recipe, workers, compiler))
|
||||
(OUT / "runs-partial.json").write_text(
|
||||
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
|
||||
groups = {}
|
||||
for dataset, recipes in RECIPES.items():
|
||||
for recipe in recipes:
|
||||
group = [row for row in rows if row["dataset"] == dataset
|
||||
and row["recipe"] == recipe]
|
||||
key = f"{dataset}/{recipe}"
|
||||
stable = all(row["hashes"] == group[0]["hashes"] for row in group)
|
||||
baseline = old[(dataset, recipe, 12, "clang")]["hashes"]
|
||||
changed = [i for i, (before, after) in enumerate(zip(baseline,
|
||||
group[0]["hashes"])) if before != after]
|
||||
groups[key] = {"all_workers_and_compilers_match": stable,
|
||||
"matches_b96dd73_v225": not changed,
|
||||
"changed_hash_rows_from_v225": changed,
|
||||
"source_stamps": sorted({row["source_stamp"] for row in group})}
|
||||
if not stable:
|
||||
raise SystemExit(f"worker/compiler hash mismatch: {key}")
|
||||
(OUT / "summary.json").write_text(
|
||||
json.dumps({"baseline_source": "b96dd73", "runs": rows,
|
||||
"groups": groups}, indent=2) + "\n", encoding="utf-8")
|
||||
print("[L130HashSummary] " + json.dumps(groups, sort_keys=True), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the six L129 b96dd73 mouth views against the L130 meshes."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
SOURCE = ROOT / "Saved/Linux/l129/captures/mouth-closeups/before"
|
||||
SAVED = ROOT / "Saved/Linux/l130/captures/mouth-closeups"
|
||||
CAPTURES = ROOT / "Standalone/Research/WorldAssemblerM3/Linux130/captures/mouth-closeups"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
HEADLESS = ROOT / "Saved/Linux/l130/bin/VoxelForgeCoreHeadless-l130-clang"
|
||||
RENDERER = ROOT / "Saved/Linux/l130/bin/VoxelForgeOffscreenCapture-l130-clang"
|
||||
HEADLESS_CONFIG = re.compile(
|
||||
r"^\[HeadlessConfig\].*?camera_m=\(([^)]*)\) camera_rotation_deg=\(([^)]*)\)", re.M)
|
||||
SCENE = re.compile(r"^\[OffscreenScene\].*?height_center_m=(-?[\d.]+)", re.M)
|
||||
DRAWN = re.compile(r"drawn_triangles=(\d+)")
|
||||
|
||||
|
||||
def wrapped(command: list[str]) -> list[str]:
|
||||
return ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", *map(str, command)]
|
||||
|
||||
|
||||
def run(label: str, command: list[str], log: Path) -> str:
|
||||
started = time.monotonic()
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log.open("w", encoding="utf-8") as output:
|
||||
result = subprocess.run(wrapped(command), cwd=ROOT, stdout=output,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
content = log.read_text(encoding="utf-8", errors="replace")
|
||||
if result.returncode:
|
||||
raise SystemExit(f"{label} failed exit={result.returncode}: {log}")
|
||||
print(f"[L130MouthProcess] {label} elapsed_s={time.monotonic()-started:.2f}",
|
||||
flush=True)
|
||||
return content
|
||||
|
||||
|
||||
def parse_vector(text: str) -> tuple[float, ...]:
|
||||
return tuple(float(value) for value in text.split(","))
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if any(not path.is_file() for path in (DATA, HEADLESS, RENDERER)):
|
||||
raise SystemExit("missing L130 mouth capture input")
|
||||
(CAPTURES / "b96dd73").mkdir(parents=True, exist_ok=True)
|
||||
(CAPTURES / "L130").mkdir(parents=True, exist_ok=True)
|
||||
rows = []
|
||||
for source_log in sorted(SOURCE.glob("*-mouth-mesh.log")):
|
||||
name = source_log.name.removesuffix("-mesh.log")
|
||||
source_render = SOURCE / f"{name}-render.log"
|
||||
baseline_mesh = SOURCE / f"{name}.vfmesh"
|
||||
if not all(path.is_file() for path in (source_render, baseline_mesh)):
|
||||
raise SystemExit(f"missing b96dd73 source artifact for {name}")
|
||||
config = HEADLESS_CONFIG.search(source_log.read_text())
|
||||
scene = SCENE.search(source_render.read_text())
|
||||
if config is None or scene is None:
|
||||
raise SystemExit(f"missing camera metadata for {name}")
|
||||
camera = parse_vector(config.group(1))
|
||||
yaw, pitch = parse_vector(config.group(2))
|
||||
height_center = float(scene.group(1))
|
||||
camera_arg = ",".join(f"{value:.6f}" for value in (*camera, yaw, pitch))
|
||||
candidate_mesh = SAVED / "L130" / f"{name}.vfmesh"
|
||||
candidate_mesh.parent.mkdir(parents=True, exist_ok=True)
|
||||
run(f"{name}-mesh", [HEADLESS, DATA, "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--workers=12",
|
||||
"--clip-radius=3", "--max-clip-level=0", "--max-level=0",
|
||||
"--force-generate", "--recipe=surface-world", f"--camera={camera_arg}",
|
||||
"--render-distance-chunks=1", f"--mesh-export={candidate_mesh}"],
|
||||
SAVED / "L130" / f"{name}-mesh.log")
|
||||
if not candidate_mesh.is_file() or candidate_mesh.stat().st_size == 0:
|
||||
raise SystemExit(f"empty L130 mesh for {name}")
|
||||
|
||||
render_args = ["--size=1920x1080",
|
||||
f"--camera-m={','.join(f'{value:.6f}' for value in camera)}",
|
||||
f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--shade=readable", "--headlamp=on", "--height-tint=on",
|
||||
f"--height-center-m={height_center:.6f}", "--height-range-m=32"]
|
||||
images = {}
|
||||
draw_counts = {}
|
||||
for variant, mesh in (("b96dd73", baseline_mesh), ("L130", candidate_mesh)):
|
||||
image = CAPTURES / variant / f"{name}.png"
|
||||
render_log = SAVED / variant / f"{name}-render.log"
|
||||
content = run(f"{name}-{variant}-render",
|
||||
[RENDERER, mesh, image, *render_args], render_log)
|
||||
drawn = DRAWN.search(content)
|
||||
if drawn is None or int(drawn.group(1)) <= 0 or not image.is_file():
|
||||
raise SystemExit(f"render has no visible triangles for {name}/{variant}")
|
||||
images[variant] = image
|
||||
draw_counts[variant] = int(drawn.group(1))
|
||||
equal = images["b96dd73"].read_bytes() == images["L130"].read_bytes()
|
||||
row = {"name": name, "camera_m": camera, "yaw_deg": yaw,
|
||||
"pitch_deg": pitch, "height_center_m": height_center,
|
||||
"source_mesh": str(baseline_mesh.relative_to(ROOT)),
|
||||
"candidate_mesh": str(candidate_mesh.relative_to(ROOT)),
|
||||
"baseline_png": str(images["b96dd73"].relative_to(ROOT)),
|
||||
"candidate_png": str(images["L130"].relative_to(ROOT)),
|
||||
"baseline_sha256": sha256(images["b96dd73"]),
|
||||
"candidate_sha256": sha256(images["L130"]),
|
||||
"byte_identical": equal, "drawn_triangles": draw_counts}
|
||||
rows.append(row)
|
||||
print(f"[L130MouthCompare] {name} byte_identical={equal} "
|
||||
f"sha256={row['candidate_sha256']}", flush=True)
|
||||
|
||||
if len(rows) != 6:
|
||||
raise SystemExit(f"expected six L129 mouth views, found {len(rows)}")
|
||||
(CAPTURES / "summary.json").write_text(json.dumps(rows, indent=2) + "\n")
|
||||
sections = []
|
||||
for row in rows:
|
||||
name = html.escape(row["name"])
|
||||
sections.append(f"<section><h2>{name}</h2><p>byte identical: "
|
||||
f"{str(row['byte_identical']).lower()}</p><div>"
|
||||
f"<figure><figcaption>b96dd73 source mesh</figcaption><img src='b96dd73/{name}.png'></figure>"
|
||||
f"<figure><figcaption>L130 reach units</figcaption><img src='L130/{name}.png'></figure>"
|
||||
"</div></section>")
|
||||
page = ("<!doctype html><meta charset='utf-8'><title>L130 mouth closeups</title>"
|
||||
"<style>body{font:14px system-ui;margin:24px;background:#17191d;color:#eee}"
|
||||
"section{margin:28px 0}div{display:grid;grid-template-columns:1fr 1fr;gap:12px}"
|
||||
"img{width:100%;height:auto}p{color:#bbb}</style>"
|
||||
"<h1>L129 mouth closeups: b96dd73 vs L130</h1>" + "\n".join(sections))
|
||||
(CAPTURES / "COMPARE.html").write_text(page)
|
||||
return 0 if all(row["byte_identical"] for row in rows) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the 23 L125 camera poses with the L130 evaluator and compare them."""
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import importlib.util
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
RESEARCH = Path(__file__).resolve().parent
|
||||
CAPTURES = RESEARCH / "captures"
|
||||
SAVED = ROOT / "Saved/Linux/l130/captures"
|
||||
BASELINE_INDEX = (ROOT / "Standalone/Research/WorldAssemblerM3/Linux119"
|
||||
/ "captures/L119/capture-index.json")
|
||||
REFERENCE = ROOT / "Standalone/Research/WorldAssemblerM3/Linux125/captures/L125"
|
||||
LAYOUT = ROOT / "Saved/Linux/l130/hashes/owner4-surface-world-w12-clang.layout.json"
|
||||
HEADLESS = ROOT / "Saved/Linux/l130/bin/VoxelForgeCoreHeadless-l130-clang"
|
||||
RENDERER = ROOT / "Saved/Linux/l130/bin/VoxelForgeOffscreenCapture-l130-clang"
|
||||
HELPER_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux113/RunL113Captures.py"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if any(not path.is_file() for path in
|
||||
(BASELINE_INDEX, REFERENCE / "passage-0-1-0-rock-floor-station-1.png",
|
||||
LAYOUT, HEADLESS, RENDERER)):
|
||||
raise SystemExit("missing pose, layout, or L130 capture input")
|
||||
spec = importlib.util.spec_from_file_location("l125_capture_runner",
|
||||
ROOT / "Standalone/Research/WorldAssemblerM3/Linux125/RunL125Captures.py")
|
||||
if spec is None or spec.loader is None:
|
||||
raise SystemExit("cannot load the verified L125 pose set")
|
||||
runner = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(runner)
|
||||
runner.RESEARCH = RESEARCH
|
||||
runner.CAPTURES = CAPTURES
|
||||
runner.SAVED = SAVED
|
||||
runner.LAYOUT = LAYOUT
|
||||
runner.CLANG = HEADLESS
|
||||
runner.RENDERER = RENDERER
|
||||
runner.HELPERS.RESEARCH = RESEARCH
|
||||
runner.HELPERS.CAPTURES = CAPTURES
|
||||
runner.HELPERS.SAVED = SAVED
|
||||
runner.HELPERS.BINARIES = {"L125": HEADLESS}
|
||||
runner.HELPERS.RENDERERS = {"L125": RENDERER}
|
||||
if runner.main() != 0:
|
||||
raise SystemExit("L130 pose generation failed")
|
||||
|
||||
new_captures = CAPTURES / "L125"
|
||||
new_saved = SAVED / "L125"
|
||||
index_path = new_captures / "capture-index.json"
|
||||
data = json.loads(index_path.read_text())
|
||||
if len(data["captures"]) != 23:
|
||||
raise SystemExit(f"expected 23 poses, found {len(data['captures'])}")
|
||||
identical = 0
|
||||
sections = []
|
||||
for row in data["captures"]:
|
||||
name = row["name"]
|
||||
old_image = REFERENCE / f"{name}.png"
|
||||
new_image = new_captures / f"{name}.png"
|
||||
if not old_image.is_file() or not new_image.is_file():
|
||||
raise SystemExit(f"missing comparison image for {name}")
|
||||
same = old_image.read_bytes() == new_image.read_bytes()
|
||||
identical += int(same)
|
||||
row["variant"] = "L130-v227"
|
||||
row["capture"] = row["capture"].replace("captures/L125/", "captures/L130/")
|
||||
row["scene"] = row["scene"].replace("captures/L125/", "captures/L130/")
|
||||
row["logs"] = [path.replace("captures/L125/", "captures/L130/")
|
||||
for path in row.get("logs", [])]
|
||||
sections.append(f"<section><h2>{html.escape(row['passage'])} — "
|
||||
f"{html.escape(row.get('station', ''))}</h2><p>{html.escape(row['kind'])} · "
|
||||
f"byte identical: {str(same).lower()}</p><div>"
|
||||
f"<figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/{html.escape(name)}.png'></figure>"
|
||||
f"<figure><figcaption>L130 v227</figcaption><img src='L130/{html.escape(name)}.png'></figure>"
|
||||
"</div></section>")
|
||||
if identical != 23:
|
||||
raise SystemExit(f"only {identical}/23 L125 pose captures are byte-identical")
|
||||
|
||||
index_path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
old_captures, old_saved = CAPTURES / "L130", SAVED / "L130"
|
||||
if old_captures.exists():
|
||||
shutil.rmtree(old_captures)
|
||||
if old_saved.exists():
|
||||
shutil.rmtree(old_saved)
|
||||
new_captures.rename(old_captures)
|
||||
new_saved.rename(old_saved)
|
||||
page = ("<!doctype html><meta charset='utf-8'><title>L130 passage captures</title>"
|
||||
"<style>body{font:14px system-ui;margin:24px;background:#17191d;color:#eee}"
|
||||
"section{margin:28px 0}div{display:grid;grid-template-columns:1fr 1fr;gap:12px}"
|
||||
"img{width:100%;height:auto}p{color:#bbb}</style>"
|
||||
"<h1>L125 v225 vs L130 v227: passage captures</h1>" + "\n".join(sections))
|
||||
(CAPTURES / "COMPARE.html").write_text(page)
|
||||
print(f"[L130PoseCapture] poses=23 byte_identical={identical}/23", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Five interleaved L80 near-ready runs per stratum/worker vs b96dd73."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l130/timing"
|
||||
PASS_ID = int(os.environ.get("VOXELFORGE_L130_TIMING_PASS", "1"))
|
||||
REPLICATE_FIRST = (PASS_ID - 1) * 5 + 1
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"b96dd73": ROOT / "Saved/Linux/l125/bin/VoxelForgeCoreHeadless-l125-clang",
|
||||
"L130": ROOT / "Saved/Linux/l130/bin/VoxelForgeCoreHeadless-l130-clang",
|
||||
}
|
||||
CAMERAS = {
|
||||
0: (8.0, 16.0, -372.2626953125),
|
||||
1: (25.8369140625, 3.19921875, -737.0),
|
||||
2: (19.166015625, -14.2001953125, -753.3740234375),
|
||||
3: (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
PIPELINE_RE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
|
||||
FIELDS_RE = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
NEAR_RE = re.compile(
|
||||
r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$",
|
||||
re.M)
|
||||
|
||||
|
||||
def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
pass_suffix = "" if PASS_ID == 1 else f"-p{PASS_ID}"
|
||||
label = f"s{strate}-w{workers}-{version}-{phase}{replicate}-l130-v227{pass_suffix}"
|
||||
log = OUT / f"{label}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARIES[version]), str(DATA),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8",
|
||||
"--max-level=8", f"--camera={camera}", "--profile-lod-work",
|
||||
"--stream-scheduler-policy=3"]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as output:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=output,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
content = log.read_text(encoding="utf-8", errors="replace")
|
||||
pipeline, near = PIPELINE_RE.search(content), NEAR_RE.search(content)
|
||||
if result.returncode or pipeline is None or near is None:
|
||||
raise SystemExit(f"timing run failed: {log}; exit={result.returncode}")
|
||||
fields = dict(FIELDS_RE.findall(pipeline.group(1)))
|
||||
row = {"strate": strate, "workers": workers, "version": version,
|
||||
"phase": phase, "replicate": replicate, "exit_code": result.returncode,
|
||||
"wall_s": round(time.monotonic() - started, 3),
|
||||
"lod02_ready_s": float(near.group(3)),
|
||||
"whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")),
|
||||
"log": str(log.relative_to(ROOT))}
|
||||
print("[L130TimingRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if any(not path.is_file() for path in (DATA, *BINARIES.values())):
|
||||
raise SystemExit("missing L130 timing input")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
load_1m = __import__("os").getloadavg()[0]
|
||||
def cpu_idle(interval: float = 2.0) -> float:
|
||||
def totals() -> tuple[int, int]:
|
||||
fields = Path("/proc/stat").read_text().splitlines()[0].split()
|
||||
values = [int(value) for value in fields[1:]]
|
||||
return sum(values), values[3] + (values[4] if len(values) > 4 else 0)
|
||||
before = totals()
|
||||
time.sleep(interval)
|
||||
after = totals()
|
||||
return 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0])
|
||||
idle = cpu_idle()
|
||||
pass_suffix = "" if PASS_ID == 1 else f"-p{PASS_ID}"
|
||||
(OUT / f"machine-load-v227{pass_suffix}-before.txt").write_text(
|
||||
f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n")
|
||||
print(f"[L130TimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%",
|
||||
flush=True)
|
||||
if load_1m >= 0.9 or idle < 97.0:
|
||||
raise SystemExit("timing refused: machine is above the quiet-load gate")
|
||||
|
||||
rows = []
|
||||
for strate in range(4):
|
||||
for workers in (8, 12):
|
||||
for version in BINARIES:
|
||||
run(strate, workers, version, "warmup", 0)
|
||||
for replicate in range(REPLICATE_FIRST, REPLICATE_FIRST + 5):
|
||||
order = tuple(BINARIES) if replicate % 2 else tuple(reversed(BINARIES))
|
||||
for version in order:
|
||||
rows.append(run(strate, workers, version, "measured", replicate))
|
||||
(OUT / f"runs-v227{pass_suffix}-partial.json").write_text(
|
||||
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
|
||||
prior_runs = []
|
||||
if PASS_ID > 1:
|
||||
previous = OUT / ("v227-summary.json" if PASS_ID == 2
|
||||
else "v227-combined-summary.json")
|
||||
if previous.is_file():
|
||||
prior_runs = json.loads(previous.read_text(encoding="utf-8")).get("runs", [])
|
||||
all_rows = prior_runs + rows
|
||||
measured_rows = [row for row in all_rows if row["phase"] == "measured"]
|
||||
summary = []
|
||||
for strate in range(4):
|
||||
for workers in (8, 12):
|
||||
group_size = sum(row["strate"] == strate and row["workers"] == workers
|
||||
for row in measured_rows) // 2
|
||||
item = {"strate": strate, "workers": workers,
|
||||
"replicates_per_version": group_size}
|
||||
medians = {}
|
||||
for version in BINARIES:
|
||||
group = [row for row in measured_rows if row["strate"] == strate
|
||||
and row["workers"] == workers and row["version"] == version]
|
||||
medians[version] = {}
|
||||
for key in ("lod02_ready_s", "whole_view_ready_s"):
|
||||
values = [row[key] for row in group]
|
||||
medians[version][key] = statistics.median(values)
|
||||
item[f"{version}_{key}_values"] = values
|
||||
item[f"{version}_{key}_median"] = medians[version][key]
|
||||
for key in ("lod02_ready_s", "whole_view_ready_s"):
|
||||
item[f"L130_vs_b96dd73_{key}_delta_pct"] = round(
|
||||
100.0*(medians["L130"][key]/medians["b96dd73"][key]-1.0), 2)
|
||||
summary.append(item)
|
||||
summary_path = OUT / ("v227-summary.json" if PASS_ID == 1
|
||||
else f"v227-combined-summary.json")
|
||||
summary_path.write_text(json.dumps(
|
||||
{"runs": all_rows, "summary": summary}, indent=2) + "\n", encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,23 @@
|
||||
<!doctype html><meta charset='utf-8'><title>L130 passage captures</title><style>body{font:14px system-ui;margin:24px;background:#17191d;color:#eee}section{margin:28px 0}div{display:grid;grid-template-columns:1fr 1fr;gap:12px}img{width:100%;height:auto}p{color:#bbb}</style><h1>L125 v225 vs L130 v227: passage captures</h1><section><h2>passage/0-1/0 — rock-floor/401.7m</h2><p>rock-floor-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-0-1-0-rock-floor-station-1.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-0-1-0-rock-floor-station-1.png'></figure></div></section>
|
||||
<section><h2>passage/0-1/0 — rock-floor/792.8m</h2><p>rock-floor-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-0-1-0-rock-floor-station-2.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-0-1-0-rock-floor-station-2.png'></figure></div></section>
|
||||
<section><h2>passage/0-1/0 — rock-floor/1634.5m</h2><p>rock-floor-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-0-1-0-rock-floor-station-3.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-0-1-0-rock-floor-station-3.png'></figure></div></section>
|
||||
<section><h2>passage/1-2/0 — rock-floor/27.0m</h2><p>rock-floor-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-1-2-0-rock-floor-station-1.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-1-2-0-rock-floor-station-1.png'></figure></div></section>
|
||||
<section><h2>passage/1-2/0 — rock-floor/52.9m</h2><p>rock-floor-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-1-2-0-rock-floor-station-2.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-1-2-0-rock-floor-station-2.png'></figure></div></section>
|
||||
<section><h2>passage/1-2/0 — rock-floor/78.9m</h2><p>rock-floor-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-1-2-0-rock-floor-station-3.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-1-2-0-rock-floor-station-3.png'></figure></div></section>
|
||||
<section><h2>passage/2-3/0 — rock-floor/323.4m</h2><p>rock-floor-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-2-3-0-rock-floor-station-1.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-2-3-0-rock-floor-station-1.png'></figure></div></section>
|
||||
<section><h2>passage/2-3/0 — rock-floor/333.4m</h2><p>rock-floor-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-2-3-0-rock-floor-station-2.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-2-3-0-rock-floor-station-2.png'></figure></div></section>
|
||||
<section><h2>passage/2-3/0 — rock-floor/343.4m</h2><p>rock-floor-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-2-3-0-rock-floor-station-3.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-2-3-0-rock-floor-station-3.png'></figure></div></section>
|
||||
<section><h2>passage/0-1/0 — section-1/4m-outside</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-0-1-0-openair-1-4m-outside.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-0-1-0-openair-1-4m-outside.png'></figure></div></section>
|
||||
<section><h2>passage/0-1/0 — section-1/4m-interior-wall</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-0-1-0-openair-1-4m-interior-wall.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-0-1-0-openair-1-4m-interior-wall.png'></figure></div></section>
|
||||
<section><h2>passage/0-1/0 — section-2/4m-outside</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-0-1-0-openair-2-4m-outside.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-0-1-0-openair-2-4m-outside.png'></figure></div></section>
|
||||
<section><h2>passage/0-1/0 — section-2/4m-interior-wall</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-0-1-0-openair-2-4m-interior-wall.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-0-1-0-openair-2-4m-interior-wall.png'></figure></div></section>
|
||||
<section><h2>passage/0-1/0 — section-3/4m-outside</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-0-1-0-openair-3-4m-outside.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-0-1-0-openair-3-4m-outside.png'></figure></div></section>
|
||||
<section><h2>passage/0-1/0 — section-3/4m-interior-wall</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-0-1-0-openair-3-4m-interior-wall.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-0-1-0-openair-3-4m-interior-wall.png'></figure></div></section>
|
||||
<section><h2>passage/1-2/0 — section-1/4m-outside</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-1-2-0-openair-1-4m-outside.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-1-2-0-openair-1-4m-outside.png'></figure></div></section>
|
||||
<section><h2>passage/1-2/0 — section-1/4m-interior-wall</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-1-2-0-openair-1-4m-interior-wall.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-1-2-0-openair-1-4m-interior-wall.png'></figure></div></section>
|
||||
<section><h2>passage/1-2/0 — section-2/4m-outside</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-1-2-0-openair-2-4m-outside.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-1-2-0-openair-2-4m-outside.png'></figure></div></section>
|
||||
<section><h2>passage/1-2/0 — section-2/4m-interior-wall</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-1-2-0-openair-2-4m-interior-wall.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-1-2-0-openair-2-4m-interior-wall.png'></figure></div></section>
|
||||
<section><h2>passage/2-3/0 — section-1/4m-outside</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-2-3-0-openair-1-4m-outside.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-2-3-0-openair-1-4m-outside.png'></figure></div></section>
|
||||
<section><h2>passage/2-3/0 — section-1/4m-interior-wall</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-2-3-0-openair-1-4m-interior-wall.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-2-3-0-openair-1-4m-interior-wall.png'></figure></div></section>
|
||||
<section><h2>passage/2-3/0 — section-2/4m-outside</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-2-3-0-openair-2-4m-outside.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-2-3-0-openair-2-4m-outside.png'></figure></div></section>
|
||||
<section><h2>passage/2-3/0 — section-2/4m-interior-wall</h2><p>open-air-close · byte identical: true</p><div><figure><figcaption>L125 v225</figcaption><img src='../../Linux125/captures/L125/passage-2-3-0-openair-2-4m-interior-wall.png'></figure><figure><figcaption>L130 v227</figcaption><img src='L130/passage-2-3-0-openair-2-4m-interior-wall.png'></figure></div></section>
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 106 KiB |
@@ -0,0 +1,6 @@
|
||||
<!doctype html><meta charset='utf-8'><title>L130 mouth closeups</title><style>body{font:14px system-ui;margin:24px;background:#17191d;color:#eee}section{margin:28px 0}div{display:grid;grid-template-columns:1fr 1fr;gap:12px}img{width:100%;height:auto}p{color:#bbb}</style><h1>L129 mouth closeups: b96dd73 vs L130</h1><section><h2>passage-0-1-0-lower-mouth</h2><p>byte identical: true</p><div><figure><figcaption>b96dd73 source mesh</figcaption><img src='b96dd73/passage-0-1-0-lower-mouth.png'></figure><figure><figcaption>L130 reach units</figcaption><img src='L130/passage-0-1-0-lower-mouth.png'></figure></div></section>
|
||||
<section><h2>passage-0-1-0-upper-mouth</h2><p>byte identical: true</p><div><figure><figcaption>b96dd73 source mesh</figcaption><img src='b96dd73/passage-0-1-0-upper-mouth.png'></figure><figure><figcaption>L130 reach units</figcaption><img src='L130/passage-0-1-0-upper-mouth.png'></figure></div></section>
|
||||
<section><h2>passage-1-2-0-lower-mouth</h2><p>byte identical: true</p><div><figure><figcaption>b96dd73 source mesh</figcaption><img src='b96dd73/passage-1-2-0-lower-mouth.png'></figure><figure><figcaption>L130 reach units</figcaption><img src='L130/passage-1-2-0-lower-mouth.png'></figure></div></section>
|
||||
<section><h2>passage-1-2-0-upper-mouth</h2><p>byte identical: true</p><div><figure><figcaption>b96dd73 source mesh</figcaption><img src='b96dd73/passage-1-2-0-upper-mouth.png'></figure><figure><figcaption>L130 reach units</figcaption><img src='L130/passage-1-2-0-upper-mouth.png'></figure></div></section>
|
||||
<section><h2>passage-2-3-0-lower-mouth</h2><p>byte identical: true</p><div><figure><figcaption>b96dd73 source mesh</figcaption><img src='b96dd73/passage-2-3-0-lower-mouth.png'></figure><figure><figcaption>L130 reach units</figcaption><img src='L130/passage-2-3-0-lower-mouth.png'></figure></div></section>
|
||||
<section><h2>passage-2-3-0-upper-mouth</h2><p>byte identical: true</p><div><figure><figcaption>b96dd73 source mesh</figcaption><img src='b96dd73/passage-2-3-0-upper-mouth.png'></figure><figure><figcaption>L130 reach units</figcaption><img src='L130/passage-2-3-0-upper-mouth.png'></figure></div></section>
|
||||
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,134 @@
|
||||
[
|
||||
{
|
||||
"name": "passage-0-1-0-lower-mouth",
|
||||
"camera_m": [
|
||||
32.688347,
|
||||
9.553723,
|
||||
-735.48053
|
||||
],
|
||||
"yaw_deg": -132.845,
|
||||
"pitch_deg": -6.052,
|
||||
"height_center_m": -736.267,
|
||||
"source_mesh": "Saved/Linux/l129/captures/mouth-closeups/before/passage-0-1-0-lower-mouth.vfmesh",
|
||||
"candidate_mesh": "Saved/Linux/l130/captures/mouth-closeups/L130/passage-0-1-0-lower-mouth.vfmesh",
|
||||
"baseline_png": "Standalone/Research/WorldAssemblerM3/Linux130/captures/mouth-closeups/b96dd73/passage-0-1-0-lower-mouth.png",
|
||||
"candidate_png": "Standalone/Research/WorldAssemblerM3/Linux130/captures/mouth-closeups/L130/passage-0-1-0-lower-mouth.png",
|
||||
"baseline_sha256": "aff7310b94e1b57e9cd1e42d3294e10964aed4a56e74bec8c1cdc0a5b2bffa98",
|
||||
"candidate_sha256": "aff7310b94e1b57e9cd1e42d3294e10964aed4a56e74bec8c1cdc0a5b2bffa98",
|
||||
"byte_identical": true,
|
||||
"drawn_triangles": {
|
||||
"b96dd73": 549170,
|
||||
"L130": 549170
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "passage-0-1-0-upper-mouth",
|
||||
"camera_m": [
|
||||
8.324777,
|
||||
20.450708,
|
||||
-371.84259
|
||||
],
|
||||
"yaw_deg": -175.826,
|
||||
"pitch_deg": -0.889,
|
||||
"height_center_m": -372.122,
|
||||
"source_mesh": "Saved/Linux/l129/captures/mouth-closeups/before/passage-0-1-0-upper-mouth.vfmesh",
|
||||
"candidate_mesh": "Saved/Linux/l130/captures/mouth-closeups/L130/passage-0-1-0-upper-mouth.vfmesh",
|
||||
"baseline_png": "Standalone/Research/WorldAssemblerM3/Linux130/captures/mouth-closeups/b96dd73/passage-0-1-0-upper-mouth.png",
|
||||
"candidate_png": "Standalone/Research/WorldAssemblerM3/Linux130/captures/mouth-closeups/L130/passage-0-1-0-upper-mouth.png",
|
||||
"baseline_sha256": "76dbb3263eb4f2018d13496f42b6eff7eb875e2029358776e22337aea8893a6d",
|
||||
"candidate_sha256": "76dbb3263eb4f2018d13496f42b6eff7eb875e2029358776e22337aea8893a6d",
|
||||
"byte_identical": true,
|
||||
"drawn_triangles": {
|
||||
"b96dd73": 243380,
|
||||
"L130": 243380
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "passage-1-2-0-lower-mouth",
|
||||
"camera_m": [
|
||||
27.129938,
|
||||
-19.07345,
|
||||
-751.877197
|
||||
],
|
||||
"yaw_deg": -58.537,
|
||||
"pitch_deg": -5.944,
|
||||
"height_center_m": -751.734,
|
||||
"source_mesh": "Saved/Linux/l129/captures/mouth-closeups/before/passage-1-2-0-lower-mouth.vfmesh",
|
||||
"candidate_mesh": "Saved/Linux/l130/captures/mouth-closeups/L130/passage-1-2-0-lower-mouth.vfmesh",
|
||||
"baseline_png": "Standalone/Research/WorldAssemblerM3/Linux130/captures/mouth-closeups/b96dd73/passage-1-2-0-lower-mouth.png",
|
||||
"candidate_png": "Standalone/Research/WorldAssemblerM3/Linux130/captures/mouth-closeups/L130/passage-1-2-0-lower-mouth.png",
|
||||
"baseline_sha256": "1c193b6fcb65b98b1c721704990392dc816486639e047b53bb72b4a17d89ae4c",
|
||||
"candidate_sha256": "1c193b6fcb65b98b1c721704990392dc816486639e047b53bb72b4a17d89ae4c",
|
||||
"byte_identical": true,
|
||||
"drawn_triangles": {
|
||||
"b96dd73": 544808,
|
||||
"L130": 544808
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "passage-1-2-0-upper-mouth",
|
||||
"camera_m": [
|
||||
23.391808,
|
||||
-15.509525,
|
||||
-731.781067
|
||||
],
|
||||
"yaw_deg": -72.785,
|
||||
"pitch_deg": -0.897,
|
||||
"height_center_m": -730.561,
|
||||
"source_mesh": "Saved/Linux/l129/captures/mouth-closeups/before/passage-1-2-0-upper-mouth.vfmesh",
|
||||
"candidate_mesh": "Saved/Linux/l130/captures/mouth-closeups/L130/passage-1-2-0-upper-mouth.vfmesh",
|
||||
"baseline_png": "Standalone/Research/WorldAssemblerM3/Linux130/captures/mouth-closeups/b96dd73/passage-1-2-0-upper-mouth.png",
|
||||
"candidate_png": "Standalone/Research/WorldAssemblerM3/Linux130/captures/mouth-closeups/L130/passage-1-2-0-upper-mouth.png",
|
||||
"baseline_sha256": "28576cefb1b59601d2e68981a7b77fc28a1d667c58ab27292deac0acd9deb338",
|
||||
"candidate_sha256": "28576cefb1b59601d2e68981a7b77fc28a1d667c58ab27292deac0acd9deb338",
|
||||
"byte_identical": true,
|
||||
"drawn_triangles": {
|
||||
"b96dd73": 593026,
|
||||
"L130": 593026
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "passage-2-3-0-lower-mouth",
|
||||
"camera_m": [
|
||||
-28.589163,
|
||||
-1.226313,
|
||||
-898.811707
|
||||
],
|
||||
"yaw_deg": 114.256,
|
||||
"pitch_deg": -6.159,
|
||||
"height_center_m": -898.713,
|
||||
"source_mesh": "Saved/Linux/l129/captures/mouth-closeups/before/passage-2-3-0-lower-mouth.vfmesh",
|
||||
"candidate_mesh": "Saved/Linux/l130/captures/mouth-closeups/L130/passage-2-3-0-lower-mouth.vfmesh",
|
||||
"baseline_png": "Standalone/Research/WorldAssemblerM3/Linux130/captures/mouth-closeups/b96dd73/passage-2-3-0-lower-mouth.png",
|
||||
"candidate_png": "Standalone/Research/WorldAssemblerM3/Linux130/captures/mouth-closeups/L130/passage-2-3-0-lower-mouth.png",
|
||||
"baseline_sha256": "58cd3ea88c754afab20d1cdcaf5f0edf676f0303d9f392d3632a6f2257205766",
|
||||
"candidate_sha256": "58cd3ea88c754afab20d1cdcaf5f0edf676f0303d9f392d3632a6f2257205766",
|
||||
"byte_identical": true,
|
||||
"drawn_triangles": {
|
||||
"b96dd73": 293832,
|
||||
"L130": 293832
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "passage-2-3-0-upper-mouth",
|
||||
"camera_m": [
|
||||
-24.455027,
|
||||
-5.631774,
|
||||
-811.975403
|
||||
],
|
||||
"yaw_deg": 82.691,
|
||||
"pitch_deg": -0.896,
|
||||
"height_center_m": -810.755,
|
||||
"source_mesh": "Saved/Linux/l129/captures/mouth-closeups/before/passage-2-3-0-upper-mouth.vfmesh",
|
||||
"candidate_mesh": "Saved/Linux/l130/captures/mouth-closeups/L130/passage-2-3-0-upper-mouth.vfmesh",
|
||||
"baseline_png": "Standalone/Research/WorldAssemblerM3/Linux130/captures/mouth-closeups/b96dd73/passage-2-3-0-upper-mouth.png",
|
||||
"candidate_png": "Standalone/Research/WorldAssemblerM3/Linux130/captures/mouth-closeups/L130/passage-2-3-0-upper-mouth.png",
|
||||
"baseline_sha256": "6b903c68fac1a20783185afbac8cdad717c6e9374e665e3618e118c5a6aadf51",
|
||||
"candidate_sha256": "6b903c68fac1a20783185afbac8cdad717c6e9374e665e3618e118c5a6aadf51",
|
||||
"byte_identical": true,
|
||||
"drawn_triangles": {
|
||||
"b96dd73": 59554,
|
||||
"L130": 59554
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,50 @@
|
||||
# Linux131 — current state and dead experiment cleanup
|
||||
|
||||
Branch `wip/l131-current-state`, based on `cbbfd46`; Core evaluator remains v227. The root [`CURRENT-STATE.md`](../../../../CURRENT-STATE.md) consolidates the shipping behavior, current references, verification commands, open gaps, and closed proposals.
|
||||
|
||||
## Removed
|
||||
|
||||
- Removed `VOXELFORGE_L86_DISABLE_PASSAGE_COMPOSITION`, `VOXELFORGE_L88_DISABLE_PASSAGES`, `VOXELFORGE_L88_DISABLE_BOUNDARY_BANDS`, and `VOXELFORGE_L88_ALLOW_PASSAGE_ABLATION_WALKABILITY_FAILURE`. These only supported archived passage/boundary ablations in `Linux86/RunL86Ceiling.py`, `Linux87/RunL87Timeline.py`, and `Linux88/RunL88CeilingSplit.py`; no current `verify.py` gate or live comparison uses them. Passage feature selection, passage composition, and boundary bands now run on the normal path, and passage-route validation cannot be waived by the old ablation flag.
|
||||
- Removed the `VOXELFORGE_AUDIT_LEGACY_ROUTE_GRADE` block from `Standalone/Source/WorldAssemblyPrototype.h`. The source contained its only reference; no test, current tool, or report invoked it.
|
||||
- Kept route-candidate grading and all user-visible behavior. This cleanup adds no evaluator version or baseline change.
|
||||
|
||||
## Switch and hook inventory
|
||||
|
||||
### Environment switches in compiled Core/Source
|
||||
|
||||
| Switch | Effect and consumer | Keep decision |
|
||||
|---|---|---|
|
||||
| `VOXELFORGE_L74_TIGHT_PROOF_CONTROL` | Proof negative control. `Linux74/FalsifierL74.py` and `Linux75/FalsifierL75.py`. | Keep for falsifier reproducibility; not a current fast-tier input. |
|
||||
| `VOXELFORGE_L75_TN_BLOCK_REGRESSION` | Fixed TunnelNetwork regression path in Core and mesher. `RunTnBlockRegressionL75.py` and `FalsifierL75.py`. | Keep; the regression tools still consume it. |
|
||||
| `VOXELFORGE_L98_PASSAGE_PREP_TRACE` | Emits preparation-stage timing/count data. `RunL98Hashes.py` and `RunL98ReuseCounts.py`. | Keep as a focused profiler for future preparation comparisons. |
|
||||
| `VOXELFORGE_L98_PASSAGE_REUSE_PROBE` | Compares the prepared projection with direct scalar queries. `RunL98ReuseCounts.py`. | Keep for exactness probes. |
|
||||
| `VOXELFORGE_L104_FAR_HIT_CACHE` | Selects indexed versus linear far-hit candidate lookup. `RunL104ReadinessPairs.py` and `RunL104ProbeStats.py`. | Keep for reproducible A/B comparisons; cache defaults on. |
|
||||
| `VOXELFORGE_L104_FAR_HIT_STATS` | Emits far-hit candidate/query counters. `RunL104ProbeStats.py`. | Keep for the measurement tool. |
|
||||
| `VOXELFORGE_L115_DENSITY_PROFILE` | Enables density and floor-query profiling. `RunL115DensityProbe.py`, `RunL119DensityProbe.py`, and `RunL125DensityProbe.py`. | Keep; these remain the focused floor-probe path. |
|
||||
| `VOXELFORGE_L130_PASSAGE_REACH_PROOF_AUDIT` | Checks passage proof/feature influence bounds. L130's reach audit and report. | Keep for reach changes and audit reproduction. |
|
||||
|
||||
Archived runners also set `VOXELFORGE_L81_PASSAGE_BOUND_PADDING_VOXELS`, `VOXELFORGE_L86_PASSAGE_RESULT_CACHE`, `VOXELFORGE_L90_PASSAGE_BRICK_METRES`, `VOXELFORGE_L90_PASSAGE_CANDIDATE_CENSUS`, `VOXELFORGE_L90_PASSAGE_REACH_METRES`, `VOXELFORGE_L91_PASSAGE_SOURCE_FLOOR_VOXELS`, `VOXELFORGE_L93_PASSAGE_CARVE_REACH_METRES`, `VOXELFORGE_L98_PASSAGE_REUSE_COUNTS`, `VOXELFORGE_L114_DENSITY_PROFILE`, and `VOXELFORGE_L118_TAG_TIMING`. None has a compiled-source reader at `cbbfd46`; they are inert historical runner strings.
|
||||
|
||||
### Other experiments, diagnostics, and legacy paths retained
|
||||
|
||||
- **World-changing tunnel ablations:** all 14 `voxel.TunnelAblate*` controls remain (`CaveWarp`, `DetailOps`, `RoomSDF`, `TunnelSDF`, `TunnelCore`, `PassageCarving`, `PassageStructuralPosts`, `NativeFloor`, `Disturbances`, `OriginSpine`, `BoundarySeal`, `LandingPosts`, `XYEdgeSeal`, `PitChimneySDF`). The editor commandlet writes their resolved mask into export and result manifests; the archived `PERF-SAMPLED.md` and `WORK-NEXT.md` record their measurements. No current runner invokes an ablation. Keep for manifest attribution and historical reproduction; removing it needs a metadata migration. Source: [`VoxelDensityAblation.cpp`](../../../../Source/VoxelForge/Private/VoxelDensityAblation.cpp), [`VoxelForgeExploreCommandlet.cpp`](../../../../Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp).
|
||||
- **Research fields:** `voxel.ResearchRedefinedDensityField`, `voxel.ResearchRedefinedDensityDetailScale`, `voxel.ResearchParameterizedCave`, and `voxel.ResearchParameterizedCaveSet` are default-off and used by `Tools/VoxelForgeTest.ps1` steps 2d/2e/2f. The region-eval commandlet consumes `SetResearchBrickBinningEnabled`, `SetResearchBrickRoomReachScale`, and `SetResearchRegionCacheWindowEnabled`; keep these editor-only experiment paths. Source: [`VoxelForgeRegionEvalExperiment.cpp`](../../../../Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.cpp), [`VoxelForgeTest.ps1`](../../../../Tools/VoxelForgeTest.ps1).
|
||||
- **Runtime profiling and trace:** `voxel.SampleStacks` still starts `FVoxelStackSampler` (the guarded `Tools/VoxelForgeTest.ps1 -SampleDensityStacks` path consumes it); `voxel.ProfileDensity`, `voxel.ProfileDensityFull`, `voxel.ProfileTileGeneration`, `voxel.ProfileCollisionCookStages`, and `voxel.PerfAttribution` remain opt-in. `voxel.StartupTraceFile`, `voxel.StartupTraceMaxTiles`, and `voxel.StartupTraceUntilEndPlay` feed the Unreal startup trace used by the arrival/movement runners. Keep. Build-time `VOXELFORGE_PROFILE_LEGACY_TERMS` (L76) and `VOXELFORGE_PROFILE_L77`/`--linux77-profile` (stack sampler and route census) remain in `Standalone/BuildLinux.sh` and Core.
|
||||
- **Standalone profiles and timelines:** retained `--profile-column-overlap`, `--profile-lod-work`, `--profile-passage-composer`, `--instrument-tile-drain`, `--cold-worker-timeline`, `--linux77-profile`, `--tls-probe=N`, `--tls-probe-touch=...`, and `--diagnostic-slow-shape-compile-ms=N`. The viewer's `--profile-attribution`, `--profile-detail-band`, and `--profile-far-column-overlap` remain. The L77 profiler, tile drain instrumentation, timelines, and TLS probe are measurement hooks; archived reports and runners still provide reproducible consumers.
|
||||
- **Standalone audits:** all source-advertised hooks remain: `--audit-ore-vein-yield`, `--audit-resolved-layout-boundaries`, `--audit-strate-transitions`, `--audit-resolved-layout-passages`, `--audit-heightfield-profile`, `--audit-motif-intervals`, `--audit-landmark-reachability`, `--audit-native-caves`, `--audit-native-lattice`, `--audit-surface-clearance`, `--audit-uniform-block-proofs`, `--audit-base-page-equivalence`, `--audit-route-parity`, `--audit-passage-tile-seams`, `--audit-passage-source-clamp`, `--audit-passage-carve-reach`, `--audit-tile-shape-readiness`, and the mesher's `--audit-raw-skirts`. `verify.py fast` consumes the current route, passage, block-proof, transition, native-field and negative-control subset; dedicated L37/L75/L125/L130 runners exercise the specialized audits. Keep the whole audit set.
|
||||
- **Evaluator A/B controls:** `voxel.FastIsFinite`, `voxel.UseFusedEvaluator`, `voxel.WormBlockSkip`, `voxel.TileCacheWindow` / its two minimum-LOD controls, `voxel.SpatialIndex`, `voxel.ChunkRoutingCache`, `voxel.UseOperatorBlock`, `voxel.CoreColumnCacheDirectMapped`, `voxel.CoreMCGradientMemoization`, `voxel.FloorRound1PassageProjectionCache`, `voxel.PlayerFitMemo`, `voxel.UseBlockEarlyOut`, `voxel.OuterClassifierMode`, and `voxel.SealedSolidProof` remain. They select optimized or comparison paths; the density block equivalence test explicitly forces `UseFusedEvaluator=0`, while most others are production defaults or historical measurement controls. `voxel.WormsForceOff` is a world-changing development measurement override. Kept; no current L131 gate changes them.
|
||||
- **Standalone A/B and geometry controls:** `--exact-midpoint-density-memo`, `--no-exact-midpoint-density-memo`, `--active-lane-batching`, `--proof-block-cells=N`, `--parallel-sign-edge-repair`, `--serial-sign-edge-repair`, and `--shape-helper-limit=8|12` remain. The L94 report documents serial edge repair; no current `verify.py` consumer was found for the other CLI A/B controls, so they remain pending a separately reviewed removal. `voxel.TilePostReachDebug`, `voxel.TilePostReachScale`, and `voxel.TileReachCostDiagnostics` remain for the tile-post-reach test; `voxel.TunnelMouthTrim` remains a development-only room-mouth geometry control. No L131 gate uses these.
|
||||
- `FLegacyGeneratorFieldAdapter` remains the Core fallback for unsupported generator/recipe combinations, and Unreal's Core switch remains default-off. `voxel.StreamingLegacyPendingResultCap=1` remains the L120–L123 admission negative control; `voxel.StreamingCoarseFirst` remains opt-in pending a rendered owner-plan check. `bStandaloneResearchRoundTunnels` and `bStandaloneResearchEnhancedWalls` remain consumed by the standalone viewer/Core adapter.
|
||||
|
||||
## Checks
|
||||
|
||||
- Clang 19 and GCC standalone builds passed with the L131 source stamp `5119cee164406856cbca496d160c0e0a8124ae2529fa7c0ebd51634a7c65a6cb`. `Standalone/BuildLinux.sh` applied the required CPU and address-space limits.
|
||||
- L80 timing: one warm-up per binary and five interleaved pairs in each of four strata × W8/W12 cells, comparing the pre-cleanup L130 binary with L131. Load was 0.81; CPU idle was 99.2%. Across 40 pairs per metric, mean relative change was −0.22% near-ready (95% CI −1.02% to +0.57%) and −0.29% whole-view (−1.04% to +0.46%); the intervals include zero. [Raw timing summary](../../../../Saved/Linux/l131/timing/summary.json).
|
||||
- Exact hashes: **24/24** owner4 SW/FI/TN/VS × Clang/GCC × W1/W8/W12 tile and whole-view identities; **18/18** canonical cases across owner4 SurfaceWorld and owner2 SurfaceWorld/FloatingIslands. All match the pre-cleanup L130 binary and agree across compiler/worker combinations. For transparency, forcing the owner4 dataset through `--recipe=floating-islands` exited during setup with “could not find a finite SurfaceWorld strate”, before hashing; it is not a valid canonical case. [Tile matrix](../../../../Saved/Linux/l131/hash-matrix/summary.json), [canonical matrix](../../../../Saved/Linux/l131/canonical-matrix/summary.json).
|
||||
- Walkability: **21,425/21,425** passage centerline samples open. Seam audit: **4 pairs / 4,900** shared samples, zero mismatches. [Audit log](../../../../Saved/Linux/l131/seam-audit.log).
|
||||
- Fast tier: **PASS**, 259 gated checks plus 5 diagnostics, 80 runs, 371.0 s. The five diagnostic-only comparisons are historical baselines; all required checks pass. [Summary](../../../../Saved/Linux/l131/fast-summary.json).
|
||||
- Linux `VFHostEditor`: **PASS**, 27/27 actions, 78.81 s with `-NoUBA -MaxParallelActions=4`, no `prlimit`. UBT's default log rotation first failed before compilation in the read-only user config directory; the successful rerun set `-Log=.../Saved/Linux/l131/ubt.log`. Three existing RealtimeMesh deprecation warnings remain. [Build log](../../../../Saved/Linux/l131/ue-build.log).
|
||||
- Unreal `Source/` files changed: **none**. No Windows compile is requested by this diff; Windows determinism remains unverified.
|
||||
- `git diff --check` passes. No commit, push, or stash was made.
|
||||
|
||||
Builds, per-run logs, timing records, hash records, and audit output are under `Saved/Linux/l131/`.
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare 18 canonical owner4/owner2 hashes with the pre-cleanup L130 binary."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l131/canonical-matrix"
|
||||
DATASETS = {
|
||||
"owner4": ROOT / "Standalone/data/owner_world.json",
|
||||
"owner2": ROOT / "Standalone/data/regression/owner_world_2strate_l61.json",
|
||||
}
|
||||
RECIPES = {"owner4": ("surface-world",), "owner2": ("surface-world", "floating-islands")}
|
||||
BINARIES = {
|
||||
"base": ROOT / "Saved/Linux/l130/bin/VoxelForgeCoreHeadless-l130-{compiler}",
|
||||
"l131": ROOT / "Saved/Linux/l131/bin/VoxelForgeCoreHeadless-l131-{compiler}",
|
||||
}
|
||||
HASH_RE = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
|
||||
|
||||
def run(dataset: str, recipe: str, compiler: str, workers: int, version: str) -> tuple[list[str], float]:
|
||||
binary = Path(str(BINARIES[version]).format(compiler=compiler))
|
||||
label = f"{dataset}-{recipe}-{compiler}-w{workers}-{version}"
|
||||
log = OUT / f"{label}.log"
|
||||
command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(binary), str(DATASETS[dataset]),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--workers={workers}", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=8", f"--recipe={recipe}", "--stream-hash"]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
process = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
hashes = HASH_RE.findall(log.read_text(encoding="utf-8", errors="replace"))
|
||||
if process.returncode or len(hashes) != 12:
|
||||
raise RuntimeError(f"hash run failed: {log}; exit={process.returncode}; rows={len(hashes)}")
|
||||
return hashes, time.monotonic() - started
|
||||
|
||||
|
||||
def main() -> int:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
rows = []
|
||||
groups = {}
|
||||
for dataset, recipes in RECIPES.items():
|
||||
for recipe in recipes:
|
||||
signatures = []
|
||||
for workers in (1, 8, 12):
|
||||
for compiler in ("clang", "gcc"):
|
||||
before, before_s = run(dataset, recipe, compiler, workers, "base")
|
||||
after, after_s = run(dataset, recipe, compiler, workers, "l131")
|
||||
row = {"dataset": dataset, "recipe": recipe, "compiler": compiler,
|
||||
"workers": workers, "hash_mismatches": sum(a != b for a, b in zip(before, after)),
|
||||
"rows": len(after), "base_s": round(before_s, 3), "l131_s": round(after_s, 3),
|
||||
"base_log": f"Saved/Linux/l131/canonical-matrix/{dataset}-{recipe}-{compiler}-w{workers}-base.log",
|
||||
"l131_log": f"Saved/Linux/l131/canonical-matrix/{dataset}-{recipe}-{compiler}-w{workers}-l131.log"}
|
||||
rows.append(row)
|
||||
signatures.append((compiler, workers, after))
|
||||
print("[L131CanonicalHash] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n")
|
||||
first = signatures[0][2]
|
||||
stable = all(sig == first for _, _, sig in signatures)
|
||||
groups[f"{dataset}/{recipe}"] = {"base_equals_l131": all(row["hash_mismatches"] == 0 for row in rows if row["dataset"] == dataset and row["recipe"] == recipe),
|
||||
"all_clang_gcc_workers_agree": stable}
|
||||
failures = [row for row in rows if row["hash_mismatches"]]
|
||||
(OUT / "summary.json").write_text(json.dumps({"runs": rows, "groups": groups,
|
||||
"passed": len(rows)-len(failures), "failed": len(failures)}, indent=2) + "\n")
|
||||
return 1 if failures or not all(group["all_clang_gcc_workers_agree"] for group in groups.values()) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare 24 owner4 tile-hash cases with the pre-cleanup L130 binary."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l131/hash-matrix"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"base": ROOT / "Saved/Linux/l130/bin/VoxelForgeCoreHeadless-l130-{compiler}",
|
||||
"l131": ROOT / "Saved/Linux/l131/bin/VoxelForgeCoreHeadless-l131-{compiler}",
|
||||
}
|
||||
CAMERAS = {
|
||||
"sw": ((8.0, 16.0, -372.2626953125), 2850),
|
||||
"fi": ((25.8369140625, 3.19921875, -737.0), 2249),
|
||||
"tn": ((19.166015625, -14.2001953125, -753.3740234375), 2233),
|
||||
"vs": ((-20.0625, -5.068359375, -900.353515625), 2329),
|
||||
}
|
||||
TILE_RE = re.compile(
|
||||
r"^\[PipelineTileHash\] lod=(-?\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) "
|
||||
r"density=(0x[0-9a-f]+) mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) "
|
||||
r"topology=(0x[0-9a-f]+)$", re.M)
|
||||
WHOLE_RE = re.compile(
|
||||
r"^\[PipelineWholeViewHash\] tiles=(\d+) density=(0x[0-9a-f]+) "
|
||||
r"mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) topology=(0x[0-9a-f]+)$", re.M)
|
||||
|
||||
|
||||
def capture(camera_name: str, compiler: str, workers: int, version: str) -> tuple[dict, tuple[str, ...], float]:
|
||||
camera, expected_tiles = CAMERAS[camera_name]
|
||||
camera_arg = ",".join(f"{value:.12f}" for value in camera) + ",0,0"
|
||||
binary = Path(str(BINARIES[version]).format(compiler=compiler))
|
||||
log = OUT / f"owner4-{camera_name}-{compiler}-w{workers}-{version}.log"
|
||||
command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", f"--workers={workers}",
|
||||
"--clip-radius=3", "--max-clip-level=8", "--max-level=8",
|
||||
f"--camera={camera_arg}", "--stream-hash-tiles"]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
tiles = {(int(match.group(1)), int(match.group(2)), int(match.group(3)), int(match.group(4))):
|
||||
match.group(5, 6, 7, 8) for match in TILE_RE.finditer(output)}
|
||||
whole = WHOLE_RE.search(output)
|
||||
if result.returncode or whole is None or len(tiles) != expected_tiles or len(tiles) != int(whole.group(1)):
|
||||
raise RuntimeError(f"incomplete {version} {camera_name}/{compiler}/W{workers}: {log}")
|
||||
return tiles, whole.groups(), elapsed
|
||||
|
||||
|
||||
def main() -> int:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
rows = []
|
||||
for camera_name in CAMERAS:
|
||||
for compiler in ("clang", "gcc"):
|
||||
for workers in (1, 8, 12):
|
||||
old_tiles, old_whole, old_s = capture(camera_name, compiler, workers, "base")
|
||||
new_tiles, new_whole, new_s = capture(camera_name, compiler, workers, "l131")
|
||||
keys = set(old_tiles) | set(new_tiles)
|
||||
tile_mismatches = sum(old_tiles.get(key) != new_tiles.get(key) for key in keys)
|
||||
row = {"owner": "owner4", "camera": camera_name, "compiler": compiler,
|
||||
"workers": workers, "tiles": len(new_tiles),
|
||||
"tile_mismatches": tile_mismatches,
|
||||
"whole_view_match": old_whole == new_whole,
|
||||
"base_s": round(old_s, 3), "l131_s": round(new_s, 3),
|
||||
"base_log": f"Saved/Linux/l131/hash-matrix/owner4-{camera_name}-{compiler}-w{workers}-base.log",
|
||||
"l131_log": f"Saved/Linux/l131/hash-matrix/owner4-{camera_name}-{compiler}-w{workers}-l131.log"}
|
||||
rows.append(row)
|
||||
print("[L131HashIdentity] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n")
|
||||
failures = [row for row in rows if row["tile_mismatches"] or not row["whole_view_match"]]
|
||||
(OUT / "summary.json").write_text(json.dumps({"runs": rows, "passed": len(rows)-len(failures),
|
||||
"failed": len(failures)}, indent=2) + "\n")
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Five interleaved L80 readiness measurements per stratum and worker count."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l131/timing"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"base": ROOT / "Saved/Linux/l130/bin/VoxelForgeCoreHeadless-l130-clang",
|
||||
"l131": ROOT / "Saved/Linux/l131/bin/VoxelForgeCoreHeadless-l131-clang",
|
||||
}
|
||||
CAMERAS = {
|
||||
0: (8.0, 16.0, -372.2626953125),
|
||||
1: (25.8369140625, 3.19921875, -737.0),
|
||||
2: (19.166015625, -14.2001953125, -753.3740234375),
|
||||
3: (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
PIPELINE_RE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
|
||||
FIELDS_RE = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
NEAR_RE = re.compile(r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$", re.M)
|
||||
|
||||
|
||||
def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict:
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
label = f"s{strate}-w{workers}-{version}-{phase}{replicate}-l131-v227"
|
||||
log = OUT / f"{label}.log"
|
||||
command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARIES[version]), str(DATA),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8",
|
||||
"--max-level=8", f"--camera={camera}", "--profile-lod-work",
|
||||
"--stream-scheduler-policy=3"]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as output:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=output,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
content = log.read_text(encoding="utf-8", errors="replace")
|
||||
pipeline, near = PIPELINE_RE.search(content), NEAR_RE.search(content)
|
||||
if result.returncode or pipeline is None or near is None:
|
||||
raise RuntimeError(f"timing run failed: {log}; exit={result.returncode}")
|
||||
fields = dict(FIELDS_RE.findall(pipeline.group(1)))
|
||||
row = {"strate": strate, "workers": workers, "version": version,
|
||||
"phase": phase, "replicate": replicate,
|
||||
"lod02_ready_s": float(near.group(3)),
|
||||
"whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")),
|
||||
"wall_s": round(time.monotonic() - started, 3),
|
||||
"log": str(log.relative_to(ROOT))}
|
||||
print("[L131TimingRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def quiet_machine() -> tuple[float, float]:
|
||||
load_1m = os.getloadavg()[0]
|
||||
def totals() -> tuple[int, int]:
|
||||
fields = Path("/proc/stat").read_text().splitlines()[0].split()
|
||||
values = [int(value) for value in fields[1:]]
|
||||
return sum(values), values[3] + (values[4] if len(values) > 4 else 0)
|
||||
before = totals()
|
||||
time.sleep(2.0)
|
||||
after = totals()
|
||||
idle = 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0])
|
||||
return load_1m, idle
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if any(not path.is_file() for path in (DATA, *BINARIES.values())):
|
||||
raise SystemExit("missing timing input")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
load_1m, idle = quiet_machine()
|
||||
(OUT / "machine-load-before.txt").write_text(
|
||||
f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n")
|
||||
print(f"[L131TimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True)
|
||||
if load_1m >= 0.9 or idle < 97.0:
|
||||
raise SystemExit("timing refused: machine is above the L80 quiet-load gate")
|
||||
|
||||
rows = []
|
||||
for strate in range(4):
|
||||
for workers in (8, 12):
|
||||
for version in BINARIES:
|
||||
rows.append(run(strate, workers, version, "warmup", 0))
|
||||
for replicate in range(1, 6):
|
||||
order = tuple(BINARIES) if replicate % 2 else tuple(reversed(BINARIES))
|
||||
for version in order:
|
||||
rows.append(run(strate, workers, version, "measured", replicate))
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n")
|
||||
|
||||
measured = [row for row in rows if row["phase"] == "measured"]
|
||||
summary = []
|
||||
for strate in range(4):
|
||||
for workers in (8, 12):
|
||||
old = [row for row in measured if row["strate"] == strate and row["workers"] == workers and row["version"] == "base"]
|
||||
new = [row for row in measured if row["strate"] == strate and row["workers"] == workers and row["version"] == "l131"]
|
||||
item = {"strate": strate, "workers": workers, "replicates_per_version": 5}
|
||||
for key in ("lod02_ready_s", "whole_view_ready_s"):
|
||||
old_med, new_med = statistics.median(row[key] for row in old), statistics.median(row[key] for row in new)
|
||||
item[f"base_{key}_median"] = old_med
|
||||
item[f"l131_{key}_median"] = new_med
|
||||
item[f"delta_pct_{key}"] = round(100.0 * (new_med / old_med - 1.0), 2)
|
||||
item[f"base_{key}_values"] = [row[key] for row in old]
|
||||
item[f"l131_{key}_values"] = [row[key] for row in new]
|
||||
summary.append(item)
|
||||
(OUT / "summary.json").write_text(json.dumps({"runs": rows, "summary": summary}, indent=2) + "\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create the isolated L132 owner snapshot with a native plan for existing FI strate 1."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
SOURCE = ROOT / "Standalone/data/owner_world.json"
|
||||
MANIFEST = ROOT / "Saved/Linux/l132/fixtures/owner-native-fi.world-assembly.json"
|
||||
LAYOUT = ROOT / "Saved/Linux/l132/fixtures/owner-native-fi.resolved-layout.json"
|
||||
OUTPUT = ROOT / "Saved/Linux/l132/fixtures/owner_world_native_fi.json"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
owner = json.loads(SOURCE.read_text(encoding="utf-8"))
|
||||
manifest_text = MANIFEST.read_text(encoding="utf-8").strip()
|
||||
layout_text = LAYOUT.read_text(encoding="utf-8").strip()
|
||||
manifest = json.loads(manifest_text)
|
||||
layout = json.loads(layout_text)
|
||||
islands = manifest.get("floating_islands", [])
|
||||
native = [row for row in islands if row.get("strate_index") == 1]
|
||||
resolved = [row for row in layout.get("strates", [])
|
||||
if row.get("strate_index") == 1]
|
||||
if len(native) != 1 or len(resolved) != 1:
|
||||
raise SystemExit("native FI snapshot must contain exactly one strate-1 island plan/layout")
|
||||
if (resolved[0].get("generator_kind") != "FloatingIslands"
|
||||
or resolved[0].get("recipe_id") != "FloatingIslands.ClusterField"
|
||||
or resolved[0].get("recipe_version") != 8):
|
||||
raise SystemExit("resolved strate 1 is not FloatingIslands.ClusterField v8")
|
||||
owner["world_assembly"] = manifest
|
||||
owner["core_world_assembly_json"] = manifest_text
|
||||
owner["resolved_layout_json"] = layout_text
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT.write_text(json.dumps(owner, separators=(",", ":")) + "\n", encoding="utf-8")
|
||||
print(f"[L132NativeFISnapshot] path={OUTPUT.relative_to(ROOT)} "
|
||||
f"islands={len(native[0].get('manifest', {}).get('plan', {}).get('islands', []))} "
|
||||
"strate=1 recipe=FloatingIslands.ClusterField/v8 status=PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,41 @@
|
||||
# L132 — owner density dispatch
|
||||
|
||||
## Result
|
||||
|
||||
With Core enabled in the owner headless pipeline, the four strates bind to four paths. Unreal’s `bUseVoxelForgeCore` remains opt-in and defaults off. The fast-tier diagnostic reports the Core bindings at the four L80 owner poses and attributes hashed tiles by tile-center height within the resolved stratum bounds.
|
||||
|
||||
| Strate | Owner recipe | Bound path | Tile centers / share |
|
||||
|---|---|---|---:|
|
||||
| SW (0) | `SurfaceWorld.Heightfield2D` v2 | `SurfaceWorld-native` | 2,640 / 100% |
|
||||
| FI (1) | `FloatingIslands.ClusterField` v0 | `compiled-analytic/FloatingIslands` | 2,128 / 100% |
|
||||
| TN (2) | `legacy.TunnelNetwork` v0 | `compiled-analytic/TunnelNetwork` | 2,208 / 100% |
|
||||
| VS (3) | `legacy.VerticalShafts` v0 | `compiled-analytic/VerticalShafts` | 1,392 / 100% |
|
||||
|
||||
SW has a matching strate/bounds `SurfaceWorld` plan, and its `Heightfield2D` evaluator is valid. FI has a legacy v0 layout record but no matching `FloatingIslandPlan`, so `FloatingIslandFieldV1` cannot bind. The Core owner manifest has zero island plans. TN and VS have legacy recipe IDs, not `Core.Caves.PrimitiveField` v1 or `Core.Lattice.PrimitiveField` v1. Their resolved layouts therefore lack the matching graph recipe and native-field capability. All three fall through to `FLegacyGeneratorFieldAdapter`.
|
||||
|
||||
The dispatch check is in the fast tier and can also be requested from the headless binary with `--density-dispatch-report`. It prints the per-strate bound adapter/path and hashed tile-center path shares. Its tile counts aggregate the four standard owner poses; tiles whose centers fall outside an interior interval are excluded, and a tile spanning a boundary is assigned by its center. Per-view logs are `Saved/Linux/l132/owner-density-dispatch-s*-clang.log`; the printed shares are in `Saved/Linux/l132/logs/verify-fast.log` and `Saved/Linux/l132/fast-summary.json`.
|
||||
|
||||
Across the 8,368 classified tile centers, path shares are 31.5% SurfaceWorld-native, 25.4% compiled-analytic/FloatingIslands, 26.4% compiled-analytic/TunnelNetwork, and 16.6% compiled-analytic/VerticalShafts.
|
||||
|
||||
## Native FI decision
|
||||
|
||||
The native evaluator is present. `BuildFloatingIslandStratumPlan` can convert the owner’s legacy island parameters to a v8 plan, but the owner’s imported manifest does not contain that plan. The existing automatic-plan branch is guarded by `bImportedOverride` inside the generated-assembly branch, so it does not fill this owner’s missing imported plan.
|
||||
|
||||
I built a candidate v8 plan for the existing owner FI strate without changing the default owner data. Its raw island-route audit passed: 29 route points, 78,336 capsule samples, zero blocked samples, zero unsupported probes, and zero grade failures. The composed owner audit did not pass:
|
||||
|
||||
- Existing compiled-analytic owner: all 3 passage interfaces pass; shell floor violations 0; ripple checks 3/3.
|
||||
- Native FI candidate: only 2/3 interfaces pass; the added FI route is not walkable (1/2 strate routes walkable); shell floor violations 13; ripple checks 2/3.
|
||||
- Passage tile seams passed for both bindings: 4 pairs, 4,900 shared samples, zero mismatches.
|
||||
|
||||
Native FI is a quality regression on the owner’s current passages, despite its standalone route plan passing. I did not switch the owner. The conditional captures, W8/W12 timing matrix, and Clang/GCC W1/W8/W12 matrix were not run because the candidate failed the composed walk/shell gate. No Core version or output baseline changed; the default owner output remains on its existing bindings. No Linux UE host build was run because there was no qualifying native change or timing matrix.
|
||||
|
||||
## TN/VS graph translation size
|
||||
|
||||
The active TN `GenerationParams` block has 47 fields; VS `VerticalShaftParams` has 11. Each also has 15 passage settings. Both have `CoreGeometry.GeneratorRecipe=0` and empty `Rooms`, `Tunnels`, and `Stamps` arrays. Native cave/lattice authoring requires at least one explicit room and one edge, with geometry, links, and route data. There is no adapter translating those legacy generator blocks into graph records. This is a substantial procedural migration, not a small recipe mapping, so I did not build it this round.
|
||||
|
||||
## Verification
|
||||
|
||||
- Fast tier: PASS, 304.0 s, 84 runs and 273 checks; includes four-pose dispatch output.
|
||||
- Passage tile seams: compiled-analytic and native FI candidate both PASS at the FI owner pose.
|
||||
- Owner walk/shell comparison: default PASS; native candidate FAIL as detailed above.
|
||||
- Build stamp: `766b94ca7638475975b0345e5907eebb96158360c384f5d916c78eccbb1aa815`.
|
||||
@@ -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 |