Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d654ff861 | ||
|
|
a45ba9bb10 | ||
|
|
a4e214561f | ||
|
|
6cd9e4da76 | ||
|
|
9c6d261d01 | ||
|
|
eb47d72258 | ||
|
|
95bf654fbc | ||
|
|
0065ee68e6 | ||
|
|
ebae71b142 | ||
|
|
81071969fc | ||
|
|
cbbfd46bd3 |
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). |
|
||||
@@ -597,6 +597,62 @@ namespace VoxelForgeCore
|
||||
}
|
||||
}
|
||||
|
||||
inline constexpr std::string_view CoreLiminalFieldRecipeIdV1 = "Core.Liminal.Field";
|
||||
inline constexpr std::uint32_t CoreLiminalFieldRecipeVersionV1 = 1;
|
||||
inline constexpr std::uint32_t CoreLiminalFieldRecipeVersionV2 = 2;
|
||||
inline constexpr std::uint32_t CoreLiminalFieldVariantCountV1 = 4;
|
||||
|
||||
enum class WorldAssemblyLiminalVariantV1 : std::uint32_t
|
||||
{
|
||||
flatFieldMonolith = 0,
|
||||
repeatingHills = 1,
|
||||
roadOverCloudFloor = 2,
|
||||
pillarGrid = 3
|
||||
};
|
||||
|
||||
struct WorldAssemblyLiminalVariantDefinitionV1
|
||||
{
|
||||
WorldAssemblyLiminalVariantV1 variant = WorldAssemblyLiminalVariantV1::flatFieldMonolith;
|
||||
std::string_view name;
|
||||
WorldAssemblySurfaceProfileKindV1 surfaceKind = WorldAssemblySurfaceProfileKindV1::flatField;
|
||||
std::int64_t hillAmplitudeMicrometres = 0;
|
||||
std::int64_t hillWavelengthMicrometres = 0;
|
||||
std::int64_t routeHalfWidthMicrometres = 0;
|
||||
std::int64_t routeHalfThicknessMicrometres = 0;
|
||||
std::int64_t supportSpacingMicrometres = 0;
|
||||
std::int64_t supportRadiusMicrometres = 0;
|
||||
std::int64_t monolithRadiusMicrometres = 0;
|
||||
std::int64_t monolithHeightMicrometres = 0;
|
||||
std::uint32_t pillarRows = 0;
|
||||
std::uint32_t pillarColumns = 0;
|
||||
std::int64_t pillarSpacingMicrometres = 0;
|
||||
std::int64_t pillarRadiusMicrometres = 0;
|
||||
std::int64_t pillarHeightMicrometres = 0;
|
||||
};
|
||||
|
||||
inline constexpr std::array<WorldAssemblyLiminalVariantDefinitionV1,
|
||||
CoreLiminalFieldVariantCountV1> CoreLiminalFieldVariantsV1{{
|
||||
{WorldAssemblyLiminalVariantV1::flatFieldMonolith, "flatField-monolith",
|
||||
WorldAssemblySurfaceProfileKindV1::flatField, 0, 0, 1750000, 500000,
|
||||
36000000, 550000, 1600000, 26000000, 0, 0, 0, 0, 0},
|
||||
{WorldAssemblyLiminalVariantV1::repeatingHills, "periodicHills",
|
||||
WorldAssemblySurfaceProfileKindV1::periodicHills, 2200000, 80000000,
|
||||
1750000, 500000, 36000000, 550000, 0, 0, 0, 0, 0, 0, 0},
|
||||
{WorldAssemblyLiminalVariantV1::roadOverCloudFloor, "road-over-cloud-floor",
|
||||
WorldAssemblySurfaceProfileKindV1::flatField, 0, 0, 2000000, 500000,
|
||||
24000000, 550000, 0, 0, 0, 0, 0, 0, 0},
|
||||
{WorldAssemblyLiminalVariantV1::pillarGrid, "pillar-grid",
|
||||
WorldAssemblySurfaceProfileKindV1::flatField, 0, 0, 1750000, 500000,
|
||||
36000000, 550000, 0, 0, 15, 15, 76000000, 5500000, 40000000}
|
||||
}};
|
||||
|
||||
inline constexpr WorldAssemblyLiminalVariantV1 SelectCoreLiminalFieldVariantV1(
|
||||
std::uint32_t seed) noexcept
|
||||
{
|
||||
return static_cast<WorldAssemblyLiminalVariantV1>(
|
||||
seed % CoreLiminalFieldVariantCountV1);
|
||||
}
|
||||
|
||||
struct WorldAssemblyRecipeV1
|
||||
{
|
||||
std::uint32_t schemaVersion = WorldAssemblySchemaVersion;
|
||||
@@ -654,6 +710,23 @@ namespace VoxelForgeCore
|
||||
std::array<std::int64_t, 4> parametersSubvoxels{};
|
||||
};
|
||||
|
||||
struct WorldAssemblyLiminalFieldPlanV1
|
||||
{
|
||||
std::uint32_t version = 0;
|
||||
WorldAssemblyLiminalVariantV1 variant = WorldAssemblyLiminalVariantV1::flatFieldMonolith;
|
||||
std::uint32_t seed = 0;
|
||||
std::array<std::int64_t, 3> arrivalMouthSubvoxels{};
|
||||
std::array<std::int64_t, 3> departureMouthSubvoxels{};
|
||||
// Fixed distance from each passage player-center anchor to its walk surface.
|
||||
std::int64_t mouthFloorOffsetSubvoxels = 0;
|
||||
std::int64_t floorHeightSubvoxels = 0;
|
||||
std::int64_t routeHalfWidthSubvoxels = 0;
|
||||
std::int64_t routeHalfThicknessSubvoxels = 0;
|
||||
std::int64_t routeOffsetSubvoxels = 0;
|
||||
std::int64_t supportSpacingSubvoxels = 0;
|
||||
std::int64_t supportRadiusSubvoxels = 0;
|
||||
};
|
||||
|
||||
struct WorldAssemblyStratumPlanV1
|
||||
{
|
||||
std::int32_t strateIndex = -1;
|
||||
@@ -678,6 +751,7 @@ namespace VoxelForgeCore
|
||||
std::vector<WorldAssemblyFeatureV1> features;
|
||||
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
||||
std::vector<WorldAssemblyRoutePointV1> route;
|
||||
WorldAssemblyLiminalFieldPlanV1 liminalField;
|
||||
std::uint64_t contentHash = 0;
|
||||
};
|
||||
|
||||
@@ -891,6 +965,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 };
|
||||
@@ -904,7 +981,8 @@ namespace VoxelForgeCore
|
||||
fillPillar = 3,
|
||||
fillDais = 4,
|
||||
fillMonolith = 5,
|
||||
fillPlatform = 6
|
||||
fillPlatform = 6,
|
||||
fillSweptDeckSegment = 7
|
||||
};
|
||||
|
||||
struct ResolvedLayoutStampRequestV1
|
||||
@@ -914,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;
|
||||
@@ -933,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{};
|
||||
@@ -1060,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;
|
||||
@@ -1089,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;
|
||||
};
|
||||
@@ -1228,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,16 +1376,63 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
const std::int64_t pad=motif
|
||||
?(stamp.smoothWidthMicrometres+5)/6+stamp.irregularityAmplitudeMicrometres
|
||||
:stamp.smoothWidthMicrometres+stamp.irregularityAmplitudeMicrometres;
|
||||
const std::array<std::int64_t,3> extents={
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad,
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad,
|
||||
stamp.halfExtentsMicrometres[2]+pad};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(stamp.boundsMinimumMicrometres[axis]
|
||||
!=stamp.positionMicrometres[axis]-extents[axis]
|
||||
||stamp.boundsMaximumMicrometres[axis]
|
||||
!=stamp.positionMicrometres[axis]+extents[axis])
|
||||
add("stamp_bounds",stamp.stableId+" conservative bounds do not match its parameters");
|
||||
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,
|
||||
stamp.halfExtentsMicrometres[2]+pad};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(stamp.boundsMinimumMicrometres[axis]
|
||||
!=stamp.positionMicrometres[axis]-extents[axis]
|
||||
||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,20 +2015,70 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
stamp.positionMicrometres[axis]=request.anchorPositionMicrometres[axis]+offset[axis];
|
||||
const std::int64_t pad=stamp.smoothWidthMicrometres
|
||||
+stamp.irregularityAmplitudeMicrometres;
|
||||
const std::array<std::int64_t,3> extents={
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad,
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad,
|
||||
stamp.halfExtentsMicrometres[2]+pad};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&stamp.sweepPathMicrometres.empty())
|
||||
{
|
||||
stamp.boundsMinimumMicrometres[axis]=stamp.positionMicrometres[axis]-extents[axis];
|
||||
stamp.boundsMaximumMicrometres[axis]=stamp.positionMicrometres[axis]+extents[axis];
|
||||
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;
|
||||
}
|
||||
}
|
||||
if(!LayoutStampSafe(candidate,stamp))continue;
|
||||
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,
|
||||
stamp.halfExtentsMicrometres[2]+pad};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
stamp.boundsMinimumMicrometres[axis]=stamp.positionMicrometres[axis]-extents[axis];
|
||||
stamp.boundsMaximumMicrometres[axis]=stamp.positionMicrometres[axis]+extents[axis];
|
||||
}
|
||||
}
|
||||
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,48 +33,22 @@
|
||||
#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 bool VF_IsL130PassageReachProofAuditEnabled()
|
||||
{
|
||||
static const bool bEnabled=VF_IsL88DiagnosticSwitchEnabled(
|
||||
static const bool bEnabled=VF_IsDiagnosticEnvSwitchEnabled(
|
||||
"VOXELFORGE_L130_PASSAGE_REACH_PROOF_AUDIT");
|
||||
return bEnabled;
|
||||
}
|
||||
@@ -327,18 +301,14 @@ public:
|
||||
{
|
||||
mayContainResolvedBoundaryBand_=shape.ResolvedBoundaryBandsIntersectSubvoxelRange(
|
||||
minimumSubvoxels_[2],maximumSubvoxels_[2]);
|
||||
const bool disablePassageComposition=VF_IsL88PassageCompositionDisabled();
|
||||
if(VF_IsL88BoundaryBandsDisabled())mayContainResolvedBoundaryBand_=false;
|
||||
mayContainTransitionPassageFade_=!disablePassageComposition
|
||||
&&shape.ResolvedTransitionPassageFadeIntersectsTileXY(
|
||||
minimumSubvoxels_,maximumSubvoxels_);
|
||||
if(!disablePassageComposition)
|
||||
featureIndices_=shape.BuildPassageFeatureTileList(
|
||||
minimumSubvoxels_,maximumSubvoxels_);
|
||||
mayContainTransitionPassageFade_=shape.ResolvedTransitionPassageFadeIntersectsTileXY(
|
||||
minimumSubvoxels_,maximumSubvoxels_);
|
||||
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_)
|
||||
@@ -1553,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;
|
||||
@@ -1561,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);
|
||||
@@ -1580,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=[]
|
||||
{
|
||||
@@ -1595,7 +1565,6 @@ public:
|
||||
?bound->Resolved->passageCapabilities.hasNativeCaveField
|
||||
:bound->Resolved->passageCapabilities.hasNativeLatticeField))
|
||||
{
|
||||
if(!disablePassageComposition)
|
||||
{
|
||||
VoxelForgeCore::Linux77Profiler::FScopedPhase l77PassagePhase(
|
||||
VoxelForgeCore::Linux77Profiler::EPhase::Passage);
|
||||
@@ -1626,7 +1595,6 @@ public:
|
||||
ELegacyFieldTerm::ComposerPassage);
|
||||
final=ApplyResolvedCavernFloorCaps(stamped,X,Y,Z);
|
||||
}
|
||||
if(!disablePassageComposition)
|
||||
{
|
||||
VoxelForgeCore::Linux77Profiler::FScopedPhase l77PassagePhase(
|
||||
VoxelForgeCore::Linux77Profiler::EPhase::Passage);
|
||||
@@ -1634,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);
|
||||
@@ -2332,18 +2306,8 @@ public:
|
||||
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;
|
||||
}
|
||||
Error="compiled shape world contains a solid passage centreline sample";
|
||||
return false;
|
||||
}
|
||||
Stats.PassageValidationSeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
|
||||
Built=true;
|
||||
@@ -3581,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;
|
||||
@@ -3746,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={
|
||||
@@ -5208,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
|
||||
{
|
||||
@@ -7162,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];
|
||||
@@ -7198,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)
|
||||
@@ -7209,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)
|
||||
{
|
||||
@@ -7293,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;
|
||||
@@ -7310,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;
|
||||
@@ -7327,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
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr,
|
||||
options.profileMesherStages?&McStageTimings:nullptr,
|
||||
options.auditUniformBlockProofs?&BlockProofAudit:nullptr,
|
||||
options.forceFullDensityCapture);
|
||||
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,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 |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the 3-strata composed Core.Liminal.Field v2 fixture."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l145"))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures")
|
||||
args = parser.parse_args()
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
world = json.loads(OWNER.read_text(encoding="utf-8"))
|
||||
if len(world.get("strates", [])) < 3:
|
||||
raise SystemExit("owner fixture needs at least three authored strate slots")
|
||||
|
||||
world["strates"] = world["strates"][:3]
|
||||
world["total_strates"] = 3
|
||||
world["settings_asset"]["TotalStrates"] = 3
|
||||
world["settings_asset"]["FixedStrates"] = world["settings_asset"]["FixedStrates"][:3]
|
||||
# Keep the resolved arrival near the departure depth. The owner's 5000-unit
|
||||
# reach clamps to liminal bedrock on most seeds and leaves no field floor.
|
||||
world["strates"][0]["passage_config"]["ReachMin"] = 1536
|
||||
world["strates"][0]["passage_config"]["ReachMax"] = 1792
|
||||
world["surface_profile_override"] = {
|
||||
"version": 1,
|
||||
"kind": "flatField",
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
middle = world["strates"][1]
|
||||
middle["asset"]["GeneratorType"] = 4 # ECaveGeneratorType::SurfaceWorld
|
||||
middle["asset"]["StrateName"] = "Liminal Field"
|
||||
# Keep the departure mouth 24–32 m above liminal bedrock so every variant
|
||||
# has room for its floor, road landing and support footings.
|
||||
middle["passage_config"]["ReachMin"] = 192
|
||||
middle["passage_config"]["ReachMax"] = 256
|
||||
world["core_liminal_field"] = {
|
||||
"version": 2,
|
||||
"strate_index": int(middle["index"]),
|
||||
"variant_policy": "seed_modulo_4",
|
||||
}
|
||||
|
||||
output = args.out / "core-liminal-field-3-strata.json"
|
||||
output.write_text(json.dumps(world, separators=(",", ":")) + "\n", encoding="utf-8")
|
||||
print(f"[L145Fixture] path={output} strates=SurfaceWorld,Core.Liminal.Field,TunnelNetwork status=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture one arrival-mouth eye view per Core.Liminal.Field v2 variant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
HERE = Path(__file__).resolve().parent
|
||||
OUT = ROOT / "Saved/Linux/l145"
|
||||
CAPTURES = HERE / "captures"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
Q_PER_METRE = 1024.0
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_step(name: str, command: list[object], log: Path, env: dict[str, str],
|
||||
stamped: bool = False) -> str:
|
||||
expected_stamp = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
binary = Path(str(command[0]))
|
||||
probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
||||
if probe is None or probe.returncode != 0 \
|
||||
or first_stamp(probe.stdout or "") != expected_stamp:
|
||||
raise RuntimeError(f"{name} refused a stale executable: {binary}")
|
||||
started = time.monotonic()
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)],
|
||||
cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=1745, check=False)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L145Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if stamped and (first_stamp(output) != expected_stamp
|
||||
or current_stamp() != expected_stamp):
|
||||
raise RuntimeError(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"{name} exit={result.returncode}; see {log}")
|
||||
return output
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
dx, dy, dz = (target[i] - camera[i] for i in range(3))
|
||||
return math.degrees(math.atan2(dx, dy)), math.degrees(
|
||||
math.atan2(dz, math.hypot(dx, dy)))
|
||||
|
||||
|
||||
def route_views(manifest_path: Path, layout_path: Path, name: str):
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
plan = next(row for row in manifest["plan"]["strates"]
|
||||
if row["strate_index"] == 1 and row.get("liminal_field", {}).get("version") == 2)
|
||||
points = [tuple(value / Q_PER_METRE for value in row["position_q"])
|
||||
for row in plan["route"]]
|
||||
first, second = points[0], points[1]
|
||||
dx, dy = second[0] - first[0], second[1] - first[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length <= 1.0e-6:
|
||||
raise RuntimeError("Core.Liminal.Field arrival tangent is degenerate")
|
||||
tx, ty = dx / length, dy / length
|
||||
eye_camera = (first[0] - tx * 1.5, first[1] - ty * 1.5, first[2] + 1.6)
|
||||
third = points[min(2, len(points) - 1)]
|
||||
dx, dy = third[0] - first[0], third[1] - first[1]
|
||||
tangent_length = math.hypot(dx, dy)
|
||||
tx, ty = dx / tangent_length, dy / tangent_length
|
||||
dz_per_metre = (third[2] - first[2]) / tangent_length
|
||||
eye_target = (first[0] + tx * 40.0, first[1] + ty * 40.0,
|
||||
first[2] + dz_per_metre * 40.0 + 1.6)
|
||||
|
||||
middle = len(points) // 2
|
||||
before, center, after = points[middle - 1], points[middle], points[middle + 1]
|
||||
dx, dy = after[0] - before[0], after[1] - before[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length <= 1.0e-6:
|
||||
raise RuntimeError("Core.Liminal.Field wide-view tangent is degenerate")
|
||||
tx, ty = dx / length, dy / length
|
||||
nx, ny = -ty, tx
|
||||
floor = plan["liminal_field"]["floor_height_q"] / Q_PER_METRE
|
||||
if name == "flatField-monolith":
|
||||
monolith = next(row for row in layout.get("stamps", [])
|
||||
if row.get("stable_id") == "core/liminal-field/monolith/000")
|
||||
monolith_center = tuple(value / 1.0e6 for value in monolith["position_um"])
|
||||
floor = monolith_center[2] - monolith["half_extents_um"][2] / 1.0e6
|
||||
wide_camera = (monolith_center[0] - tx * 30.0 + nx * 18.0,
|
||||
monolith_center[1] - ty * 30.0 + ny * 18.0, floor + 25.0)
|
||||
wide_target = monolith_center
|
||||
elif name == "periodicHills":
|
||||
wide_camera = (center[0] - 20.0, center[1] - 25.0, floor + 10.0)
|
||||
wide_target = (center[0] + 10.0, center[1] + 8.0, floor + 1.0)
|
||||
elif name == "pillar-grid":
|
||||
pillars = [row for row in layout.get("stamps", [])
|
||||
if row.get("stable_id", "").startswith("core/liminal-field/pillars/")]
|
||||
if not pillars:
|
||||
raise RuntimeError("pillar-grid layout has no resolved grid stamps")
|
||||
pairs = []
|
||||
for i, first_pillar in enumerate(pillars):
|
||||
for second_pillar in pillars[i + 1:]:
|
||||
a = first_pillar["position_um"]
|
||||
b = second_pillar["position_um"]
|
||||
dx, dy = (b[0] - a[0]) / 1.0e6, (b[1] - a[1]) / 1.0e6
|
||||
separation = math.hypot(dx, dy)
|
||||
if 60.0 <= separation <= 90.0:
|
||||
mx = (a[0] + b[0]) / 2.0e6
|
||||
my = (a[1] + b[1]) / 2.0e6
|
||||
pairs.append((math.hypot(mx - center[0], my - center[1]),
|
||||
mx, my, dx / separation, dy / separation,
|
||||
(a[2] + b[2]) / 2.0e6,
|
||||
first_pillar["half_extents_um"][2] / 1.0e6))
|
||||
if not pairs:
|
||||
raise RuntimeError("pillar-grid has no neighboring resolved columns")
|
||||
_, mx, my, tx, ty, pillar_z, half_height = min(pairs)
|
||||
floor = pillar_z - half_height
|
||||
nx, ny = -ty, tx
|
||||
wide_camera = (center[0] - tx * 20.0 + nx * 25.0,
|
||||
center[1] - ty * 20.0 + ny * 25.0, center[2] + 8.0)
|
||||
wide_target = (center[0], center[1], floor + 2.0)
|
||||
else:
|
||||
wide_camera = (center[0] - tx * 20.0 + nx * 25.0,
|
||||
center[1] - ty * 20.0 + ny * 25.0, center[2] + 8.0)
|
||||
wide_target = (center[0], center[1], floor + 2.0)
|
||||
return {"eye": (eye_camera, eye_target), "wide": (wide_camera, wide_target)}
|
||||
|
||||
|
||||
def capture_variant(seed: int, name: str, *, headless: Path, renderer: Path,
|
||||
fixture: Path, env: dict[str, str]):
|
||||
stem = f"core-liminal-{name}-seed{seed:02d}"
|
||||
plan_dir = OUT / "capture-plans" / stem
|
||||
plan_dir.mkdir(parents=True, exist_ok=True)
|
||||
manifest = plan_dir / "manifest.json"
|
||||
layout = plan_dir / "layout.json"
|
||||
command = [headless, fixture, "--workers=1", f"--seed={seed}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=1", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--camera=0,0,-540,0,0",
|
||||
f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
output = run_step(f"plan-{stem}", command, OUT / "logs" / f"capture-{stem}-plan.log",
|
||||
env, stamped=True)
|
||||
if "[CoreLiminalFieldDraw]" not in output:
|
||||
raise RuntimeError(f"{stem} plan omitted its Core liminal deck")
|
||||
views = route_views(manifest, layout, name)
|
||||
for view, (eye, target) in {"eye": views["eye"]}.items():
|
||||
work = OUT / "captures" / f"{stem}-{view}"
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
view_manifest = work / "manifest.json"
|
||||
view_layout = work / "layout.json"
|
||||
mesh = work / "world.vfmesh"
|
||||
yaw, pitch = camera_angles(eye, target)
|
||||
camera = (*eye, yaw, pitch)
|
||||
view_clip_radius = (5 if view == "wide" and name in {
|
||||
"flatField-monolith", "periodicHills"} else
|
||||
3)
|
||||
command = [headless, fixture, "--workers=12", f"--seed={seed}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={view_clip_radius}",
|
||||
"--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--capture-skip-walkability",
|
||||
f"--camera={fmt(camera)}", f"--mesh-export={mesh}",
|
||||
f"--world-assembler-manifest-output={view_manifest}",
|
||||
f"--resolved-layout-output={view_layout}"]
|
||||
output = run_step(f"export-{stem}-{view}", command,
|
||||
OUT / "logs" / f"capture-{stem}-{view}-export.log", env, stamped=True)
|
||||
if "[CoreLiminalFieldDraw]" not in output or not mesh.is_file() \
|
||||
or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"{stem}-{view} export omitted its Core liminal deck or mesh")
|
||||
png = work / f"{view}.png"
|
||||
image = CAPTURES / f"{stem}-{view}.jpg"
|
||||
fog = {
|
||||
"flatField-monolith": (30, 180, "0.66,0.73,0.77", -708.0, 46.0),
|
||||
"periodicHills": (140, 850, "0.78,0.84,0.88", -720.0, 14.0),
|
||||
"road-over-cloud-floor": (24, 420, "0.88,0.93,0.95", -720.0, 65.0),
|
||||
"pillar-grid": (180, 1200, "0.82,0.88,0.91", -720.0, 80.0),
|
||||
}[name]
|
||||
fog_start, fog_end, fog_color, height_center, height_range = fog
|
||||
render = [renderer, mesh, png, "--size=1600x900", "--fov-deg=72",
|
||||
f"--camera-m={fmt(eye)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--tile-skirts=on", "--shade=readable", "--headlamp=off", "--height-tint=on",
|
||||
f"--height-center-m={height_center:.6f}", f"--height-range-m={height_range:.6f}",
|
||||
"--light-direction=-0.42,-0.58,0.70", "--fog=on", f"--fog-start-m={fog_start}",
|
||||
f"--fog-end-m={fog_end}", f"--fog-color={fog_color}"]
|
||||
run_step(f"render-{stem}-{view}", render,
|
||||
OUT / "logs" / f"capture-{stem}-{view}-render.log", env, stamped=True)
|
||||
run_step(f"jpeg-{stem}-{view}", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel",
|
||||
"error", "-y", "-i", png, "-frames:v", "1", "-q:v", "4", image],
|
||||
OUT / "logs" / f"capture-{stem}-{view}-jpeg.log", env)
|
||||
run_step(f"quality-{stem}-{view}", [sys.executable,
|
||||
ROOT / "Standalone/Tools/verify/capture_quality.py", image],
|
||||
OUT / "logs" / f"capture-{stem}-{view}-quality.log", env)
|
||||
print(f"[L145Capture] variant={name} seed={seed} view={view} "
|
||||
f"eye_height={'1.600m' if view == 'eye' else 'wide'} bytes={image.stat().st_size} "
|
||||
f"sha256={sha256(image)} status=PASS", flush=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
headless = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l145-clang"
|
||||
renderer = ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l145-clang"
|
||||
fixture = OUT / "fixtures/core-liminal-field-3-strata.json"
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(OUT)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT / "Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
CAPTURES.mkdir(parents=True, exist_ok=True)
|
||||
variants = ("flatField-monolith", "periodicHills",
|
||||
"road-over-cloud-floor", "pillar-grid")
|
||||
selected = set(os.environ.get("L145_CAPTURE_VARIANTS", "").split(","))
|
||||
for seed, name in enumerate(variants):
|
||||
if selected and name not in selected:
|
||||
continue
|
||||
capture_variant(seed, name, headless=headless, renderer=renderer,
|
||||
fixture=fixture, env=env)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L145Capture] status=FAIL error={error}", file=sys.stderr, flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,51 @@
|
||||
# Linux145 — Core.Liminal.Field v2 landing pads
|
||||
|
||||
**Result: endpoint landing pads alone did not make the liminal join solvable.** The pads bind the passage mouth to certified ground and remove grade failures in the final seed 5 probe, but the passage capsule still intersects native Surface World terrain about 29 m from the departure pad. This matches R48's warning that a pad must cover the approach and tube exclusion envelope; stop here and use a bounded joint plan.
|
||||
|
||||
## What changed
|
||||
|
||||
- Added an opt-in landing-pad record to resolved strata: stable ID, arrival/departure role, position, floor height, Q16 approach direction, clearance, support depth, and a certification bit. The passage planner validates the record and binds the endpoint and tangent to it. Empty pad lists are omitted from canonical serialization, preserving legacy owner layouts and hashes.
|
||||
- Added `Core.Liminal.Field` v2 pads on the field profile: flat ground, hill/valley profile, road endpoints, and pillar-grid clearings. Its layer route connects the pads; the road uses that route as its road centerline.
|
||||
- Added an uncertified-pad negative control. Corrected the ripple audit to retain each valid sample's actual route distance across masked gaps; a 5 m synthetic gap control now confirms that the measurement detects a 5.8 cm ripple and rejects the same smooth cubic control.
|
||||
- Fixed the composed audit's footing placement so it counts actual structural supports. No Unreal `Source/` files changed.
|
||||
|
||||
## Count first: failure cause
|
||||
|
||||
L144's fixed-mouth route already showed both end-connector grade and capsule blockage. For the flat-field seed 0 probe, the liminal route had 222 blocked capsule samples and 7 grade failures, with no unsupported samples or structural-support failures. Pillar-grid seed 3 had 289 blocked samples and 9 grade failures. In the separate seed 5 audit, interface 1–2 had 10 blocked passage-capsule samples and the liminal route had 9 blocked samples, both with zero grade failures. These results say support alone was not the cause; the fixed endpoint geometry sometimes made the approach too steep, while other cases had occupied space at the join. [L144 report](../Linux144/REPORT.md); logs: `Saved/Linux/l144/logs/probe8-seed00.log`, `probe8-seed03.log`, `audit-seed05.log`.
|
||||
|
||||
The pad experiment removed the endpoint-grade part in seed 5, but not the occupied-space part:
|
||||
|
||||
| Probe | Passage 1–2 capsule | Grade / support | Shell | Liminal route |
|
||||
|---|---:|---|---|---|
|
||||
| L144 seed 5, fixed mouths | 10 blocked | 0 grade failures; 0 unsupported | not the failing gate | 9 blocked; 0 grade failures |
|
||||
| L145 seed 5, 45 m ground margin | 140 blocked; 20 interior shell violations | 0 grade failures; 0 unsupported | FAIL | PASS |
|
||||
| L145 seed 5, final 20 m ground margin | 289 blocked | 0 grade failures; 0 unsupported | PASS | PASS |
|
||||
|
||||
The final L145 Clang W1 probe (source stamp `82e3b58c…`) certified both pads. The departure pad was `(12, 0, -716.451) m`, with floor `-717.581 m`, 6 m clearance, and 2 m support depth. Yet the interface-1–2 passage reported 289/50,904 capsule samples blocked, with 0/4,040 unsupported and 0 grade failures. A blocked sample at `(-16.087, 2.915, -722.743) m` had native density `-3.629` and composed density `-0.162`; it is about 29 m from the departure pad. The sample is inside the passage tube, so the layer's field remains solid across the approach even though its pad itself is certified. The lower interface, both shell audits, both ripple audits, and the liminal walk audit passed; the final composed passage summary failed and seam audit did not run for this seed. See `Saved/Linux/l145/logs/audit-seed05-clang-w1.log`.
|
||||
|
||||
For the 45 m attempt, the original seed 0–4 Clang W1 probes passed their composed gates, then seed 5 failed. The seed 5 log was overwritten by the final 20 m rerun; its counts above are from that earlier run. The corrected ripple fitter also removed a false 5.99 cm reading caused by compressing masked samples: at 45 m, seed 0 measured 2.87 cm after retaining route distances. These are probe results, not timing measurements.
|
||||
|
||||
The third repeated seed 5 failure reaches the requested stop condition. This does not show that a full candidate-and-approach contract is impossible; it shows the tested endpoint-only pad contract is too weak. The passage planner needs to choose a candidate while checking the full capsule approach and tube against the layer field, as R48 recommends.
|
||||
|
||||
## Captures
|
||||
|
||||
One 1.6 m eye-height capture was rendered at each arrival pad. All four passed `capture_quality.py`; mesh export used `--capture-skip-walkability`, so these images show appearance and are not walkability evidence.
|
||||
|
||||
| Variant | Capture |
|
||||
|---|---|
|
||||
| Flat field + monolith |  |
|
||||
| Repeating hills |  |
|
||||
| Road over cloud floor |  |
|
||||
| Pillar grid |  |
|
||||
|
||||
## Checks
|
||||
|
||||
- Clang 19 headless and offscreen-capture binaries reported the final source stamp. Four arrival captures passed quality checks.
|
||||
- Fast tier passed on the final source: `Saved/Linux/l145/fast/fast-summary.json` (280.8 s tier duration). The initial default-output attempt refused a stale L42 binary before tests started; rerunning into the isolated L145 output directory with the current L145 binary passed.
|
||||
- Owner canonical hash checks passed: Surface World style 1 and Floating Islands style 2 each matched all 12 pinned L130-v227 rows at W1 and W12, and each recipe's W1/W12 rows matched. These exercise the no-pad owner path.
|
||||
- `git diff --check` passed.
|
||||
- The landing-pad composed matrix stopped at seed 5 after the repeated failure. It did not complete 8 seeds per variant (32 runs). Clang/GCC W1/W8/W12 identity for the liminal fixture was not run. Final-source seeds 0–4 were not rerun after the margin returned to 20 m. No performance timing was collected.
|
||||
|
||||
Changed standalone files: `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h`, `Standalone/Core/src/WorldAssemblerInternal.h`, `Standalone/Source/StreamBenchmarkHeadless.cpp`, `Standalone/Source/StreamBenchmarkPipeline.h`, and `Standalone/Source/WorldAssemblyPrototype.h`.
|
||||
|
||||
Remaining risk: the current pad record certifies a local landing and direction, not an approach corridor. A future joint planner must reserve/check the full capsule and tube against the layer before finalizing pad choice and passage geometry.
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L145's composed liminal audits and the worker/compiler identity matrix."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l145"
|
||||
FIXTURE = OUT / "fixtures/core-liminal-field-3-strata.json"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
AUDIT_FLAGS = ("--audit-resolved-layout-passages", "--audit-liminal-road-walk",
|
||||
"--audit-passage-tile-seams")
|
||||
VARIANTS = ("flatField-monolith", "periodicHills",
|
||||
"road-over-cloud-floor", "pillar-grid")
|
||||
PASSAGE_RE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||
SHELL_RE = re.compile(r"^\[PassageShellAudit\].*status=PASS$", re.M)
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import current_stamp, first_stamp
|
||||
|
||||
|
||||
def parse_seeds(value: str) -> list[int]:
|
||||
result: set[int] = set()
|
||||
for token in value.split(","):
|
||||
token = token.strip()
|
||||
if not token:
|
||||
continue
|
||||
if "-" in token:
|
||||
first, last = (int(part) for part in token.split("-", 1))
|
||||
if first > last:
|
||||
raise argparse.ArgumentTypeError(f"reversed seed range: {token}")
|
||||
result.update(range(first, last + 1))
|
||||
else:
|
||||
result.add(int(token))
|
||||
if not result or min(result) < 0:
|
||||
raise argparse.ArgumentTypeError("seeds must be nonnegative and nonempty")
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def validate_audit(output: str, seed: int, compiler: str, workers: int) -> None:
|
||||
checks = {
|
||||
"landing-pad negative control":
|
||||
"[LandingPadNegativeControl] uncertified_rejected=true status=PASS" in output,
|
||||
"passage walk audits": len(PASSAGE_RE.findall(output)) == 2,
|
||||
"passage shell audits": len(SHELL_RE.findall(output)) == 2,
|
||||
"passage ripple audits": len(re.findall(
|
||||
r"^\[PassageRippleAudit\].*status=PASS$", output, re.M)) == 2,
|
||||
"masked ripple sampling controls": len(re.findall(
|
||||
r"^\[PassageRippleSamplingControl\].*status=PASS$", output, re.M)) == 2,
|
||||
"passage shell summary": bool(re.search(
|
||||
r"^\[PassageShellAuditSummary\].*status=PASS$", output, re.M)),
|
||||
"liminal walk audit": bool(re.search(
|
||||
r"^\[LiminalLayerWalkAudit\].*status=PASS$", output, re.M)),
|
||||
"liminal negative control":
|
||||
"[LiminalLayerNegativeControl] altered_departure_rejected=true status=PASS" in output,
|
||||
"passage summary": bool(re.search(
|
||||
r"^\[ResolvedPassageAuditSummary\].*status=PASS$", output, re.M)),
|
||||
"seam audit": bool(re.search(
|
||||
r"^\[PassageTileSeamAudit\].*status=PASS$", output, re.M)),
|
||||
}
|
||||
failed = [name for name, passed in checks.items() if not passed]
|
||||
if failed:
|
||||
raise RuntimeError(
|
||||
f"seed={seed} compiler={compiler} workers={workers} failed checks: "
|
||||
+ ", ".join(failed))
|
||||
|
||||
|
||||
def run_audit(seed: int, compiler: str, workers: int, *, audit: bool = True) -> dict:
|
||||
binary = ROOT / f"Standalone/bin/linux/VoxelForgeCoreHeadless-l145-{compiler}"
|
||||
if not binary.is_file():
|
||||
raise RuntimeError(f"missing binary: {binary}")
|
||||
expected_stamp = current_stamp()
|
||||
probe = subprocess.run([str(binary), "--print-source-stamp"], cwd=ROOT,
|
||||
capture_output=True, text=True, timeout=15, check=False)
|
||||
if probe.returncode != 0 or first_stamp(probe.stdout) != expected_stamp:
|
||||
raise RuntimeError(f"stale executable: {binary}")
|
||||
|
||||
tag = f"seed{seed:02d}-{compiler}-w{workers}"
|
||||
log = OUT / "logs" / f"audit-{tag}.log"
|
||||
manifest = OUT / "manifests" / f"audit-{tag}.json"
|
||||
layout = OUT / "layouts" / f"audit-{tag}.json"
|
||||
manifest.parent.mkdir(parents=True, exist_ok=True)
|
||||
layout.parent.mkdir(parents=True, exist_ok=True)
|
||||
command = [str(binary), str(FIXTURE), f"--workers={workers}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=1", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--camera=0,0,-540,0,0", f"--seed={seed}",
|
||||
f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
if audit:
|
||||
command.extend(AUDIT_FLAGS)
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(OUT)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT / "Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, "timeout", "1740s", *command],
|
||||
cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=1745, check=False)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"{tag} exit={result.returncode}; see {log}")
|
||||
if first_stamp(output) != expected_stamp or current_stamp() != expected_stamp:
|
||||
raise RuntimeError(f"{tag} source stamp changed during run; see {log}")
|
||||
if audit:
|
||||
validate_audit(output, seed, compiler, workers)
|
||||
if not manifest.is_file() or not layout.is_file():
|
||||
raise RuntimeError(f"{tag} omitted canonical manifest or layout")
|
||||
manifest_value = json.loads(manifest.read_text(encoding="utf-8"))
|
||||
layout_value = json.loads(layout.read_text(encoding="utf-8"))
|
||||
identity = {
|
||||
"seed": seed,
|
||||
"variant": VARIANTS[seed % len(VARIANTS)],
|
||||
"compiler": compiler,
|
||||
"workers": workers,
|
||||
"manifest_hash": manifest_value["content_hash"],
|
||||
"resolved_layout_identity": layout_value["stable_identity"],
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
"elapsed_s": round(time.monotonic() - started, 3),
|
||||
"audited": audit,
|
||||
}
|
||||
print(f"[L145AuditRun] seed={seed} variant={identity['variant']} compiler={compiler} "
|
||||
f"workers={workers} manifest={identity['manifest_hash']} "
|
||||
f"layout={identity['resolved_layout_identity']} "
|
||||
f"elapsed_s={identity['elapsed_s']:.3f} status=PASS", flush=True)
|
||||
return identity
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--seeds", default="0-31", type=parse_seeds,
|
||||
help="audit seeds, e.g. 0-31 or 0,1,2,3")
|
||||
parser.add_argument("--determinism", action="store_true",
|
||||
help="also compare seed 0 on Clang/GCC at W1/W8/W12")
|
||||
parser.add_argument("--only-determinism", action="store_true",
|
||||
help="run only the Clang/GCC seed-0 W1/W8/W12 identity matrix")
|
||||
args = parser.parse_args()
|
||||
if not FIXTURE.is_file():
|
||||
raise SystemExit(f"missing fixture: {FIXTURE}; run BuildL145Fixtures.py first")
|
||||
|
||||
records = []
|
||||
if not args.only_determinism:
|
||||
for seed in args.seeds:
|
||||
records.append(run_audit(seed, "clang", 1))
|
||||
if args.determinism or args.only_determinism:
|
||||
matrix = [run_audit(0, compiler, workers)
|
||||
for compiler in ("clang", "gcc") for workers in (1, 8, 12)]
|
||||
identities = {(row["manifest_hash"], row["resolved_layout_identity"])
|
||||
for row in matrix}
|
||||
if len(identities) != 1:
|
||||
raise RuntimeError(f"Clang/GCC W1/W8/W12 identity mismatch: {matrix}")
|
||||
records.extend(matrix)
|
||||
|
||||
summary = {
|
||||
"audit_seeds": args.seeds if not args.only_determinism else [],
|
||||
"variant_counts": {
|
||||
name: sum(1 for seed in args.seeds if seed % len(VARIANTS) == index)
|
||||
for index, name in enumerate(VARIANTS)
|
||||
} if not args.only_determinism else {},
|
||||
"records": records,
|
||||
"determinism_pass": bool(args.determinism or args.only_determinism),
|
||||
"status": "PASS",
|
||||
}
|
||||
summary_path = OUT / "audit-matrix-summary.json"
|
||||
summary_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
summary_path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L145AuditMatrix] records={len(records)} summary={summary_path} status=PASS",
|
||||
flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L145AuditMatrix] status=FAIL error={error}", file=sys.stderr, flush=True)
|
||||
raise SystemExit(1)
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,35 @@
|
||||
# VoxelForge — research R46 (read-only): generator capability matrix
|
||||
|
||||
You are a researcher. **Read only: do not edit, create or delete any file, and do not build, run or test anything.**
|
||||
Read the code, reports and design docs. Be thorough: verify each claim by hand in the code, and mark each one
|
||||
[CODE] file:line, [MEASURED] report, or [INFERENCE]. Branch `assembler/world` (`cbbfd46`).
|
||||
|
||||
## Why
|
||||
|
||||
The owner approved the next focus (from `Standalone/Research/WorldAssemblerM3/Astra4/REVIEW.md` #3): turn generator
|
||||
breadth into playable variety. First we need an honest picture of what each strate/generator family can do today.
|
||||
|
||||
## Task
|
||||
|
||||
Find every generator family / strate type: surface world, floating islands, caves/cavern, tunnel network, vertical
|
||||
shafts, dreamcore/liminal, and any others in the code (Core native, legacy fallback, op stack, composer). For each,
|
||||
fill one row:
|
||||
- **Core path:** native Core evaluator, legacy fallback, or not in Core at all? Where is it dispatched?
|
||||
- **Entrances/exits:** arrival and departure mouths, and the primordial law (arrival → departure walkable).
|
||||
- **Interior connectivity:** walkable, and audited by what?
|
||||
- **Edits:** does digging (diff layer) work on it in the Core path?
|
||||
- **Materials/content:** material IDs, decorations, foliage, lights, anything placeable.
|
||||
- **Far terrain:** does it have far LOD/shells?
|
||||
- **Performance:** latest measured near-ready, with the report.
|
||||
- **Identity:** what makes it look and feel distinct today; is it recognisable without a label?
|
||||
- **Owner world:** used in `Standalone/data/owner_world.json`? Which strate letter (SW/FI/TN/VS)?
|
||||
- **Gaps:** what's missing for it to be a "place" with its own identity and a destination.
|
||||
|
||||
Then:
|
||||
1. Which three families are closest to being distinct places (the owner's example: a grounded cave, an island
|
||||
environment, one liminal identity)? What is the smallest work to get each there?
|
||||
2. What does the composer (`VoxelSeasonManifest.cpp`, `COMPOSER-NOTES.md`) already do for tagged content, and what's
|
||||
the smallest next step?
|
||||
3. Risks: places where the Core path silently falls back, or where a family exists in docs but not in code.
|
||||
|
||||
Write plain Markdown: the matrix plus ≤ ~800 words of findings.
|
||||
@@ -0,0 +1,41 @@
|
||||
# VoxelForge R46 — generator capability matrix
|
||||
|
||||
Read-only review of `assembler/world` at `cbbfd46`; no files changed, and no builds, runs, or tests were performed.
|
||||
|
||||
## Shared dispatch and contracts
|
||||
|
||||
[CODE] The eight `ECaveGeneratorType` values are TunnelNetwork, FlatPlain, CrystalChamber, Maze, SurfaceWorld, VerticalShafts, FloatingIslands, and Underwater. `NativeCaves`, `NativeLattice`, and `ProfiledHeightfield` are optional Core recipes layered onto those types, not additional strate types. [VoxelStrateTypes.h:175](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelStrateTypes.h:175>) [VoxelStrateTypes.h:203](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelStrateTypes.h:203>)
|
||||
|
||||
[CODE] Core binds native SurfaceWorld and FloatingIslands evaluators when matching plans exist; it binds native cave/lattice graph fields when their capabilities are present. Otherwise it attaches a legacy generator adapter. The density dispatcher exposes paths such as `SurfaceWorld-native`, `SurfaceWorld-legacy`, `Caves-native`, and `compiled-analytic/<kind>`. [ShapeWorldInternal.h:600](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:600>) [ShapeWorldInternal.h:640](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:640>) [ShapeWorldInternal.h:649](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:649>) [ShapeWorldInternal.h:3101](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3101>)
|
||||
|
||||
[CODE] The adapter plans inter-strate passages and resolves the layout before Core evaluates it. The composer’s primordial law checks a player-fit, walk-only path between the center-column interior endpoints; it is an arrival-to-departure test, not a requirement that every disconnected room or shaft join the largest component. [VoxelForgeCoreAdapter.cpp:1545](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1545>) [VoxelSeasonManifest.cpp:2194](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelSeasonManifest.cpp:2194>) [VoxelStrateMeasure.h:315](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelStrateMeasure.h:315>)
|
||||
|
||||
[CODE] Core edit transactions become density brushes applied after field composition. The same stage covers native and fallback density paths. [VoxelForgeCore.cpp:3307](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:3307>) [ShapeWorldInternal.h:3036](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3036>) [ShapeWorldInternal.h:5201](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5201>)
|
||||
|
||||
[CODE] Any strate can author decorations, landmarks, ambient actors, and creatures. Placement supports actors or instanced meshes; Core material samples default to host ID 1 and can carry two resource IDs. These are shared content hooks, not a distinct palette per generator family. [VoxelStrateDefinition.h:352](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelStrateDefinition.h:352>) [VoxelStrateTypes.h:2204](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelStrateTypes.h:2204>) [VoxelForgeCore.h:1273](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h:1273>)
|
||||
|
||||
**Performance key:** L125 is the latest absolute owner four-strata LOD0–2 timing table I found; L130’s paired comparison found no measurable change. L125 labels rows S0–S3, so mapping them to SW/FI/TN/VS in the explicit L109 order is an **[INFERENCE]**. Values below are W8/W12 medians in seconds; they measure the owner layout, not isolated native evaluator costs. [L125:24](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux125/REPORT.md:24>) [L125:28](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux125/REPORT.md:28>) [L125:35](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux125/REPORT.md:35>) [L130:9](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux130/REPORT.md:9>) [L109:21](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux109/REPORT.md:21>)
|
||||
|
||||
## Matrix
|
||||
|
||||
| Family / owner | Core path | Entrances, connectivity, and primordial law | Edits, materials, and content | Far terrain and performance | Identity and gaps |
|
||||
|---|---|---|---|---|---|
|
||||
| **SurfaceWorld — SW** | [CODE] Native `Heightfield2D` when a matching surface plan binds; legacy SurfaceWorld fallback otherwise. [ShapeWorldInternal.h:600](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:600>) [ShapeWorldInternal.h:6685](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6685>) | [CODE] Owner export enables an open surface entry; SW also supplies the 0–1 departure mouth. [MEASURED] The shared route audit passed 21,425/21,425 centerline samples. [owner_world.json:1](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/data/owner_world.json:1>) [L125:39](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux125/REPORT.md:39>) | [CODE] Digging works. Surface supports biome terrain/material signals, water, and the shared placement hooks; no family-specific content pack is proven in the owner export. [VoxelStrateTypes.h:1609](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelStrateTypes.h:1609>) | [CODE] Common mesh LOD and far shells; native surface plans also get far ground/cap refinement. [MEASURED] **SW:** 0.780/0.807 near-ready. [VoxelForgeCore.cpp:2996](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2996>) [VoxelForgeCore.cpp:3073](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:3073>) | [INFERENCE] Outdoor hills, mountains, rivers, and beaches have a legible silhouette. It still needs distinctive foliage/materials, sound, and a destination to read as a complete place. |
|
||||
| **FloatingIslands — FI** | [CODE] Native island field when a matching island plan binds; otherwise falls through to the legacy analytic path. [ShapeWorldInternal.h:620](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:620>) [ShapeWorldInternal.h:6692](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6692>) | [CODE] Owner route enters from SW and departs toward TN; native island route audits check capsule clearance and support. [MEASURED] The owner passage checks passed. [MEASURED] L67 reported the owner FI path as `compiled-analytic/FloatingIslands`, not native; the reason was unresolved in R36. [L67:5](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux67/REPORT.md:5>) [Research36:11](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research36/REPORT.md:11>) | [CODE] Digging works; foliage, landmarks, ambient actors, and lights can use shared placement systems. [INFERENCE] No island-specific owner palette/content is established by the export. | [CODE] Common far mesh and shell paths. [MEASURED] **FI:** 0.981/0.962 near-ready in the owner layout. | [INFERENCE] Floating land silhouettes can be immediately recognizable. Confirm native dispatch, then add an island ecology, a landmark, and a clear route/destination. |
|
||||
| **TunnelNetwork — TN** | [CODE] The owner’s regular path is Core’s legacy/compiled-analytic fallback. `NativeCaves` is optional for TN/Underwater and evaluates authored room/tunnel graphs. [VoxelForgeCoreAdapter.cpp:145](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:145>) [VoxelGenerator.cpp:2645](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2645>) | [CODE] Owner arrival from FI and departure toward VS. [MEASURED] Inter-strate walkability passes; that does not prove every interior room is connected. Native cave fixture flood-fill connected its chosen arrival/departure pair. [L54:21](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux54/REPORT.md:21>) | [CODE] Digging works. Generic cave decorations/landmarks/lighting are available; there is no family-specific Core material palette. | [CODE] Common far LOD/shells. [MEASURED] **TN:** 1.025/0.965 near-ready in the owner layout. Optional native-cave fixture: 0.177 s near ring and 0.706 s cold whole view, a different workload. [L54:37](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux54/REPORT.md:37>) | [INFERENCE] Rooms and tunnels read as caves, but the owner route lacks a content-defined destination. Small cave graph and a landmark/content pack would make it a place; translating the owner’s legacy parameters to native cave records remains separate work. |
|
||||
| **FlatPlain** | [CODE] Legacy fallback/op stack only; it uses the slab builder. [VoxelGenerator.cpp:2600](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2600>) [VoxelDensityOpStack.cpp:9373](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDensityOpStack.cpp:9373>) | [CODE] No intrinsic mouths; shared passage planner supplies endpoints. [INFERENCE] Its open floor/ceiling band may connect the selected endpoints, but no family-specific arrival/departure audit was found. | [CODE] Digging works; shared decorations/landmarks can populate the chamber. | [CODE] Common far LOD/shells. [MEASURED] No separate near-ready timing found. | [INFERENCE] Open underground plain with columns. Needs a distinctive floor/ceiling profile, content, and destination; it has no native Core slab recipe. |
|
||||
| **CrystalChamber** | [CODE] Same slab op-stack builder as FlatPlain; distinctness comes from parameter values, especially ceiling roughness. [VoxelGenerator.cpp:2600](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2600>) | [CODE] Same shared-mouth arrangement as FlatPlain; no family-specific walk audit found. | [CODE] Digging works. Crystal decoration is authorable through shared placement, not guaranteed by the geometry type. [VoxelStrateTypes.h:169](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelStrateTypes.h:169>) | [CODE] Common far LOD/shells. [MEASURED] No separate near-ready timing found. | [INFERENCE] Hanging formations could distinguish it, but parameters alone do not supply crystal materials, lighting, or a destination. |
|
||||
| **Maze** | [CODE] Legacy maze op stack; optional `NativeLattice` graph recipe, which is shared with VerticalShafts rather than a dedicated native maze generator. [VoxelForgeCoreAdapter.cpp:159](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:159>) [VoxelGenerator.cpp:2592](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2592>) | [CODE] Mouths are shared-planner endpoints; native lattice fixtures test capsule reachability between chosen endpoints. [MEASURED] L60 native lattice fixture: 0.561 s near ring, 1.701 s whole view—not the owner near-ready workload. [L60:19](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux60/REPORT.md:19>) [L60:36](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux60/REPORT.md:36>) | [CODE] Digging works; common content hooks apply. | [CODE] Common far LOD/shells; L60 streamed the lattice fixture through LOD0–8. [L60:15](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux60/REPORT.md:15>) | [INFERENCE] Tight branching corridors are a recognizable grammar, but the native fixture’s helix/maze graph needs a content destination and stronger per-seed identity. |
|
||||
| **VerticalShafts — VS** | [CODE] Legacy shaft op stack; optional `NativeLattice` is a generic authored graph, not a shaft-specific Core evaluator. [VoxelGenerator.cpp:2622](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2622>) | [CODE] Owner arrival comes from TN; the four-strata export has no lower fifth-stratum passage. [MEASURED] The composer found a VerticalShafts arrival/departure violation; L58’s lattice route passed but was a helix. [COMPOSER-NOTES.md:1437](</home/jahni/VoxelM/Plugins/VoxelForge/COMPOSER-NOTES.md:1437>) [Linux58:11](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md:11>) | [CODE] Digging works; generic props and lights can be placed. | [CODE] Common far LOD/shells. [MEASURED] **VS:** 0.879/0.838 near-ready in owner layout. | [INFERENCE] Vertical silhouettes could distinguish it, but the native graph example is not an open shaft. Needs a shaft-specific core, safe landings, and a proven route. |
|
||||
| **Underwater** | [CODE] TunnelNetwork density plus water behavior; optional NativeCaves recipe is accepted. No separate native water generator recipe. [VoxelGenerator.cpp:2638](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2638>) [Unreal1:131](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Unreal1/REPORT.md:131>) | [CODE] Shared passage mouths; composer’s law is walk-only, so it does not certify swimming or water traversal. [INFERENCE] A flooded route needs dry walkable sections or a separate traversal rule. | [CODE] Digging works; water and shared content are available, but the native assembly recipe does not itself provide an authored water feature. | [CODE] Common far terrain; far water presentation is not established here. [MEASURED] No separate near-ready timing found. | [INFERENCE] “Flooded cave” is a plausible identity, but water, lighting, air pockets, and destination spaces need authored treatment. |
|
||||
| **Dreamcore / liminal** | [CODE] No `Dreamcore` generator type. Existing Core stamps/motif fields are overlays; motif override currently requires a resolved SurfaceWorld strate. [VoxelStrateTypes.h:175](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelStrateTypes.h:175>) [WorldAssemblyPrototype.h:568](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/WorldAssemblyPrototype.h:568>) | [CODE] Inherits mouths and connectivity from its base family; motif fixtures preserve route keepouts. [MEASURED] L60 motif overlay: 0.493 s near ring, 1.326 s whole view. [Linux56:21](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux56/REPORT.md:21>) [L60:35](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux60/REPORT.md:35>) | [CODE] Digging applies to the composed field; current motif kinds include carve arch/ellipsoid and fill pillar/dais/monolith/platform. [VoxelStrateTypes.h:222](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelStrateTypes.h:222>) | [CODE] Base field uses common far LOD/shells. Motifs have LOD coverage, but no liminal-specific far treatment. | [INFERENCE] Motifs alone do not read as dreamcore. It needs a repeated spatial grammar, coherent palette/light/sound, and an explorable destination. Not in owner world. |
|
||||
|
||||
Owner mapping: [MEASURED] the owner export references four fixed strata and opens the surface; the named four-family owner route is SW/FI/TN/VS. [CODE] The L125–L130 timings are for that owner layout. [owner_world.json:1](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/data/owner_world.json:1>) [L109:21](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux109/REPORT.md:21>) [L120:23](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux120/REPORT.md:23>)
|
||||
|
||||
## Findings
|
||||
|
||||
1. **Closest to distinct places: TN caves, FI, and liminal overlays.** [INFERENCE] TN already has cave geometry and route proof: add one authored destination chamber plus a small landmark/material/light kit. FI already has a distinctive suspended-land silhouette and route support: first confirm its native dispatch, then add foliage, a landmark, and route cues. Liminal is the smallest overlay slice: reuse the flat/profiled base and existing monolith/platform stamps, then define one repeatable anomaly pattern and a matching content kit. These need authored destinations and themes more than new density families.
|
||||
|
||||
2. **Composer status and smallest next step.** [CODE] `VoxelSeasonManifest.cpp` rolls candidates from a corpus, measures them, rejects vacuous/fragmented/primordial-law failures, and selects for grounded/outlier mix plus archetype/recipe variety. Promoted records carry an archetype tag and corpus provenance is tagged `project`, `default`, or `promoted`. [VoxelSeasonManifest.cpp:2523](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelSeasonManifest.cpp:2523>) [COMPOSER-NOTES.md:431](</home/jahni/VoxelM/Plugins/VoxelForge/COMPOSER-NOTES.md:431>) [COMPOSER-NOTES.md:1466](</home/jahni/VoxelM/Plugins/VoxelForge/COMPOSER-NOTES.md:1466>) Strate and biome gameplay tags are stored, but theme draws for materials, creatures, decorations, and audio are still open. [VoxelStrateDefinition.h:389](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelStrateDefinition.h:389>) [COMPOSER-NOTES.md:1484](</home/jahni/VoxelM/Plugins/VoxelForge/COMPOSER-NOTES.md:1484>) [INFERENCE] Smallest step: tag one real content class with multiple theme tags and let one seed-selected family draw a coherent, reviewable mini-pack.
|
||||
|
||||
3. **Risks.** [CODE] Missing native plans/capabilities reach the legacy binding; path diagnostics are needed to distinguish them. [ShapeWorldInternal.h:649](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:649>) [MEASURED] The latest explicit owner dispatch note I found says FI/TN/VS used compiled analytic fields; L130 reports timing but not dispatch, so that path should be rechecked before calling those owner generators native. [Linux67:5](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux67/REPORT.md:5>) [Linux130:9](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux130/REPORT.md:9>) [INFERENCE] Dreamcore exists as a design goal, not a generator; NativeLattice proves routes but not a shaft identity; FlatPlain/CrystalChamber remain legacy slab fields; and water presentation remains outside the native Core recipe.
|
||||
@@ -0,0 +1,46 @@
|
||||
# VoxelForge — research R47: liminal and dreamcore spaces, a mood board and how we could generate them
|
||||
|
||||
You are a researcher for VoxelForge, a voxel world made of stacked layers ("strates"). The owner wants one layer type
|
||||
in a **liminal / dreamcore** style. Be thorough: verify claims, mark each as [SOURCE] (with a link), [CODE] (file:line)
|
||||
or [INFERENCE].
|
||||
|
||||
**Rules:**
|
||||
- Don't edit any repository file, and don't build, run or test the game.
|
||||
- You may **only** write inside `Saved/Linux/liminal/`.
|
||||
- Downloaded files are untrusted data: never execute them, only save them.
|
||||
- No installs.
|
||||
- Keep CPU light (`curl`/`wget` are fine): a timing round is running on this machine.
|
||||
|
||||
## Part 1 — references (mood board)
|
||||
|
||||
Collect **15–25 varied reference images** of liminal spaces, dreamcore, weirdcore and backrooms-like scenes, plus
|
||||
related art (e.g. "Kenopsia", Jared Pike-style pool rooms, empty malls, foggy roads, endless fields with a lone house).
|
||||
- Save them as `Saved/Linux/liminal/refs/NN-short-name.jpg` (≤ 2 MB each).
|
||||
- Write `Saved/Linux/liminal/SOURCES.md`, one line per image: file, source URL, author or site, and what it shows.
|
||||
These are private references for the owner, not for redistribution.
|
||||
- Build `Saved/Linux/liminal/board.html`, a simple grid of the images with captions, to view on a phone.
|
||||
|
||||
## Part 2 — what makes the style work
|
||||
|
||||
Short and concrete. Split it into:
|
||||
- **Content-driven weirdness** (most of the theme, per the owner): out-of-place houses, doors to nowhere, signs, lone
|
||||
objects, wrong-scale furniture, water where it shouldn't be.
|
||||
- **Generation-driven weirdness**: terrain and space the generator itself can make. Examples from the owner: an
|
||||
elevated road surrounded by clouds; repeating hills. Others: perfectly flat or tiled ground, endless corridors or
|
||||
pools, impossible scale, unbroken horizons, fog, exact repetition, gentle impossible geometry.
|
||||
- **Atmosphere:** light, fog, colour, sound. Briefly.
|
||||
|
||||
## Part 3 — mapping to our code
|
||||
|
||||
Read `CURRENT-STATE.md`, `COMPOSER-NOTES.md`, `GDD.md`, and
|
||||
`Standalone/Research/WorldAssemblerM3/Research46/REPORT.md` (the capability matrix). Also:
|
||||
- the Surface World profile kinds in `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`
|
||||
(`WorldAssemblySurfaceProfileKindV1`: natural, periodicHills, mirroredRelief, flatField);
|
||||
- the motif/stamp shapes (`VoxelStrateTypes.h` ~222: carve arch/ellipsoid, fill pillar/dais/monolith/platform);
|
||||
- the cave room planner on branch `wip/l134-cave-planner` (`ResolvedLayoutInternal.h`, `makeCompound`).
|
||||
|
||||
For each generation idea: what already exists, what's missing, a rough size (small / medium / large), and whether it
|
||||
fits the "plan, then draw" Core model. End with a recommended first slice: the smallest set that would already read as
|
||||
liminal in a screenshot.
|
||||
|
||||
Write `Saved/Linux/liminal/REPORT.md` (≤ ~1,200 words) and print a short summary at the end.
|
||||
@@ -0,0 +1,30 @@
|
||||
# VoxelForge R47 — liminal / dreamcore
|
||||
|
||||
## Result
|
||||
|
||||
[INFERENCE] Start with a rare, content-led anomaly on a flat SurfaceWorld: one ordinary, recognizable object placed where it makes no sense, framed by a quiet empty field and mild horizon fog. This can read in a screenshot without a new generator family.
|
||||
|
||||
[CODE] The requested references describe different snapshots: `CURRENT-STATE.md` labels the tree L132/Core v227 (`CURRENT-STATE.md:3,7`), while the live Core header says evaluator v236 (`Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h:513`). R46 calls itself a review of an earlier `assembler/world` snapshot (`Standalone/Research/WorldAssemblerM3/Research46/REPORT.md:3`). The mapping below follows the live source; R46 is historical context.
|
||||
|
||||
## 1. What makes it work
|
||||
|
||||
- **Content-driven weirdness (the main signal).** [INFERENCE] Keep most geometry ordinary, then break one semantic expectation: a lone house in an indoor hall; a door standing in a field; a sign that points nowhere; a child-sized chair in a huge room; or still pool water in a dry corridor. Repeat one object exactly or vary its scale to make the wrongness legible. [SOURCE] ETH Zürich describes dreamcore as familiar scenes that are hard to place, including impossible proportions; Jared Pike’s *Dream Pools* frames endless pool environments as eerie nostalgia. [ETH Zürich](https://dbt.arch.ethz.ch/latent-spaces/) · [Jared Pike](https://www.jaredpike.art/projects/dream-pools/)
|
||||
- **Generation-driven weirdness.** [INFERENCE] Use perfectly flat ground, exact repeated hills, a corridor or pool that returns to itself, an elevated road over clouds, oversized rooms, or a horizon with no visible edge. Favor one simple rule repeated cleanly over unrelated noise. [SOURCE] Liminal spaces often use familiar passage places with their expected people or activity absent, making them uncanny. [Liminal space](https://en.wikipedia.org/wiki/Liminal_space) [SOURCE] ETH Zürich describes dreamcore as recognizable but ambiguous scenes. [ETH Zürich](https://dbt.arch.ethz.ch/latent-spaces/)
|
||||
- **Atmosphere.** [INFERENCE] Try cool fluorescent light, a weak warm pool of color, low horizon fog and sparse room tone (fluorescent hum, wind, or a looping distant splash); leave large areas quiet. [SOURCE] ETH Zürich notes that looping sound and ambiguous cues help make dreamcore recognizable but not fully explainable. [ETH Zürich](https://dbt.arch.ethz.ch/latent-spaces/)
|
||||
|
||||
## 2. Mapping to VoxelForge
|
||||
|
||||
| Idea | Already exists / missing | Size and “plan, then draw” fit |
|
||||
|---|---|---|
|
||||
| Wrong-place props and landmarks | [CODE] Shared decorations and rare landmarks can spawn actors or instanced meshes with deterministic placements (`ARCHITECTURE.md:267-349`; `Source/VoxelForge/Public/VoxelStrateTypes.h:2407,2471`). [CODE] Generated schema-2 slots currently carry no content/visual record, and theme draws for materials, creatures and audio remain open (`COMPOSER-NOTES.md:115-117,1484-1485`). | [INFERENCE] **Small** for one authored prop kit; **medium** to make the composer select/store coherent content. [INFERENCE] Fits a resolved placement plan and later Unreal draw; these props are not Core density geometry. |
|
||||
| Flat ground and repeating hills | [CODE] Surface profile kinds include `flatField`, `periodicHills` and `mirroredRelief`; seeded phases are resolved during planning (`VoxelForgeCore.h:560-610`). No profile kind describes tiled ground materials. | [INFERENCE] **Small** to select a flat or periodic profile; **medium** for a dedicated tiled material/pattern pass. [CODE] Yes: profile parameters are recipe/plan data consumed by the Core field. |
|
||||
| Raised road among clouds | [CODE] Floating Islands is an existing family (`VoxelStrateTypes.h:196-197`); persistent ceiling/floor actors can make a cloud sea (`ARCHITECTURE.md:457-464`), and landmarks can carry a mesh bridge. [INFERENCE] There is no demonstrated Core road/path structure that couples its geometry to the island route. | [INFERENCE] **Medium** for a finite authored bridge or platform chain; **large** for a robust generated road network. [INFERENCE] A finite road can be planned as records and drawn as mesh/content; cloud actors stay in Unreal presentation, outside Core density evaluation. |
|
||||
| Endless corridors and pools | [CODE] The cave planner makes a deterministic finite room-and-tunnel graph between passage mouths (`VoxelForgeCore.h:1314-1324`); `makeCompound` emits two to four overlapping room primitives (`ResolvedLayoutInternal.h:3037-3045`). Core exposes water channels (`VoxelForgeCore.h:515-519`). [INFERENCE] The planner is not an endless loop grammar and does not specify a tiled pool basin/waterline. | [INFERENCE] **Medium** for a finite repeated room/pool set-piece; **large** for continuous repeat with reliable traversal and water presentation. [CODE] Finite rooms/edges fit plan-then-draw; truly unbounded repetition needs a new deterministic local-repeat field/streaming rule. |
|
||||
| Impossible scale, horizon and geometry | [CODE] Resolved stamps already include carve ellipsoid/arch and fill pillar/dais/monolith/platform with bounded, stable placement (`VoxelForgeCore.h:912-941`); stamps are composed after boundaries/passages (`ARCHITECTURE.md:2242-2265`). [INFERENCE] They can sell oversized objects or a standalone arch, but not non-Euclidean topology or a genuinely impossible connection. | [INFERENCE] **Small** for bounded silhouette changes; **large** for topology-changing spaces. [CODE] Bounded stamp records fit plan-then-draw; topology changes do not fit the current finite stamp vocabulary. |
|
||||
| Fog, color and sound | [CODE] Per-strate fog, skylight tint/intensity and persistent layer actors exist (`ARCHITECTURE.md:457-470`; `VoxelStrateDefinition.h:283-362`). [CODE] Composer theme draws for audio are still open (`COMPOSER-NOTES.md:1484-1485`). | [INFERENCE] **Small** to reuse fog/light settings; **medium** to author a coherent audio/theme selection. [INFERENCE] Fits a presentation record selected before draw, but remains outside Core density planning. |
|
||||
|
||||
[CODE] The game design requires every strate to keep a findable open-air entrance and exit and says anomalies should be rare and stand apart from grounded neighbours (`GDD.md:132-153`). [CODE] The `motif_family_override` prototype path is restricted to resolved SurfaceWorld/flatField recipes (`Standalone/Source/WorldAssemblyPrototype.h:921-943`); the shared stamp vocabulary itself is broader.
|
||||
|
||||
## Recommended first slice
|
||||
|
||||
[INFERENCE] Author one rare SurfaceWorld `flatField` layer with a single familiar doorway or small house mesh placed alone at an implausible scale, plus low horizon fog and one soft light tint. Keep the rest empty; the viewer should recognize the object before noticing the place has no sensible context. This uses existing profile, placement and atmosphere hooks, so the screenshot slice is **small** and does not require a new density family. [CODE] The mesh placement/presentation remains separate from Core’s density plan, and generated schema-2 slots do not yet carry its visual record (`COMPOSER-NOTES.md:115-117`); a composer-ready version will need that record and theme tags.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Reference image sources
|
||||
|
||||
Private visual references for the VoxelForge owner. This is not a redistribution set; consult each source page and its rights statement before reusing an image. These are local display copies; some were resized to meet the per-image size limit.
|
||||
|
||||
- `01-backrooms-origin.jpg` — [SOURCE] [Bill Magritz (creator) / Bob Mazza (copyright holder)](https://commons.wikimedia.org/wiki/File:HobbyTown_USA_Oshkosh_interior_under_construction_2002_(The_Backrooms).jpg); HobbyTown USA Oshkosh interior under construction 2002, the image associated with “The Backrooms”.
|
||||
- `02-empty-office.jpg` — [SOURCE] [Ged Carroll](https://commons.wikimedia.org/wiki/File:Empty_office.jpg); Empty office interior.
|
||||
- `03-empty-mall-hallway.jpg` — [SOURCE] [Michael Rivera (Mjrmtg)](https://commons.wikimedia.org/wiki/File:Empty_Hallway,_Tallahassee_Mall.JPG); Tallahassee Mall hallway.
|
||||
- `04-mall-source.jpg` — [SOURCE] [Jtalledo](https://commons.wikimedia.org/wiki/File:Mall_at_The_Source_empty.jpg); Mall at The Source, with people present.
|
||||
- `05-ruoholahti-evening.jpg` — [SOURCE] [JIP](https://commons.wikimedia.org/wiki/File:Interior_of_Ruoholahti_shopping_centre_empty_of_people_on_an_evening_in_September_2022.jpg); Ruoholahti shopping centre interior, September 2022.
|
||||
- `06-city-center-night.jpg` — [SOURCE] [JIP](https://commons.wikimedia.org/wiki/File:Interior_of_City-Center_empty_of_people_on_a_night_in_January_2025.jpg); City-Center shopping-centre interior at night, January 2025.
|
||||
- `07-school-hallway.jpg` — [SOURCE] [Warren LeMay](https://commons.wikimedia.org/wiki/File:Hallway,_Log_Cabin_Association_Elementary_School,_Barkers_Creek,_NC_(39684209853).jpg); Hallway in Log Cabin Association Elementary School, Barkers Creek, North Carolina.
|
||||
- `08-central-high-hallway.jpg` — [SOURCE] [Thomas J. O’Halloran](https://commons.wikimedia.org/wiki/File:Mostly_empty_hallway_at_Central_High_School_in_Little_Rock.tif); Mostly empty hallway at Central High School, Little Rock.
|
||||
- `09-hearst-pool.jpg` — [SOURCE] [Mike McBey](https://commons.wikimedia.org/wiki/File:Indoor_pool,_Hearst_Castle_(32062000267).jpg); Indoor pool at Hearst Castle.
|
||||
- `10-ymca-pool.jpg` — [SOURCE] [Joe Mabel](https://commons.wikimedia.org/wiki/File:Vancouver_-_Robert_Lee_YMCA_pool_01.jpg); Robert Lee YMCA pool, Vancouver.
|
||||
- `11-jerma-hotel-pool.jpg` — [SOURCE] [Anonymous uploader](https://commons.wikimedia.org/wiki/File:Jerma_Hotel_Indoor_Pool_(first_angle).jpg); Jerma Hotel indoor pool.
|
||||
- `12-dream-pools-01.jpg` — [SOURCE] [Jared Pike (artist’s official site)](https://www.jaredpike.art/projects/dream-pools/); Dream Pools series.
|
||||
- `13-dream-pools-02.jpg` — [SOURCE] [Jared Pike (artist’s official site)](https://www.jaredpike.art/projects/dream-pools/); Dream Pools series.
|
||||
- `14-foggy-road.jpg` — [SOURCE] [Dennis Bruijn](https://commons.wikimedia.org/wiki/File:Foggy_Road_(67154193).jpeg); Foggy Road (500px image).
|
||||
- `15-driving-through-mist.jpg` — [SOURCE] [Tomasz Zagórski (Unsplash contributor)](https://commons.wikimedia.org/wiki/File:Driving_through_mist_(Unsplash).jpg); Driving through mist.
|
||||
- `16-foggy-campus.jpg` — [SOURCE] [Ramesh Lalwani](https://commons.wikimedia.org/wiki/File:Foggy_Morning_in_IIT.jpg); Foggy Morning in IIT.
|
||||
- `17-empty-playground.jpg` — [SOURCE] [Ian Paterson](https://commons.wikimedia.org/wiki/File:Empty_playground_-_geograph.org.uk_-_4118218.jpg); Empty playground, Geograph image 4118218.
|
||||
- `18-playground-park.jpg` — [SOURCE] [mycurrency.com (uploader credit)](https://commons.wikimedia.org/wiki/File:Empty_Playground_in_a_park.jpg); Empty playground in a park.
|
||||
- `19-lone-house.jpg` — [SOURCE] [Mayqel](https://commons.wikimedia.org/wiki/File:Lone_House.jpg); Lone House, Blender render.
|
||||
- `20-door-to-nowhere.jpg` — [SOURCE] [Benmckune](https://commons.wikimedia.org/wiki/File:Door_to_Nowhere_Winchester_House_6-5-06.jpg); “Door to Nowhere” at Winchester Mystery House.
|
||||
- `21-abandoned-theater.jpg` — [SOURCE] [Noki Va](https://commons.wikimedia.org/wiki/File:Abandoned_Theater_(97702985).jpeg); Abandoned Theater.
|
||||
- `22-empty-retail-space.jpg` — [SOURCE] [Wikimedia Commons (uploader credit on source page)](https://commons.wikimedia.org/wiki/File:Empty_Retail_Space.jpg); Empty retail space.
|
||||
- `23-historic-rail-platform.jpg` — [SOURCE] [Ben Brooksbank](https://commons.wikimedia.org/wiki/File:Euston_railway_station_geograph-2197358-by-Ben-Brooksbank.jpg); Euston railway station, historic view.
|
||||
- `24-empty-airport-terminal.jpg` — [SOURCE] [Matt @ PEK](https://commons.wikimedia.org/wiki/File:Empty_terminal_in_TPE_airport_(4287773502).jpg); Empty terminal in TPE airport.
|
||||
@@ -0,0 +1,36 @@
|
||||
# VoxelForge — research R48 (read-only): who owns the tunnel mouth?
|
||||
|
||||
You are a researcher. **Read only:** don't edit, create or delete any file, and don't build, run or test anything.
|
||||
Read code, reports and design docs. Be thorough, verify by hand, and mark each claim [CODE] file:line, [MEASURED]
|
||||
report, or [INFERENCE]. This checkout (`Saved/Linux/r48-tree`) is branch `wip/l134-cave-planner` at `4f93f3f`.
|
||||
|
||||
## The problem
|
||||
|
||||
Inter-strate passages (tunnels between layers) are planned first, and each layer's content is planned afterwards
|
||||
around their fixed mouths and tubes.
|
||||
- **Tunnel Network** (L134–L138) works, because caves are carved into rock and can bend to the mouths.
|
||||
- **Vertical Shafts** (L139): 15/16 seeds pass; seed 2 fails the shell and seams at the arrival mouth. Three local
|
||||
geometry fixes failed.
|
||||
- **Floating Islands** (L133 twice, then L140): 0/16 seeds pass.
|
||||
- In 14 of 16 seeds, no island arrangement clears the fixed tubes and reaches both mouth heights.
|
||||
- One seed has a composed collision.
|
||||
- Wall ripple is 6–7 cm against the 3.5 cm limit on the passages.
|
||||
|
||||
Read the L133, L139 and L140 reports and patches in `Standalone/Research/WorldAssemblerM3/`.
|
||||
|
||||
## Questions
|
||||
|
||||
1. Where and in what order are passages and layer contents planned (adapter, `ResolvedLayoutInternal.h`, the
|
||||
passage planner, the island and cave planners)? What does each stage fix that later stages must obey?
|
||||
2. Which designs would make mouths solvable by construction for every layer type? For example:
|
||||
- **content first:** the layer plans its landing spots, and passages are routed to target them;
|
||||
- **negotiated:** passages reserve a mouth landing region, and every layer planner must provide support there,
|
||||
a "landing pad" contract;
|
||||
- **a joint plan:** a corridor and the island anchors are solved together.
|
||||
For each: what changes, the risks (determinism, the existing audits, the passage smoothness/ripple work), and the
|
||||
size.
|
||||
3. Why does ripple fail at 6–7 cm on the upper passages with native islands (and in L67)? Is it the passage, or the
|
||||
host field under it?
|
||||
4. Recommend one design, with steps and the evidence that would prove or kill it.
|
||||
|
||||
Write plain Markdown, ≤ ~1,200 words.
|
||||
@@ -0,0 +1,43 @@
|
||||
# R48 read only — who owns the tunnel mouth?
|
||||
|
||||
**The passage planner currently chooses the mouth.** Floating Islands then has to fit its islands and walk route around fixed tubes; caves can instead carve toward them. L140’s fixed tube layout left no valid island plan for most seeds. [CODE] `Standalone/Core/src/ResolvedLayoutInternal.h:3699–3792`; [MEASURED] `Standalone/Research/WorldAssemblerM3/Linux140/REPORT.md:15–19`
|
||||
|
||||
## 1. Planning order and constraints
|
||||
|
||||
There are two adapter paths, so there isn’t one universal order today:
|
||||
|
||||
- The geometry only adapter builds strata and passage configs, plans passages, then resolves authored Core geometry. [CODE] `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1455–1585`
|
||||
- The validated assembly path can build a manifest, including an imported Floating Islands plan, before creating the resolved layout. It copies plan anchors into that layout, then calls the passage planner. [CODE] `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1621–1685, 1950–2019, 2021–2041`
|
||||
- L140’s kept patch changes the Floating Islands path: first resolve the fixed passage layout, then plan islands around those passages and export the resulting anchors. The patch is not applied in the R48 source: the current adapter still calls `PlanFloatingIslandCluster` without a resolved layout. [CODE] `Standalone/Research/WorldAssemblerM3/Linux140/l140-fi-around-tunnels.patch:102–219`; `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:893–987`
|
||||
|
||||
`PlanResolvedLayoutPassagesV1` sorts the strata, uses existing route anchors where present, otherwise chooses seeded mouths, and builds passage control points with radii. Once resolved, those mouths and tubes are inputs to later planners. [CODE] `Standalone/Core/src/ResolvedLayoutInternal.h:3699–3792, 3820–3874, 4713–4819`
|
||||
|
||||
The generic island planner lays out seeded island centers and a route without seeing the passage layout. L133’s alternate planner bound endpoints, but did not plan against both full passage volumes. Its composed walk still had collisions and unsupported route points. [CODE] `Standalone/Core/src/FloatingIslandsInternal.h:1991–2050, 2166–2233`; historical patch `Linux133/l133-native-fi-v9.patch` in Git commit `f8cba7c:79–134`; [MEASURED] `Linux133/REPORT.md` in that commit:3–7
|
||||
|
||||
The cavern planner reads incoming and outgoing passages and requires them to exist, except for a Vertical Shafts basal terminal. Caves can carve through host rock to reach those mouths; L134’s default composed route passed, while L139 still failed the shell and seam checks at seed 2’s incoming mouth after three local geometry attempts. [CODE] `Standalone/Core/src/ResolvedLayoutInternal.h:2344–2359`; [MEASURED] `Linux134/REPORT.md:3–7`; `Linux139/REPORT.md:5–8, 43–52, 89–91`
|
||||
|
||||
## 2. Design choices
|
||||
|
||||
- **Content first:** each layer produces supported landing candidates and a connected approach; the passage planner chooses compatible candidates and routes to them. This can solve the fixed-height problem by letting the tunnel target viable terrain. It requires every layer type to provide reliable candidates, stable ordering and tie breaks, and passage rerouting; changed routes must still pass the existing grade, shell, seam, and ripple audits. **Size: medium to large.** [INFERENCE]
|
||||
|
||||
- **Negotiated landing pad:** the passage reserves a landing region and the layer guarantees support there. A point or pad contract alone is too weak: L140 checked five support probes, yet 14 seeds had no arrangement clearing the fixed tubes or could not reach both mouth heights. A useful contract must cover the capsule approach and passage exclusion envelope too. That grows toward a joint plan. **Size: medium for a pad; large for a full approach and clearance contract.** [MEASURED] `Linux140/REPORT.md:7, 15–17`; [INFERENCE]
|
||||
|
||||
- **Joint plan:** choose passage geometry and layer anchors together. This gives the solver room to avoid the fixed-tube infeasibility seen in L140, but costs the most: bounded deterministic search, new planner interfaces, and careful preservation of passage smoothness and audit behavior. A bounded search only guarantees feasibility within its candidate space. **Size: large.** [MEASURED] `Linux140/REPORT.md:17`; [INFERENCE]
|
||||
|
||||
## 3. Why ripple reaches 6–7 cm
|
||||
|
||||
The ripple audit samples the **composed field** passed as `Shape`, at `pose.radius − 0.18 m`, then estimates wall radius as that sample radius plus the average signed density. It cubic-detrends each 32 m window, preserving authored radius variation. Thus the result includes both the passage’s radius profile and any host-field effect on the sampled wall; it is not a passage-only measurement. [CODE] `Standalone/Source/StreamBenchmarkPipeline.h:4186–4239, 4414–4432, 4941–4944`
|
||||
|
||||
L67 traced a prior gauge error: omitting `pose.radius` hid an authored-radius beat. The corrected gauge found a 4.77 cm beat at 7.06 m and measured 7.27 cm on passage 1–2. L140 measured 6.22–7.19 cm on its upper passages with native islands. [MEASURED] `Linux67/REPORT.md:64–79`; `Linux140/REPORT.md:15–19`
|
||||
|
||||
The evidence shows that passage-radius variation alone is enough to explain a failure at this scale; it does not isolate how much native island density adds or masks. L140’s shell-preserving composition change still failed the unchanged ripple gate. [INFERENCE] `Linux140/l140-fi-around-tunnels.patch:912–924`; [MEASURED] `Linux140/REPORT.md:15–19`
|
||||
|
||||
## 4. Recommendation
|
||||
|
||||
**Use a bounded pairwise joint plan.** Have each layer planner publish certified landing candidates with capsule support and approach constraints. Let the passage planner choose compatible ports and route geometry together, then check the complete tube and composed layer before finalizing the resolved layout. Start with Floating Islands; preserve cave planners’ ability to carve to selected ports. [INFERENCE]
|
||||
|
||||
Keep candidate generation and tie breaking seed-stable, and finalize the layout identity only after the selected mouths, tubes, and content anchors are known. Keep the existing thresholds unchanged. [CODE] `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:2021–2041`; [INFERENCE]
|
||||
|
||||
The design passes if all 16 seeds clear the existing route, support, shell, seam, grade, mouth-profile, and 3.5 cm ripple gates, with matching Clang/GCC W1/W8/W12 hashes. Separately compare ripple on the same tube in a smooth host field and in the composed field; that isolates passage-profile ripple from host-field contribution. Kill the design if joint movement still leaves seeds infeasible, or if the passage-only profile fails ripple. [INFERENCE]
|
||||
|
||||
L133’s report and patch were absent from the R48 working tree; I read them from Git commit `f8cba7c`. No files were changed, and I did not build or run tests. [MEASURED]
|
||||
@@ -592,6 +592,11 @@ uniform float uHeightCenter;
|
||||
uniform float uHeightRange;
|
||||
uniform float uTileEdgeWidth;
|
||||
uniform vec3 uLightDirection;
|
||||
uniform vec3 uCameraWorld;
|
||||
uniform vec3 uFogColor;
|
||||
uniform float uFogStart;
|
||||
uniform float uFogEnd;
|
||||
uniform int uFogEnabled;
|
||||
uniform int uHeadlamp;
|
||||
uniform int uGrazingLight;
|
||||
uniform int uClipEnabled;
|
||||
@@ -643,6 +648,11 @@ void main()
|
||||
color=mix(color,resourceColor/max(resourceWeight,0.001),
|
||||
clamp(resourceWeight,0.0,0.95))*illumination;
|
||||
}
|
||||
if(uFogEnabled!=0)
|
||||
{
|
||||
float fogT=smoothstep(uFogStart,uFogEnd,length(vWorldPosition-uCameraWorld));
|
||||
color=mix(color,uFogColor,fogT);
|
||||
}
|
||||
if(uTileEdges!=0&&vTileData.z>0.0)
|
||||
{
|
||||
vec2 local=mod(vWorldPosition.xy-vTileData.xy,vTileData.z);
|
||||
@@ -707,7 +717,8 @@ void main()
|
||||
bool HeightTint,bool Headlamp,bool TileEdges,
|
||||
double TileEdgeWidthMeters,bool HasLightDirection,
|
||||
const std::array<double,3>& LightDirection,bool ClipPlaneEnabled,
|
||||
const std::array<double,4>& ClipPlane)
|
||||
const std::array<double,4>& ClipPlane,bool FogEnabled,
|
||||
double FogStartMeters,double FogEndMeters,const std::array<double,3>& FogColor)
|
||||
{
|
||||
EGLDisplay Display=MakeDisplay();
|
||||
EGLint Major=0,Minor=0;
|
||||
@@ -796,7 +807,10 @@ void main()
|
||||
glViewport(0,0,Width,Height);glDisable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST);
|
||||
// A covered empty ray represents open air. Keep it visibly blue so it cannot
|
||||
// be mistaken for the old near-black result from a scene that lacked tiles.
|
||||
glDepthFunc(GL_LEQUAL);glClearColor(0.30f,0.48f,0.62f,1.0f);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
glClearColor(FogEnabled?static_cast<float>(FogColor[0]):0.30f,
|
||||
FogEnabled?static_cast<float>(FogColor[1]):0.48f,
|
||||
FogEnabled?static_cast<float>(FogColor[2]):0.62f,1.0f);
|
||||
glClearDepthf(1.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
|
||||
glUseProgram(Program);
|
||||
glUniform1f(glGetUniformLocation(Program,"uTanHalfHorizontal"),static_cast<float>(TanHalfHorizontal));
|
||||
@@ -825,6 +839,18 @@ void main()
|
||||
static_cast<float>(ActiveLightDirection[0]),
|
||||
static_cast<float>(ActiveLightDirection[1]),
|
||||
static_cast<float>(ActiveLightDirection[2]));
|
||||
glUniform3f(glGetUniformLocation(Program,"uCameraWorld"),
|
||||
static_cast<float>(Scene.CameraMeters[0]),
|
||||
static_cast<float>(Scene.CameraMeters[1]),
|
||||
static_cast<float>(Scene.CameraMeters[2]));
|
||||
glUniform3f(glGetUniformLocation(Program,"uFogColor"),
|
||||
static_cast<float>(FogColor[0]),static_cast<float>(FogColor[1]),
|
||||
static_cast<float>(FogColor[2]));
|
||||
glUniform1f(glGetUniformLocation(Program,"uFogStart"),
|
||||
static_cast<float>(FogStartMeters));
|
||||
glUniform1f(glGetUniformLocation(Program,"uFogEnd"),
|
||||
static_cast<float>(FogEndMeters));
|
||||
glUniform1i(glGetUniformLocation(Program,"uFogEnabled"),FogEnabled?1:0);
|
||||
glDrawElements(GL_TRIANGLES,static_cast<GLsizei>(Scene.Indices.size()),GL_UNSIGNED_INT,nullptr);
|
||||
glFinish();
|
||||
std::vector<std::uint8_t> Pixels(static_cast<std::size_t>(Width)*Height*4);
|
||||
@@ -863,7 +889,7 @@ int main(int argc,char** argv)
|
||||
if(argc==2&&std::string_view(argv[1])=="--print-source-stamp")return 0;
|
||||
if(argc<3)
|
||||
{
|
||||
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--height-range-m=N] [--height-center-m=N] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--height-tint=on|off] [--headlamp=on|off] [--light-direction=X,Y,Z] [--tile-edges=on|off] [--tile-skirts=on|off] [--tile-edge-width-m=N] [--clip-plane=NX,NY,NZ,D] [--focus-region=X,Y,Z,R]\n",argv[0]);
|
||||
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--height-range-m=N] [--height-center-m=N] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--height-tint=on|off] [--headlamp=on|off] [--light-direction=X,Y,Z] [--fog=on|off] [--fog-start-m=N] [--fog-end-m=N] [--fog-color=R,G,B] [--tile-edges=on|off] [--tile-skirts=on|off] [--tile-edge-width-m=N] [--clip-plane=NX,NY,NZ,D] [--focus-region=X,Y,Z,R]\n",argv[0]);
|
||||
return 2;
|
||||
}
|
||||
int Width=2560,Height=1440;double Fov=90.0,HeightRangeMeters=64.0;
|
||||
@@ -871,6 +897,8 @@ int main(int argc,char** argv)
|
||||
bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0;
|
||||
bool HasCamera=false;std::array<double,3> Camera{};
|
||||
bool HasLightDirection=false;std::array<double,3> LightDirection{};
|
||||
bool FogEnabled=false;double FogStartMeters=50.0,FogEndMeters=200.0;
|
||||
std::array<double,3> FogColor={0.62,0.70,0.75};
|
||||
bool ReadableMode=true,HeightTint=true,TileEdges=false,TileSkirts=true,Headlamp=false;
|
||||
double TileEdgeWidthMeters=0.15;
|
||||
bool ClipPlaneEnabled=false;std::array<double,4> ClipPlane{};
|
||||
@@ -967,6 +995,38 @@ int main(int argc,char** argv)
|
||||
{std::fprintf(stderr,"Invalid light direction; use X,Y,Z with non-zero length\n");return 2;}
|
||||
HasLightDirection=true;
|
||||
}
|
||||
else if(Arg.starts_with("--fog="))
|
||||
{
|
||||
const std::string_view Mode=Arg.substr(6);
|
||||
if(Mode=="on")FogEnabled=true;
|
||||
else if(Mode=="off")FogEnabled=false;
|
||||
else {std::fprintf(stderr,"Invalid fog mode\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--fog-start-m="))
|
||||
{
|
||||
char* End=nullptr;const std::string Value(Arg.substr(14));
|
||||
FogStartMeters=std::strtod(Value.c_str(),&End);
|
||||
if(End==Value.c_str()||*End!='\0'||!std::isfinite(FogStartMeters)
|
||||
||FogStartMeters<0.0||FogStartMeters>20000.0)
|
||||
{std::fprintf(stderr,"Invalid fog start distance\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--fog-end-m="))
|
||||
{
|
||||
char* End=nullptr;const std::string Value(Arg.substr(12));
|
||||
FogEndMeters=std::strtod(Value.c_str(),&End);
|
||||
if(End==Value.c_str()||*End!='\0'||!std::isfinite(FogEndMeters)
|
||||
||FogEndMeters<=0.0||FogEndMeters>20000.0)
|
||||
{std::fprintf(stderr,"Invalid fog end distance\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--fog-color="))
|
||||
{
|
||||
const std::string Value(Arg.substr(12));
|
||||
if(std::sscanf(Value.c_str(),"%lf,%lf,%lf",&FogColor[0],&FogColor[1],
|
||||
&FogColor[2])!=3
|
||||
||std::any_of(FogColor.begin(),FogColor.end(),[](double Channel)
|
||||
{return !std::isfinite(Channel)||Channel<0.0||Channel>1.0;}))
|
||||
{std::fprintf(stderr,"Invalid fog color; use R,G,B in 0..1\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--tile-edges="))
|
||||
{
|
||||
const std::string_view Mode=Arg.substr(13);
|
||||
@@ -1002,6 +1062,8 @@ int main(int argc,char** argv)
|
||||
}
|
||||
else {std::fprintf(stderr,"Unknown option: %.*s\n",static_cast<int>(Arg.size()),Arg.data());return 2;}
|
||||
}
|
||||
if(FogEnabled&&FogEndMeters<=FogStartMeters)
|
||||
{std::fprintf(stderr,"Fog end distance must be greater than fog start distance\n");return 2;}
|
||||
FScene Scene;std::string Error;
|
||||
if(!LoadScene(argv[1],Scene,Error))
|
||||
{std::fprintf(stderr,"[OffscreenCapture] FAIL: %s\n",Error.c_str());return 2;}
|
||||
@@ -1077,14 +1139,16 @@ int main(int argc,char** argv)
|
||||
FocusRadius,static_cast<unsigned long long>(Scene.DrawnTriangles));
|
||||
}
|
||||
}
|
||||
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f height_center_m=%.3f shade=%s height_tint=%s tile_edges=%s vertices=%zu triangles=%llu\n",
|
||||
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f height_center_m=%.3f shade=%s height_tint=%s tile_edges=%s fog=%s fog_distance_m=%.1f..%.1f fog_color=(%.2f,%.2f,%.2f) vertices=%zu triangles=%llu\n",
|
||||
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
|
||||
Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight,
|
||||
HeightCenterMeters,
|
||||
ReadableMode?"readable":"normal",HeightTint?"on":"off",
|
||||
TileEdges?"on":"off",Scene.Vertices.size(),
|
||||
TileEdges?"on":"off",FogEnabled?"on":"off",FogStartMeters,FogEndMeters,
|
||||
FogColor[0],FogColor[1],FogColor[2],Scene.Vertices.size(),
|
||||
static_cast<unsigned long long>(Scene.Triangles));
|
||||
return Render(Scene,argv[2],Width,Height,Fov,HeightRangeMeters,HeightCenterMeters,ReadableMode,
|
||||
HeightTint,Headlamp,TileEdges,
|
||||
TileEdgeWidthMeters,HasLightDirection,LightDirection,ClipPlaneEnabled,ClipPlane)?0:3;
|
||||
TileEdgeWidthMeters,HasLightDirection,LightDirection,ClipPlaneEnabled,ClipPlane,
|
||||
FogEnabled,FogStartMeters,FogEndMeters,FogColor)?0:3;
|
||||
}
|
||||
@@ -70,6 +70,8 @@ namespace
|
||||
bool HasRenderDistanceOverride=false;
|
||||
int RenderDistanceOverride=0;
|
||||
bool FloatingIslandsRecipe=false;
|
||||
bool NativeOwnerFloatingIslandsRecipe=false;
|
||||
bool DensityDispatchReport=false;
|
||||
bool CavesRecipe=false;
|
||||
bool LatticeRecipe=false;
|
||||
int LatticeStrateIndex=-1;
|
||||
@@ -106,6 +108,7 @@ namespace
|
||||
bool ResolvedLayoutOnly=false;
|
||||
bool AuditResolvedLayoutBoundaries=false;
|
||||
bool AuditResolvedLayoutPassages=false;
|
||||
bool AuditLiminalRoadWalk=false;
|
||||
bool AuditSurfaceClearance=false;
|
||||
bool AuditStrateTransitions=false;
|
||||
int TransitionModeOverride=-1;
|
||||
@@ -1432,6 +1435,8 @@ namespace
|
||||
" --core-snapshot=FILE (benchmark the exact resolved Core snapshot from Unreal)\n"
|
||||
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
||||
" --seed=N --recipe=surface-world|floating-islands|caves|lattice\n"
|
||||
" --native-owner-fi (build the existing owner FI plan; leave other strates intact)\n"
|
||||
" --density-dispatch-report (print bound path per strate and tile hashes)\n"
|
||||
" --lattice-strate=N (opt one Maze or VerticalShafts strate into Core.Lattice)\n"
|
||||
" --landmark-chamber-demo (opt-in SW cavern room and swept tunnel)\n"
|
||||
" --ore-veins-demo (opt-in material-only SW capsule veins)\n"
|
||||
@@ -1443,6 +1448,7 @@ namespace
|
||||
" --audit-strate-transitions (measure interface crossing, band thickness and holes)\n"
|
||||
" --transition-mode=hard|gradient|interleaved --transition-width-chunks=N (fixture override)\n"
|
||||
" --audit-resolved-layout-passages (capsule, support, route-binding and blocked-control audit)\n"
|
||||
" --audit-liminal-road-walk (capsule, support, width and grade audit for the liminal road)\n"
|
||||
" --audit-heightfield-profile (brute-force the profiled field intervals and route)\n"
|
||||
" --audit-motif-intervals (brute-force motif bounds, BVH and shape-free guards)\n"
|
||||
" --audit-landmark-reachability (capsule, support, grade and buried-L43 control)\n"
|
||||
@@ -1490,6 +1496,10 @@ namespace
|
||||
else if(arg=="--resolved-layout-only")options.ResolvedLayoutOnly=true;
|
||||
else if(arg=="--audit-resolved-layout-boundaries")options.AuditResolvedLayoutBoundaries=true;
|
||||
else if(arg=="--audit-resolved-layout-passages")options.AuditResolvedLayoutPassages=true;
|
||||
else if(arg=="--audit-liminal-road-walk")options.AuditLiminalRoadWalk=true;
|
||||
else if(arg=="--native-owner-fi")options.NativeOwnerFloatingIslandsRecipe=true;
|
||||
else if(arg=="--density-dispatch-report")
|
||||
{options.DensityDispatchReport=true;options.StreamHash=true;options.StreamHashTiles=true;}
|
||||
else if(arg=="--audit-landmark-reachability")options.AuditLandmarkReachability=true;
|
||||
else if(arg=="--audit-native-caves")options.AuditNativeCaves=true;
|
||||
else if(arg=="--audit-native-lattice")options.AuditNativeLattice=true;
|
||||
@@ -1819,29 +1829,12 @@ int main(int argc,char** argv)
|
||||
else if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
||||
*ownerWorld,ownerJsonRoot,error,{},&worldAssembly))
|
||||
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;}
|
||||
if(!useExportedCorePlan&&options.FloatingIslandsRecipe
|
||||
if(((!useExportedCorePlan&&options.FloatingIslandsRecipe)
|
||||
||(options.NativeOwnerFloatingIslandsRecipe
|
||||
&&worldAssembly.floatingIslands.empty()))
|
||||
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
||||
*ownerWorld,ownerJsonRoot,worldAssembly,error))
|
||||
{std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
||||
if(!options.WorldAssemblyManifestOutputPath.empty())
|
||||
{
|
||||
const std::string manifestText=worldAssembly.ToJson();
|
||||
const auto& outputPath=options.WorldAssemblyManifestOutputPath;
|
||||
std::error_code directoryError;
|
||||
if(!outputPath.parent_path().empty())
|
||||
std::filesystem::create_directories(outputPath.parent_path(),directoryError);
|
||||
if(directoryError)
|
||||
{std::fprintf(stderr,"Could not create the complete world assembly manifest output directory.\n");return 2;}
|
||||
std::ofstream output(outputPath,std::ios::binary);
|
||||
if(!output)
|
||||
{std::fprintf(stderr,"Could not open the complete world assembly manifest output path.\n");return 2;}
|
||||
output.write(manifestText.data(),static_cast<std::streamsize>(manifestText.size()));
|
||||
if(!output)
|
||||
{std::fprintf(stderr,"Could not write the complete world assembly manifest payload.\n");return 2;}
|
||||
std::printf("[WorldAssemblerFullManifest] bytes=%zu surface_strates=%zu floating_islands=%zu path=%s status=PASS\n",
|
||||
manifestText.size(),worldAssembly.plan.strates.size(),worldAssembly.floatingIslands.size(),
|
||||
outputPath.string().c_str());
|
||||
}
|
||||
if (options.AuditHeightfieldProfile && !RunHeightfieldProfileAudit(worldAssembly))
|
||||
{
|
||||
std::fprintf(stderr,"Profiled SurfaceWorld interval, classification or route audit failed.\n");
|
||||
@@ -1849,22 +1842,205 @@ int main(int argc,char** argv)
|
||||
}
|
||||
VoxelForgeCore::ResolvedLayoutSnapshotV3 resolvedLayout;
|
||||
{
|
||||
const VFJson::Value& coreLiminalConfig=ownerJsonRoot.Get("core_liminal_field");
|
||||
const bool hasCoreLiminalField=!std::holds_alternative<std::nullptr_t>(
|
||||
coreLiminalConfig.Data);
|
||||
const std::string variant=options.LatticeRecipe
|
||||
?"lattice/"+std::to_string(options.LatticeStrateIndex)
|
||||
:options.CavesRecipe?"caves"
|
||||
:options.FloatingIslandsRecipe?"floating-islands":"surface-world";
|
||||
:(options.FloatingIslandsRecipe||options.NativeOwnerFloatingIslandsRecipe)
|
||||
?"floating-islands":"surface-world";
|
||||
const bool layoutResolved=useExportedCorePlan
|
||||
&&!options.NativeOwnerFloatingIslandsRecipe
|
||||
?StandaloneWorldAssemblyPrototype::ImportExportedResolvedLayout(
|
||||
ownerJsonRoot,resolvedLayout,error)
|
||||
:StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
||||
*ownerWorld,worldAssembly,variant,resolvedLayout,error);
|
||||
*ownerWorld,worldAssembly,variant,resolvedLayout,error,
|
||||
!hasCoreLiminalField);
|
||||
if(!layoutResolved)
|
||||
{std::fprintf(stderr,"Resolved-layout snapshot failed: %s\n",error.c_str());return 2;}
|
||||
if(useExportedCorePlan
|
||||
if(useExportedCorePlan&&!options.NativeOwnerFloatingIslandsRecipe
|
||||
&&!StandaloneWorldAssemblyPrototype::ValidateExportedCorePlanMatchesOwner(
|
||||
*ownerWorld,worldAssembly,resolvedLayout,error))
|
||||
{std::fprintf(stderr,"Unreal-resolved Core plan/owner slot validation failed: %s\n",error.c_str());return 2;}
|
||||
if(!std::holds_alternative<std::nullptr_t>(ownerJsonRoot.Get("motif_family_override").Data))
|
||||
if(hasCoreLiminalField)
|
||||
{
|
||||
const VFJson::Value& versionValue=coreLiminalConfig.Get("version");
|
||||
const VFJson::Value& strateValue=coreLiminalConfig.Get("strate_index");
|
||||
const std::int64_t strateIndex=strateValue.IsNumber()
|
||||
?strateValue.Integer():-1;
|
||||
const auto* policy=std::get_if<std::string>(&coreLiminalConfig.Get("variant_policy").Data);
|
||||
if(!coreLiminalConfig.IsObject()
|
||||
||!versionValue.IsNumber()
|
||||
||versionValue.Number()!=VoxelForgeCore::CoreLiminalFieldRecipeVersionV2
|
||||
||!strateValue.IsNumber()
|
||||
||std::trunc(strateValue.Number())!=strateValue.Number()
|
||||
||strateValue.Number()<0.0
|
||||
||strateValue.Number()>std::numeric_limits<std::int32_t>::max()
|
||||
||strateIndex<0||!policy||*policy!="seed_modulo_4")
|
||||
{std::fprintf(stderr,"core_liminal_field requires version 1, a strate_index, and seed_modulo_4 variant selection\n");return 2;}
|
||||
const auto resolved=std::find_if(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
||||
[&](const auto& value){return value.strateIndex==strateIndex
|
||||
&&value.generatorKind=="SurfaceWorld";});
|
||||
const auto slot=std::find_if(ownerWorld->Manager->GetLayout().begin(),
|
||||
ownerWorld->Manager->GetLayout().end(),[&](const auto& value)
|
||||
{return value.StrateIndex==strateIndex&&value.Definition;});
|
||||
if(resolved==resolvedLayout.strates.end()||slot==ownerWorld->Manager->GetLayout().end())
|
||||
{std::fprintf(stderr,"Core.Liminal.Field must target a resolved SurfaceWorld owner slot\n");return 2;}
|
||||
const auto order=static_cast<std::size_t>(resolved-resolvedLayout.strates.begin());
|
||||
if(order==0||order+1>=resolvedLayout.strates.size()
|
||||
||resolvedLayout.strates.size()!=3
|
||||
||resolvedLayout.strates[order-1].generatorKind!="SurfaceWorld"
|
||||
||resolvedLayout.strates[order+1].generatorKind!="TunnelNetwork")
|
||||
{std::fprintf(stderr,"Core.Liminal.Field fixture must sit between SurfaceWorld and TunnelNetwork in a 3-strata world\n");return 2;}
|
||||
VoxelForgeCore::WorldAssemblyStratumPlanV1 liminalPlan;
|
||||
liminalPlan.strateIndex=static_cast<std::int32_t>(strateIndex);
|
||||
liminalPlan.topChunkZ=slot->TopChunkZ;
|
||||
liminalPlan.bottomChunkZ=slot->BottomChunkZ;
|
||||
resolvedLayout.passages.clear();
|
||||
if(!VoxelForgeCore::PrepareCoreLiminalLandingPadsV2(worldAssembly.recipe,
|
||||
liminalPlan,resolvedLayout,error))
|
||||
{std::fprintf(stderr,"Core.Liminal.Field landing-pad preparation failed: %s\n",error.c_str());return 2;}
|
||||
const auto passageConfigs=
|
||||
StandaloneWorldAssemblyPrototype::BuildResolvedLayoutPassageConfigs(*ownerWorld);
|
||||
auto uncertifiedControl=resolvedLayout;
|
||||
const auto controlStratum=std::find_if(uncertifiedControl.strates.begin(),
|
||||
uncertifiedControl.strates.end(),[&](const auto& value)
|
||||
{return value.strateIndex==strateIndex;});
|
||||
bool landingPadNegativeControlRejected=false;
|
||||
if(controlStratum!=uncertifiedControl.strates.end()
|
||||
&&!controlStratum->landingPads.empty())
|
||||
{
|
||||
controlStratum->landingPads.front().certifiedWalkableWithSupport=false;
|
||||
std::string controlError;
|
||||
landingPadNegativeControlRejected=
|
||||
!VoxelForgeCore::PlanResolvedLayoutPassagesV1(uncertifiedControl,
|
||||
ownerWorld->Seed,passageConfigs,controlError);
|
||||
}
|
||||
std::printf("[LandingPadNegativeControl] uncertified_rejected=%s status=%s\n",
|
||||
landingPadNegativeControlRejected?"true":"false",
|
||||
landingPadNegativeControlRejected?"PASS":"FAIL");
|
||||
if(!landingPadNegativeControlRejected)return 2;
|
||||
if(!VoxelForgeCore::PlanResolvedLayoutPassagesV1(resolvedLayout,
|
||||
ownerWorld->Seed,passageConfigs,error))
|
||||
{std::fprintf(stderr,"Core.Liminal.Field pad-bound passage planning failed: %s\n",error.c_str());return 2;}
|
||||
if(!VoxelForgeCore::PlanCoreLiminalFieldV2(worldAssembly.recipe,
|
||||
liminalPlan,resolvedLayout,error))
|
||||
{std::fprintf(stderr,"Core.Liminal.Field planning failed: %s\n",error.c_str());return 2;}
|
||||
worldAssembly.plan.strates.push_back(std::move(liminalPlan));
|
||||
if(!worldAssembly.finalize(error))
|
||||
{std::fprintf(stderr,"Core.Liminal.Field manifest finalization failed: %s\n",error.c_str());return 2;}
|
||||
const std::string canonicalManifest=worldAssembly.ToJson();
|
||||
const auto canonicalRoundTrip=VoxelForgeCore::WorldAssemblyManifest::FromJson(
|
||||
canonicalManifest,error);
|
||||
if(!canonicalRoundTrip||canonicalRoundTrip->ToJson()!=canonicalManifest
|
||||
||canonicalRoundTrip->contentHash!=worldAssembly.contentHash)
|
||||
{std::fprintf(stderr,"Core.Liminal.Field manifest failed canonical JSON round-trip: %s\n",error.c_str());return 2;}
|
||||
const auto plan=std::find_if(worldAssembly.plan.strates.begin(),
|
||||
worldAssembly.plan.strates.end(),[&](const auto& value)
|
||||
{return value.strateIndex==strateIndex
|
||||
&&value.liminalField.version==VoxelForgeCore::CoreLiminalFieldRecipeVersionV2;});
|
||||
if(plan==worldAssembly.plan.strates.end())
|
||||
{std::fprintf(stderr,"Core.Liminal.Field finalized plan is missing\n");return 2;}
|
||||
auto& resolvedLiminal=*std::find_if(resolvedLayout.strates.begin(),
|
||||
resolvedLayout.strates.end(),[&](const auto& value)
|
||||
{return value.strateIndex==strateIndex;});
|
||||
resolvedLiminal.recipeId=std::string(VoxelForgeCore::CoreLiminalFieldRecipeIdV1);
|
||||
resolvedLiminal.recipeVersion=VoxelForgeCore::CoreLiminalFieldRecipeVersionV2;
|
||||
resolvedLiminal.anchors.erase(std::remove_if(resolvedLiminal.anchors.begin(),
|
||||
resolvedLiminal.anchors.end(),[](const auto& anchor)
|
||||
{return anchor.kind=="heightfield_profile"||anchor.kind=="route_point";}),
|
||||
resolvedLiminal.anchors.end());
|
||||
VoxelForgeCore::ResolvedLayoutAnchorV2 profileAnchor;
|
||||
profileAnchor.kind="heightfield_profile";
|
||||
profileAnchor.stableId="heightfield-profile/liminal/"
|
||||
+std::to_string(static_cast<std::uint32_t>(plan->surfaceProfile.kind));
|
||||
profileAnchor.units="profile kind enum";
|
||||
profileAnchor.values={1,static_cast<std::int64_t>(plan->surfaceProfile.kind)};
|
||||
resolvedLiminal.anchors.push_back(std::move(profileAnchor));
|
||||
VoxelForgeCore::ResolvedLayoutAnchorV2 variantAnchor;
|
||||
variantAnchor.kind="liminal_variant";variantAnchor.stableId="liminal-field/variant";
|
||||
variantAnchor.units="version,variant,seed,1/256 Core voxel";
|
||||
variantAnchor.values={plan->liminalField.version,
|
||||
static_cast<std::int64_t>(plan->liminalField.variant),plan->liminalField.seed,
|
||||
plan->liminalField.floorHeightSubvoxels,
|
||||
plan->liminalField.routeHalfWidthSubvoxels,
|
||||
plan->liminalField.routeHalfThicknessSubvoxels};
|
||||
resolvedLiminal.anchors.push_back(std::move(variantAnchor));
|
||||
for(std::size_t pointIndex=0;pointIndex<plan->route.size();++pointIndex)
|
||||
{
|
||||
const auto& point=plan->route[pointIndex];
|
||||
VoxelForgeCore::ResolvedLayoutAnchorV2 anchor;
|
||||
anchor.kind="liminal_route_point";
|
||||
anchor.stableId="liminal-field/route/"+std::to_string(pointIndex);
|
||||
anchor.units="1/256 Core voxel";
|
||||
anchor.values={point.positionSubvoxels[0],point.positionSubvoxels[1],
|
||||
point.positionSubvoxels[2],point.radiusSubvoxels};
|
||||
resolvedLiminal.anchors.push_back(std::move(anchor));
|
||||
}
|
||||
if(!resolvedLayout.finalize(error))
|
||||
{std::fprintf(stderr,"Core.Liminal.Field resolved layout finalization failed: %s\n",error.c_str());return 2;}
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutIssueV2> strictIssues;
|
||||
std::string strictError;
|
||||
if(!resolvedLayout.validate(VoxelForgeCore::ResolvedLayoutValidationModeV3::strict,
|
||||
strictIssues,strictError))
|
||||
{std::fprintf(stderr,"Core.Liminal.Field resolved layout failed strict validation: %s\n",strictError.c_str());return 2;}
|
||||
VFJson::Value assemblyValue;
|
||||
if(!VFJson::Parser(worldAssembly.ToJson()).Parse(assemblyValue,error))
|
||||
{std::fprintf(stderr,"Core.Liminal.Field manifest JSON encoding failed: %s\n",error.c_str());return 2;}
|
||||
auto* rootObject=std::get_if<VFJson::Value::Object>(&ownerJsonRoot.Data);
|
||||
if(!rootObject)
|
||||
{std::fprintf(stderr,"Core.Liminal.Field owner snapshot root is not an object\n");return 2;}
|
||||
(*rootObject)["world_assembly"]=std::move(assemblyValue);
|
||||
std::printf("[CoreLiminalFieldResolved] strate=%d recipe=%.*s version=%u variant=%u seed=%u arrival=%s departure=%s route_points=%zu status=PASS\n",
|
||||
static_cast<int>(strateIndex),static_cast<int>(VoxelForgeCore::CoreLiminalFieldRecipeIdV1.size()),
|
||||
VoxelForgeCore::CoreLiminalFieldRecipeIdV1.data(),plan->liminalField.version,
|
||||
static_cast<unsigned>(plan->liminalField.variant),plan->liminalField.seed,
|
||||
resolvedLayout.passages.front().stableId.c_str(),resolvedLayout.passages.back().stableId.c_str(),
|
||||
plan->route.size());
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> liminalRequests;
|
||||
if(!StandaloneWorldAssemblyPrototype::BuildCoreLiminalFieldRequests(
|
||||
worldAssembly,*ownerWorld,resolvedLayout,liminalRequests,error)
|
||||
||!VoxelForgeCore::ResolveResolvedLayoutStampsV1(
|
||||
resolvedLayout,seed,liminalRequests,error))
|
||||
{std::fprintf(stderr,"Core.Liminal.Field drawing plan failed: %s\n",error.c_str());return 2;}
|
||||
std::printf("[CoreLiminalFieldDraw] variant=%u stamps=%zu deck=1 supports=%zu status=PASS\n",
|
||||
static_cast<unsigned>(plan->liminalField.variant),resolvedLayout.stamps.size(),
|
||||
static_cast<std::size_t>(std::count_if(resolvedLayout.stamps.begin(),
|
||||
resolvedLayout.stamps.end(),[](const auto& stamp)
|
||||
{return stamp.stableId.starts_with("core/liminal-field/route/support/");})));
|
||||
}
|
||||
if(!options.WorldAssemblyManifestOutputPath.empty())
|
||||
{
|
||||
const std::string manifestText=worldAssembly.ToJson();
|
||||
const auto& outputPath=options.WorldAssemblyManifestOutputPath;
|
||||
std::error_code directoryError;
|
||||
if(!outputPath.parent_path().empty())
|
||||
std::filesystem::create_directories(outputPath.parent_path(),directoryError);
|
||||
if(directoryError)
|
||||
{std::fprintf(stderr,"Could not create the complete world assembly manifest output directory.\n");return 2;}
|
||||
std::ofstream output(outputPath,std::ios::binary);
|
||||
if(!output)
|
||||
{std::fprintf(stderr,"Could not open the complete world assembly manifest output path.\n");return 2;}
|
||||
output.write(manifestText.data(),static_cast<std::streamsize>(manifestText.size()));
|
||||
if(!output)
|
||||
{std::fprintf(stderr,"Could not write the complete world assembly manifest payload.\n");return 2;}
|
||||
std::printf("[WorldAssemblerFullManifest] bytes=%zu surface_strates=%zu floating_islands=%zu path=%s status=PASS\n",
|
||||
manifestText.size(),worldAssembly.plan.strates.size(),worldAssembly.floatingIslands.size(),
|
||||
outputPath.string().c_str());
|
||||
}
|
||||
const bool hasMotifFamily=!std::holds_alternative<std::nullptr_t>(
|
||||
ownerJsonRoot.Get("motif_family_override").Data);
|
||||
const bool hasLiminalRoad=!std::holds_alternative<std::nullptr_t>(
|
||||
ownerJsonRoot.Get("liminal_road_override").Data);
|
||||
const bool hasLiminalPillarGrid=!std::holds_alternative<std::nullptr_t>(
|
||||
ownerJsonRoot.Get("liminal_pillar_grid_override").Data);
|
||||
if((hasMotifFamily&&hasLiminalRoad)||(hasMotifFamily&&hasLiminalPillarGrid)
|
||||
||(hasLiminalRoad&&hasLiminalPillarGrid))
|
||||
{std::fprintf(stderr,"liminal content overrides must be used in separate fixtures\n");return 2;}
|
||||
if(hasCoreLiminalField&&(hasMotifFamily||hasLiminalRoad||hasLiminalPillarGrid))
|
||||
{std::fprintf(stderr,"Core.Liminal.Field owns its variant geometry and cannot be combined with prototype content overrides\n");return 2;}
|
||||
if(hasMotifFamily)
|
||||
{
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
||||
if(!StandaloneWorldAssemblyPrototype::BuildMotifFamilyRequests(ownerJsonRoot,
|
||||
@@ -1892,6 +2068,76 @@ int main(int argc,char** argv)
|
||||
static_cast<unsigned long long>(motif.irregularitySeed));
|
||||
}
|
||||
}
|
||||
if(hasLiminalRoad)
|
||||
{
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
||||
if(!StandaloneWorldAssemblyPrototype::BuildLiminalRoadRequests(ownerJsonRoot,
|
||||
*ownerWorld,worldAssembly,resolvedLayout,requests,error)
|
||||
||!VoxelForgeCore::ResolveResolvedLayoutStampsV1(
|
||||
resolvedLayout,seed,requests,error))
|
||||
{std::fprintf(stderr,"Liminal road resolution failed: %s\n",error.c_str());return 2;}
|
||||
std::vector<const VoxelForgeCore::ResolvedLayoutStampV1*> deck;
|
||||
std::size_t supportCount=0;
|
||||
const std::string deckPrefix="liminal/road-clouds-v2/deck/";
|
||||
const std::string supportPrefix="liminal/road-clouds-v2/support/";
|
||||
for(const auto& stamp:resolvedLayout.stamps)
|
||||
{
|
||||
if(stamp.stableId.starts_with(deckPrefix))deck.push_back(&stamp);
|
||||
else if(stamp.stableId.starts_with(supportPrefix))++supportCount;
|
||||
}
|
||||
std::sort(deck.begin(),deck.end(),[](const auto* a,const auto* b)
|
||||
{return a->stableId<b->stableId;});
|
||||
double pathLength=0.0,maxGradeDegrees=0.0;
|
||||
std::vector<std::array<std::int64_t,3>> centerline;
|
||||
if(deck.size()==1&&!deck.front()->sweepPathMicrometres.empty())
|
||||
centerline=deck.front()->sweepPathMicrometres;
|
||||
else
|
||||
{
|
||||
for(std::size_t i=0;i<deck.size();++i)
|
||||
{
|
||||
const auto* segment=deck[i];
|
||||
if(i==0)centerline.push_back(segment->sweepEndpointsMicrometres[0]);
|
||||
centerline.push_back(segment->sweepEndpointsMicrometres[1]);
|
||||
}
|
||||
}
|
||||
for(std::size_t i=1;i<centerline.size();++i)
|
||||
{
|
||||
const auto& a=centerline[i-1];const auto& b=centerline[i];
|
||||
const double dx=(b[0]-a[0])/1.0e6;
|
||||
const double dy=(b[1]-a[1])/1.0e6;
|
||||
const double dz=std::abs(b[2]-a[2])/1.0e6;
|
||||
const double horizontal=std::hypot(dx,dy);
|
||||
pathLength+=horizontal;
|
||||
maxGradeDegrees=std::max(maxGradeDegrees,
|
||||
std::atan2(dz,horizontal)*57.29577951308232);
|
||||
}
|
||||
const double width=deck.empty()?0.0:
|
||||
2.0*deck.front()->halfExtentsMicrometres[1]/1.0e6;
|
||||
std::printf("[LiminalRoadResolved] vocabulary=%u id=liminal/road-clouds-v2 "
|
||||
"strate=%d shape=continuous_swept_deck deck_stamps=%zu centerline_points=%zu supports=%zu "
|
||||
"path_length_m=%.3f width_m=%.3f grade_max_deg=%.3f "
|
||||
"placement=free route_keepout=PASS band_clearance=PASS status=%s\n",
|
||||
resolvedLayout.stampVocabularyVersion,
|
||||
deck.empty()?-1:deck.front()->strateIndex,deck.size(),centerline.size(),
|
||||
supportCount,pathLength,width,maxGradeDegrees,
|
||||
deck.size()==1&¢erline.size()>=2&&pathLength>=300.0&&width>=3.0
|
||||
&&maxGradeDegrees<=15.0?"PASS":"FAIL");
|
||||
if(deck.size()!=1||centerline.size()<2||pathLength<80.0
|
||||
||width<3.0||maxGradeDegrees>15.0)
|
||||
{std::fprintf(stderr,"Liminal road resolved outside the walkable road bounds\n");return 2;}
|
||||
}
|
||||
if(hasLiminalPillarGrid)
|
||||
{
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
||||
if(!StandaloneWorldAssemblyPrototype::BuildLiminalPillarGridRequests(ownerJsonRoot,
|
||||
*ownerWorld,worldAssembly,resolvedLayout,requests,error)
|
||||
||!VoxelForgeCore::ResolveResolvedLayoutStampsV1(
|
||||
resolvedLayout,seed,requests,error))
|
||||
{std::fprintf(stderr,"Liminal pillar-grid resolution failed: %s\n",error.c_str());return 2;}
|
||||
std::printf("[LiminalPillarGridResolved] vocabulary=%u pillars=%zu shape=identical_vertical_cylinders "
|
||||
"placement=regular_grid route_keepout=PASS band_clearance=PASS status=PASS\n",
|
||||
resolvedLayout.stampVocabularyVersion,resolvedLayout.stamps.size());
|
||||
}
|
||||
if(options.LandmarkChamberDemo)
|
||||
{
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
||||
@@ -2113,6 +2359,65 @@ int main(int argc,char** argv)
|
||||
controls.controlsRejectedAsExpected,controls.controlsRun,controls.passed?"PASS":"FAIL");
|
||||
if(!controls.passed)
|
||||
{for(const auto& failure:controls.failures)std::printf("[ResolvedLayoutValidatorControlFailure] %s %s\n",failure.code.c_str(),failure.detail.c_str());return 2;}
|
||||
if(options.DensityDispatchReport)
|
||||
{
|
||||
const auto& slots=ownerWorld->Manager->GetLayout();
|
||||
for(const auto& stratum:resolvedLayout.strates)
|
||||
{
|
||||
const auto slot=std::find_if(slots.begin(),slots.end(),[&](const auto& candidate)
|
||||
{return candidate.StrateIndex==stratum.strateIndex;});
|
||||
bool native=false;
|
||||
std::string path,adapter;
|
||||
if(slot!=slots.end()&&stratum.generatorKind=="SurfaceWorld")
|
||||
{
|
||||
const auto plan=std::find_if(worldAssembly.plan.strates.begin(),
|
||||
worldAssembly.plan.strates.end(),[&](const auto& candidate)
|
||||
{return candidate.strateIndex==stratum.strateIndex
|
||||
&&candidate.bottomChunkZ==slot->BottomChunkZ
|
||||
&&candidate.topChunkZ==slot->TopChunkZ;});
|
||||
if(plan!=worldAssembly.plan.strates.end())
|
||||
{
|
||||
const VoxelForgeCore::Heightfield2D field(worldAssembly.recipe,*plan);
|
||||
native=field.isValid();
|
||||
}
|
||||
if(native){path="SurfaceWorld-native";adapter="FSurfaceWorldFieldAdapter";}
|
||||
}
|
||||
else if(slot!=slots.end()&&stratum.generatorKind=="FloatingIslands")
|
||||
{
|
||||
const auto plan=std::find_if(worldAssembly.floatingIslands.begin(),
|
||||
worldAssembly.floatingIslands.end(),[&](const auto& candidate)
|
||||
{return candidate.strateIndex==stratum.strateIndex
|
||||
&&candidate.bottomChunkZ==slot->BottomChunkZ
|
||||
&&candidate.topChunkZ==slot->TopChunkZ;});
|
||||
if(plan!=worldAssembly.floatingIslands.end())
|
||||
{
|
||||
const VoxelForgeCore::FloatingIslandFieldV1 field(
|
||||
plan->manifest.recipe,plan->manifest.plan);
|
||||
native=field.isValid();
|
||||
}
|
||||
if(native){path="FloatingIslands-native";adapter="FFloatingIslandFieldAdapter";}
|
||||
}
|
||||
else if(VoxelForgeCore::HasNativeGraphField(stratum)
|
||||
&&(VoxelForgeCore::IsNativeCaveFieldStratum(stratum)
|
||||
?stratum.passageCapabilities.hasNativeCaveField
|
||||
:stratum.passageCapabilities.hasNativeLatticeField))
|
||||
{
|
||||
native=true;
|
||||
path=VoxelForgeCore::IsNativeLatticeFieldStratum(stratum)
|
||||
?"Lattice-native":"Caves-native";
|
||||
adapter="FCavesFieldAdapter";
|
||||
}
|
||||
if(!native)
|
||||
{
|
||||
path=stratum.generatorKind=="SurfaceWorld"
|
||||
?"SurfaceWorld-legacy":"compiled-analytic/"+stratum.generatorKind;
|
||||
adapter="FLegacyGeneratorFieldAdapter";
|
||||
}
|
||||
std::printf("[DensityDispatch] strate=%d generator=%s recipe=%s version=%u bound_path=%s adapter=%s\n",
|
||||
stratum.strateIndex,stratum.generatorKind.c_str(),stratum.recipeId.c_str(),
|
||||
stratum.recipeVersion,path.c_str(),adapter.c_str());
|
||||
}
|
||||
}
|
||||
if(!options.ResolvedLayoutOutputPath.empty())
|
||||
std::printf("[ResolvedLayoutSnapshotOutput] path=%s\n",options.ResolvedLayoutOutputPath.c_str());
|
||||
if(auto* root=std::get_if<VFJson::Value::Object>(&ownerJsonRoot.Data))
|
||||
@@ -2334,5 +2639,5 @@ int main(int argc,char** argv)
|
||||
options.MovingCameraSpeedMetersPerSecond,options.MovingCameraDurationSeconds,
|
||||
options.MovingCameraDwellSeconds,options.ColdWorkerTimeline,
|
||||
options.AuditPassageSourceClamp,options.AuditPassageCarveReach,
|
||||
options.AuditPassageTileSeams);
|
||||
options.AuditPassageTileSeams,options.AuditLiminalRoadWalk);
|
||||
}
|
||||
@@ -2384,7 +2384,8 @@ namespace StandaloneStreamBenchmark
|
||||
bool bMovingCameraTrace=false,double MovingCameraSpeedMetersPerSecond=0.0,
|
||||
double MovingCameraDurationSeconds=12.0,double MovingCameraDwellSeconds=5.0,
|
||||
bool bColdWorkerTimeline=false,bool bAuditPassageSourceClamp=false,
|
||||
bool bAuditPassageCarveReach=false,bool bAuditPassageTileSeams=false)
|
||||
bool bAuditPassageCarveReach=false,bool bAuditPassageTileSeams=false,
|
||||
bool bAuditLiminalRoadWalk=false)
|
||||
{
|
||||
using Clock=std::chrono::steady_clock;
|
||||
const std::clock_t ColdCpuOrigin=std::clock();
|
||||
@@ -2554,7 +2555,7 @@ namespace StandaloneStreamBenchmark
|
||||
||bAuditOreVeinYield||bAuditLandmarkReachability||bAuditStrateTransitions
|
||||
||bAuditResolvedLayoutPassages||bAuditResolvedLayoutBoundaries
|
||||
||bAuditTileShapeReadiness||bAuditBasePageEquivalence||bAuditRouteParity
|
||||
||bAuditPassageSourceClamp||bAuditPassageCarveReach;
|
||||
||bAuditPassageSourceClamp||bAuditPassageCarveReach||bAuditLiminalRoadWalk;
|
||||
const bool bDeferFarWorld=bIncludeFarWorld&&!bMovingCameraTrace&&!bFarWorldAudit;
|
||||
VoxelForgeCore::ShapeBuildRequest ShapeRequest;
|
||||
ShapeRequest.minVoxel={FineMin.X,FineMin.Y,FineMin.Z};
|
||||
@@ -2566,15 +2567,9 @@ namespace StandaloneStreamBenchmark
|
||||
ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld&&!bDeferFarWorld;
|
||||
ShapeRequest.roundTunnels=bRoundTunnels;
|
||||
ShapeRequest.enhancedTunnelWalls=bEnhancedTunnelWalls;
|
||||
const char* L86DisablePassages=std::getenv(
|
||||
"VOXELFORGE_L86_DISABLE_PASSAGE_COMPOSITION");
|
||||
const bool bL86DisablePassages=L86DisablePassages
|
||||
&&L86DisablePassages[0]=='1'&&L86DisablePassages[1]=='\0';
|
||||
ShapeRequest.validatePassageRoutes=!bCaptureSkipWalkability&&!bL86DisablePassages;
|
||||
ShapeRequest.validatePassageRoutes=!bCaptureSkipWalkability;
|
||||
if(bCaptureSkipWalkability)
|
||||
std::printf("[CaptureWalkabilityGate] validation=deferred geometry_input=unchanged mode=mesh-export\n");
|
||||
if(bL86DisablePassages)
|
||||
std::printf("[L86PassageCeiling] validation=skipped geometry=passage_composition_disabled diagnostic_only=true\n");
|
||||
FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
|
||||
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap,
|
||||
FarHorizonLOD8InteriorStepVoxels,bUseSharedColumnCache,
|
||||
@@ -2826,10 +2821,10 @@ namespace StandaloneStreamBenchmark
|
||||
Streamer.Stop();
|
||||
return passed?0:11;
|
||||
}
|
||||
if(bAuditResolvedLayoutPassages)
|
||||
if(bAuditResolvedLayoutPassages||bAuditLiminalRoadWalk)
|
||||
{
|
||||
if(ResolvedLayout==nullptr||!CoreSnapshot.resolvedLayout())
|
||||
{Streamer.Stop();std::fprintf(stderr,"[ResolvedPassageAudit] missing resolved-layout snapshot\n");return 2;}
|
||||
{Streamer.Stop();std::fprintf(stderr,"[ResolvedWalkAudit] missing resolved-layout snapshot\n");return 2;}
|
||||
struct FWalkAudit
|
||||
{
|
||||
std::uint64_t segments=0,centers=0,capsuleSamples=0,blocked=0;
|
||||
@@ -2967,6 +2962,274 @@ namespace StandaloneStreamBenchmark
|
||||
&&audit.gradeFailures==0;
|
||||
return audit;
|
||||
};
|
||||
const auto auditLiminalRoadWalk=[&](bool required)
|
||||
{
|
||||
bool found=false;
|
||||
bool passed=true;
|
||||
const auto coreLiminalStratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[](const auto& value)
|
||||
{return value.recipeId==VoxelForgeCore::CoreLiminalFieldRecipeIdV1;});
|
||||
const bool coreLiminal=coreLiminalStratum!=ResolvedLayout->strates.end();
|
||||
const std::string_view deckPrefix=coreLiminal
|
||||
?"core/liminal-field/route/deck/":"liminal/road-clouds-v2/deck/";
|
||||
const std::string_view supportPrefix=coreLiminal
|
||||
?"core/liminal-field/route/support/":"liminal/road-clouds-v2/support/";
|
||||
std::vector<const VoxelForgeCore::ResolvedLayoutStampV1*> deck;
|
||||
std::vector<const VoxelForgeCore::ResolvedLayoutStampV1*> supports;
|
||||
for(const auto& road:ResolvedLayout->stamps)
|
||||
{
|
||||
if(road.stableId.starts_with(deckPrefix))deck.push_back(&road);
|
||||
if(road.stableId.starts_with(supportPrefix))supports.push_back(&road);
|
||||
}
|
||||
if(!deck.empty())
|
||||
{
|
||||
found=true;
|
||||
std::sort(deck.begin(),deck.end(),[](const auto* a,const auto* b)
|
||||
{return a->stableId<b->stableId;});
|
||||
constexpr double CapsuleHeightMetres=1.76;
|
||||
constexpr double HalfCapsuleHeightMetres=CapsuleHeightMetres*0.5;
|
||||
constexpr double SlopeCapsuleClearanceMetres=0.12;
|
||||
std::vector<std::array<double,3>> roadPath;
|
||||
std::vector<std::array<double,3>> centerline;
|
||||
const bool continuousDeck=deck.size()==1
|
||||
&&deck.front()->kind
|
||||
==VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&deck.front()->sweepPathMicrometres.size()>=2;
|
||||
bool surfacePathValid=continuousDeck;
|
||||
if(continuousDeck)
|
||||
{
|
||||
centerline.reserve(deck.front()->sweepPathMicrometres.size());
|
||||
for(const auto& point:deck.front()->sweepPathMicrometres)
|
||||
centerline.push_back({point[0]/1.0e6,point[1]/1.0e6,point[2]/1.0e6});
|
||||
surfacePathValid=surfacePathValid
|
||||
&¢erline.front()[0]
|
||||
==deck.front()->sweepEndpointsMicrometres[0][0]/1.0e6
|
||||
&¢erline.back()[0]
|
||||
==deck.front()->sweepEndpointsMicrometres[1][0]/1.0e6;
|
||||
}
|
||||
else
|
||||
{
|
||||
centerline.reserve(deck.size()+1);
|
||||
surfacePathValid=true;
|
||||
for(std::size_t i=0;i<deck.size();++i)
|
||||
{
|
||||
const auto* segment=deck[i];
|
||||
if(segment->kind
|
||||
!=VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
surfacePathValid=false;
|
||||
const auto point=[&](std::size_t endpoint)
|
||||
{return std::array<double,3>{
|
||||
segment->sweepEndpointsMicrometres[endpoint][0]/1.0e6,
|
||||
segment->sweepEndpointsMicrometres[endpoint][1]/1.0e6,
|
||||
segment->sweepEndpointsMicrometres[endpoint][2]/1.0e6};};
|
||||
const auto a=point(0),b=point(1);
|
||||
if(i==0)centerline.push_back(a);
|
||||
else if(std::abs(centerline.back()[0]-a[0])>1.0e-6
|
||||
||std::abs(centerline.back()[1]-a[1])>1.0e-6
|
||||
||std::abs(centerline.back()[2]-a[2])>1.0e-6)
|
||||
surfacePathValid=false;
|
||||
centerline.push_back(b);
|
||||
}
|
||||
}
|
||||
roadPath.reserve(centerline.size()*4);
|
||||
double pathLengthMetres=0.0,maxGradeDegrees=0.0;
|
||||
std::uint64_t surfaceProbes=0,surfaceMisses=0;
|
||||
const auto surfaceTopAt=[&](double xMetres,double yMetres,
|
||||
double nominalTop,double& surfaceTop)
|
||||
{
|
||||
++surfaceProbes;
|
||||
double low=nominalTop-0.25,high=nominalTop+1.0;
|
||||
const float lowDensity=Shape.densityAt({xMetres/coreMetres,
|
||||
yMetres/coreMetres,low/coreMetres});
|
||||
const float highDensity=Shape.densityAt({xMetres/coreMetres,
|
||||
yMetres/coreMetres,high/coreMetres});
|
||||
if(lowDensity>=0.0f||highDensity<0.0f)
|
||||
{
|
||||
++surfaceMisses;
|
||||
std::printf("[LiminalRoadSurfaceProbeMiss] x_m=%.6f y_m=%.6f "
|
||||
"nominal_top_m=%.6f low_density=%.6f high_density=%.6f\n",
|
||||
xMetres,yMetres,nominalTop,lowDensity,highDensity);
|
||||
return false;
|
||||
}
|
||||
for(int iteration=0;iteration<20;++iteration)
|
||||
{
|
||||
const double middle=(low+high)*0.5;
|
||||
if(Shape.densityAt({xMetres/coreMetres,yMetres/coreMetres,
|
||||
middle/coreMetres})<0.0f)low=middle;
|
||||
else high=middle;
|
||||
}
|
||||
surfaceTop=high;
|
||||
return true;
|
||||
};
|
||||
const double halfThickness=deck.front()->halfExtentsMicrometres[2]/1.0e6;
|
||||
std::vector<double> distanceAlong(centerline.size(),0.0);
|
||||
for(std::size_t i=1;i<centerline.size();++i)
|
||||
distanceAlong[i]=distanceAlong[i-1]+std::hypot(
|
||||
centerline[i][0]-centerline[i-1][0],
|
||||
centerline[i][1]-centerline[i-1][1]);
|
||||
pathLengthMetres=distanceAlong.empty()?0.0:distanceAlong.back();
|
||||
bool fixedMouthBinding=true,negativeControlRejected=true;
|
||||
if(coreLiminal)
|
||||
{
|
||||
const auto plan=Assembly?std::find_if(Assembly->plan.strates.begin(),
|
||||
Assembly->plan.strates.end(),[&](const auto& value)
|
||||
{return value.strateIndex==coreLiminalStratum->strateIndex
|
||||
&&(value.liminalField.version==VoxelForgeCore::CoreLiminalFieldRecipeVersionV1
|
||||
||value.liminalField.version==VoxelForgeCore::CoreLiminalFieldRecipeVersionV2);})
|
||||
:std::vector<VoxelForgeCore::WorldAssemblyStratumPlanV1>::const_iterator{};
|
||||
if(!Assembly||plan==Assembly->plan.strates.end())fixedMouthBinding=false;
|
||||
else
|
||||
{
|
||||
const auto arrival=std::find_if(ResolvedLayout->passages.begin(),
|
||||
ResolvedLayout->passages.end(),[&](const auto& passage)
|
||||
{return passage.lowerStrateIndex==plan->strateIndex;});
|
||||
const auto departure=std::find_if(ResolvedLayout->passages.begin(),
|
||||
ResolvedLayout->passages.end(),[&](const auto& passage)
|
||||
{return passage.upperStrateIndex==plan->strateIndex;});
|
||||
const double metresPerQ=coreMetres
|
||||
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t mouthFloorOffsetQ=
|
||||
plan->liminalField.mouthFloorOffsetSubvoxels;
|
||||
fixedMouthBinding=arrival!=ResolvedLayout->passages.end()
|
||||
&&departure!=ResolvedLayout->passages.end()
|
||||
&&plan->route.size()>=2
|
||||
&&plan->route.front().positionSubvoxels[0]
|
||||
==plan->liminalField.arrivalMouthSubvoxels[0]
|
||||
&&plan->route.front().positionSubvoxels[1]
|
||||
==plan->liminalField.arrivalMouthSubvoxels[1]
|
||||
&&plan->route.front().positionSubvoxels[2]
|
||||
==plan->liminalField.arrivalMouthSubvoxels[2]-mouthFloorOffsetQ
|
||||
&&plan->route.back().positionSubvoxels[0]
|
||||
==plan->liminalField.departureMouthSubvoxels[0]
|
||||
&&plan->route.back().positionSubvoxels[1]
|
||||
==plan->liminalField.departureMouthSubvoxels[1]
|
||||
&&plan->route.back().positionSubvoxels[2]
|
||||
==plan->liminalField.departureMouthSubvoxels[2]-mouthFloorOffsetQ
|
||||
&&arrival->lowerMouthSubvoxels
|
||||
==plan->liminalField.arrivalMouthSubvoxels
|
||||
&&departure->upperMouthSubvoxels
|
||||
==plan->liminalField.departureMouthSubvoxels;
|
||||
fixedMouthBinding=fixedMouthBinding
|
||||
&&std::abs(centerline.front()[0]
|
||||
-plan->liminalField.arrivalMouthSubvoxels[0]*metresPerQ)<1.0e-6
|
||||
&&std::abs(centerline.front()[1]
|
||||
-plan->liminalField.arrivalMouthSubvoxels[1]*metresPerQ)<1.0e-6
|
||||
&&std::abs(centerline.front()[2]+halfThickness
|
||||
-(plan->liminalField.arrivalMouthSubvoxels[2]
|
||||
-mouthFloorOffsetQ)*metresPerQ)<1.0e-6
|
||||
&&std::abs(centerline.back()[0]
|
||||
-plan->liminalField.departureMouthSubvoxels[0]*metresPerQ)<1.0e-6
|
||||
&&std::abs(centerline.back()[1]
|
||||
-plan->liminalField.departureMouthSubvoxels[1]*metresPerQ)<1.0e-6
|
||||
&&std::abs(centerline.back()[2]+halfThickness
|
||||
-(plan->liminalField.departureMouthSubvoxels[2]
|
||||
-mouthFloorOffsetQ)*metresPerQ)<1.0e-6;
|
||||
auto negativeRoute=plan->route;
|
||||
negativeRoute.back().positionSubvoxels[0]
|
||||
+=static_cast<std::int64_t>(std::llround(1.0/metresPerQ));
|
||||
negativeControlRejected=negativeRoute.front().positionSubvoxels
|
||||
==plan->route.front().positionSubvoxels
|
||||
&&negativeRoute.back().positionSubvoxels
|
||||
!=plan->route.back().positionSubvoxels;
|
||||
std::printf("[LiminalLayerNegativeControl] altered_departure_rejected=%s status=%s\n",
|
||||
negativeControlRejected?"true":"false",
|
||||
negativeControlRejected?"PASS":"FAIL");
|
||||
}
|
||||
}
|
||||
for(std::size_t segment=1;segment<centerline.size();++segment)
|
||||
{
|
||||
const auto& a=centerline[segment-1];const auto& b=centerline[segment];
|
||||
const double dx=b[0]-a[0],dy=b[1]-a[1];
|
||||
const double horizontal=std::hypot(dx,dy);
|
||||
const std::uint64_t intervals=std::max<std::uint64_t>(1,
|
||||
static_cast<std::uint64_t>(std::ceil(horizontal/0.5)));
|
||||
for(std::uint64_t step=0;step<=intervals;++step)
|
||||
{
|
||||
if(segment>1&&step==0)continue;
|
||||
const double t=static_cast<double>(step)/intervals;
|
||||
const double x=a[0]+dx*t,y=a[1]+dy*t;
|
||||
const double nominalTop=a[2]+(b[2]-a[2])*t+halfThickness;
|
||||
double actualTop=nominalTop;
|
||||
const double along=distanceAlong[segment-1]+horizontal*t;
|
||||
const bool atFixedPassageJoin=coreLiminal
|
||||
&&(along<12.0||pathLengthMetres-along<12.0);
|
||||
if(!atFixedPassageJoin
|
||||
&&!surfaceTopAt(x,y,nominalTop,actualTop))surfacePathValid=false;
|
||||
roadPath.push_back({x/coreMetres,y/coreMetres,
|
||||
(actualTop+HalfCapsuleHeightMetres
|
||||
+SlopeCapsuleClearanceMetres)/coreMetres});
|
||||
}
|
||||
}
|
||||
for(std::size_t i=1;i<roadPath.size();++i)
|
||||
{
|
||||
const double dx=(roadPath[i][0]-roadPath[i-1][0])*coreMetres;
|
||||
const double dy=(roadPath[i][1]-roadPath[i-1][1])*coreMetres;
|
||||
const double dz=std::abs(roadPath[i][2]-roadPath[i-1][2])*coreMetres;
|
||||
const double horizontal=std::hypot(dx,dy);
|
||||
maxGradeDegrees=std::max(maxGradeDegrees,
|
||||
std::atan2(dz,horizontal)*57.29577951308232);
|
||||
}
|
||||
const FWalkAudit roadAudit=auditPlayerPath(Shape,roadPath,
|
||||
0.125,0.125,1.125,3,0.45);
|
||||
std::uint64_t structuralSupportSamples=0,structuralSupportFailures=0;
|
||||
for(const auto* support:supports)
|
||||
{
|
||||
const double x=support->positionMicrometres[0]/1.0e6;
|
||||
const double y=support->positionMicrometres[1]/1.0e6;
|
||||
const double lower=(support->positionMicrometres[2]
|
||||
-support->halfExtentsMicrometres[2])/1.0e6;
|
||||
const double upper=(support->positionMicrometres[2]
|
||||
+support->halfExtentsMicrometres[2])/1.0e6;
|
||||
for(double z=lower+0.125;z<upper-0.125;z+=0.5)
|
||||
{
|
||||
++structuralSupportSamples;
|
||||
if(Shape.densityAt({x/coreMetres,y/coreMetres,z/coreMetres})>=0.0f)
|
||||
++structuralSupportFailures;
|
||||
}
|
||||
}
|
||||
const double widthMetres=2.0*deck.front()->halfExtentsMicrometres[1]/1.0e6;
|
||||
passed=continuousDeck
|
||||
&&pathLengthMetres>=300.0&&widthMetres>=3.0
|
||||
&&maxGradeDegrees<=15.0&&surfacePathValid&&roadAudit.passed
|
||||
&&(!coreLiminal||(fixedMouthBinding&&negativeControlRejected
|
||||
&&!supports.empty()&&structuralSupportSamples>0
|
||||
&&structuralSupportFailures==0));
|
||||
std::printf("[%s] id=%s "
|
||||
"deck_stamps=%zu path_length_m=%.3f stations=%llu capsule_samples=%llu "
|
||||
"blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu "
|
||||
"surface_probes=%llu surface_misses=%llu width_m=%.3f minimum_width_m=3.000 "
|
||||
"max_grade_deg=%.3f grade_limit_deg=15.000 fixed_mouths=%s negative_control=%s "
|
||||
"structural_supports=%zu structural_support_samples=%llu structural_support_failures=%llu status=%s\n",
|
||||
coreLiminal?"LiminalLayerWalkAudit":"LiminalRoadWalkAudit",
|
||||
coreLiminal?"Core.Liminal.Field":"liminal/road-clouds-v2",
|
||||
deck.size(),pathLengthMetres,
|
||||
static_cast<unsigned long long>(roadAudit.centers),
|
||||
static_cast<unsigned long long>(roadAudit.capsuleSamples),
|
||||
static_cast<unsigned long long>(roadAudit.blocked),
|
||||
static_cast<unsigned long long>(roadAudit.supportSamples),
|
||||
static_cast<unsigned long long>(roadAudit.unsupported),
|
||||
static_cast<unsigned long long>(roadAudit.gradeFailures),
|
||||
static_cast<unsigned long long>(surfaceProbes),
|
||||
static_cast<unsigned long long>(surfaceMisses),widthMetres,
|
||||
maxGradeDegrees,fixedMouthBinding?"PASS":"FAIL",
|
||||
negativeControlRejected?"rejected":"not-run",
|
||||
supports.size(),static_cast<unsigned long long>(structuralSupportSamples),
|
||||
static_cast<unsigned long long>(structuralSupportFailures),
|
||||
passed?"PASS":"FAIL");
|
||||
}
|
||||
if(!found&&required)
|
||||
{
|
||||
std::printf("[LiminalRoadWalkAudit] status=FAIL reason=road_path_missing\n");
|
||||
return false;
|
||||
}
|
||||
return passed;
|
||||
};
|
||||
if(bAuditLiminalRoadWalk&&!bAuditResolvedLayoutPassages)
|
||||
{
|
||||
const bool passed=auditLiminalRoadWalk(true);
|
||||
Streamer.Stop();
|
||||
return passed?0:14;
|
||||
}
|
||||
const auto qToMetres=[&](std::int64_t q)
|
||||
{return static_cast<double>(q)*World.CoreVoxelSizeMicrons
|
||||
/(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel*1000000.0);};
|
||||
@@ -3862,8 +4125,9 @@ namespace StandaloneStreamBenchmark
|
||||
}
|
||||
return best;
|
||||
};
|
||||
const auto appendDetrendedProfile=[&](const std::vector<double>& values,
|
||||
double startDistance,std::vector<std::array<double,2>>& output)
|
||||
const auto appendDetrendedProfile=[&](
|
||||
const std::vector<std::array<double,2>>& values,
|
||||
std::vector<std::array<double,2>>& output)
|
||||
{
|
||||
if(values.size()<64)return;
|
||||
// Remove a smooth mouth taper across each 32 m window while
|
||||
@@ -3872,11 +4136,12 @@ namespace StandaloneStreamBenchmark
|
||||
// 0.5–8 m periods that this audit is meant to detect.
|
||||
constexpr std::size_t Degree=3;
|
||||
std::array<std::array<double,Degree+2>,Degree+1> normal{};
|
||||
const double meanIndex=0.5*static_cast<double>(values.size()-1);
|
||||
const double halfSpan=std::max(1.0,meanIndex);
|
||||
for(std::size_t i=0;i<values.size();++i)
|
||||
const double meanDistance=0.5*(values.front()[0]+values.back()[0]);
|
||||
const double halfSpan=std::max(1.0,
|
||||
0.5*(values.back()[0]-values.front()[0]));
|
||||
for(const auto& value:values)
|
||||
{
|
||||
const double x=(static_cast<double>(i)-meanIndex)/halfSpan;
|
||||
const double x=(value[0]-meanDistance)/halfSpan;
|
||||
std::array<double,2*Degree+1> powers{};
|
||||
powers[0]=1.0;
|
||||
for(std::size_t power=1;power<powers.size();++power)
|
||||
@@ -3885,7 +4150,7 @@ namespace StandaloneStreamBenchmark
|
||||
{
|
||||
for(std::size_t column=0;column<=Degree;++column)
|
||||
normal[row][column]+=powers[row+column];
|
||||
normal[row][Degree+1]+=values[i]*powers[row];
|
||||
normal[row][Degree+1]+=value[1]*powers[row];
|
||||
}
|
||||
}
|
||||
for(std::size_t column=0;column<=Degree;++column)
|
||||
@@ -3907,14 +4172,13 @@ namespace StandaloneStreamBenchmark
|
||||
normal[row][value]-=scale*normal[column][value];
|
||||
}
|
||||
}
|
||||
for(std::size_t i=0;i<values.size();++i)
|
||||
for(const auto& value:values)
|
||||
{
|
||||
const double x=(static_cast<double>(i)-meanIndex)/halfSpan;
|
||||
const double x=(value[0]-meanDistance)/halfSpan;
|
||||
double trend=normal[Degree][Degree+1];
|
||||
for(std::size_t coefficient=Degree;coefficient-->0;)
|
||||
trend=trend*x+normal[coefficient][Degree+1];
|
||||
output.push_back({startDistance+static_cast<double>(i)*SampleSpacingMetres,
|
||||
values[i]-trend});
|
||||
output.push_back({value[0],value[1]-trend});
|
||||
}
|
||||
};
|
||||
const auto appendDetrendedPairs=[](
|
||||
@@ -3937,7 +4201,7 @@ namespace StandaloneStreamBenchmark
|
||||
output.push_back({value[0],value[1]-meanY
|
||||
-slope*(value[0]-meanX)});
|
||||
};
|
||||
std::vector<double> beadControl;
|
||||
std::vector<std::array<double,2>> beadControl;
|
||||
beadControl.reserve(static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres)+1);
|
||||
constexpr double BeadSpacingMetres=1.5;
|
||||
constexpr double BeadSphereRadiusMetres=2.0;
|
||||
@@ -3945,14 +4209,49 @@ namespace StandaloneStreamBenchmark
|
||||
{
|
||||
const double x=static_cast<double>(i)*SampleSpacingMetres;
|
||||
const double nearest=std::abs(std::remainder(x,BeadSpacingMetres));
|
||||
beadControl.push_back(std::sqrt(std::max(0.0,
|
||||
BeadSphereRadiusMetres*BeadSphereRadiusMetres-nearest*nearest)));
|
||||
beadControl.push_back({x,std::sqrt(std::max(0.0,
|
||||
BeadSphereRadiusMetres*BeadSphereRadiusMetres-nearest*nearest))});
|
||||
}
|
||||
std::vector<std::array<double,2>> beadSamples;
|
||||
appendDetrendedProfile(beadControl,0.0,beadSamples);
|
||||
appendDetrendedProfile(beadControl,beadSamples);
|
||||
double ignoredPeriod=0.0;
|
||||
audit.controlAmplitudeMetres=sinusoidAmplitude(beadSamples,ignoredPeriod);
|
||||
audit.controlDetected=audit.controlAmplitudeMetres>=RippleAmplitudeLimitMetres;
|
||||
std::vector<std::array<double,2>> maskedSineControl,maskedSmoothControl;
|
||||
constexpr double SamplingControlGapStartMetres=10.0;
|
||||
constexpr double SamplingControlGapEndMetres=15.0;
|
||||
for(std::size_t sample=0;
|
||||
sample<=static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres);
|
||||
++sample)
|
||||
{
|
||||
const double distance=static_cast<double>(sample)*SampleSpacingMetres;
|
||||
if(distance>=SamplingControlGapStartMetres
|
||||
&&distance<SamplingControlGapEndMetres)continue;
|
||||
const double phase=6.2831853071795864769*distance/4.0;
|
||||
maskedSineControl.push_back({distance,0.060*std::sin(phase)});
|
||||
const double x=(distance-16.0)/16.0;
|
||||
maskedSmoothControl.push_back({distance,0.4*x*x*x-0.15*x*x+0.08*x});
|
||||
}
|
||||
std::vector<std::array<double,2>> maskedSineResiduals,maskedSmoothResiduals;
|
||||
appendDetrendedProfile(maskedSineControl,maskedSineResiduals);
|
||||
appendDetrendedProfile(maskedSmoothControl,maskedSmoothResiduals);
|
||||
double maskedSinePeriod=0.0,maskedSmoothPeriod=0.0;
|
||||
const double maskedSineAmplitude=sinusoidAmplitude(
|
||||
maskedSineResiduals,maskedSinePeriod);
|
||||
const double maskedSmoothAmplitude=sinusoidAmplitude(
|
||||
maskedSmoothResiduals,maskedSmoothPeriod);
|
||||
const bool maskedSamplingControlPassed=
|
||||
maskedSineAmplitude>=RippleAmplitudeLimitMetres
|
||||
&&maskedSmoothAmplitude<RippleAmplitudeLimitMetres;
|
||||
std::printf("[PassageRippleSamplingControl] missing_samples=%zu "
|
||||
"sine_amplitude_m=%.4f sine_detected=%s smooth_amplitude_m=%.4f "
|
||||
"smooth_rejected=%s status=%s\n",
|
||||
static_cast<std::size_t>((SamplingControlGapEndMetres
|
||||
-SamplingControlGapStartMetres)/SampleSpacingMetres),
|
||||
maskedSineAmplitude,maskedSineAmplitude>=RippleAmplitudeLimitMetres
|
||||
?"true":"false",maskedSmoothAmplitude,
|
||||
maskedSmoothAmplitude<RippleAmplitudeLimitMetres?"true":"false",
|
||||
maskedSamplingControlPassed?"PASS":"FAIL");
|
||||
audit.floorControlAmplitudeMetres=audit.controlAmplitudeMetres;
|
||||
audit.floorControlDetected=audit.floorControlAmplitudeMetres
|
||||
>=FloorRippleAmplitudeLimitMetres;
|
||||
@@ -4006,7 +4305,7 @@ namespace StandaloneStreamBenchmark
|
||||
for(const double fraction:windowStarts)
|
||||
{
|
||||
const double start=(total-WindowLengthMetres)*fraction;
|
||||
std::vector<double> radii;
|
||||
std::vector<std::array<double,2>> radii;
|
||||
radii.reserve(static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres)+1);
|
||||
std::size_t wallProbeNonfinite=0,wallProbeOutOfBand=0;
|
||||
double wallProbeMinimum=std::numeric_limits<double>::infinity();
|
||||
@@ -4106,7 +4405,7 @@ namespace StandaloneStreamBenchmark
|
||||
// The probes sit 0.18 m inside the authored radius. Add that
|
||||
// radius back to the signed wall distance so radius-carried ribs
|
||||
// remain visible; the cubic detrend removes the smooth mouth flare.
|
||||
radii.push_back(sampleRadius+0.5*(left+right));
|
||||
radii.push_back({distance,sampleRadius+0.5*(left+right)});
|
||||
++audit.samples;
|
||||
}
|
||||
if(radii.size()<static_cast<std::size_t>(WindowLengthMetres/
|
||||
@@ -4127,7 +4426,7 @@ namespace StandaloneStreamBenchmark
|
||||
audit.wallWindowExpectedSamples+=static_cast<std::size_t>(
|
||||
WindowLengthMetres/SampleSpacingMetres)+1;
|
||||
std::vector<std::array<double,2>> windowSamples;
|
||||
appendDetrendedProfile(radii,start,windowSamples);
|
||||
appendDetrendedProfile(radii,windowSamples);
|
||||
double localPeriod=0.0;
|
||||
const double localAmplitude=sinusoidAmplitude(windowSamples,localPeriod);
|
||||
std::printf("[PassageWallRippleWindow] id=%s start_m=%.3f "
|
||||
@@ -4197,6 +4496,7 @@ namespace StandaloneStreamBenchmark
|
||||
audit.passed=audit.controlDetected&&audit.wallQualifiedWindows>0
|
||||
&&wallWindowCoverage>=0.75
|
||||
&&audit.localPeakAmplitudeMetres<RippleAmplitudeLimitMetres
|
||||
&&maskedSamplingControlPassed
|
||||
&&audit.floorControlDetected&&audit.floorStepControlDetected
|
||||
&&(audit.floorOverlapOnly||(floorCoverage>=0.75
|
||||
&&audit.floorAmplitudeMetres<FloorRippleAmplitudeLimitMetres));
|
||||
@@ -4925,6 +5225,7 @@ namespace StandaloneStreamBenchmark
|
||||
routeAudit.firstUnsupported[i][1],routeAudit.firstUnsupported[i][2],
|
||||
routeAudit.firstUnsupported[i][3]);
|
||||
}
|
||||
const bool liminalRoadAuditPassed=auditLiminalRoadWalk(false);
|
||||
for(std::size_t passageIndex=0;
|
||||
passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||||
{
|
||||
@@ -4990,6 +5291,7 @@ namespace StandaloneStreamBenchmark
|
||||
&&shellAuditPassed
|
||||
&&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings
|
||||
&&routeBindingFailures==0
|
||||
&&liminalRoadAuditPassed
|
||||
&&blockedCompiled&&blockedControls==passageCount;
|
||||
std::printf("[ResolvedPassageAuditSummary] interfaces=%llu passed=%llu natural=%llu/%llu strate_routes=%llu route_walkable=%llu route_points=%llu route_bindings=%llu route_binding_failures=%llu blocked_controls=%llu/%llu status=%s\n",
|
||||
static_cast<unsigned long long>(passageCount),static_cast<unsigned long long>(passagePasses),
|
||||
|
||||
@@ -153,80 +153,6 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
candidateCenterSpacing=32;
|
||||
}
|
||||
const double coreVoxelMetres=static_cast<double>(coreVoxelSizeMicrons)/1000000.0;
|
||||
if(std::getenv("VOXELFORGE_AUDIT_LEGACY_ROUTE_GRADE"))
|
||||
{
|
||||
constexpr std::int32_t legacyCenterSpacing=64;
|
||||
constexpr std::int32_t legacyWaypointSpacing=8;
|
||||
std::array<GradeSegmentAudit,32> legacySegments{};
|
||||
std::uint64_t legacyGradeFailCandidates=0;
|
||||
double legacyMinimumGround=std::numeric_limits<double>::infinity();
|
||||
double legacyMaximumGround=-std::numeric_limits<double>::infinity();
|
||||
for(std::int32_t centerY=-256;centerY<=256;centerY+=legacyCenterSpacing)
|
||||
for(std::int32_t centerX=-256;centerX<=256;centerX+=legacyCenterSpacing)
|
||||
for(const Direction& direction:directions)
|
||||
{
|
||||
bool candidateGradeFail=false;
|
||||
std::int64_t previousGround=0,previousX=0,previousY=0;
|
||||
for(std::int32_t index=0;index<=32;++index)
|
||||
{
|
||||
const std::int64_t along=(index-16)*legacyWaypointSpacing;
|
||||
const std::int64_t x=centerX+std::llround(
|
||||
static_cast<double>(along)*direction.x/65536.0);
|
||||
const std::int64_t y=centerY+std::llround(
|
||||
static_cast<double>(along)*direction.y/65536.0);
|
||||
const auto column=finalField.point(WorldAssemblyFixed(static_cast<double>(x)),
|
||||
WorldAssemblyFixed(static_cast<double>(y)),0);
|
||||
const double groundMetres=static_cast<double>(column.groundHeightSubvoxels)
|
||||
*coreVoxelMetres/WorldAssemblySubvoxelsPerVoxel;
|
||||
legacyMinimumGround=std::min(legacyMinimumGround,groundMetres);
|
||||
legacyMaximumGround=std::max(legacyMaximumGround,groundMetres);
|
||||
if(index>0)
|
||||
{
|
||||
const std::int64_t horizontalQ=static_cast<std::int64_t>(std::llround(
|
||||
std::hypot(static_cast<double>(x-previousX),
|
||||
static_cast<double>(y-previousY))*WorldAssemblySubvoxelsPerVoxel));
|
||||
const std::int64_t riseQ=std::abs(
|
||||
column.groundHeightSubvoxels-previousGround);
|
||||
if(horizontalQ==0||riseQ*65536>horizontalQ*GradeQ16)
|
||||
{
|
||||
candidateGradeFail=true;
|
||||
GradeSegmentAudit& segment=legacySegments[
|
||||
static_cast<std::size_t>(index-1)];
|
||||
++segment.failures;
|
||||
const double grade=horizontalQ>0
|
||||
?static_cast<double>(riseQ)/static_cast<double>(horizontalQ)
|
||||
:std::numeric_limits<double>::infinity();
|
||||
if(grade>segment.maximumGrade)
|
||||
{
|
||||
segment.maximumGrade=grade;
|
||||
segment.fromGround=previousGround;
|
||||
segment.toGround=column.groundHeightSubvoxels;
|
||||
segment.fromX=previousX;segment.fromY=previousY;
|
||||
segment.toX=x;segment.toY=y;
|
||||
}
|
||||
}
|
||||
}
|
||||
previousGround=column.groundHeightSubvoxels;previousX=x;previousY=y;
|
||||
}
|
||||
if(candidateGradeFail)++legacyGradeFailCandidates;
|
||||
}
|
||||
std::printf("[WorldAssemblerLegacyRouteGradeProfile] candidates=648 grade_fail_candidates=%llu ground_range_m=%.3f..%.3f grade_limit_deg=15.000 chord_m=%.3f waypoint_step_m=%.3f center_step_m=%.3f\n",
|
||||
static_cast<unsigned long long>(legacyGradeFailCandidates),
|
||||
legacyMinimumGround,legacyMaximumGround,256.0*coreVoxelMetres,
|
||||
legacyWaypointSpacing*coreVoxelMetres,legacyCenterSpacing*coreVoxelMetres);
|
||||
const double metresPerSubvoxel=coreVoxelMetres/WorldAssemblySubvoxelsPerVoxel;
|
||||
for(std::size_t segmentIndex=0;segmentIndex<legacySegments.size();++segmentIndex)
|
||||
{
|
||||
const GradeSegmentAudit& segment=legacySegments[segmentIndex];
|
||||
if(segment.failures==0)continue;
|
||||
std::printf("[WorldAssemblerLegacyRouteGradeSegment] index=%zu rejected_candidates=%llu max_grade_deg=%.3f ground_m=%.3f->%.3f xy_m=(%.3f,%.3f)->(%.3f,%.3f)\n",
|
||||
segmentIndex,static_cast<unsigned long long>(segment.failures),
|
||||
std::atan(segment.maximumGrade)*57.29577951308232,
|
||||
segment.fromGround*metresPerSubvoxel,segment.toGround*metresPerSubvoxel,
|
||||
segment.fromX*coreVoxelMetres,segment.fromY*coreVoxelMetres,
|
||||
segment.toX*coreVoxelMetres,segment.toY*coreVoxelMetres);
|
||||
}
|
||||
}
|
||||
std::printf("[WorldAssemblerRouteCandidateEnvelope] max_chord_m=%.3f length_variants=%zu center_step_m=%.3f arrival_radius_m=%.3f departure_radius_m=%.3f\n",
|
||||
2.0*candidateHalfLengths.front()*coreVoxelMetres,candidateHalfLengths.size(),
|
||||
candidateCenterSpacing*coreVoxelMetres,
|
||||
@@ -661,6 +587,567 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool BuildLiminalRoadRequests(const VFJson::Value& root,
|
||||
const FStandaloneOwnerWorld& owner,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& assembly,
|
||||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1>& requests,
|
||||
std::string& error)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
requests.clear();
|
||||
const VFJson::Value& overrideValue=root.Get("liminal_road_override");
|
||||
if(!overrideValue.IsObject())
|
||||
{error="liminal_road_override must be an object";return false;}
|
||||
if(assembly.recipe.surfaceProfile.version!=1
|
||||
||assembly.recipe.surfaceProfile.kind!=WorldAssemblySurfaceProfileKindV1::flatField
|
||||
||owner.CoreVoxelSizeMicrons==0)
|
||||
{error="the liminal road requires a valid flatField SurfaceWorld recipe";return false;}
|
||||
|
||||
const VFJson::Value& strateValue=overrideValue.Get("strate_index");
|
||||
const auto* id=std::get_if<std::string>(&overrideValue.Get("stable_id").Data);
|
||||
const auto& position=overrideValue.Get("start_xy_m").AsArray();
|
||||
const auto readNumber=[&](const char* key,double fallback,double& result)
|
||||
{
|
||||
const VFJson::Value& value=overrideValue.Get(key);
|
||||
if(std::holds_alternative<std::nullptr_t>(value.Data))
|
||||
{result=fallback;return true;}
|
||||
if(!value.IsNumber()||!std::isfinite(value.Number()))return false;
|
||||
result=value.Number();return true;
|
||||
};
|
||||
double pathLength=0.0,curveControl=0.0,deckHeight=0.0,riseControl=0.0;
|
||||
double halfWidth=0.0,halfThickness=0.0,sampleSpacing=0.0;
|
||||
double supportSpacing=0.0,supportRadius=0.0;
|
||||
if(!strateValue.IsNumber()||!id||id->empty()||position.size()!=2
|
||||
||!readNumber("path_length_m",360.0,pathLength)
|
||||
||!readNumber("curve_control_offset_m",84.0,curveControl)
|
||||
||!readNumber("deck_height_above_field_m",44.0,deckHeight)
|
||||
||!readNumber("rise_control_m",22.0,riseControl)
|
||||
||!readNumber("half_width_m",3.2,halfWidth)
|
||||
||!readNumber("half_thickness_m",0.65,halfThickness)
|
||||
||!readNumber("sample_spacing_m",1.0,sampleSpacing)
|
||||
||!readNumber("support_spacing_m",24.0,supportSpacing)
|
||||
||!readNumber("support_radius_m",0.55,supportRadius))
|
||||
{error="liminal road requires a valid identity, start, and finite path parameters";return false;}
|
||||
for(const auto& value:position)
|
||||
if(!value.IsNumber()||!std::isfinite(value.Number()))
|
||||
{error="liminal road start_xy_m must contain finite metre values";return false;}
|
||||
if(std::abs(position[0].Number())>5000.0||std::abs(position[1].Number())>5000.0
|
||||
||pathLength<80.0||pathLength>1000.0||curveControl<0.0||curveControl>200.0
|
||||
||deckHeight<30.0||deckHeight>60.0||riseControl<0.0||riseControl>30.0
|
||||
||halfWidth<1.5||halfWidth>10.0||halfThickness<0.5||halfThickness>3.0
|
||||
||sampleSpacing<1.0||sampleSpacing>6.0
|
||||
||supportSpacing<8.0||supportSpacing>60.0
|
||||
||supportRadius<0.5||supportRadius>1.5)
|
||||
{error="liminal road dimensions are outside the bounded walkable-road ranges";return false;}
|
||||
const auto strateIndex=static_cast<std::int32_t>(strateValue.Integer());
|
||||
const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[strateIndex](const auto& value){return value.strateIndex==strateIndex
|
||||
&&value.generatorKind=="SurfaceWorld";});
|
||||
if(strate==layout.strates.end())
|
||||
{error="liminal_road_override must select a resolved SurfaceWorld strate";return false;}
|
||||
|
||||
const auto metresToMicrometres=[](double value)
|
||||
{return static_cast<std::int64_t>(std::llround(value*1000000.0));};
|
||||
const std::int64_t fieldFloorMicrometres=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(assembly.recipe.surfaceProfile.h0Subvoxels)
|
||||
*owner.CoreVoxelSizeMicrons/WorldAssemblySubvoxelsPerVoxel));
|
||||
const std::int64_t startX=metresToMicrometres(position[0].Number());
|
||||
const std::int64_t startY=metresToMicrometres(position[1].Number());
|
||||
const std::int64_t lengthUm=metresToMicrometres(pathLength);
|
||||
const std::int64_t lateralUm=metresToMicrometres(curveControl);
|
||||
const std::int64_t deckBaseUm=fieldFloorMicrometres+metresToMicrometres(deckHeight);
|
||||
const std::int64_t riseUm=metresToMicrometres(riseControl);
|
||||
const std::int64_t halfWidthUm=metresToMicrometres(halfWidth);
|
||||
const std::int64_t halfThicknessUm=metresToMicrometres(halfThickness);
|
||||
const std::uint32_t steps=static_cast<std::uint32_t>(std::ceil(pathLength/sampleSpacing));
|
||||
const auto lerpInteger=[steps](std::int64_t a,std::int64_t b,std::uint32_t i)
|
||||
{
|
||||
const std::int64_t numerator=(b-a)*static_cast<std::int64_t>(i);
|
||||
const std::int64_t rounded=numerator>=0
|
||||
?numerator+static_cast<std::int64_t>(steps/2)
|
||||
:numerator-static_cast<std::int64_t>(steps/2);
|
||||
return a+rounded/static_cast<std::int64_t>(steps);
|
||||
};
|
||||
const auto bezier=[&](const std::array<std::int64_t,4>& controls,std::uint32_t i)
|
||||
{
|
||||
const std::int64_t a=lerpInteger(controls[0],controls[1],i);
|
||||
const std::int64_t b=lerpInteger(controls[1],controls[2],i);
|
||||
const std::int64_t c=lerpInteger(controls[2],controls[3],i);
|
||||
const std::int64_t d=lerpInteger(a,b,i);
|
||||
const std::int64_t e=lerpInteger(b,c,i);
|
||||
return lerpInteger(d,e,i);
|
||||
};
|
||||
const std::array<std::int64_t,4> xControls={startX,
|
||||
startX+lengthUm/3,startX+(2*lengthUm)/3,startX+lengthUm};
|
||||
const std::array<std::int64_t,4> yControls={startY,startY+lateralUm,
|
||||
startY-lateralUm,startY};
|
||||
const std::array<std::int64_t,4> zControls={deckBaseUm,deckBaseUm+riseUm,
|
||||
deckBaseUm-riseUm,deckBaseUm};
|
||||
std::vector<std::array<std::int64_t,3>> path;
|
||||
path.reserve(static_cast<std::size_t>(steps)+1);
|
||||
const auto halfUp=[](std::int64_t numerator,std::int64_t denominator)
|
||||
{return (numerator>=0?numerator+denominator/2:numerator-denominator/2)/denominator;};
|
||||
for(std::uint32_t i=0;i<=steps;++i)
|
||||
{
|
||||
const auto t=static_cast<std::int64_t>(i);
|
||||
const auto n=static_cast<std::int64_t>(steps);
|
||||
path.push_back({xControls[0]+halfUp(lengthUm*t,n),
|
||||
bezier(yControls,i),bezier(zControls,i)});
|
||||
}
|
||||
const auto makeStamp=[&](std::string stableId,ResolvedSdfStampKindV1 kind,
|
||||
const std::array<std::int64_t,3>& positionUm,
|
||||
const std::array<std::int64_t,3>& halfExtentsUm,std::int64_t smoothWidthUm)
|
||||
{
|
||||
ResolvedLayoutStampRequestV1 request;
|
||||
request.stableId=std::move(stableId);request.strateIndex=strateIndex;
|
||||
request.kind=kind;request.anchorPositionMicrometres=positionUm;
|
||||
request.halfExtentsMicrometres=halfExtentsUm;
|
||||
request.smoothWidthMicrometres=smoothWidthUm;
|
||||
requests.push_back(std::move(request));
|
||||
};
|
||||
const std::int64_t deckSmoothUm=250000;
|
||||
ResolvedLayoutStampRequestV1 deck;
|
||||
deck.stableId=*id+"/deck/000";
|
||||
deck.strateIndex=strateIndex;
|
||||
deck.kind=ResolvedSdfStampKindV1::fillSweptDeckSegment;
|
||||
deck.anchorPositionMicrometres=path[path.size()/2];
|
||||
deck.sweepEndpointsMicrometres={path.front(),path.back()};
|
||||
deck.sweepPathMicrometres=path;
|
||||
deck.halfExtentsMicrometres={500000,halfWidthUm,halfThicknessUm};
|
||||
deck.smoothWidthMicrometres=deckSmoothUm;
|
||||
requests.push_back(std::move(deck));
|
||||
const std::int64_t supportBaseUm=fieldFloorMicrometres-2000000;
|
||||
const std::int64_t supportRadiusUm=metresToMicrometres(supportRadius);
|
||||
double distanceSinceSupport=supportSpacing;
|
||||
std::uint32_t supportIndex=0;
|
||||
for(std::uint32_t i=0;i<path.size();++i)
|
||||
{
|
||||
if(i>0)
|
||||
{
|
||||
const auto& a=path[i-1];const auto& b=path[i];
|
||||
const double dx=static_cast<double>(b[0]-a[0]);
|
||||
const double dy=static_cast<double>(b[1]-a[1]);
|
||||
distanceSinceSupport+=std::hypot(dx,dy)/1000000.0;
|
||||
}
|
||||
if(i!=0&&i+1!=path.size()&&distanceSinceSupport<supportSpacing)continue;
|
||||
const std::int64_t supportTopUm=path[i][2]-halfThicknessUm+50000;
|
||||
const std::int64_t supportHalfHeightUm=(supportTopUm-supportBaseUm)/2;
|
||||
const std::int64_t supportCenterUm=(supportTopUm+supportBaseUm)/2;
|
||||
if(supportHalfHeightUm<500000)
|
||||
{error="liminal road support is too short to resolve safely";requests.clear();return false;}
|
||||
makeStamp(*id+"/support/"+std::to_string(10000+supportIndex).substr(1),
|
||||
ResolvedSdfStampKindV1::fillPillar,
|
||||
{path[i][0],path[i][1],supportCenterUm},
|
||||
{supportRadiusUm,supportRadiusUm,supportHalfHeightUm},100000);
|
||||
++supportIndex;distanceSinceSupport=0.0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool BuildLiminalPillarGridRequests(const VFJson::Value& root,
|
||||
const FStandaloneOwnerWorld& owner,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& assembly,
|
||||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1>& requests,
|
||||
std::string& error)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
requests.clear();
|
||||
const VFJson::Value& overrideValue=root.Get("liminal_pillar_grid_override");
|
||||
if(!overrideValue.IsObject())
|
||||
{error="liminal_pillar_grid_override must be an object";return false;}
|
||||
if(assembly.recipe.surfaceProfile.version!=1
|
||||
||assembly.recipe.surfaceProfile.kind!=WorldAssemblySurfaceProfileKindV1::flatField
|
||||
||owner.CoreVoxelSizeMicrons==0)
|
||||
{error="the liminal pillar grid requires a valid flatField SurfaceWorld recipe";return false;}
|
||||
const auto* id=std::get_if<std::string>(&overrideValue.Get("stable_id").Data);
|
||||
const VFJson::Value& strateValue=overrideValue.Get("strate_index");
|
||||
const auto& center=overrideValue.Get("center_xy_m").AsArray();
|
||||
const auto readNumber=[&](const char* key,double fallback,double& result)
|
||||
{
|
||||
const VFJson::Value& value=overrideValue.Get(key);
|
||||
if(std::holds_alternative<std::nullptr_t>(value.Data))
|
||||
{result=fallback;return true;}
|
||||
if(!value.IsNumber()||!std::isfinite(value.Number()))return false;
|
||||
result=value.Number();return true;
|
||||
};
|
||||
double spacing=0.0,radius=0.0,height=0.0;
|
||||
const VFJson::Value& rowsValue=overrideValue.Get("rows");
|
||||
const VFJson::Value& columnsValue=overrideValue.Get("columns");
|
||||
if(!id||id->empty()||!strateValue.IsNumber()||center.size()!=2
|
||||
||!readNumber("spacing_m",76.0,spacing)
|
||||
||!readNumber("radius_m",4.0,radius)
|
||||
||!readNumber("height_m",28.0,height)
|
||||
||!rowsValue.IsNumber()||!columnsValue.IsNumber())
|
||||
{error="liminal pillar grid requires an identity, center, dimensions and counts";return false;}
|
||||
for(const auto& value:center)
|
||||
if(!value.IsNumber()||!std::isfinite(value.Number()))
|
||||
{error="liminal pillar grid center_xy_m must contain finite metre values";return false;}
|
||||
const int rows=static_cast<int>(rowsValue.Integer());
|
||||
const int columns=static_cast<int>(columnsValue.Integer());
|
||||
if(rows<3||rows>15||columns<3||columns>15||spacing<32.0||spacing>120.0
|
||||
||radius<2.0||radius>12.0||height<12.0||height>60.0
|
||||
||std::abs(center[0].Number())>5000.0||std::abs(center[1].Number())>5000)
|
||||
{error="liminal pillar grid dimensions are outside their bounded ranges";return false;}
|
||||
const auto strateIndex=static_cast<std::int32_t>(strateValue.Integer());
|
||||
const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[strateIndex](const auto& value){return value.strateIndex==strateIndex
|
||||
&&value.generatorKind=="SurfaceWorld";});
|
||||
if(strate==layout.strates.end())
|
||||
{error="liminal_pillar_grid_override must select a resolved SurfaceWorld strate";return false;}
|
||||
const auto toMicrometres=[](double value)
|
||||
{return static_cast<std::int64_t>(std::llround(value*1000000.0));};
|
||||
const std::int64_t floorUm=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(assembly.recipe.surfaceProfile.h0Subvoxels)
|
||||
*owner.CoreVoxelSizeMicrons/WorldAssemblySubvoxelsPerVoxel));
|
||||
const std::int64_t spacingUm=toMicrometres(spacing);
|
||||
const std::int64_t radiusUm=toMicrometres(radius);
|
||||
const std::int64_t halfHeightUm=toMicrometres(height*0.5);
|
||||
const int rowStart=-(rows/2),columnStart=-(columns/2);
|
||||
for(int row=0;row<rows;++row)
|
||||
for(int column=0;column<columns;++column)
|
||||
{
|
||||
ResolvedLayoutStampRequestV1 request;
|
||||
request.stableId=*id+"/pillar/"+std::to_string(1000+row*columns+column).substr(1);
|
||||
request.strateIndex=strateIndex;
|
||||
request.kind=ResolvedSdfStampKindV1::fillPillar;
|
||||
request.anchorPositionMicrometres={
|
||||
toMicrometres(center[0].Number())+(columnStart+column)*spacingUm,
|
||||
toMicrometres(center[1].Number())+(rowStart+row)*spacingUm,
|
||||
floorUm+halfHeightUm};
|
||||
request.halfExtentsMicrometres={radiusUm,radiusUm,halfHeightUm};
|
||||
requests.push_back(std::move(request));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool BuildCoreLiminalFieldRequests(
|
||||
const VoxelForgeCore::WorldAssemblyManifest& assembly,
|
||||
const FStandaloneOwnerWorld& owner,
|
||||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1>& requests,
|
||||
std::string& error)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
requests.clear();
|
||||
if(owner.CoreVoxelSizeMicrons==0)
|
||||
{error="Core.Liminal.Field stamp planning needs the resolved Core voxel size";return false;}
|
||||
const auto plan=std::find_if(assembly.plan.strates.begin(),assembly.plan.strates.end(),
|
||||
[](const auto& value){return value.liminalField.version==CoreLiminalFieldRecipeVersionV1
|
||||
||value.liminalField.version==CoreLiminalFieldRecipeVersionV2;});
|
||||
if(plan==assembly.plan.strates.end()||plan->route.size()<2)
|
||||
{error="Core.Liminal.Field has no planned route for stamp resolution";return false;}
|
||||
const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[&](const auto& value){return value.strateIndex==plan->strateIndex
|
||||
&&value.recipeId==CoreLiminalFieldRecipeIdV1;});
|
||||
if(strate==layout.strates.end())
|
||||
{error="Core.Liminal.Field has no matching resolved SurfaceWorld slot";return false;}
|
||||
const auto variantIndex=static_cast<std::size_t>(plan->liminalField.variant);
|
||||
if(variantIndex>=CoreLiminalFieldVariantsV1.size())
|
||||
{error="Core.Liminal.Field selected an unknown geometry variant";return false;}
|
||||
const auto& definition=CoreLiminalFieldVariantsV1[variantIndex];
|
||||
const auto qToMicrometres=[&](std::int64_t q)
|
||||
{
|
||||
const std::int64_t numerator=q*static_cast<std::int64_t>(owner.CoreVoxelSizeMicrons);
|
||||
return numerator>=0?(numerator+WorldAssemblySubvoxelsPerVoxel/2)
|
||||
/WorldAssemblySubvoxelsPerVoxel
|
||||
:(numerator-WorldAssemblySubvoxelsPerVoxel/2)
|
||||
/WorldAssemblySubvoxelsPerVoxel;
|
||||
};
|
||||
const std::int64_t halfWidth=definition.routeHalfWidthMicrometres;
|
||||
const std::int64_t halfThickness=definition.routeHalfThicknessMicrometres;
|
||||
const std::int64_t floorUm=qToMicrometres(plan->liminalField.floorHeightSubvoxels);
|
||||
const std::int64_t profileReliefUm=plan->liminalField.variant
|
||||
==WorldAssemblyLiminalVariantV1::repeatingHills
|
||||
?2*definition.hillAmplitudeMicrometres:0;
|
||||
const std::int64_t supportBaseUm=strate->content.bottomMicrometres+100000;
|
||||
|
||||
std::vector<std::array<std::int64_t,3>> controls;
|
||||
controls.reserve(plan->route.size());
|
||||
for(const auto& point:plan->route)
|
||||
controls.push_back({qToMicrometres(point.positionSubvoxels[0]),
|
||||
qToMicrometres(point.positionSubvoxels[1]),
|
||||
qToMicrometres(point.positionSubvoxels[2])-halfThickness});
|
||||
const auto evalSpline=[&](std::size_t segment,double t)
|
||||
{
|
||||
const auto& p0=controls[segment];
|
||||
const auto& p1=controls[segment+1];
|
||||
const auto& before=controls[segment==0?segment:segment-1];
|
||||
const auto& after=controls[segment+2<controls.size()?segment+2:segment+1];
|
||||
const double t2=t*t,t3=t2*t;
|
||||
const double h00=2.0*t3-3.0*t2+1.0;
|
||||
const double h10=t3-2.0*t2+t;
|
||||
const double h01=-2.0*t3+3.0*t2;
|
||||
const double h11=t3-t2;
|
||||
std::array<double,3> point{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double m0=segment==0?static_cast<double>(p1[axis]-p0[axis])
|
||||
:0.5*static_cast<double>(p1[axis]-before[axis]);
|
||||
const double m1=segment+2>=controls.size()
|
||||
?static_cast<double>(p1[axis]-p0[axis])
|
||||
:0.5*static_cast<double>(after[axis]-p0[axis]);
|
||||
point[axis]=h00*p0[axis]+h10*m0+h01*p1[axis]+h11*m1;
|
||||
}
|
||||
return point;
|
||||
};
|
||||
std::vector<std::array<std::int64_t,3>> sampledPath;
|
||||
sampledPath.push_back(controls.front());
|
||||
for(std::size_t segment=0;segment+1<controls.size();++segment)
|
||||
{
|
||||
const double dx=static_cast<double>(controls[segment+1][0]-controls[segment][0]);
|
||||
const double dy=static_cast<double>(controls[segment+1][1]-controls[segment][1]);
|
||||
const double dz=static_cast<double>(controls[segment+1][2]-controls[segment][2]);
|
||||
const std::uint32_t steps=static_cast<std::uint32_t>(std::clamp(
|
||||
std::ceil(std::sqrt(dx*dx+dy*dy+dz*dz)/4000000.0),32.0,384.0));
|
||||
for(std::uint32_t step=1;step<=steps;++step)
|
||||
{
|
||||
const auto point=step==steps?std::array<double,3>{
|
||||
static_cast<double>(controls[segment+1][0]),
|
||||
static_cast<double>(controls[segment+1][1]),
|
||||
static_cast<double>(controls[segment+1][2])}
|
||||
:evalSpline(segment,static_cast<double>(step)/steps);
|
||||
std::array<std::int64_t,3> fixed{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
fixed[axis]=static_cast<std::int64_t>(std::llround(point[axis]));
|
||||
if(fixed!=sampledPath.back())sampledPath.push_back(fixed);
|
||||
}
|
||||
}
|
||||
if(sampledPath.size()<2||sampledPath.size()>1025)
|
||||
{error="Core.Liminal.Field sampled route exceeds the continuous-deck path bounds";return false;}
|
||||
|
||||
const auto distancePointSegment=[](const std::array<double,3>& point,
|
||||
const std::array<double,3>& a,const std::array<double,3>& b)
|
||||
{
|
||||
double lengthSquared=0.0,dot=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double delta=b[axis]-a[axis];
|
||||
lengthSquared+=delta*delta;dot+=(point[axis]-a[axis])*delta;
|
||||
}
|
||||
const double t=lengthSquared>0.0?std::clamp(dot/lengthSquared,0.0,1.0):0.0;
|
||||
double distanceSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double delta=point[axis]-(a[axis]+(b[axis]-a[axis])*t);
|
||||
distanceSquared+=delta*delta;
|
||||
}
|
||||
return std::sqrt(distanceSquared);
|
||||
};
|
||||
const auto clearOfLiminalRoute=[&](const std::array<std::int64_t,3>& center,
|
||||
const std::array<std::int64_t,3>& extents)
|
||||
{
|
||||
const double boundRadius=std::sqrt(static_cast<double>(extents[0])*extents[0]
|
||||
+static_cast<double>(extents[1])*extents[1]
|
||||
+static_cast<double>(extents[2])*extents[2]);
|
||||
const double keepout=boundRadius+halfWidth+1000000.0;
|
||||
const std::array<double,3> point={static_cast<double>(center[0]),
|
||||
static_cast<double>(center[1]),static_cast<double>(center[2])};
|
||||
for(std::size_t i=1;i<sampledPath.size();++i)
|
||||
{
|
||||
const std::array<double,3> a={static_cast<double>(sampledPath[i-1][0]),
|
||||
static_cast<double>(sampledPath[i-1][1]),
|
||||
static_cast<double>(sampledPath[i-1][2])};
|
||||
const std::array<double,3> b={static_cast<double>(sampledPath[i][0]),
|
||||
static_cast<double>(sampledPath[i][1]),
|
||||
static_cast<double>(sampledPath[i][2])};
|
||||
if(distancePointSegment(point,a,b)<=keepout)return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const auto clearOfPassages=[&](const std::array<std::int64_t,3>& center,
|
||||
const std::array<std::int64_t,3>& extents)
|
||||
{
|
||||
const double boundRadius=std::sqrt(static_cast<double>(extents[0])*extents[0]
|
||||
+static_cast<double>(extents[1])*extents[1]
|
||||
+static_cast<double>(extents[2])*extents[2]);
|
||||
const std::array<double,3> point={static_cast<double>(center[0]),
|
||||
static_cast<double>(center[1]),static_cast<double>(center[2])};
|
||||
for(const auto& passage:layout.passages)
|
||||
{
|
||||
if(passage.upperStrateIndex!=plan->strateIndex
|
||||
&&passage.lowerStrateIndex!=plan->strateIndex)continue;
|
||||
for(std::size_t i=1;i<passage.controlPoints.size();++i)
|
||||
{
|
||||
const auto toMicrometres=[&](const auto& q)
|
||||
{
|
||||
std::array<double,3> result{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
result[axis]=static_cast<double>(q[axis])
|
||||
*owner.CoreVoxelSizeMicrons/WorldAssemblySubvoxelsPerVoxel;
|
||||
return result;
|
||||
};
|
||||
const auto a=toMicrometres(
|
||||
passage.controlPoints[i-1].positionSubvoxels);
|
||||
const auto b=toMicrometres(passage.controlPoints[i].positionSubvoxels);
|
||||
const double radius=std::max(passage.controlPoints[i-1].radiusSubvoxels,
|
||||
passage.controlPoints[i].radiusSubvoxels)
|
||||
*owner.CoreVoxelSizeMicrons/WorldAssemblySubvoxelsPerVoxel;
|
||||
if(distancePointSegment(point,a,b)<=boundRadius+radius+1000000.0)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
ResolvedLayoutStampRequestV1 deck;
|
||||
deck.stableId="core/liminal-field/route/deck/000";
|
||||
deck.strateIndex=plan->strateIndex;
|
||||
deck.kind=ResolvedSdfStampKindV1::fillSweptDeckSegment;
|
||||
deck.anchorPositionMicrometres=sampledPath[sampledPath.size()/2];
|
||||
deck.sweepEndpointsMicrometres={sampledPath.front(),sampledPath.back()};
|
||||
deck.sweepPathMicrometres=sampledPath;
|
||||
deck.halfExtentsMicrometres={500000,halfWidth,halfThickness};
|
||||
deck.smoothWidthMicrometres=250000;
|
||||
requests.push_back(deck);
|
||||
|
||||
std::vector<double> cumulative(sampledPath.size(),0.0);
|
||||
for(std::size_t i=1;i<sampledPath.size();++i)
|
||||
{
|
||||
const double dx=static_cast<double>(sampledPath[i][0]-sampledPath[i-1][0]);
|
||||
const double dy=static_cast<double>(sampledPath[i][1]-sampledPath[i-1][1]);
|
||||
cumulative[i]=cumulative[i-1]+std::hypot(dx,dy);
|
||||
}
|
||||
const double routeLength=cumulative.back();
|
||||
const double supportSpacing=static_cast<double>(definition.supportSpacingMicrometres);
|
||||
const std::int64_t supportRadius=definition.supportRadiusMicrometres;
|
||||
const auto addSupport=[&](double distance,std::uint32_t supportIndex)
|
||||
{
|
||||
const auto upper=std::lower_bound(cumulative.begin(),cumulative.end(),distance);
|
||||
const std::size_t index=std::clamp<std::size_t>(
|
||||
static_cast<std::size_t>(upper-cumulative.begin()),1,sampledPath.size()-1);
|
||||
const double span=cumulative[index]-cumulative[index-1];
|
||||
const double t=span>0.0?(distance-cumulative[index-1])/span:0.0;
|
||||
std::array<std::int64_t,3> center{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
center[axis]=static_cast<std::int64_t>(std::llround(
|
||||
sampledPath[index-1][axis]
|
||||
+(sampledPath[index][axis]-sampledPath[index-1][axis])*t));
|
||||
const std::int64_t top=center[2]-halfThickness+250000;
|
||||
if(top<=supportBaseUm+1000000)return;
|
||||
constexpr std::int64_t MaximumSupportSegmentMicrometres=180000000;
|
||||
const std::int64_t fullHeight=top-supportBaseUm;
|
||||
const std::uint32_t parts=static_cast<std::uint32_t>(
|
||||
(fullHeight+MaximumSupportSegmentMicrometres-1)
|
||||
/MaximumSupportSegmentMicrometres);
|
||||
const std::int64_t segmentHeight=(fullHeight+parts-1)/parts;
|
||||
std::int64_t bottom=supportBaseUm;
|
||||
for(std::uint32_t part=0;part<parts;++part)
|
||||
{
|
||||
const std::int64_t segmentTop=std::min(top,bottom+segmentHeight+100000);
|
||||
ResolvedLayoutStampRequestV1 support;
|
||||
support.stableId="core/liminal-field/route/support/"
|
||||
+std::to_string(100000+supportIndex).substr(1)+"/"
|
||||
+std::to_string(part);
|
||||
support.strateIndex=plan->strateIndex;
|
||||
support.kind=ResolvedSdfStampKindV1::fillPillar;
|
||||
support.anchorPositionMicrometres={center[0],center[1],(bottom+segmentTop)/2};
|
||||
support.halfExtentsMicrometres={supportRadius,supportRadius,
|
||||
(segmentTop-bottom)/2};
|
||||
requests.push_back(std::move(support));
|
||||
bottom=segmentTop;
|
||||
}
|
||||
};
|
||||
std::uint32_t supportIndex=0;
|
||||
for(double distance=supportSpacing;distance<routeLength-10000000.0;
|
||||
distance+=supportSpacing)
|
||||
addSupport(distance,supportIndex++);
|
||||
// The passage mouth anchor is at player-center height; the deck is at
|
||||
// its walk surface 1.13 m lower. These footings carry that landing down
|
||||
// to the liminal field floor without changing the open route above it.
|
||||
addSupport(0.0,supportIndex++);
|
||||
addSupport(routeLength,supportIndex++);
|
||||
|
||||
if(plan->liminalField.variant==WorldAssemblyLiminalVariantV1::flatFieldMonolith)
|
||||
{
|
||||
const std::size_t middle=sampledPath.size()/2;
|
||||
const auto& before=sampledPath[middle==0?0:middle-1];
|
||||
const auto& after=sampledPath[std::min(middle+1,sampledPath.size()-1)];
|
||||
const double dx=static_cast<double>(after[0]-before[0]);
|
||||
const double dy=static_cast<double>(after[1]-before[1]);
|
||||
const double tangent=std::hypot(dx,dy);
|
||||
if(!(tangent>0.0)){error="Core.Liminal.Field monolith placement has a degenerate route tangent";return false;}
|
||||
const std::array<std::int64_t,3> monolithExtents={
|
||||
definition.monolithRadiusMicrometres,definition.monolithRadiusMicrometres,
|
||||
definition.monolithHeightMicrometres/2};
|
||||
std::array<std::int64_t,3> monolithCenter{};
|
||||
bool placed=false;
|
||||
for(const std::int64_t offset:{42000000ll,60000000ll,78000000ll,
|
||||
96000000ll,-42000000ll,-60000000ll,-78000000ll,-96000000ll})
|
||||
{
|
||||
monolithCenter={
|
||||
static_cast<std::int64_t>(std::llround(sampledPath[middle][0]-dy/tangent*offset)),
|
||||
static_cast<std::int64_t>(std::llround(sampledPath[middle][1]+dx/tangent*offset)),
|
||||
floorUm+monolithExtents[2]};
|
||||
if(clearOfLiminalRoute(monolithCenter,monolithExtents)
|
||||
&&clearOfPassages(monolithCenter,monolithExtents))
|
||||
{placed=true;break;}
|
||||
}
|
||||
if(!placed)
|
||||
{error="Core.Liminal.Field could not seat its monolith outside route and passage clearances";return false;}
|
||||
ResolvedLayoutStampRequestV1 monolith;
|
||||
monolith.stableId="core/liminal-field/monolith/000";
|
||||
monolith.strateIndex=plan->strateIndex;
|
||||
monolith.kind=ResolvedSdfStampKindV1::fillPillar;
|
||||
monolith.anchorPositionMicrometres=monolithCenter;
|
||||
monolith.halfExtentsMicrometres={definition.monolithRadiusMicrometres,
|
||||
definition.monolithRadiusMicrometres,definition.monolithHeightMicrometres/2};
|
||||
requests.push_back(std::move(monolith));
|
||||
}
|
||||
if(plan->liminalField.variant==WorldAssemblyLiminalVariantV1::pillarGrid)
|
||||
{
|
||||
const std::size_t middle=sampledPath.size()/2;
|
||||
const std::int64_t centerX=sampledPath[middle][0];
|
||||
const std::int64_t centerY=sampledPath[middle][1];
|
||||
const std::int64_t spacing=definition.pillarSpacingMicrometres;
|
||||
const std::int64_t halfHeight=definition.pillarHeightMicrometres/2;
|
||||
std::uint32_t ordinal=0;
|
||||
for(std::uint32_t row=0;row<definition.pillarRows;++row)
|
||||
for(std::uint32_t column=0;column<definition.pillarColumns;++column)
|
||||
{
|
||||
const std::int64_t x=centerX
|
||||
+(static_cast<std::int64_t>(column)-definition.pillarColumns/2)*spacing;
|
||||
const std::int64_t y=centerY
|
||||
+(static_cast<std::int64_t>(row)-definition.pillarRows/2)*spacing;
|
||||
double closestSquared=std::numeric_limits<double>::infinity();
|
||||
for(std::size_t i=1;i<sampledPath.size();++i)
|
||||
{
|
||||
const double ax=sampledPath[i-1][0],ay=sampledPath[i-1][1];
|
||||
const double dx=static_cast<double>(sampledPath[i][0]-sampledPath[i-1][0]);
|
||||
const double dy=static_cast<double>(sampledPath[i][1]-sampledPath[i-1][1]);
|
||||
const double lengthSquared=dx*dx+dy*dy;
|
||||
const double t=lengthSquared>0.0?std::clamp(
|
||||
((x-ax)*dx+(y-ay)*dy)/lengthSquared,0.0,1.0):0.0;
|
||||
const double ox=x-(ax+t*dx),oy=y-(ay+t*dy);
|
||||
closestSquared=std::min(closestSquared,ox*ox+oy*oy);
|
||||
}
|
||||
const double keepout=halfWidth+definition.pillarRadiusMicrometres+1000000.0;
|
||||
if(closestSquared<=keepout*keepout)continue;
|
||||
const std::array<std::int64_t,3> pillarExtents={
|
||||
definition.pillarRadiusMicrometres,definition.pillarRadiusMicrometres,
|
||||
halfHeight};
|
||||
const std::array<std::int64_t,3> pillarCenter={x,y,floorUm+halfHeight};
|
||||
if(!clearOfLiminalRoute(pillarCenter,pillarExtents)
|
||||
||!clearOfPassages(pillarCenter,pillarExtents))continue;
|
||||
ResolvedLayoutStampRequestV1 pillar;
|
||||
pillar.stableId="core/liminal-field/pillars/"
|
||||
+std::to_string(1000+ordinal++).substr(1);
|
||||
pillar.strateIndex=plan->strateIndex;
|
||||
pillar.kind=ResolvedSdfStampKindV1::fillPillar;
|
||||
pillar.anchorPositionMicrometres={x,y,floorUm+halfHeight};
|
||||
pillar.halfExtentsMicrometres={definition.pillarRadiusMicrometres,
|
||||
definition.pillarRadiusMicrometres,halfHeight};
|
||||
requests.push_back(std::move(pillar));
|
||||
}
|
||||
if(ordinal<100)
|
||||
{error="Core.Liminal.Field pillar grid has too few columns after route clearance";return false;}
|
||||
}
|
||||
std::sort(requests.begin(),requests.end(),[](const auto& a,const auto& b)
|
||||
{return a.stableId<b.stableId;});
|
||||
return !requests.empty();
|
||||
}
|
||||
|
||||
inline bool AttachRound1WorldAssemblyPrototype(FStandaloneOwnerWorld& owner,VFJson::Value& root,
|
||||
std::string& error,const std::string& manifestOutputPath,
|
||||
VoxelForgeCore::WorldAssemblyManifest* outManifest=nullptr)
|
||||
@@ -1226,9 +1713,46 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::vector<VoxelForgeCore::ResolvedLayoutPassageConfigV1>
|
||||
BuildResolvedLayoutPassageConfigs(const FStandaloneOwnerWorld& owner)
|
||||
{
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutPassageConfigV1> passageConfigs;
|
||||
if(!owner.Manager)return passageConfigs;
|
||||
for(const FStrateSlot& slot:owner.Manager->GetLayout())
|
||||
{
|
||||
if(!slot.Definition)continue;
|
||||
const auto& authored=slot.Definition->PassageConfig;
|
||||
const auto ownerVoxelsToCoreQ=[&](double value)
|
||||
{return static_cast<std::int64_t>(std::llround(value*owner.OwnerVoxelSizeMicrons
|
||||
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/owner.CoreVoxelSizeMicrons));};
|
||||
VoxelForgeCore::ResolvedLayoutPassageConfigV1 config;
|
||||
config.upperStrateIndex=slot.StrateIndex;
|
||||
config.connections=authored.Connections;
|
||||
config.style=static_cast<std::uint32_t>(authored.Style);
|
||||
config.mouthRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(authored.MouthRadius),
|
||||
ownerVoxelsToCoreQ(36.0));
|
||||
config.midRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(authored.MidRadius),
|
||||
ownerVoxelsToCoreQ(24.0));
|
||||
config.reachMinimumSubvoxels=ownerVoxelsToCoreQ(authored.ReachMin);
|
||||
config.reachMaximumSubvoxels=ownerVoxelsToCoreQ(authored.ReachMax);
|
||||
config.distanceMinimumSubvoxels=ownerVoxelsToCoreQ(authored.DistanceMin);
|
||||
config.distanceMaximumSubvoxels=ownerVoxelsToCoreQ(authored.DistanceMax);
|
||||
config.wanderSubvoxels=ownerVoxelsToCoreQ(authored.Wander);
|
||||
config.segments=static_cast<std::uint32_t>(std::clamp(authored.Segments,1,48));
|
||||
config.verticalWobbleSubvoxels=ownerVoxelsToCoreQ(authored.VerticalWobble);
|
||||
config.spiralRadiusSubvoxels=ownerVoxelsToCoreQ(authored.SpiralRadius);
|
||||
config.spiralTurnsQ16=static_cast<std::int64_t>(std::llround(authored.SpiralTurns*65536.0));
|
||||
config.cascadeSteps=static_cast<std::uint32_t>(std::clamp(authored.CascadeSteps,1,16));
|
||||
config.cascadeLedgeSubvoxels=ownerVoxelsToCoreQ(authored.CascadeLedge);
|
||||
passageConfigs.push_back(config);
|
||||
}
|
||||
return passageConfigs;
|
||||
}
|
||||
|
||||
inline bool BuildResolvedLayoutSnapshot(const FStandaloneOwnerWorld& owner,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& assembly,std::string_view recipeVariant,
|
||||
VoxelForgeCore::ResolvedLayoutSnapshotV3& out,std::string& error)
|
||||
VoxelForgeCore::ResolvedLayoutSnapshotV3& out,std::string& error,
|
||||
bool planPassages=true)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
if(!owner.Manager){error="resolved layout builder requires the initialized director";return false;}
|
||||
@@ -1452,35 +1976,11 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
}
|
||||
out.strates.push_back(std::move(s));
|
||||
}
|
||||
std::vector<ResolvedLayoutPassageConfigV1> passageConfigs;
|
||||
for(const FStrateSlot& slot:owner.Manager->GetLayout())
|
||||
if(planPassages)
|
||||
{
|
||||
if(!slot.Definition)continue;
|
||||
const auto& authored=slot.Definition->PassageConfig;
|
||||
const auto ownerVoxelsToCoreQ=[&](double value)
|
||||
{return static_cast<std::int64_t>(std::llround(value*owner.OwnerVoxelSizeMicrons
|
||||
*WorldAssemblySubvoxelsPerVoxel/owner.CoreVoxelSizeMicrons));};
|
||||
ResolvedLayoutPassageConfigV1 config;config.upperStrateIndex=slot.StrateIndex;
|
||||
config.connections=authored.Connections;
|
||||
config.style=static_cast<std::uint32_t>(authored.Style);
|
||||
config.mouthRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(authored.MouthRadius),
|
||||
ownerVoxelsToCoreQ(36.0));
|
||||
config.midRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(authored.MidRadius),
|
||||
ownerVoxelsToCoreQ(24.0));
|
||||
config.reachMinimumSubvoxels=ownerVoxelsToCoreQ(authored.ReachMin);
|
||||
config.reachMaximumSubvoxels=ownerVoxelsToCoreQ(authored.ReachMax);
|
||||
config.distanceMinimumSubvoxels=ownerVoxelsToCoreQ(authored.DistanceMin);
|
||||
config.distanceMaximumSubvoxels=ownerVoxelsToCoreQ(authored.DistanceMax);
|
||||
config.wanderSubvoxels=ownerVoxelsToCoreQ(authored.Wander);
|
||||
config.segments=static_cast<std::uint32_t>(std::clamp(authored.Segments,1,48));
|
||||
config.verticalWobbleSubvoxels=ownerVoxelsToCoreQ(authored.VerticalWobble);
|
||||
config.spiralRadiusSubvoxels=ownerVoxelsToCoreQ(authored.SpiralRadius);
|
||||
config.spiralTurnsQ16=static_cast<std::int64_t>(std::llround(authored.SpiralTurns*65536.0));
|
||||
config.cascadeSteps=static_cast<std::uint32_t>(std::clamp(authored.CascadeSteps,1,16));
|
||||
config.cascadeLedgeSubvoxels=ownerVoxelsToCoreQ(authored.CascadeLedge);
|
||||
passageConfigs.push_back(config);
|
||||
const auto passageConfigs=BuildResolvedLayoutPassageConfigs(owner);
|
||||
if(!PlanResolvedLayoutPassagesV1(out,owner.Seed,passageConfigs,error))return false;
|
||||
}
|
||||
if(!PlanResolvedLayoutPassagesV1(out,owner.Seed,passageConfigs,error))return false;
|
||||
return out.finalize(error);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,9 @@ TILE_HASH_RE = re.compile(
|
||||
r"^\[PipelineTileHash\] lod=(-?\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) "
|
||||
r"density=(0x[0-9a-f]+) mesh=(0x[0-9a-f]+) "
|
||||
r"payload=(0x[0-9a-f]+) topology=(0x[0-9a-f]+)$", re.M)
|
||||
DENSITY_DISPATCH_RE = re.compile(
|
||||
r"^\[DensityDispatch\] strate=(\d+) generator=([^ ]+) recipe=([^ ]+) "
|
||||
r"version=(\d+) bound_path=([^ ]+) adapter=([^ ]+)$", re.M)
|
||||
TILE_MATERIAL_HASH_RE = re.compile(
|
||||
r"^\[PipelineTileMaterialHash\] lod=(-?\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) "
|
||||
r"material=(0x[0-9a-f]+)$", re.M)
|
||||
@@ -493,6 +496,117 @@ def run_new_owner_plan_stage(runner: VerificationRunner, binary: Path) -> None:
|
||||
+ ", ".join(deferred_failures))
|
||||
|
||||
|
||||
def run_density_dispatch_report(runner: VerificationRunner, binary: Path) -> None:
|
||||
"""Report selected owner density paths and generated tile shares by path."""
|
||||
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),
|
||||
}
|
||||
aggregate: dict[int, dict[str, int]] = {}
|
||||
bound_paths: dict[int, tuple[str, str, str, int, str]] = {}
|
||||
failures: list[str] = []
|
||||
for focus_strate, position in cameras.items():
|
||||
name = f"owner-density-dispatch-s{focus_strate}-clang"
|
||||
camera = "--camera=" + ",".join(
|
||||
f"{value:.12f}" for value in (*position, 0.0, 0.0))
|
||||
layout_path = OUT / "layouts" / f"{name}.json"
|
||||
try:
|
||||
row, output = runner.run_cpp(name, binary, OWNER_NEW, "surface-world",
|
||||
["--density-dispatch-report", "--render-distance-chunks=0", camera],
|
||||
expected_style=1, workers=8, max_level=8, clip_radius=3,
|
||||
layout_path=layout_path)
|
||||
except VerificationFailure as error:
|
||||
failures.append(f"{name}: {error}")
|
||||
continue
|
||||
|
||||
dispatch_rows = DENSITY_DISPATCH_RE.findall(output)
|
||||
dispatch = {int(index): (path, generator, recipe, int(version), adapter)
|
||||
for index, generator, recipe, version, path, adapter in dispatch_rows}
|
||||
if len(dispatch_rows) != 4 or set(dispatch) != {0, 1, 2, 3}:
|
||||
failures.append(f"{name}: expected four unique dispatch rows, got {dispatch_rows}")
|
||||
continue
|
||||
if not layout_path.is_file():
|
||||
failures.append(f"{name}: resolved layout missing at {layout_path}")
|
||||
continue
|
||||
try:
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
strata = {int(value["strate_index"]): value for value in layout.get("strates", [])}
|
||||
if set(strata) != {0, 1, 2, 3}:
|
||||
raise ValueError(f"expected four strata, got {sorted(strata)}")
|
||||
voxel_size_um = int(strata[0]["voxel_size_um"])
|
||||
if voxel_size_um <= 0:
|
||||
raise ValueError(f"invalid voxel_size_um={voxel_size_um}")
|
||||
except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as error:
|
||||
failures.append(f"{name}: cannot classify tiles from resolved layout: {error}")
|
||||
continue
|
||||
|
||||
tile_rows = TILE_HASH_RE.findall(output)
|
||||
camera_counts = {index: 0 for index in strata}
|
||||
for lod_text, _x, _y, z_text, *_hashes in tile_rows:
|
||||
lod, z = int(lod_text), int(z_text)
|
||||
if lod < 0 or lod > 30:
|
||||
failures.append(f"{name}: invalid tile lod={lod}")
|
||||
continue
|
||||
extent_voxels = 32 << lod
|
||||
center_um = (z * extent_voxels + extent_voxels // 2) * voxel_size_um
|
||||
matches = [index for index, stratum in strata.items()
|
||||
if int(stratum["interior_bottom_um"]) <= center_um
|
||||
< int(stratum["interior_top_um"])]
|
||||
if len(matches) == 1:
|
||||
index = matches[0]
|
||||
camera_counts[index] += 1
|
||||
aggregate.setdefault(index, {}).setdefault(dispatch[index][0], 0)
|
||||
aggregate[index][dispatch[index][0]] += 1
|
||||
if not tile_rows:
|
||||
failures.append(f"{name}: --density-dispatch-report emitted no tile hashes")
|
||||
observed = sum(camera_counts.values())
|
||||
print(f"[DensityDispatchView] focus_strate={focus_strate} tiles={len(tile_rows)} "
|
||||
f"classified_by_center={observed} status={'PASS' if observed else 'FAIL'}",
|
||||
flush=True)
|
||||
runner.require(f"{name}:dispatch-and-tile-counts",
|
||||
observed > 0 and row["status"] == "PASS",
|
||||
f"dispatch={dispatch} center_attributed_tiles={camera_counts} "
|
||||
f"hashed_tiles={len(tile_rows)}")
|
||||
for index, value in dispatch.items():
|
||||
previous = bound_paths.get(index)
|
||||
if previous is not None and previous != value:
|
||||
failures.append(f"strate {index}: bound path changed between views: "
|
||||
f"{previous} vs {value}")
|
||||
bound_paths[index] = value
|
||||
|
||||
global_path_counts: dict[str, int] = {}
|
||||
for index in range(4):
|
||||
path, generator, recipe, version, adapter = bound_paths.get(
|
||||
index, ("MISSING", "?", "?", 0, "?"))
|
||||
path_counts = aggregate.get(index, {})
|
||||
total = sum(path_counts.values())
|
||||
for path_name, count in path_counts.items():
|
||||
global_path_counts[path_name] = global_path_counts.get(path_name, 0) + count
|
||||
details = ",".join(
|
||||
f"{key}:{value}/{total} ({100.0 * value / max(1, total):.1f}%)"
|
||||
for key, value in sorted(path_counts.items())) or "no tile centers observed"
|
||||
print(f"[DensityDispatchStrate] strate={index} generator={generator} recipe={recipe} "
|
||||
f"version={version} bound_path={path} adapter={adapter} "
|
||||
f"tile_centers={total} path_shares={details}", flush=True)
|
||||
if path == "MISSING" or total == 0:
|
||||
failures.append(f"strate {index}: dispatch path or tile-center share missing")
|
||||
else:
|
||||
runner.require(f"owner-density-dispatch-strate-{index}", True,
|
||||
f"bound_path={path} tile_center_path_shares={details}")
|
||||
all_tiles = sum(global_path_counts.values())
|
||||
for path_name, count in sorted(global_path_counts.items()):
|
||||
share = 100.0 * count / max(1, all_tiles)
|
||||
print(f"[DensityDispatchPathShare] path={path_name} tiles={count}/{all_tiles} "
|
||||
f"share={share:.1f}%", flush=True)
|
||||
if not all_tiles:
|
||||
failures.append("no tile centers were attributed to any bound path")
|
||||
runner.require("owner-density-dispatch-report", not failures,
|
||||
"; ".join(failures) if failures else
|
||||
"four owner poses reported per-strate bound paths and tile-center path shares")
|
||||
|
||||
|
||||
def run_landmark_reachability(runner: VerificationRunner,
|
||||
binaries: tuple[Path, ...]) -> None:
|
||||
for binary in binaries:
|
||||
@@ -3144,6 +3258,7 @@ def main() -> int:
|
||||
except VerificationFailure as error:
|
||||
new_plan_failure = str(error)
|
||||
print(f"[{runner.tier}] new-owner-plan-stage FAIL {error}", flush=True)
|
||||
run_density_dispatch_report(runner, CLANG)
|
||||
run_landmark_reachability(runner, (CLANG,))
|
||||
run_regression_controls(runner)
|
||||
run_hash_pairs(runner, owners)
|
||||
|
||||