Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a97023b24 | ||
|
|
379093401e | ||
|
|
75c6ad251d | ||
|
|
0065ee68e6 | ||
|
|
ebae71b142 | ||
|
|
81071969fc | ||
|
|
cbbfd46bd3 | ||
|
|
044cd6d9c5 | ||
|
|
b96dd739b8 | ||
|
|
4041d4bb4b | ||
|
|
aeb60bbda3 | ||
|
|
01a50946cb | ||
|
|
4111b967bd | ||
|
|
103fa75e1c | ||
|
|
c743f2abc7 | ||
|
|
5e7e0eb1c1 | ||
|
|
bae1144ee4 | ||
|
|
5c519f5206 | ||
|
|
161565936c | ||
|
|
707bc61553 | ||
|
|
6e52fb48b2 | ||
|
|
c2b8d39573 | ||
|
|
89504ecc97 | ||
|
|
f403af1fc1 | ||
|
|
5e875ae504 | ||
|
|
36d954cf2f | ||
|
|
16554cd426 | ||
|
|
5ec09d1ff1 | ||
|
|
5ea01988c4 |
No files matched your search
@@ -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). |
|
||||
@@ -423,6 +423,7 @@ void RecordApplyFrame(const FApplyFrameSample& Sample)
|
||||
"\"active_tasks\":%d,\"running_workers\":%d,\"task_cap\":%d,"
|
||||
"\"pending_tiles\":%d,\"pending_results_estimate\":%d,"
|
||||
"\"results_drained\":%d,\"apply_results\":%d,\"meshes_applied\":%d,\"max_applies\":%d,"
|
||||
"\"supplemental_far_applies\":%d,"
|
||||
"\"apply_budget_ms\":%s,\"count_limit_reached\":%d,\"time_limit_reached\":%d,"
|
||||
"\"pending_by_lod\":%s,\"apply_results_by_lod\":%s,"
|
||||
"\"meshes_by_lod\":%s,\"apply_ms_by_lod\":%s"),
|
||||
@@ -432,7 +433,8 @@ void RecordApplyFrame(const FApplyFrameSample& Sample)
|
||||
Sample.RunningWorkerTasks, Sample.TaskCap, Sample.PendingTiles,
|
||||
Sample.PendingResultsEstimate, Sample.ResultsDrained, Sample.ApplyResultsByLevelTotal,
|
||||
Sample.MeshesApplied,
|
||||
Sample.MaxApplies, *Number(Sample.ApplyBudgetMilliseconds),
|
||||
Sample.MaxApplies, Sample.SupplementalFarApplies,
|
||||
*Number(Sample.ApplyBudgetMilliseconds),
|
||||
Sample.bCountLimitReached ? 1 : 0, Sample.bTimeLimitReached ? 1 : 0,
|
||||
*PendingByLevel, *ApplyResultsByLevel, *MeshesByLevel, *ApplyMillisecondsByLevel));
|
||||
}
|
||||
|
||||
@@ -268,6 +268,12 @@ namespace
|
||||
GVoxelForgeStreamingResultBacklogCap,
|
||||
TEXT("Maximum queued results plus admitted tile tasks; 0 disables the safety cap. This is separate from the worker-slot cap."));
|
||||
|
||||
int32 GVoxelForgeStreamingFarApplySupplement = 1;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingFarApplySupplement(
|
||||
TEXT("voxel.StreamingFarApplySupplement"),
|
||||
GVoxelForgeStreamingFarApplySupplement,
|
||||
TEXT("After the normal visible-apply count is reached, allow one far mesh apply when half of the frame time budget remains; 0 disables the supplemental far lane."));
|
||||
|
||||
int32 GVoxelForgeStreamingReadyQueueDepth = 64;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingReadyQueueDepth(
|
||||
TEXT("voxel.StreamingReadyQueueDepth"),
|
||||
@@ -2223,6 +2229,12 @@ void AVoxelWorld::BeginPlay()
|
||||
}
|
||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingStartupApplyBoost="),
|
||||
GVoxelForgeStreamingStartupApplyBoost);
|
||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingFarApplySupplement="),
|
||||
GVoxelForgeStreamingFarApplySupplement))
|
||||
{
|
||||
GVoxelForgeStreamingFarApplySupplement =
|
||||
GVoxelForgeStreamingFarApplySupplement != 0 ? 1 : 0;
|
||||
}
|
||||
// Headless BuildHost does not reliably apply arbitrary console-variable command-line tokens
|
||||
// before BeginPlay; parse the test switches explicitly so A/B runs exercise the requested path.
|
||||
// BuildHost sans interface n'applique pas toujours les CVar arbitraires avant BeginPlay ;
|
||||
@@ -2737,6 +2749,7 @@ void AVoxelWorld::BeginPlay()
|
||||
"\"streaming_collision_lookahead\":%d,\"core_region_concurrent_admission\":%d,"
|
||||
"\"collision_gate\":%d,"
|
||||
"\"streaming_result_backlog_cap\":%d,\"streaming_startup_apply_boost\":%d,"
|
||||
"\"streaming_far_apply_supplement\":%d,"
|
||||
"\"streaming_background_worker_reserve\":%d,\"core_parallel_for_background_priority\":%d,"
|
||||
"\"streaming_tile_background_low\":%d,\"streaming_tile_background_normal\":%d,"
|
||||
"\"core_column_cache_direct_mapped\":%d,"
|
||||
@@ -2758,6 +2771,7 @@ void AVoxelWorld::BeginPlay()
|
||||
GVoxelForgeStreamingCollisionLookahead, GVoxelForgeCoreRegionConcurrentAdmission,
|
||||
GVoxelForgeCollisionGate,
|
||||
GVoxelForgeStreamingResultBacklogCap, GVoxelForgeStreamingStartupApplyBoost,
|
||||
GVoxelForgeStreamingFarApplySupplement,
|
||||
GVoxelForgeStreamingBackgroundWorkerReserve,
|
||||
GVoxelForgeCoreParallelForBackgroundPriority,
|
||||
GVoxelForgeStreamingTileBackgroundLow,
|
||||
@@ -9881,6 +9895,7 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
const double ApplyBudgetSeconds =
|
||||
static_cast<double>(ApplyBudgetMilliseconds) * 0.001;
|
||||
int32 MeshesApplied = 0;
|
||||
int32 SupplementalFarApplies = 0;
|
||||
int32 ResultsDrained = 0;
|
||||
int32 ApplyResultsByLevelTotal = 0;
|
||||
double ApplySeconds = 0.0;
|
||||
@@ -9888,6 +9903,8 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
TStaticArray<int32, 9> MeshesAppliedByLevel{};
|
||||
TStaticArray<double, 9> ApplySecondsByLevel{};
|
||||
bool bTimeBudgetHit = false;
|
||||
bool bForceSupplementalFarDequeue = false;
|
||||
bool bDequeuedSupplementalFar = false;
|
||||
const auto RecordApplyFrame = [&]()
|
||||
{
|
||||
if (!VoxelForgeStartupTrace::IsActive()) return;
|
||||
@@ -9911,6 +9928,7 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
Sample.ResultsDrained = ResultsDrained;
|
||||
Sample.ApplyResultsByLevelTotal = ApplyResultsByLevelTotal;
|
||||
Sample.MeshesApplied = MeshesApplied;
|
||||
Sample.SupplementalFarApplies = SupplementalFarApplies;
|
||||
Sample.MaxApplies = MaxApplies;
|
||||
Sample.ApplyBudgetMilliseconds = ApplyBudgetMilliseconds;
|
||||
Sample.bCountLimitReached = MeshesApplied >= MaxApplies;
|
||||
@@ -9939,6 +9957,7 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
FChunkResult DequeuedChunk;
|
||||
const auto DequeueNextResult = [&]() -> bool
|
||||
{
|
||||
bDequeuedSupplementalFar = false;
|
||||
if (bStep1UploadCorpusReleased
|
||||
&& Step1HeldApplyCursor < Step1HeldApplyResults.Num())
|
||||
{
|
||||
@@ -9954,8 +9973,19 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
// Mirror the admission order. A horizon result that completed earlier must not consume
|
||||
// the game-thread apply budget ahead of collision support, an edit, or ordinary LOD0.
|
||||
if (CriticalProcessQueue.Dequeue(DequeuedChunk)
|
||||
|| EditedProcessQueue.Dequeue(DequeuedChunk)
|
||||
|| NearProcessQueue.Dequeue(DequeuedChunk)
|
||||
|| EditedProcessQueue.Dequeue(DequeuedChunk))
|
||||
{
|
||||
RecordStreamingResultDequeued(DequeuedChunk);
|
||||
return true;
|
||||
}
|
||||
if (bForceSupplementalFarDequeue)
|
||||
{
|
||||
if (!ProcessQueue.Dequeue(DequeuedChunk)) return false;
|
||||
bDequeuedSupplementalFar = true;
|
||||
RecordStreamingResultDequeued(DequeuedChunk);
|
||||
return true;
|
||||
}
|
||||
if (NearProcessQueue.Dequeue(DequeuedChunk)
|
||||
|| ProcessQueue.Dequeue(DequeuedChunk))
|
||||
{
|
||||
RecordStreamingResultDequeued(DequeuedChunk);
|
||||
@@ -10104,6 +10134,10 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
if (bMeshApplied)
|
||||
{
|
||||
++MeshesApplied;
|
||||
if (bDequeuedSupplementalFar && ResultLevel >= 3)
|
||||
{
|
||||
++SupplementalFarApplies;
|
||||
}
|
||||
if (ResultLevel >= 0 && ResultLevel < 9)
|
||||
{
|
||||
++MeshesAppliedByLevel[ResultLevel];
|
||||
@@ -10122,6 +10156,19 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
}
|
||||
if (MeshesApplied >= MaxApplies)
|
||||
{
|
||||
const double DrainElapsedSeconds = FPlatformTime::ToSeconds64(
|
||||
FPlatformTime::Cycles64() - DrainStartCycles);
|
||||
const bool bCanSupplementFar = MeshesApplied == MaxApplies
|
||||
&& SupplementalFarApplies == 0
|
||||
&& !bUseStartupApplyBoost
|
||||
&& GVoxelForgeStreamingFarApplySupplement != 0
|
||||
&& ApplyBudgetSeconds > 0.0
|
||||
&& DrainElapsedSeconds < ApplyBudgetSeconds * 0.5;
|
||||
if (bCanSupplementFar)
|
||||
{
|
||||
bForceSupplementalFarDequeue = true;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -15360,6 +15407,8 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
const int32 NumIndices = Result.NumTriangles * 3;
|
||||
const int32 FirstCapIndex = FMath::Clamp(
|
||||
NumIndices - Result.NumCeilingTriangles * 3, 0, NumIndices);
|
||||
TFuture<ERealtimeMeshProxyUpdateStatus> ProxyPublicationFuture;
|
||||
bool bHasProxyPublicationFuture = false;
|
||||
{
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_RMC_CreateSections);
|
||||
if (bHasGroundTris)
|
||||
@@ -15369,9 +15418,10 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
const FRealtimeMeshStreamRange GroundRange =
|
||||
URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange(
|
||||
0, Result.NumVertices, 0, FirstCapIndex);
|
||||
RTMesh->CreateSection(
|
||||
ProxyPublicationFuture = RTMesh->CreateSection(
|
||||
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 0),
|
||||
GroundConfig, GroundRange, /*bShouldCreateCollision*/ bWantsCollision);
|
||||
bHasProxyPublicationFuture = true;
|
||||
}
|
||||
if (bHasCeilingTris)
|
||||
{
|
||||
@@ -15380,11 +15430,48 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
const FRealtimeMeshStreamRange CapRange =
|
||||
URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange(
|
||||
0, Result.NumVertices, FirstCapIndex, NumIndices);
|
||||
RTMesh->CreateSection(
|
||||
ProxyPublicationFuture = RTMesh->CreateSection(
|
||||
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 1),
|
||||
CapConfig, CapRange, /*bShouldCreateCollision*/ bWantsCollision);
|
||||
bHasProxyPublicationFuture = true;
|
||||
}
|
||||
}
|
||||
const uint64 ProxyPublicationSubmitCycles = FPlatformTime::Cycles64();
|
||||
if (bTraceRmcTiming && bHasProxyPublicationFuture)
|
||||
{
|
||||
const TWeakObjectPtr<AVoxelWorld> WeakWorld(this);
|
||||
const TWeakObjectPtr<URealtimeMeshComponent> WeakComponent(MeshComp);
|
||||
const FVoxelTileKey PublishedTile = Tile;
|
||||
const uint64 RequestOrdinal = Result.RequestOrdinal;
|
||||
const uint64 ApplyStartedCycles = ApplyStartCycles;
|
||||
ProxyPublicationFuture.Next(
|
||||
[WeakWorld, WeakComponent, PublishedTile, RequestOrdinal,
|
||||
ApplyStartedCycles, ProxyPublicationSubmitCycles](
|
||||
ERealtimeMeshProxyUpdateStatus Status)
|
||||
{
|
||||
if (!VoxelForgeStartupTrace::IsActive()) return;
|
||||
const uint64 PublishedCycles = FPlatformTime::Cycles64();
|
||||
const URealtimeMeshComponent* Component = WeakComponent.Get();
|
||||
VoxelForgeStartupTrace::RecordEvent(TEXT("rmc_proxy_published"), FString::Printf(
|
||||
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"request_ordinal\":%llu,"
|
||||
"\"proxy_update_status\":%d,\"apply_to_proxy_ms\":%.3f,"
|
||||
"\"section_submit_to_proxy_ms\":%.3f,\"component_visible\":%d,"
|
||||
"\"world_valid\":%d"),
|
||||
PublishedTile.Coord.X, PublishedTile.Coord.Y, PublishedTile.Coord.Z,
|
||||
PublishedTile.Level, static_cast<unsigned long long>(RequestOrdinal),
|
||||
static_cast<int32>(Status),
|
||||
ApplyStartedCycles != 0 && PublishedCycles >= ApplyStartedCycles
|
||||
? FPlatformTime::ToSeconds64(PublishedCycles - ApplyStartedCycles) * 1000.0
|
||||
: 0.0,
|
||||
ProxyPublicationSubmitCycles != 0
|
||||
&& PublishedCycles >= ProxyPublicationSubmitCycles
|
||||
? FPlatformTime::ToSeconds64(
|
||||
PublishedCycles - ProxyPublicationSubmitCycles) * 1000.0
|
||||
: 0.0,
|
||||
Component && Component->IsVisible() ? 1 : 0,
|
||||
WeakWorld.IsValid() ? 1 : 0));
|
||||
});
|
||||
}
|
||||
if (bTraceRmcTiming)
|
||||
{
|
||||
TraceSectionConfigMilliseconds = FPlatformTime::ToSeconds64(
|
||||
|
||||
@@ -59,6 +59,7 @@ namespace VoxelForgeStartupTrace
|
||||
int32 ResultsDrained = 0;
|
||||
int32 ApplyResultsByLevelTotal = 0;
|
||||
int32 MeshesApplied = 0;
|
||||
int32 SupplementalFarApplies = 0;
|
||||
int32 MaxApplies = 0;
|
||||
double ApplyBudgetMilliseconds = 0.0;
|
||||
bool bCountLimitReached = false;
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace VoxelForgeCore::PassageSpline
|
||||
{
|
||||
using FPoint = std::array<double,4>;
|
||||
inline constexpr std::size_t ArcTableSteps = 32;
|
||||
inline constexpr double UpperMouthFloorBlendRadiusMetres = 3.5;
|
||||
|
||||
struct FSegment
|
||||
{
|
||||
FPoint a{},b{};
|
||||
double alongMetres=0.0,lengthMetres=0.0;
|
||||
std::size_t controlSpan=0;
|
||||
double tA=0.0,tB=1.0;
|
||||
double plannedFloorA=0.0,plannedFloorB=0.0;
|
||||
};
|
||||
|
||||
inline double KnotSpan(const FPoint& a,const FPoint& b,double unitsPerMetre)
|
||||
{
|
||||
const double dx=(b[0]-a[0])/unitsPerMetre;
|
||||
const double dy=(b[1]-a[1])/unitsPerMetre;
|
||||
return std::max(1.0e-6,std::sqrt(std::sqrt(dx*dx+dy*dy)));
|
||||
}
|
||||
|
||||
inline FPoint EvaluateRaw(std::span<const FPoint> controls,std::size_t span,
|
||||
double t,double unitsPerMetre)
|
||||
{
|
||||
const auto& p1=controls[span];
|
||||
const auto& p2=controls[span+1];
|
||||
const auto& p0=controls[span==0?0:span-1];
|
||||
const auto& p3=controls[std::min(span+2,controls.size()-1)];
|
||||
const double dt0=KnotSpan(p0,p1,unitsPerMetre);
|
||||
const double dt1=KnotSpan(p1,p2,unitsPerMetre);
|
||||
const double dt2=KnotSpan(p2,p3,unitsPerMetre);
|
||||
const double t2=t*t,t3=t2*t;
|
||||
FPoint result{};
|
||||
for(const std::size_t axis:{0u,1u,3u})
|
||||
{
|
||||
const double m1=dt1*((p1[axis]-p0[axis])/dt0
|
||||
-(p2[axis]-p0[axis])/(dt0+dt1)
|
||||
+(p2[axis]-p1[axis])/dt1);
|
||||
const double m2=dt1*((p2[axis]-p1[axis])/dt1
|
||||
-(p3[axis]-p1[axis])/(dt1+dt2)
|
||||
+(p3[axis]-p2[axis])/dt2);
|
||||
result[axis]=(2.0*t3-3.0*t2+1.0)*p1[axis]
|
||||
+(-2.0*t3+3.0*t2)*p2[axis]
|
||||
+(t3-2.0*t2+t)*m1+(t3-t2)*m2;
|
||||
}
|
||||
result[2]=std::lerp(p1[2],p2[2],t);
|
||||
result[3]=std::clamp(result[3],std::min(p1[3],p2[3]),
|
||||
std::max(p1[3],p2[3]));
|
||||
return result;
|
||||
}
|
||||
|
||||
inline bool ClosestRouteHeightAtXY(std::span<const std::array<double,3>> route,
|
||||
double x,double y,double& height,double& distanceSquared)
|
||||
{
|
||||
if(route.empty())return false;
|
||||
distanceSquared=std::numeric_limits<double>::infinity();
|
||||
const auto consider=[&](const std::array<double,3>& point)
|
||||
{
|
||||
const double dx=x-point[0],dy=y-point[1];
|
||||
const double candidate=dx*dx+dy*dy;
|
||||
if(candidate<distanceSquared)
|
||||
{distanceSquared=candidate;height=point[2];}
|
||||
};
|
||||
if(route.size()==1)
|
||||
{
|
||||
consider(route.front());
|
||||
return true;
|
||||
}
|
||||
for(std::size_t i=1;i<route.size();++i)
|
||||
{
|
||||
const auto& a=route[i-1];
|
||||
const auto& b=route[i];
|
||||
const double dx=b[0]-a[0],dy=b[1]-a[1];
|
||||
const double lengthSquared=dx*dx+dy*dy;
|
||||
const double t=lengthSquared>1.0e-12
|
||||
?std::clamp(((x-a[0])*dx+(y-a[1])*dy)/lengthSquared,0.0,1.0):0.0;
|
||||
const std::array<double,3> point={a[0]+t*dx,a[1]+t*dy,
|
||||
std::lerp(a[2],b[2],t)};
|
||||
consider(point);
|
||||
}
|
||||
return std::isfinite(distanceSquared);
|
||||
}
|
||||
|
||||
inline double BlendUpperMouthFloorAtXY(
|
||||
std::span<const std::array<double,3>> receivingRoute,double x,double y,
|
||||
double plannedFloor,double mouthX,double mouthY,double mouthFloor,
|
||||
double tangentX,double tangentY,
|
||||
double blendRadiusMetres,double playerCenterAboveFloorMetres,
|
||||
double unitsPerMetre=1.0)
|
||||
{
|
||||
if(receivingRoute.empty()||!(blendRadiusMetres>0.0)
|
||||
||!(unitsPerMetre>0.0))return plannedFloor;
|
||||
const double dx=(x-mouthX)/unitsPerMetre;
|
||||
const double dy=(y-mouthY)/unitsPerMetre;
|
||||
if(dx*tangentX+dy*tangentY>=0.0)return plannedFloor;
|
||||
const double dz=(plannedFloor-mouthFloor)/unitsPerMetre;
|
||||
const double distance=std::sqrt(dx*dx+dy*dy+dz*dz);
|
||||
double routeHeight=0.0,routeDistanceSquared=0.0;
|
||||
if(distance>blendRadiusMetres
|
||||
||!ClosestRouteHeightAtXY(receivingRoute,x,y,routeHeight,
|
||||
routeDistanceSquared)
|
||||
||routeDistanceSquared>std::pow(2.0*blendRadiusMetres*unitsPerMetre,2.0))
|
||||
return plannedFloor;
|
||||
const double t=std::clamp(distance/blendRadiusMetres,0.0,1.0);
|
||||
const double blend=t*t*(3.0-2.0*t);
|
||||
const double receivingFloor=routeHeight
|
||||
-playerCenterAboveFloorMetres*unitsPerMetre;
|
||||
return std::lerp(plannedFloor,receivingFloor,blend);
|
||||
}
|
||||
|
||||
inline std::vector<FSegment> BuildSegments(std::span<const FPoint> controls,
|
||||
std::size_t spanCount,double spacingMetres,double unitsPerMetre=1.0,
|
||||
std::span<const double> plannedFloorProfile={},
|
||||
double maximumChordErrorMetres=0.0)
|
||||
{
|
||||
std::vector<FSegment> result;
|
||||
if(controls.size()<2||!(spacingMetres>0.0)||!(unitsPerMetre>0.0))return result;
|
||||
spanCount=std::min(spanCount,controls.size()-1);
|
||||
double alongMetres=0.0;
|
||||
for(std::size_t span=0;span<spanCount;++span)
|
||||
{
|
||||
std::array<double,ArcTableSteps+1> horizontalArc{};
|
||||
auto previous=EvaluateRaw(controls,span,0.0,unitsPerMetre);
|
||||
for(std::size_t step=1;step<=ArcTableSteps;++step)
|
||||
{
|
||||
const auto current=EvaluateRaw(controls,span,
|
||||
static_cast<double>(step)/ArcTableSteps,unitsPerMetre);
|
||||
const double dx=(current[0]-previous[0])/unitsPerMetre;
|
||||
const double dy=(current[1]-previous[1])/unitsPerMetre;
|
||||
horizontalArc[step]=horizontalArc[step-1]+std::sqrt(dx*dx+dy*dy);
|
||||
previous=current;
|
||||
}
|
||||
const double spanLength=horizontalArc.back();
|
||||
const std::size_t baseSegmentCount=std::max<std::size_t>(1,
|
||||
static_cast<std::size_t>(std::ceil(spanLength/spacingMetres)));
|
||||
std::vector<FPoint> points;
|
||||
std::vector<double> parameters;
|
||||
points.reserve(baseSegmentCount+1);
|
||||
parameters.reserve(baseSegmentCount+1);
|
||||
const auto parameterAtArc=[&](double targetArc)
|
||||
{
|
||||
const auto upper=std::lower_bound(horizontalArc.begin()+1,
|
||||
horizontalArc.end(),targetArc);
|
||||
const std::size_t upperIndex=static_cast<std::size_t>(
|
||||
upper-horizontalArc.begin());
|
||||
const std::size_t lowerIndex=upperIndex-1;
|
||||
const double arcSpan=horizontalArc[upperIndex]-horizontalArc[lowerIndex];
|
||||
const double arcT=arcSpan>1.0e-12
|
||||
?std::clamp((targetArc-horizontalArc[lowerIndex])/arcSpan,0.0,1.0):0.0;
|
||||
return (static_cast<double>(lowerIndex)+arcT)/ArcTableSteps;
|
||||
};
|
||||
parameters.push_back(0.0);
|
||||
const double chordError=std::max(0.0,maximumChordErrorMetres);
|
||||
const auto appendAdaptive=[&](auto&& self,double tA,double tB,
|
||||
const FPoint& a,const FPoint& b,unsigned depth)->void
|
||||
{
|
||||
constexpr std::array<double,3> CheckFractions={0.25,0.5,0.75};
|
||||
double maximumError=0.0,polylineLength=0.0;
|
||||
FPoint previous=a;
|
||||
for(const double fraction:CheckFractions)
|
||||
{
|
||||
const FPoint current=EvaluateRaw(controls,span,
|
||||
std::lerp(tA,tB,fraction),unitsPerMetre);
|
||||
const double chordX=std::lerp(a[0],b[0],fraction);
|
||||
const double chordY=std::lerp(a[1],b[1],fraction);
|
||||
const double dx=(current[0]-chordX)/unitsPerMetre;
|
||||
const double dy=(current[1]-chordY)/unitsPerMetre;
|
||||
const double dr=(current[3]-std::lerp(a[3],b[3],fraction))
|
||||
/unitsPerMetre;
|
||||
maximumError=std::max(maximumError,
|
||||
std::sqrt(dx*dx+dy*dy+dr*dr));
|
||||
const double segmentX=(current[0]-previous[0])/unitsPerMetre;
|
||||
const double segmentY=(current[1]-previous[1])/unitsPerMetre;
|
||||
polylineLength+=std::sqrt(segmentX*segmentX+segmentY*segmentY);
|
||||
previous=current;
|
||||
}
|
||||
const double finalX=(b[0]-previous[0])/unitsPerMetre;
|
||||
const double finalY=(b[1]-previous[1])/unitsPerMetre;
|
||||
polylineLength+=std::sqrt(finalX*finalX+finalY*finalY);
|
||||
const bool split=(polylineLength>spacingMetres*(1.0+1.0e-9)
|
||||
||(chordError>0.0&&maximumError>chordError))&&depth<12;
|
||||
if(split)
|
||||
{
|
||||
const double tMid=(tA+tB)*0.5;
|
||||
const FPoint middle=EvaluateRaw(controls,span,tMid,unitsPerMetre);
|
||||
self(self,tA,tMid,a,middle,depth+1);
|
||||
self(self,tMid,tB,middle,b,depth+1);
|
||||
return;
|
||||
}
|
||||
parameters.push_back(tB);
|
||||
};
|
||||
for(std::size_t segment=1;segment<=baseSegmentCount;++segment)
|
||||
{
|
||||
const double targetA=spanLength*static_cast<double>(segment-1)
|
||||
/baseSegmentCount;
|
||||
const double targetB=spanLength*static_cast<double>(segment)
|
||||
/baseSegmentCount;
|
||||
const double tA=parameterAtArc(targetA);
|
||||
const double tB=parameterAtArc(targetB);
|
||||
const FPoint a=EvaluateRaw(controls,span,tA,unitsPerMetre);
|
||||
const FPoint b=EvaluateRaw(controls,span,tB,unitsPerMetre);
|
||||
appendAdaptive(appendAdaptive,tA,tB,a,b,0);
|
||||
}
|
||||
points.reserve(parameters.size());
|
||||
for(const double t:parameters)
|
||||
points.push_back(EvaluateRaw(controls,span,t,unitsPerMetre));
|
||||
std::vector<double> sampledHorizontalArc(points.size(),0.0);
|
||||
for(std::size_t i=1;i<points.size();++i)
|
||||
{
|
||||
const double dx=(points[i][0]-points[i-1][0])/unitsPerMetre;
|
||||
const double dy=(points[i][1]-points[i-1][1])/unitsPerMetre;
|
||||
sampledHorizontalArc[i]=sampledHorizontalArc[i-1]
|
||||
+std::sqrt(dx*dx+dy*dy);
|
||||
}
|
||||
const double sampledSpanLength=sampledHorizontalArc.back();
|
||||
for(std::size_t i=0;i<points.size();++i)
|
||||
{
|
||||
const double progress=sampledSpanLength>1.0e-12
|
||||
?sampledHorizontalArc[i]/sampledSpanLength:parameters[i];
|
||||
points[i][2]=std::lerp(controls[span][2],controls[span+1][2],progress);
|
||||
}
|
||||
for(std::size_t i=1;i<points.size();++i)
|
||||
{
|
||||
const double dx=(points[i][0]-points[i-1][0])/unitsPerMetre;
|
||||
const double dy=(points[i][1]-points[i-1][1])/unitsPerMetre;
|
||||
const double dz=(points[i][2]-points[i-1][2])/unitsPerMetre;
|
||||
const double length=std::sqrt(dx*dx+dy*dy+dz*dz);
|
||||
const auto floorAt=[&](std::size_t index)
|
||||
{
|
||||
if(plannedFloorProfile.size()!=controls.size())
|
||||
return points[index][2];
|
||||
const double progress=sampledSpanLength>1.0e-12
|
||||
?sampledHorizontalArc[index]/sampledSpanLength:parameters[index];
|
||||
return std::lerp(plannedFloorProfile[span],
|
||||
plannedFloorProfile[span+1],progress);
|
||||
};
|
||||
result.push_back({points[i-1],points[i],alongMetres,length,span,
|
||||
parameters[i-1],parameters[i],floorAt(i-1),floorAt(i)});
|
||||
alongMetres+=length;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "VoxelForgeCore/Linux77Profiler.h"
|
||||
#include "VoxelForgeCore/PassageSpline.h"
|
||||
|
||||
// UBT supplies this export/import macro for the runtime module. The standalone target has no
|
||||
// module boundary, so leave it empty there. UBT fournit la macro; le viewer autonome reste neutre.
|
||||
@@ -137,46 +138,44 @@ namespace VoxelForgeCore
|
||||
bool passageComposerProfiled = false;
|
||||
std::uint64_t passageTileFeatureCount = 0;
|
||||
std::uint64_t passageTileRouteSegmentCandidates = 0;
|
||||
std::uint64_t passageTileMouthSegmentCandidates = 0;
|
||||
std::uint64_t resolvedBoundaryBandQueries = 0;
|
||||
std::uint64_t passageComposerQueries = 0;
|
||||
std::uint64_t passageIterations = 0;
|
||||
std::uint64_t passageTileBoundChecks = 0;
|
||||
std::uint64_t passageTileBoundRejects = 0;
|
||||
std::uint64_t passageTileBoundSkippedIterations = 0;
|
||||
std::uint64_t passageUpperMouthFilterChecks = 0;
|
||||
std::uint64_t passageColumnCandidates = 0;
|
||||
std::uint64_t passageColumnDistanceCalls = 0;
|
||||
std::uint64_t passageNegativeBoundChecks = 0;
|
||||
std::uint64_t passageNegativeBoundRejects = 0;
|
||||
std::uint64_t passagePositiveBoundChecks = 0;
|
||||
std::uint64_t passagePositiveBoundRejects = 0;
|
||||
std::uint64_t passageCarveSegmentVisits = 0;
|
||||
std::uint64_t passageSmoothCarveJoins = 0;
|
||||
std::uint64_t passageSupportSegmentVisits = 0;
|
||||
std::uint64_t passageSmoothSupportJoins = 0;
|
||||
std::uint64_t passageSupportCapsuleChecks = 0;
|
||||
std::uint64_t passageLaterBoreChecks = 0;
|
||||
std::uint64_t passageOtherCapsuleChecks = 0;
|
||||
std::uint64_t passageMouthSupportCalls = 0;
|
||||
std::uint64_t passageMouthRouteSegmentChecks = 0;
|
||||
std::uint64_t passageReliefFieldCalls = 0;
|
||||
std::uint64_t passageSmoothstepEvaluations = 0;
|
||||
// L99 tile-local swept-tube base and detail passes. These are populated only
|
||||
// by the opt-in passage composer profile, except the mesh composition itself.
|
||||
std::uint64_t passageSweepRouteSegmentCandidates = 0;
|
||||
std::uint64_t passageSweepSegmentSamplePairs = 0;
|
||||
std::uint64_t passageSweepPrepareNanoseconds = 0;
|
||||
std::uint64_t passageSweepOutsideInfluencePairs = 0;
|
||||
std::uint64_t passageSweepShellSupportSkips = 0;
|
||||
std::uint64_t passageSweepMinimumUpdates = 0;
|
||||
std::uint64_t passageSweepBaseInfluenceSamples = 0;
|
||||
std::uint64_t passageSweepDetailShellSamples = 0;
|
||||
std::uint64_t passageSweepGridSamples = 0;
|
||||
std::uint64_t passageSweepNoInfluenceGridSamples = 0;
|
||||
std::uint64_t passageSweepTouchedSamples = 0;
|
||||
std::uint64_t passageSweepStampedOutsideSamples = 0;
|
||||
std::uint64_t passageSweepCacheValidSamples = 0;
|
||||
std::uint64_t passageSweepCacheValidUniqueSamples = 0;
|
||||
std::uint64_t passageSweepUniqueTouchedSamples = 0;
|
||||
std::uint64_t passageSweepBodyBandSamples = 0;
|
||||
std::uint64_t passageSweepShellBandSamples = 0;
|
||||
std::uint64_t passageSweepUniqueBandSamples = 0;
|
||||
std::uint64_t passageSweepOverlapPairs = 0;
|
||||
std::uint64_t passageSweepContributingPairs = 0;
|
||||
std::uint64_t passageSweepCrossPassageOverlaps = 0;
|
||||
std::uint64_t passageSweepFloorProfileCalls = 0;
|
||||
std::uint64_t passageSweepAllocatedBytes = 0;
|
||||
std::uint64_t passageSweepPrepareScratchBytes = 0;
|
||||
std::uint64_t passageSweepLookups = 0;
|
||||
std::uint64_t passageSweepCacheHits = 0;
|
||||
std::uint64_t passageSweepBaseInfluenceHits = 0;
|
||||
std::uint64_t passageSweepOutOfInfluenceHits = 0;
|
||||
std::uint64_t passageSweepMissingSamples = 0;
|
||||
std::uint64_t passageSweepOffGridMisses = 0;
|
||||
std::uint64_t passageSweepUnpreparedMisses = 0;
|
||||
std::array<std::uint64_t,12> passageSweepMissingLookupsByCaller{};
|
||||
std::array<std::uint64_t,12> passageSweepExactRouteFallbacksByCaller{};
|
||||
std::array<std::uint64_t,12> passageSweepFeatureBoundSkipsByCaller{};
|
||||
@@ -488,7 +487,19 @@ namespace VoxelForgeCore
|
||||
// Version 210 keeps bulk native-cave sampling on the scalar field path at strata edges.
|
||||
// Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB.
|
||||
// Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 216;
|
||||
// Version 218 sweeps passages around a dense smooth spline and limits exposed shells to a trough.
|
||||
// Version 219 smooths passage XY while mapping each planned height chord over its sampled horizontal arc.
|
||||
// Version 220 adaptively samples passage curves and rounds open-air wall crests.
|
||||
// Version 221 ranks passage segments before floor shaping and preserves their signed exterior distance.
|
||||
// Version 223 projects the floor onto one continuous arc-length profile and groups
|
||||
// dense passage samples into coarse tile candidates. Version 224 keeps the same
|
||||
// continuous floor while relaxing the spline chord tolerance and bounding shell work.
|
||||
// Version 226 unifies the passage influence reach in metres and converts it to core
|
||||
// voxels for every broadphase and proof bound. Version 227 uses the exact smooth-max
|
||||
// support width/6 when deriving that reach. Version 228 composes deterministic cave
|
||||
// graphs from fixed passage mouths and the owner Tunnel Network settings. Version 229
|
||||
// budgets cave floor support; 230 records a shared backbone floor profile and joint cap.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 230;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
@@ -883,6 +894,7 @@ namespace VoxelForgeCore
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV3 = 3;
|
||||
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV2 = 2;
|
||||
inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1;
|
||||
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
||||
|
||||
@@ -947,6 +959,11 @@ namespace VoxelForgeCore
|
||||
std::array<std::int64_t, 3> centerMicrometres{};
|
||||
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||
std::int64_t floorMicrometres = 0;
|
||||
// Optional shared backbone profile reference. The room floor plane is
|
||||
// floorMicrometres plus this Q16 XY gradient from centerMicrometres.
|
||||
std::string floorProfileStableId;
|
||||
std::int64_t floorProfileDistanceMicrometres = -1;
|
||||
std::array<std::int32_t, 2> floorGradientQ16{};
|
||||
std::int64_t smoothWidthMicrometres = 0;
|
||||
// Optional feature-to-feature union width. Zero preserves the historical
|
||||
// behavior where smoothWidthMicrometres also controls the graph union.
|
||||
@@ -966,6 +983,11 @@ namespace VoxelForgeCore
|
||||
std::int32_t strateIndex = -1;
|
||||
// A single cubic Bezier in XY. Its four Z values are the cubic floor profile.
|
||||
std::array<std::array<std::int64_t, 3>, 4> controlPointsMicrometres{};
|
||||
// Edges with the same profile ID are adjacent pieces of one shared h(s).
|
||||
// Distances are signed along that profile, so a terminal edge may be reversed.
|
||||
std::string floorProfileStableId;
|
||||
std::int64_t floorProfileStartDistanceMicrometres = -1;
|
||||
std::int64_t floorProfileEndDistanceMicrometres = -1;
|
||||
// Resolve-time, error-bounded approximation of the cubic centerline. Each
|
||||
// consecutive pair is a physical segment used by the native cave field.
|
||||
std::vector<std::array<std::int64_t, 3>> polylineMicrometres;
|
||||
@@ -982,6 +1004,37 @@ namespace VoxelForgeCore
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
};
|
||||
|
||||
// Inputs to the reusable, plan-time cave graph composer. All distances are
|
||||
// integer micrometres and unit-interval values are Q16 so the resolved graph
|
||||
// does not depend on worker count or floating-point random distributions.
|
||||
struct ResolvedCavernPlannerSettingsV1
|
||||
{
|
||||
std::int64_t roomSpacingMicrometres = 16000000;
|
||||
std::int64_t minimumRoomRadiusMicrometres = 2000000;
|
||||
std::int64_t maximumRoomRadiusMicrometres = 5000000;
|
||||
std::int64_t destinationRadiusMicrometres = 6000000;
|
||||
// Solid floor backing below the planned edge floor, preserved after graph
|
||||
// unions and passage composition. This is separate from bore height.
|
||||
std::int64_t floorSupportDepthMicrometres = 750000;
|
||||
std::int64_t minimumTunnelRadiusMicrometres = 750000;
|
||||
std::int64_t maximumTunnelRadiusMicrometres = 1000000;
|
||||
std::int64_t maximumTunnelLengthMicrometres = 45000000;
|
||||
std::int64_t tunnelWarpMicrometres = 0;
|
||||
std::int64_t caveWarpMicrometres = 0;
|
||||
std::int64_t smoothWidthMicrometres = 500000;
|
||||
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::int32_t roomDensityQ16 = 22938;
|
||||
std::int32_t tunnelDensityQ16 = 26214;
|
||||
std::int32_t roomShapeVarietyQ16 = 32768;
|
||||
std::int32_t roomHeightRatioQ16 = 55706;
|
||||
std::int32_t roomFloorCutMinimumQ16 = 45875;
|
||||
std::int32_t roomFloorCutMaximumQ16 = 58982;
|
||||
std::int32_t tunnelHorizontalBiasQ16 = 32768;
|
||||
std::int32_t originRoomMaximumConnections = 4;
|
||||
bool tunnelsFlowTowardOrigin = true;
|
||||
};
|
||||
|
||||
// Immutable material-only deposits. The capsule and its conservative bounds are in
|
||||
// micrometres; bounds include irregularity and the blend envelope.
|
||||
struct ResolvedLayoutOreVeinRequestV1
|
||||
@@ -1041,7 +1094,6 @@ namespace VoxelForgeCore
|
||||
{
|
||||
bool hasSurfaceLanding = false;
|
||||
bool hasFloatingIslandLanding = false;
|
||||
bool usePassageAnchorForColumns = false;
|
||||
bool hasNativeCaveField = false;
|
||||
bool hasNativeLatticeField = false;
|
||||
};
|
||||
@@ -1087,7 +1139,7 @@ namespace VoxelForgeCore
|
||||
|
||||
inline bool IsNativeCaveFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
|
||||
{
|
||||
return stratum.generatorKind=="Caves"
|
||||
return (stratum.generatorKind=="Caves"||stratum.generatorKind=="TunnelNetwork")
|
||||
&&stratum.recipeId=="Core.Caves.PrimitiveField"
|
||||
&&stratum.recipeVersion==1;
|
||||
}
|
||||
@@ -1145,28 +1197,6 @@ namespace VoxelForgeCore
|
||||
std::vector<ResolvedLayoutPassagePointV1> controlPoints;
|
||||
};
|
||||
|
||||
struct PassageVoidCrossingAuditV1
|
||||
{
|
||||
std::size_t passageIndex = 0;
|
||||
std::size_t firstSegment = 0;
|
||||
std::size_t lastSegment = 0;
|
||||
std::uint64_t unsupportedSamples = 0;
|
||||
double unsupportedLengthMetres = 0.0;
|
||||
std::array<double, 4> startPointSubvoxels{};
|
||||
std::array<double, 4> endPointSubvoxels{};
|
||||
bool needsColumn = false;
|
||||
bool connectedAtStart = false;
|
||||
bool connectedAtEnd = false;
|
||||
std::array<std::int64_t, 2> columnAxisSubvoxels{};
|
||||
std::int64_t columnTopZSubvoxels = 0;
|
||||
std::int64_t columnBottomZSubvoxels = 0;
|
||||
std::int64_t columnMinimumRadiusSubvoxels = 0;
|
||||
std::int64_t columnNominalRadiusSubvoxels = 0;
|
||||
std::int64_t columnMaximumRadiusSubvoxels = 0;
|
||||
double maximumRouteOffsetMetres = 0.0;
|
||||
double maximumBoreRadiusMetres = 0.0;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutPassageConfigV1
|
||||
{
|
||||
std::int32_t upperStrateIndex = -1;
|
||||
@@ -1270,6 +1300,17 @@ namespace VoxelForgeCore
|
||||
std::span<const ResolvedCavernRoomV1> rooms,
|
||||
std::span<const ResolvedCavernEdgeV1> edges,
|
||||
std::string& error);
|
||||
// Build a deterministic, walkable room-and-tunnel graph between the two
|
||||
// passage mouths that terminate in strateIndex. Compound rooms are emitted
|
||||
// as overlapping room primitives; the route is planned below a 15 degree
|
||||
// walking grade and every corridor carries a 4.5 m vertical clearance.
|
||||
VFCORE_API bool PlanResolvedCavernNetworkV1(
|
||||
const ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||
std::int32_t strateIndex,
|
||||
const ResolvedCavernPlannerSettingsV1& settings,
|
||||
std::vector<ResolvedCavernRoomV1>& rooms,
|
||||
std::vector<ResolvedCavernEdgeV1>& edges,
|
||||
std::string& error);
|
||||
VFCORE_API bool ResolveResolvedLayoutOreVeinsV1(
|
||||
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||
std::span<const ResolvedLayoutOreVeinRequestV1> requests,
|
||||
@@ -1836,10 +1877,6 @@ namespace VoxelForgeCore
|
||||
bool profileMesherStages = false;
|
||||
// Research-only: count passage-composer work per generated tile.
|
||||
bool profilePassageComposer = false;
|
||||
// Research-only field comparison. A nonnegative value clamps the source density
|
||||
// entering passage composition to at least -N voxel units for this tile.
|
||||
// A negative value leaves the process-level diagnostic override in control.
|
||||
double diagnosticPassageSourceFloorVoxels = -1.0;
|
||||
// Research-only field comparison. Zero keeps the unbounded passage carve;
|
||||
// a positive value bounds its signed-distance reach in metres. A negative
|
||||
// value leaves the process-level L93 setting in control.
|
||||
@@ -1932,12 +1969,6 @@ namespace VoxelForgeCore
|
||||
OreMiningYieldV1 measureOreMiningYieldForSphere(
|
||||
Vec3d centerVoxel, double radiusMetres,
|
||||
double sampleSpacingMetres = 0.25) const;
|
||||
// Audit-only view of density-field columns and short spans supporting passage voids.
|
||||
std::vector<PassageVoidCrossingAuditV1> passageVoidCrossings() const;
|
||||
bool isPassageColumnSample(std::size_t passageIndex, std::size_t segmentIndex,
|
||||
Vec3d voxelPosition) const;
|
||||
bool isPassageShortSpanSample(std::size_t passageIndex, std::size_t segmentIndex,
|
||||
Vec3d voxelPosition) const;
|
||||
// Audit-only selector provenance for the same point evaluator; never used by meshing.
|
||||
std::string densityDispatchPathAt(Vec3d voxelPosition) const;
|
||||
// Analytic source distance without the occupancy-preserving near-zero clamp or packed proxy.
|
||||
|
||||
@@ -621,6 +621,10 @@ namespace
|
||||
<<"],\"half_extents_um\":["<<room.halfExtentsMicrometres[0]<<','
|
||||
<<room.halfExtentsMicrometres[1]<<','<<room.halfExtentsMicrometres[2]
|
||||
<<"],\"floor_um\":"<<room.floorMicrometres
|
||||
<<",\"floor_profile_id\":"<<LayoutEscape(room.floorProfileStableId)
|
||||
<<",\"floor_profile_distance_um\":"<<room.floorProfileDistanceMicrometres
|
||||
<<",\"floor_gradient_q16\":["<<room.floorGradientQ16[0]<<','
|
||||
<<room.floorGradientQ16[1]<<']'
|
||||
<<",\"smooth_width_um\":"<<room.smoothWidthMicrometres
|
||||
;
|
||||
if(room.unionWidthMicrometres>0)
|
||||
@@ -647,6 +651,11 @@ namespace
|
||||
out<<'['<<p[0]<<','<<p[1]<<','<<p[2]<<']';
|
||||
}
|
||||
out<<']';
|
||||
out<<",\"floor_profile_id\":"<<LayoutEscape(edge.floorProfileStableId)
|
||||
<<",\"floor_profile_start_distance_um\":"
|
||||
<<edge.floorProfileStartDistanceMicrometres
|
||||
<<",\"floor_profile_end_distance_um\":"
|
||||
<<edge.floorProfileEndDistanceMicrometres;
|
||||
if(!edge.polylineMicrometres.empty())
|
||||
{
|
||||
out<<",\"polyline_um\":[";
|
||||
@@ -888,10 +897,7 @@ bool ResolvedLayoutSnapshotV3::finalize(std::string& error)
|
||||
else if(IsNativeLatticeFieldStratum(x))
|
||||
x.passageCapabilities.hasNativeLatticeField=true;
|
||||
else if(x.generatorKind=="FloatingIslands")
|
||||
{
|
||||
x.passageCapabilities.hasFloatingIslandLanding=true;
|
||||
x.passageCapabilities.usePassageAnchorForColumns=true;
|
||||
}
|
||||
if(i+1<strates.size())
|
||||
{
|
||||
const auto& lower=strates[i+1];
|
||||
@@ -1238,7 +1244,8 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
add("cavern_graph_vocabulary_version",
|
||||
"cavern graph vocabulary version is set without graph records");
|
||||
}
|
||||
else if(cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV1)
|
||||
else if(cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV1
|
||||
&&cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV2)
|
||||
add("cavern_graph_vocabulary_version",
|
||||
"cavern graph records require vocabulary version 1");
|
||||
if(cavernGraphVocabularyVersion!=0)
|
||||
@@ -1271,6 +1278,16 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
||(room.irregularityAmplitudeMicrometres>0
|
||||
&&room.irregularityWavelengthMicrometres<=0))
|
||||
add("cavern_room_parameters",room.stableId+" has invalid or unbounded parameters");
|
||||
const std::int64_t gradientSquared=static_cast<std::int64_t>(room.floorGradientQ16[0])
|
||||
*room.floorGradientQ16[0]+static_cast<std::int64_t>(room.floorGradientQ16[1])
|
||||
*room.floorGradientQ16[1];
|
||||
if((room.floorProfileStableId.empty()
|
||||
&&room.floorProfileDistanceMicrometres!=-1)
|
||||
||(!room.floorProfileStableId.empty()
|
||||
&&room.floorProfileDistanceMicrometres<0)
|
||||
||gradientSquared>static_cast<std::int64_t>(13108)*13108)
|
||||
add("cavern_room_floor_profile",room.stableId+
|
||||
" has an invalid shared profile reference or floor grade");
|
||||
const std::int64_t pad=room.smoothWidthMicrometres
|
||||
+room.unionWidthMicrometres+room.irregularityAmplitudeMicrometres;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
@@ -1311,6 +1328,15 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
||(edge.irregularityAmplitudeMicrometres>0
|
||||
&&edge.irregularityWavelengthMicrometres<=0))
|
||||
add("cavern_edge_parameters",edge.stableId+" has invalid or unbounded parameters");
|
||||
const bool hasFloorProfile=!edge.floorProfileStableId.empty();
|
||||
if((hasFloorProfile&&(edge.floorProfileStartDistanceMicrometres<0
|
||||
||edge.floorProfileEndDistanceMicrometres<0
|
||||
||edge.floorProfileStartDistanceMicrometres
|
||||
==edge.floorProfileEndDistanceMicrometres))
|
||||
||(!hasFloorProfile&&(edge.floorProfileStartDistanceMicrometres!=-1
|
||||
||edge.floorProfileEndDistanceMicrometres!=-1)))
|
||||
add("cavern_edge_floor_profile",edge.stableId+
|
||||
" has an invalid shared profile distance interval");
|
||||
std::array<std::int64_t,3> low=edge.controlPointsMicrometres[0];
|
||||
std::array<std::int64_t,3> high=low;
|
||||
if(nativeLattice&&!edge.polylineMicrometres.empty())
|
||||
@@ -1366,6 +1392,114 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
add("cavern_edge_polyline",edge.stableId+
|
||||
" has no bounded resolve-time polyline with matching endpoints");
|
||||
}
|
||||
std::vector<std::string> floorProfileIds;
|
||||
for(const auto& edge:cavernEdges)
|
||||
if(!edge.floorProfileStableId.empty()
|
||||
&&std::find(floorProfileIds.begin(),floorProfileIds.end(),
|
||||
edge.floorProfileStableId)==floorProfileIds.end())
|
||||
floorProfileIds.push_back(edge.floorProfileStableId);
|
||||
for(const std::string& profileId:floorProfileIds)
|
||||
{
|
||||
std::vector<const ResolvedCavernEdgeV1*> profileEdges;
|
||||
for(const auto& edge:cavernEdges)
|
||||
if(edge.floorProfileStableId==profileId)profileEdges.push_back(&edge);
|
||||
std::sort(profileEdges.begin(),profileEdges.end(),[](const auto* a,const auto* b)
|
||||
{
|
||||
return std::min(a->floorProfileStartDistanceMicrometres,
|
||||
a->floorProfileEndDistanceMicrometres)
|
||||
<std::min(b->floorProfileStartDistanceMicrometres,
|
||||
b->floorProfileEndDistanceMicrometres);
|
||||
});
|
||||
const auto endpointZ=[](const ResolvedCavernEdgeV1& edge,bool atMinimum)
|
||||
{
|
||||
const bool forward=edge.floorProfileStartDistanceMicrometres
|
||||
<edge.floorProfileEndDistanceMicrometres;
|
||||
const std::size_t point=(atMinimum==forward)?0:3;
|
||||
return edge.controlPointsMicrometres[point][2];
|
||||
};
|
||||
const auto endpointSlope=[](const ResolvedCavernEdgeV1& edge,bool atMinimum)
|
||||
{
|
||||
const double span=static_cast<double>(edge.floorProfileEndDistanceMicrometres
|
||||
-edge.floorProfileStartDistanceMicrometres);
|
||||
if(span==0.0)return std::numeric_limits<double>::infinity();
|
||||
const bool forward=edge.floorProfileStartDistanceMicrometres
|
||||
<edge.floorProfileEndDistanceMicrometres;
|
||||
const std::size_t control=atMinimum==forward?0:2;
|
||||
return 3.0*(edge.controlPointsMicrometres[control+1][2]
|
||||
-edge.controlPointsMicrometres[control][2])/span;
|
||||
};
|
||||
const ResolvedCavernEdgeV1* previous=nullptr;
|
||||
for(const auto* edge:profileEdges)
|
||||
{
|
||||
const std::int64_t low=std::min(edge->floorProfileStartDistanceMicrometres,
|
||||
edge->floorProfileEndDistanceMicrometres);
|
||||
const std::int64_t high=std::max(edge->floorProfileStartDistanceMicrometres,
|
||||
edge->floorProfileEndDistanceMicrometres);
|
||||
const double slope0=endpointSlope(*edge,true);
|
||||
const double slope1=endpointSlope(*edge,false);
|
||||
if(std::max(std::abs(slope0),std::abs(slope1))>0.200001)
|
||||
add("cavern_floor_profile_grade",edge->stableId+
|
||||
" exceeds the shared backbone 1:5 grade limit");
|
||||
if(previous)
|
||||
{
|
||||
const std::int64_t previousHigh=std::max(
|
||||
previous->floorProfileStartDistanceMicrometres,
|
||||
previous->floorProfileEndDistanceMicrometres);
|
||||
if(low!=previousHigh
|
||||
||std::abs(endpointZ(*previous,false)-endpointZ(*edge,true))>1000
|
||||
||std::abs(endpointSlope(*previous,false)-slope0)>0.000001)
|
||||
add("cavern_floor_profile_joint",profileId+
|
||||
" has a gap, height step, or slope step between adjacent edges");
|
||||
}
|
||||
previous=edge;
|
||||
(void)high;
|
||||
}
|
||||
for(const auto& room:cavernRooms)
|
||||
{
|
||||
if(room.floorProfileStableId!=profileId)continue;
|
||||
const ResolvedCavernEdgeV1* containing=nullptr;
|
||||
for(const auto* edge:profileEdges)
|
||||
{
|
||||
const auto low=std::min(edge->floorProfileStartDistanceMicrometres,
|
||||
edge->floorProfileEndDistanceMicrometres);
|
||||
const auto high=std::max(edge->floorProfileStartDistanceMicrometres,
|
||||
edge->floorProfileEndDistanceMicrometres);
|
||||
if(room.floorProfileDistanceMicrometres>=low
|
||||
&&room.floorProfileDistanceMicrometres<=high)
|
||||
{containing=edge;break;}
|
||||
}
|
||||
if(!containing)
|
||||
{
|
||||
add("cavern_room_floor_profile",room.stableId+
|
||||
" does not lie on its referenced continuous floor profile");
|
||||
continue;
|
||||
}
|
||||
const auto& edge=*containing;
|
||||
const double t=static_cast<double>(room.floorProfileDistanceMicrometres
|
||||
-edge.floorProfileStartDistanceMicrometres)
|
||||
/(edge.floorProfileEndDistanceMicrometres
|
||||
-edge.floorProfileStartDistanceMicrometres);
|
||||
const double one=1.0-t;
|
||||
const double expected=one*one*one*edge.controlPointsMicrometres[0][2]
|
||||
+3.0*one*one*t*edge.controlPointsMicrometres[1][2]
|
||||
+3.0*one*t*t*edge.controlPointsMicrometres[2][2]
|
||||
+t*t*t*edge.controlPointsMicrometres[3][2];
|
||||
std::array<double,2> routeXY{};
|
||||
for(std::size_t axis=0;axis<2;++axis)
|
||||
routeXY[axis]=one*one*one*edge.controlPointsMicrometres[0][axis]
|
||||
+3.0*one*one*t*edge.controlPointsMicrometres[1][axis]
|
||||
+3.0*one*t*t*edge.controlPointsMicrometres[2][axis]
|
||||
+t*t*t*edge.controlPointsMicrometres[3][axis];
|
||||
const double roomFloorAtProfile=room.floorMicrometres
|
||||
+room.floorGradientQ16[0]/65536.0
|
||||
*(routeXY[0]-room.centerMicrometres[0])
|
||||
+room.floorGradientQ16[1]/65536.0
|
||||
*(routeXY[1]-room.centerMicrometres[1]);
|
||||
if(std::abs(expected-roomFloorAtProfile)>1000.0)
|
||||
add("cavern_room_floor_profile",room.stableId+
|
||||
" floor does not match its referenced h(s)");
|
||||
}
|
||||
}
|
||||
}
|
||||
if(oreVeins.empty())
|
||||
{
|
||||
@@ -1529,6 +1663,12 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
readTriplet("bounds_min_um",room.boundsMinimumMicrometres);
|
||||
readTriplet("bounds_max_um",room.boundsMaximumMicrometres);
|
||||
room.floorMicrometres=LayoutInt(v,"floor_um");
|
||||
room.floorProfileStableId=LayoutString(v,"floor_profile_id");
|
||||
if(v.Get("floor_profile_distance_um").IsNumber())
|
||||
room.floorProfileDistanceMicrometres=LayoutInt(v,"floor_profile_distance_um");
|
||||
const auto& floorGradient=v.Get("floor_gradient_q16").AsArray();
|
||||
for(std::size_t axis=0;axis<2&&axis<floorGradient.size();++axis)
|
||||
room.floorGradientQ16[axis]=static_cast<std::int32_t>(floorGradient[axis].Integer());
|
||||
room.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um");
|
||||
room.unionWidthMicrometres=LayoutInt(v,"union_width_um");
|
||||
room.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
|
||||
@@ -1542,6 +1682,13 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
edge.startAnchorStableId=LayoutString(v,"start_anchor_stable_id");
|
||||
edge.endRoomStableId=LayoutString(v,"end_room_stable_id");
|
||||
edge.strateIndex=static_cast<std::int32_t>(LayoutInt(v,"strate_index"));
|
||||
edge.floorProfileStableId=LayoutString(v,"floor_profile_id");
|
||||
if(v.Get("floor_profile_start_distance_um").IsNumber())
|
||||
edge.floorProfileStartDistanceMicrometres=
|
||||
LayoutInt(v,"floor_profile_start_distance_um");
|
||||
if(v.Get("floor_profile_end_distance_um").IsNumber())
|
||||
edge.floorProfileEndDistanceMicrometres=
|
||||
LayoutInt(v,"floor_profile_end_distance_um");
|
||||
const auto& points=v.Get("control_points_um").AsArray();
|
||||
for(std::size_t i=0;i<edge.controlPointsMicrometres.size()&&i<points.size();++i)
|
||||
{
|
||||
@@ -1882,7 +2029,7 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
if(!candidate.finalize(error))return false;
|
||||
layout=std::move(candidate);return true;
|
||||
}
|
||||
candidate.cavernGraphVocabularyVersion=ResolvedLayoutCavernGraphVocabularyVersionV1;
|
||||
candidate.cavernGraphVocabularyVersion=ResolvedLayoutCavernGraphVocabularyVersionV2;
|
||||
candidate.cavernRooms.assign(rooms.begin(),rooms.end());
|
||||
candidate.cavernEdges.assign(edges.begin(),edges.end());
|
||||
const auto isNativeCavesStrate=[&candidate](std::int32_t strateIndex)
|
||||
@@ -2034,6 +2181,537 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
layout=std::move(candidate);return true;
|
||||
}
|
||||
|
||||
bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout,
|
||||
std::int32_t seed,std::int32_t strateIndex,
|
||||
const ResolvedCavernPlannerSettingsV1& settings,
|
||||
std::vector<ResolvedCavernRoomV1>& rooms,
|
||||
std::vector<ResolvedCavernEdgeV1>& edges,std::string& error)
|
||||
{
|
||||
rooms.clear();edges.clear();error.clear();
|
||||
const auto stratum=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[strateIndex](const auto& value){return value.strateIndex==strateIndex;});
|
||||
if(stratum==layout.strates.end()||!IsNativeCaveFieldStratum(*stratum))
|
||||
{error="cavern planner target is not a resolved native cave stratum";return false;}
|
||||
if(settings.roomSpacingMicrometres<=0
|
||||
||settings.minimumRoomRadiusMicrometres<500000
|
||||
||settings.maximumRoomRadiusMicrometres<settings.minimumRoomRadiusMicrometres
|
||||
||settings.maximumRoomRadiusMicrometres>100000000
|
||||
||settings.destinationRadiusMicrometres<settings.maximumRoomRadiusMicrometres
|
||||
||settings.destinationRadiusMicrometres>100000000
|
||||
||settings.floorSupportDepthMicrometres<250000
|
||||
||settings.floorSupportDepthMicrometres>2000000
|
||||
||settings.maximumTunnelLengthMicrometres<=0
|
||||
||settings.minimumTunnelRadiusMicrometres<0
|
||||
||settings.maximumTunnelRadiusMicrometres<settings.minimumTunnelRadiusMicrometres
|
||||
||settings.smoothWidthMicrometres<0
|
||||
||settings.smoothWidthMicrometres>10000000
|
||||
||settings.irregularityAmplitudeMicrometres<0
|
||||
||settings.irregularityAmplitudeMicrometres>10000000
|
||||
||(settings.irregularityAmplitudeMicrometres>0
|
||||
&&settings.irregularityWavelengthMicrometres<=0))
|
||||
{error="cavern planner settings are outside the bounded room, tunnel or relief ranges";return false;}
|
||||
|
||||
const ResolvedLayoutPassageV1* arrivalPassage=nullptr;
|
||||
const ResolvedLayoutPassageV1* departurePassage=nullptr;
|
||||
for(const auto& passage:layout.passages)
|
||||
{
|
||||
if(passage.lowerStrateIndex==strateIndex&&passage.upperStrateIndex<strateIndex
|
||||
&&(!arrivalPassage||passage.stableId<arrivalPassage->stableId))
|
||||
arrivalPassage=&passage;
|
||||
if(passage.upperStrateIndex==strateIndex&&passage.lowerStrateIndex>strateIndex
|
||||
&&(!departurePassage||passage.stableId<departurePassage->stableId))
|
||||
departurePassage=&passage;
|
||||
}
|
||||
if(!arrivalPassage||!departurePassage)
|
||||
{error="cavern planner needs an incoming lower mouth and an outgoing upper mouth in the target strate";return false;}
|
||||
if(layout.coreVoxelSizeMicrometres==0)
|
||||
{error="cavern planner requires a nonzero Core voxel size";return false;}
|
||||
|
||||
using FPoint=std::array<std::int64_t,3>;
|
||||
constexpr std::int64_t MicrometresPerMetre=1000000;
|
||||
constexpr std::int64_t MinimumBoreRadius=1750000;
|
||||
constexpr std::int64_t MinimumBoreHeight=2200000;
|
||||
constexpr std::int64_t PlannedBoreHeight=4500000;
|
||||
constexpr std::int64_t MouthOverlap=3000000;
|
||||
constexpr std::int64_t MaximumBoreRadius=10000000;
|
||||
constexpr std::int64_t MaximumWalkGradeDenominator=5; // 1:5 = 11.31 degrees.
|
||||
const auto integerSqrt=[](std::uint64_t value)
|
||||
{
|
||||
std::uint64_t result=0;
|
||||
std::uint64_t bit=std::uint64_t{1}<<62;
|
||||
while(bit>value)bit>>=2;
|
||||
while(bit!=0)
|
||||
{
|
||||
if(value>=result+bit)
|
||||
{value-=result+bit;result=(result>>1)+bit;}
|
||||
else result>>=1;
|
||||
bit>>=2;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const auto distance2d=[&](const FPoint& a,const FPoint& b)
|
||||
{
|
||||
const std::int64_t dx=b[0]-a[0],dy=b[1]-a[1];
|
||||
const auto squared=static_cast<std::uint64_t>(dx*dx+dy*dy);
|
||||
return static_cast<std::int64_t>(integerSqrt(squared));
|
||||
};
|
||||
const auto qToMicrometres=[&](std::int64_t q)
|
||||
{return q*static_cast<std::int64_t>(layout.coreVoxelSizeMicrometres)
|
||||
/WorldAssemblySubvoxelsPerVoxel;};
|
||||
const auto mouthInnerPoint=[&](const ResolvedLayoutPassageV1& passage,
|
||||
bool lowerSide,FPoint& point)
|
||||
{
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
point[axis]=qToMicrometres(lowerSide?passage.lowerMouthSubvoxels[axis]
|
||||
:passage.upperMouthSubvoxels[axis]);
|
||||
if(passage.controlPoints.size()<2)return false;
|
||||
const std::size_t mouthIndex=lowerSide
|
||||
?std::min(passage.lowerMouthControlIndex,passage.controlPoints.size()-1):0;
|
||||
const std::size_t tangentIndex=lowerSide
|
||||
?(mouthIndex>0?mouthIndex-1:1):1;
|
||||
const std::int64_t tx=passage.controlPoints[mouthIndex].positionSubvoxels[0]
|
||||
-passage.controlPoints[tangentIndex].positionSubvoxels[0];
|
||||
const std::int64_t ty=passage.controlPoints[mouthIndex].positionSubvoxels[1]
|
||||
-passage.controlPoints[tangentIndex].positionSubvoxels[1];
|
||||
const auto tangentLength=static_cast<std::int64_t>(integerSqrt(
|
||||
static_cast<std::uint64_t>(tx*tx+ty*ty)));
|
||||
if(tangentLength<=0)return false;
|
||||
point[0]+=tx*MouthOverlap/tangentLength;
|
||||
point[1]+=ty*MouthOverlap/tangentLength;
|
||||
return true;
|
||||
};
|
||||
FPoint routeStart{},routeEnd{};
|
||||
if(!mouthInnerPoint(*arrivalPassage,true,routeStart)
|
||||
||!mouthInnerPoint(*departurePassage,false,routeEnd))
|
||||
{error="cavern passage mouth is missing a usable horizontal route tangent";return false;}
|
||||
|
||||
const std::int64_t dx=routeEnd[0]-routeStart[0];
|
||||
const std::int64_t dy=routeEnd[1]-routeStart[1];
|
||||
const std::int64_t dz=routeEnd[2]-routeStart[2];
|
||||
const std::int64_t straightLength=distance2d(routeStart,routeEnd);
|
||||
const std::int64_t horizontalBias=std::clamp<std::int64_t>(
|
||||
settings.tunnelHorizontalBiasQ16,0,65536);
|
||||
const std::int64_t planGradeDenominator=MaximumWalkGradeDenominator
|
||||
+horizontalBias/32768;
|
||||
const std::int64_t requiredLength=std::abs(dz)*planGradeDenominator+1000;
|
||||
std::int64_t lateral=std::max<std::int64_t>(1000000,
|
||||
std::max(settings.tunnelWarpMicrometres,settings.caveWarpMicrometres)/2);
|
||||
const std::uint64_t routeHash=LayoutStampSeed(seed,strateIndex,
|
||||
"tunnel-network/main-route");
|
||||
if((routeHash&1u)!=0)lateral=-lateral;
|
||||
std::int64_t pathAxisX=dx,pathAxisY=dy;
|
||||
const auto makeRoute=[&](std::int64_t lateralOffset)
|
||||
{
|
||||
std::array<FPoint,4> points{};
|
||||
points[0]=routeStart;points[3]=routeEnd;
|
||||
std::int64_t axisX=dx,axisY=dy;
|
||||
std::int64_t axisLength=straightLength;
|
||||
if(axisLength<=0)
|
||||
{
|
||||
axisX=1;axisY=0;axisLength=1;
|
||||
}
|
||||
pathAxisX=axisX;pathAxisY=axisY;
|
||||
const std::int64_t perpX=-axisY*lateralOffset/axisLength;
|
||||
const std::int64_t perpY= axisX*lateralOffset/axisLength;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const std::int64_t delta=routeEnd[axis]-routeStart[axis];
|
||||
points[1][axis]=routeStart[axis]+delta/3;
|
||||
points[2][axis]=routeStart[axis]+(delta*2)/3;
|
||||
}
|
||||
points[1][0]+=perpX;points[1][1]+=perpY;
|
||||
points[2][0]-=perpX;points[2][1]-=perpY;
|
||||
return points;
|
||||
};
|
||||
const auto routeLength=[&](const std::array<FPoint,4>& points)
|
||||
{
|
||||
std::int64_t length=0;
|
||||
for(std::size_t i=1;i<points.size();++i)length+=distance2d(points[i-1],points[i]);
|
||||
return length;
|
||||
};
|
||||
auto routePoints=makeRoute(lateral);
|
||||
for(int attempt=0;routeLength(routePoints)<requiredLength&&attempt<32;++attempt)
|
||||
{
|
||||
if(lateral>0)lateral=std::min<std::int64_t>(lateral*2,1000000000ll);
|
||||
else lateral=std::max<std::int64_t>(lateral*2,-1000000000ll);
|
||||
routePoints=makeRoute(lateral);
|
||||
}
|
||||
const std::int64_t totalRouteLength=routeLength(routePoints);
|
||||
if(totalRouteLength<=0||totalRouteLength<requiredLength)
|
||||
{error="cavern planner cannot fit the fixed mouth floor difference under the 1:5 grade budget";return false;}
|
||||
|
||||
std::array<std::int64_t,4> cumulative{};
|
||||
for(std::size_t i=1;i<routePoints.size();++i)
|
||||
cumulative[i]=cumulative[i-1]+distance2d(routePoints[i-1],routePoints[i]);
|
||||
if(cumulative.back()!=totalRouteLength)
|
||||
{error="cavern planner route length accounting failed";return false;}
|
||||
const std::string backboneFloorProfileId="tunnel-network/strate-"
|
||||
+std::to_string(strateIndex)+"/backbone-floor";
|
||||
const auto routeFloorAt=[&](std::int64_t distance)
|
||||
{
|
||||
distance=std::clamp<std::int64_t>(distance,0,totalRouteLength);
|
||||
return routeStart[2]+dz*distance/totalRouteLength;
|
||||
};
|
||||
const auto routeFloorGradientAt=[&](std::int64_t distance)
|
||||
{
|
||||
distance=std::clamp<std::int64_t>(distance,0,totalRouteLength);
|
||||
std::size_t segment=1;
|
||||
while(segment<cumulative.size()-1&&distance>cumulative[segment])++segment;
|
||||
const auto unitTangent=[&](std::size_t index)
|
||||
{
|
||||
const double tx=static_cast<double>(routePoints[index][0]
|
||||
-routePoints[index-1][0]);
|
||||
const double ty=static_cast<double>(routePoints[index][1]
|
||||
-routePoints[index-1][1]);
|
||||
const double length=std::hypot(tx,ty);
|
||||
return length>0.0?std::array<double,2>{tx/length,ty/length}
|
||||
:std::array<double,2>{0.0,0.0};
|
||||
};
|
||||
auto tangent=unitTangent(segment);
|
||||
if(segment+1<cumulative.size()&&distance==cumulative[segment])
|
||||
{
|
||||
const auto next=unitTangent(segment+1);
|
||||
tangent[0]+=next[0];tangent[1]+=next[1];
|
||||
const double length=std::hypot(tangent[0],tangent[1]);
|
||||
if(length>1.0e-12){tangent[0]/=length;tangent[1]/=length;}
|
||||
}
|
||||
constexpr double Q16=65536.0;
|
||||
const double profileSlope=static_cast<double>(dz)/totalRouteLength;
|
||||
return std::array<std::int32_t,2>{
|
||||
static_cast<std::int32_t>(std::llround(tangent[0]*profileSlope*Q16)),
|
||||
static_cast<std::int32_t>(std::llround(tangent[1]*profileSlope*Q16))};
|
||||
};
|
||||
const auto pointAt=[&](std::int64_t distance)
|
||||
{
|
||||
distance=std::clamp<std::int64_t>(distance,0,totalRouteLength);
|
||||
std::size_t segment=1;
|
||||
while(segment<cumulative.size()-1&&distance>cumulative[segment])++segment;
|
||||
const std::int64_t startDistance=cumulative[segment-1];
|
||||
const std::int64_t segmentLength=std::max<std::int64_t>(1,
|
||||
cumulative[segment]-startDistance);
|
||||
const std::int64_t local=distance-startDistance;
|
||||
FPoint point{};
|
||||
for(std::size_t axis=0;axis<2;++axis)
|
||||
{
|
||||
const std::int64_t delta=routePoints[segment][axis]
|
||||
-routePoints[segment-1][axis];
|
||||
point[axis]=routePoints[segment-1][axis]+delta*local/segmentLength;
|
||||
}
|
||||
point[2]=routeFloorAt(distance);
|
||||
return point;
|
||||
};
|
||||
std::int64_t spacing=std::min(std::max<std::int64_t>(settings.roomSpacingMicrometres,
|
||||
4000000),settings.maximumTunnelLengthMicrometres);
|
||||
spacing=std::max<std::int64_t>(4000000,spacing);
|
||||
if(spacing>settings.maximumTunnelLengthMicrometres)
|
||||
spacing=settings.maximumTunnelLengthMicrometres;
|
||||
if(spacing<=0)
|
||||
{error="cavern planner has no positive route spacing after applying tunnel length limits";return false;}
|
||||
std::vector<std::int64_t> nodeDistances;
|
||||
for(std::size_t leg=1;leg<routePoints.size();++leg)
|
||||
{
|
||||
const std::int64_t legStart=cumulative[leg-1];
|
||||
const std::int64_t legLength=cumulative[leg]-legStart;
|
||||
const std::int64_t segments=std::max<std::int64_t>(1,
|
||||
(legLength+spacing-1)/spacing);
|
||||
if(segments>256)
|
||||
{error="cavern planner would exceed the 256-node bounded backbone";return false;}
|
||||
for(std::int64_t part=1;part<=segments;++part)
|
||||
{
|
||||
const std::int64_t distance=legStart+legLength*part/segments;
|
||||
if(distance>0&&distance<totalRouteLength)nodeDistances.push_back(distance);
|
||||
}
|
||||
}
|
||||
nodeDistances.push_back(totalRouteLength/2);
|
||||
std::sort(nodeDistances.begin(),nodeDistances.end());
|
||||
nodeDistances.erase(std::unique(nodeDistances.begin(),nodeDistances.end()),nodeDistances.end());
|
||||
if(nodeDistances.empty())nodeDistances.push_back(totalRouteLength/2);
|
||||
if(nodeDistances.size()>256)
|
||||
{error="cavern planner would exceed the 256-node bounded backbone";return false;}
|
||||
|
||||
const auto clampQ16=[](std::int32_t value)
|
||||
{return std::clamp<std::int64_t>(value,0,65536);};
|
||||
const std::int64_t smooth=std::clamp<std::int64_t>(
|
||||
std::max<std::int64_t>(1500000,settings.smoothWidthMicrometres),
|
||||
50000,10000000);
|
||||
const std::int64_t irregularity=std::clamp<std::int64_t>(
|
||||
settings.irregularityAmplitudeMicrometres,0,500000);
|
||||
const std::int64_t routeBoreRadius=std::clamp<std::int64_t>(
|
||||
std::max(MinimumBoreRadius,settings.minimumTunnelRadiusMicrometres),
|
||||
MinimumBoreRadius,MaximumBoreRadius);
|
||||
const std::int64_t maximumRouteBoreRadius=std::clamp<std::int64_t>(
|
||||
std::max(routeBoreRadius,settings.maximumTunnelRadiusMicrometres),
|
||||
routeBoreRadius,MaximumBoreRadius);
|
||||
const auto selectedBoreRadius=[&](const std::string& stableId)
|
||||
{
|
||||
const std::uint64_t span=static_cast<std::uint64_t>(
|
||||
maximumRouteBoreRadius-routeBoreRadius);
|
||||
return routeBoreRadius+(span==0?0:static_cast<std::int64_t>(
|
||||
LayoutStampSeed(seed,strateIndex,stableId)%(span+1)));
|
||||
};
|
||||
const auto passageBoreRadius=[&](const ResolvedLayoutPassageV1& passage,bool lowerSide)
|
||||
{
|
||||
std::int64_t radius=qToMicrometres(passage.mouthRadiusSubvoxels);
|
||||
if(!passage.controlPoints.empty())
|
||||
{
|
||||
const auto control=lowerSide?std::min(passage.lowerMouthControlIndex,
|
||||
passage.controlPoints.size()-1):std::size_t{0};
|
||||
radius=std::max(radius,qToMicrometres(passage.controlPoints[control].radiusSubvoxels));
|
||||
}
|
||||
return std::clamp<std::int64_t>(std::max(routeBoreRadius,radius),
|
||||
routeBoreRadius,MaximumBoreRadius);
|
||||
};
|
||||
const auto fractional=[](std::int64_t value,std::int64_t q16)
|
||||
{return value*std::clamp<std::int64_t>(q16,0,65536)/65536;};
|
||||
struct FRoomNode
|
||||
{
|
||||
std::string id;
|
||||
FPoint point{};
|
||||
std::string floorProfileStableId;
|
||||
std::int64_t floorProfileDistanceMicrometres=-1;
|
||||
std::array<std::int32_t,2> floorGradientQ16{};
|
||||
std::int64_t radiusMicrometres=0;
|
||||
bool destination=false;
|
||||
};
|
||||
std::vector<FRoomNode> nodes;nodes.reserve(nodeDistances.size());
|
||||
const std::int64_t floorCutMinimum=clampQ16(settings.roomFloorCutMinimumQ16);
|
||||
const std::int64_t floorCutMaximum=std::max(floorCutMinimum,
|
||||
clampQ16(settings.roomFloorCutMaximumQ16));
|
||||
const std::int64_t floorRange=floorCutMaximum-floorCutMinimum;
|
||||
const std::int64_t heightRatio=std::clamp<std::int64_t>(
|
||||
settings.roomHeightRatioQ16,6554,65536);
|
||||
const std::int64_t baseRoomSpan=settings.maximumRoomRadiusMicrometres
|
||||
-settings.minimumRoomRadiusMicrometres;
|
||||
const std::int64_t mainRouteMidpoint=totalRouteLength/2;
|
||||
for(std::size_t nodeIndex=0;nodeIndex<nodeDistances.size();++nodeIndex)
|
||||
{
|
||||
const std::int64_t distance=nodeDistances[nodeIndex];
|
||||
FRoomNode node;node.point=pointAt(distance);
|
||||
node.floorProfileStableId=backboneFloorProfileId;
|
||||
node.floorProfileDistanceMicrometres=distance;
|
||||
node.floorGradientQ16=routeFloorGradientAt(distance);
|
||||
node.destination=distance==mainRouteMidpoint;
|
||||
node.id="tunnel-network/strate-"+std::to_string(strateIndex)+"/room-"
|
||||
+std::to_string(nodeIndex);
|
||||
const std::uint64_t hash=LayoutStampSeed(seed,strateIndex,node.id);
|
||||
node.radiusMicrometres=node.destination?settings.destinationRadiusMicrometres
|
||||
:settings.minimumRoomRadiusMicrometres
|
||||
+static_cast<std::int64_t>(hash%static_cast<std::uint64_t>(baseRoomSpan+1));
|
||||
nodes.push_back(std::move(node));
|
||||
}
|
||||
const auto makeCompound=[&](const FRoomNode& node,bool destination,
|
||||
std::vector<ResolvedCavernRoomV1>& outRooms)
|
||||
{
|
||||
const std::uint64_t hash=LayoutStampSeed(seed,strateIndex,node.id+"/compound");
|
||||
const std::int64_t variety=clampQ16(settings.roomShapeVarietyQ16);
|
||||
const std::size_t additionalPrimitiveCount=static_cast<std::size_t>(
|
||||
variety*2/65536);
|
||||
const std::size_t primitiveCount=2+static_cast<std::size_t>(
|
||||
hash%(additionalPrimitiveCount+1));
|
||||
std::uint64_t state=hash;
|
||||
const std::int64_t radius=node.radiusMicrometres;
|
||||
const std::int64_t floorCutBase=clampQ16(settings.roomFloorCutMinimumQ16);
|
||||
const std::int64_t componentCount=static_cast<std::int64_t>(primitiveCount);
|
||||
for(std::int64_t component=0;component<componentCount;++component)
|
||||
{
|
||||
const std::string stableId=node.id+"/primitive-"+std::to_string(component);
|
||||
const std::uint64_t componentHash=LayoutStampNext(state);
|
||||
const std::int64_t componentScaleQ16=component==0?65536
|
||||
:42000+static_cast<std::int64_t>(componentHash%22001);
|
||||
const std::int64_t componentRadius=std::max<std::int64_t>(500000,
|
||||
fractional(radius,componentScaleQ16));
|
||||
const std::int64_t axisScaleQ16=component==0?65536
|
||||
:47000+static_cast<std::int64_t>((componentHash>>8)%19001);
|
||||
const bool swapAxes=((componentHash>>24)&1u)!=0;
|
||||
const std::int64_t halfX=swapAxes?fractional(componentRadius,axisScaleQ16)
|
||||
:componentRadius;
|
||||
const std::int64_t halfY=swapAxes?componentRadius
|
||||
:fractional(componentRadius,axisScaleQ16);
|
||||
const std::int64_t halfZ=std::max<std::int64_t>(500000,
|
||||
fractional(componentRadius,heightRatio));
|
||||
const std::int64_t floorCut=floorCutBase
|
||||
+(floorRange==0?0:static_cast<std::int64_t>(componentHash%(
|
||||
static_cast<std::uint64_t>(floorRange+1))));
|
||||
const std::int64_t offsetMagnitude=component==0?0
|
||||
:fractional(radius,12000+static_cast<std::int64_t>(componentHash%11001));
|
||||
const std::int64_t routeLengthXY=std::max<std::int64_t>(1,
|
||||
static_cast<std::int64_t>(integerSqrt(static_cast<std::uint64_t>(
|
||||
pathAxisX*pathAxisX+pathAxisY*pathAxisY))));
|
||||
const std::int64_t centerOffset=(component%2==0?1:-1)*offsetMagnitude;
|
||||
ResolvedCavernRoomV1 room;room.stableId=stableId;
|
||||
room.strateIndex=strateIndex;
|
||||
room.floorProfileStableId=node.floorProfileStableId;
|
||||
room.floorProfileDistanceMicrometres=node.floorProfileDistanceMicrometres;
|
||||
room.floorGradientQ16=node.floorGradientQ16;
|
||||
room.halfExtentsMicrometres={halfX,halfY,halfZ};
|
||||
room.centerMicrometres={node.point[0]+(-pathAxisY)*centerOffset/routeLengthXY,
|
||||
node.point[1]+pathAxisX*centerOffset/routeLengthXY,
|
||||
node.point[2]+fractional(halfZ,floorCut)};
|
||||
if(component>0)
|
||||
{
|
||||
const std::int64_t headroom=std::max<std::int64_t>(0,
|
||||
halfZ-fractional(halfZ,floorCut));
|
||||
const std::int64_t jitterRange=std::min(headroom,halfZ/5);
|
||||
const std::int64_t ceilingJitter=jitterRange>0
|
||||
?static_cast<std::int64_t>((componentHash>>32)
|
||||
%static_cast<std::uint64_t>(jitterRange+1)):0;
|
||||
room.centerMicrometres[2]+=ceilingJitter;
|
||||
}
|
||||
room.floorMicrometres=node.point[2]
|
||||
-(static_cast<std::int64_t>(room.floorGradientQ16[0])
|
||||
*(room.centerMicrometres[0]-node.point[0])
|
||||
+static_cast<std::int64_t>(room.floorGradientQ16[1])
|
||||
*(room.centerMicrometres[1]-node.point[1]))/65536;
|
||||
room.smoothWidthMicrometres=smooth;
|
||||
room.irregularityAmplitudeMicrometres=irregularity;
|
||||
room.irregularityWavelengthMicrometres=settings.irregularityWavelengthMicrometres;
|
||||
outRooms.push_back(std::move(room));
|
||||
}
|
||||
(void)destination;
|
||||
};
|
||||
for(const auto& node:nodes)makeCompound(node,node.destination,rooms);
|
||||
|
||||
const auto componentRoomId=[](const FRoomNode& node)
|
||||
{return node.id+"/primitive-0";};
|
||||
const auto makeEdge=[&](std::string id,std::string startAnchor,
|
||||
const FPoint& start,const FRoomNode& endNode,std::int64_t boreRadius,
|
||||
std::int64_t profileStartDistance,std::int64_t profileEndDistance)
|
||||
{
|
||||
ResolvedCavernEdgeV1 edge;edge.stableId=std::move(id);
|
||||
edge.startAnchorStableId=std::move(startAnchor);
|
||||
edge.endRoomStableId=componentRoomId(endNode);edge.strateIndex=strateIndex;
|
||||
edge.boreRadiusMicrometres=boreRadius;
|
||||
edge.boreHeightMicrometres=std::max(PlannedBoreHeight,MinimumBoreHeight);
|
||||
edge.floorSupportDepthMicrometres=settings.floorSupportDepthMicrometres;
|
||||
if(profileStartDistance>=0&&profileEndDistance>=0)
|
||||
{
|
||||
edge.floorProfileStableId=backboneFloorProfileId;
|
||||
edge.floorProfileStartDistanceMicrometres=profileStartDistance;
|
||||
edge.floorProfileEndDistanceMicrometres=profileEndDistance;
|
||||
}
|
||||
edge.smoothWidthMicrometres=smooth;
|
||||
edge.irregularityAmplitudeMicrometres=irregularity;
|
||||
edge.irregularityWavelengthMicrometres=settings.irregularityWavelengthMicrometres;
|
||||
const FPoint end={endNode.point[0],endNode.point[1],endNode.point[2]};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const std::int64_t delta=end[axis]-start[axis];
|
||||
edge.controlPointsMicrometres[0][axis]=start[axis];
|
||||
edge.controlPointsMicrometres[1][axis]=start[axis]+delta/3;
|
||||
edge.controlPointsMicrometres[2][axis]=start[axis]+(delta*2)/3;
|
||||
edge.controlPointsMicrometres[3][axis]=end[axis];
|
||||
}
|
||||
if(profileStartDistance>=0&&profileEndDistance>=0)
|
||||
{
|
||||
const std::int64_t floorStart=routeFloorAt(profileStartDistance);
|
||||
const std::int64_t floorEnd=routeFloorAt(profileEndDistance);
|
||||
const std::int64_t floorDelta=floorEnd-floorStart;
|
||||
edge.controlPointsMicrometres[0][2]=floorStart;
|
||||
edge.controlPointsMicrometres[1][2]=floorStart+floorDelta/3;
|
||||
edge.controlPointsMicrometres[2][2]=floorStart+(floorDelta*2)/3;
|
||||
edge.controlPointsMicrometres[3][2]=floorEnd;
|
||||
}
|
||||
return edge;
|
||||
};
|
||||
const auto arrivalPoint=routeStart;
|
||||
FPoint previousPoint=arrivalPoint;
|
||||
std::int64_t previousProfileDistance=0;
|
||||
std::string previousAnchor=arrivalPassage->stableId+"/lower-mouth";
|
||||
for(std::size_t nodeIndex=0;nodeIndex<nodes.size();++nodeIndex)
|
||||
{
|
||||
const auto& node=nodes[nodeIndex];
|
||||
const bool portal=nodeIndex==0;
|
||||
const std::string edgeId="tunnel-network/strate-"+std::to_string(strateIndex)
|
||||
+"/route-edge-"+std::to_string(nodeIndex);
|
||||
edges.push_back(makeEdge(edgeId,previousAnchor,previousPoint,node,
|
||||
portal?passageBoreRadius(*arrivalPassage,true):selectedBoreRadius(edgeId),
|
||||
previousProfileDistance,node.floorProfileDistanceMicrometres));
|
||||
previousPoint={node.point[0],node.point[1],node.point[2]};
|
||||
previousProfileDistance=node.floorProfileDistanceMicrometres;
|
||||
previousAnchor=componentRoomId(node);
|
||||
}
|
||||
if(!nodes.empty())
|
||||
{
|
||||
const std::string departureEdgeId="tunnel-network/strate-"
|
||||
+std::to_string(strateIndex)+"/departure-edge";
|
||||
edges.push_back(makeEdge(departureEdgeId,departurePassage->stableId+"/upper-mouth",
|
||||
routeEnd,nodes.back(),passageBoreRadius(*departurePassage,false),
|
||||
totalRouteLength,nodes.back().floorProfileDistanceMicrometres));
|
||||
}
|
||||
|
||||
const std::int64_t roomDensity=clampQ16(settings.roomDensityQ16);
|
||||
const std::int64_t tunnelDensity=clampQ16(settings.tunnelDensityQ16);
|
||||
const std::int64_t branchThreshold=roomDensity*tunnelDensity/65536;
|
||||
const std::size_t branchLimit=settings.originRoomMaximumConnections<=0
|
||||
?32u:static_cast<std::size_t>(std::clamp(settings.originRoomMaximumConnections,0,32));
|
||||
std::size_t branchCount=0;
|
||||
for(std::size_t nodeIndex=1;nodeIndex+1<nodes.size()
|
||||
&&branchCount<branchLimit&&rooms.size()<2048;++nodeIndex)
|
||||
{
|
||||
const auto& parent=nodes[nodeIndex];
|
||||
const std::uint64_t branchHash=LayoutStampSeed(seed,strateIndex,
|
||||
parent.id+"/side-branch");
|
||||
if(static_cast<std::int64_t>(branchHash&0xffffu)>=branchThreshold)continue;
|
||||
const std::int64_t branchLength=std::max<std::int64_t>(
|
||||
routeBoreRadius*3,std::min(spacing/2,settings.maximumTunnelLengthMicrometres/2));
|
||||
const std::int64_t direction=(branchHash&0x10000u)?1:-1;
|
||||
FRoomNode branch;branch.id=parent.id+"/side-"+std::to_string(branchCount);
|
||||
branch.radiusMicrometres=settings.minimumRoomRadiusMicrometres
|
||||
+static_cast<std::int64_t>((branchHash>>17)
|
||||
%static_cast<std::uint64_t>(baseRoomSpan+1));
|
||||
const std::int64_t pathAxisLength=std::max<std::int64_t>(1,
|
||||
static_cast<std::int64_t>(integerSqrt(static_cast<std::uint64_t>(
|
||||
pathAxisX*pathAxisX+pathAxisY*pathAxisY))));
|
||||
branch.point={parent.point[0]+(-pathAxisY)*branchLength/pathAxisLength*direction,
|
||||
parent.point[1]+pathAxisX*branchLength/pathAxisLength*direction,
|
||||
parent.point[2]};
|
||||
branch.floorProfileStableId=parent.floorProfileStableId;
|
||||
branch.floorProfileDistanceMicrometres=parent.floorProfileDistanceMicrometres;
|
||||
branch.floorGradientQ16=parent.floorGradientQ16;
|
||||
makeCompound(branch,false,rooms);
|
||||
const std::string branchEdgeId="tunnel-network/strate-"
|
||||
+std::to_string(strateIndex)+"/side-edge-"+std::to_string(branchCount);
|
||||
ResolvedCavernEdgeV1 branchEdge;
|
||||
if(settings.tunnelsFlowTowardOrigin)
|
||||
branchEdge=makeEdge(branchEdgeId,componentRoomId(branch),
|
||||
branch.point,parent,selectedBoreRadius(branchEdgeId),-1,-1);
|
||||
else
|
||||
branchEdge=makeEdge(branchEdgeId,componentRoomId(parent),
|
||||
parent.point,branch,selectedBoreRadius(branchEdgeId),-1,-1);
|
||||
// Side branches retain their cave floor SDF, but they do not cap the
|
||||
// descending backbone when their tubes overlap it near a junction.
|
||||
branchEdge.floorSupportDepthMicrometres=0;
|
||||
edges.push_back(std::move(branchEdge));
|
||||
++branchCount;
|
||||
}
|
||||
if(nodes.empty()||rooms.empty()||edges.empty())
|
||||
{error="cavern planner emitted an empty arrival-to-departure graph";return false;}
|
||||
|
||||
double maximumGradeDegrees=0.0;
|
||||
for(const auto& edge:edges)
|
||||
{
|
||||
const auto& a=edge.controlPointsMicrometres.front();
|
||||
const auto& b=edge.controlPointsMicrometres.back();
|
||||
const std::int64_t run=static_cast<std::int64_t>(integerSqrt(
|
||||
static_cast<std::uint64_t>((b[0]-a[0])*(b[0]-a[0])
|
||||
+(b[1]-a[1])*(b[1]-a[1]))));
|
||||
const std::int64_t rise=std::abs(b[2]-a[2]);
|
||||
if(run<=0&&rise>0)
|
||||
{error="cavern planner produced a vertical route edge with no walking run";return false;}
|
||||
if(run>0&&rise*MaximumWalkGradeDenominator>run)
|
||||
{error="cavern planner emitted an edge beyond its 1:5 route grade budget";return false;}
|
||||
if(run>0)maximumGradeDegrees=std::max(maximumGradeDegrees,
|
||||
std::atan2(static_cast<double>(rise),static_cast<double>(run))
|
||||
*180.0/3.14159265358979323846);
|
||||
}
|
||||
std::printf("[CavernPlannerV1] strate=%d seed=%d mouths=%s->%s route_nodes=%zu room_primitives=%zu edges=%zu branches=%zu route_horizontal_m=%.3f floor_delta_m=%.3f max_grade_deg=%.3f grade_limit_deg=11.310 clearance_radius_m=%.3f clearance_height_m=%.3f destination_radius_m=%.3f status=PASS\n",
|
||||
strateIndex,seed,arrivalPassage->stableId.c_str(),departurePassage->stableId.c_str(),
|
||||
nodes.size(),rooms.size(),edges.size(),branchCount,totalRouteLength/1.0e6,
|
||||
std::abs(dz)/1.0e6,maximumGradeDegrees,routeBoreRadius/1.0e6,PlannedBoreHeight/1.0e6,
|
||||
settings.destinationRadiusMicrometres/1.0e6);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResolveResolvedLayoutOreVeinsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
|
||||
std::span<const ResolvedLayoutOreVeinRequestV1> requests,std::string& error)
|
||||
{
|
||||
|
||||
@@ -1284,103 +1284,104 @@ namespace VoxelForgeCore
|
||||
constexpr double Q=static_cast<double>(WorldAssemblySubvoxelsPerVoxel);
|
||||
const double voxelsPerMetre=coreVoxelSizeMicrons>0
|
||||
?1000000.0/coreVoxelSizeMicrons:4.0;
|
||||
const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre;
|
||||
const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre;
|
||||
constexpr double WallReliefMetres=0.21;
|
||||
constexpr double ShellThicknessMetres=0.75;
|
||||
const double tubeExtentMetres=(WallReliefMetres+ShellThicknessMetres)
|
||||
*voxelsPerMetre;
|
||||
const double qPerMetre=Q*voxelsPerMetre;
|
||||
const double tileMinimum[3]={static_cast<double>(minimum.X),
|
||||
static_cast<double>(minimum.Y),static_cast<double>(minimum.Z)};
|
||||
const double tileMaximum[3]={static_cast<double>(maximum.X),
|
||||
static_cast<double>(maximum.Y),static_cast<double>(maximum.Z)};
|
||||
for(const auto& passage:layout.passages)
|
||||
{
|
||||
if(passage.controlPoints.size()<2)continue;
|
||||
const std::size_t routeEnd=std::clamp<std::size_t>(
|
||||
passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1);
|
||||
std::vector<double> routeDistanceMetres(routeEnd+1,0.0);
|
||||
for(std::size_t pointIndex=1;pointIndex<=routeEnd;++pointIndex)
|
||||
{
|
||||
const auto& a=passage.controlPoints[pointIndex-1].positionSubvoxels;
|
||||
const auto& b=passage.controlPoints[pointIndex].positionSubvoxels;
|
||||
const double dx=static_cast<double>(b[0]-a[0])/qPerMetre;
|
||||
const double dy=static_cast<double>(b[1]-a[1])/qPerMetre;
|
||||
const double dz=static_cast<double>(b[2]-a[2])/qPerMetre;
|
||||
routeDistanceMetres[pointIndex]=routeDistanceMetres[pointIndex-1]
|
||||
+std::sqrt(dx*dx+dy*dy+dz*dz);
|
||||
}
|
||||
const double totalRouteMetres=routeDistanceMetres[routeEnd];
|
||||
double minimumRadiusMetres=std::numeric_limits<double>::infinity();
|
||||
double minimumBodyRadiusMetres=minimumRadiusMetres;
|
||||
for(std::size_t pointIndex=0;pointIndex<=routeEnd;++pointIndex)
|
||||
{
|
||||
const double radius=static_cast<double>(
|
||||
passage.controlPoints[pointIndex].radiusSubvoxels)/qPerMetre;
|
||||
minimumRadiusMetres=std::min(minimumRadiusMetres,radius);
|
||||
if(routeDistanceMetres[pointIndex]>=10.0
|
||||
&&totalRouteMetres-routeDistanceMetres[pointIndex]>=10.0)
|
||||
minimumBodyRadiusMetres=std::min(minimumBodyRadiusMetres,radius);
|
||||
}
|
||||
if(!std::isfinite(minimumBodyRadiusMetres))
|
||||
minimumBodyRadiusMetres=minimumRadiusMetres;
|
||||
const double bodyFloorDepthMetres=std::max(0.5,
|
||||
minimumBodyRadiusMetres-0.23);
|
||||
const double upperMouthDepthMetres=std::max(0.0,
|
||||
static_cast<double>(passage.controlPoints.front().positionSubvoxels[2]
|
||||
-passage.upperMouthSubvoxels[2])/qPerMetre+0.90);
|
||||
const double lowerMouthDepthMetres=std::max(0.0,
|
||||
static_cast<double>(passage.controlPoints[routeEnd].positionSubvoxels[2]
|
||||
-passage.lowerMouthSubvoxels[2])/qPerMetre+0.90);
|
||||
const auto routeFloorDepth=[&](double alongMetres,double radiusMetres)
|
||||
{
|
||||
const double localRadiusDepth=std::max(0.0,radiusMetres-0.23);
|
||||
const double mouthDistance=std::max(0.0,std::min(alongMetres,
|
||||
totalRouteMetres-alongMetres));
|
||||
const double linear=std::clamp(mouthDistance/5.0,0.0,1.0);
|
||||
const double ramp=linear*linear*(3.0-2.0*linear);
|
||||
const double target=alongMetres<=totalRouteMetres*0.5
|
||||
?upperMouthDepthMetres:lowerMouthDepthMetres;
|
||||
return std::lerp(std::clamp(target,0.0,localRadiusDepth),
|
||||
localRadiusDepth,ramp);
|
||||
};
|
||||
std::vector<double> effectiveCenterZVoxels(
|
||||
passage.controlPoints.size(),0.0);
|
||||
for(std::size_t pointIndex=0;pointIndex<passage.controlPoints.size();
|
||||
++pointIndex)
|
||||
{
|
||||
const std::size_t radiusIndex=std::min(pointIndex,routeEnd);
|
||||
const double radius=static_cast<double>(
|
||||
passage.controlPoints[radiusIndex].radiusSubvoxels)/qPerMetre;
|
||||
const double localTubeDepth=std::min(bodyFloorDepthMetres,
|
||||
std::max(0.0,radius-0.23));
|
||||
const double routeDepth=pointIndex<=routeEnd
|
||||
?routeFloorDepth(routeDistanceMetres[pointIndex],radius)
|
||||
:std::max(0.0,radius-0.23);
|
||||
effectiveCenterZVoxels[pointIndex]=
|
||||
static_cast<double>(passage.controlPoints[pointIndex]
|
||||
.positionSubvoxels[2])/Q
|
||||
+(-routeDepth+localTubeDepth)*voxelsPerMetre;
|
||||
}
|
||||
for(std::size_t segment=1;segment<passage.controlPoints.size();++segment)
|
||||
{
|
||||
const auto& a=passage.controlPoints[segment-1];
|
||||
const auto& b=passage.controlPoints[segment];
|
||||
const double radius=static_cast<double>(std::max(
|
||||
a.radiusSubvoxels,b.radiusSubvoxels))/Q;
|
||||
const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels);
|
||||
const double low[3]={
|
||||
std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-horizontalExtent,
|
||||
std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-horizontalExtent,
|
||||
std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius
|
||||
-bridgeFootDepthVoxels};
|
||||
const double high[3]={
|
||||
std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent,
|
||||
std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent,
|
||||
std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius};
|
||||
const double extent=std::max(static_cast<double>(a.radiusSubvoxels),
|
||||
static_cast<double>(b.radiusSubvoxels))/Q+tubeExtentMetres;
|
||||
double low[3]{},high[3]{};
|
||||
for(int axis=0;axis<3;++axis)
|
||||
{
|
||||
const double aPosition=axis==2?effectiveCenterZVoxels[segment-1]
|
||||
:static_cast<double>(a.positionSubvoxels[axis])/Q;
|
||||
const double bPosition=axis==2?effectiveCenterZVoxels[segment]
|
||||
:static_cast<double>(b.positionSubvoxels[axis])/Q;
|
||||
low[axis]=std::min(aPosition,bPosition)-extent;
|
||||
high[axis]=std::max(aPosition,bPosition)+extent;
|
||||
}
|
||||
bool overlap=true;
|
||||
for(int axis=0;axis<3;++axis)
|
||||
overlap&=high[axis]>=tileMinimum[axis]&&low[axis]<=tileMaximum[axis];
|
||||
if(overlap)return true;
|
||||
}
|
||||
|
||||
// L27 can add a long support column around an unsupported passage span. Its
|
||||
// radius can exceed the route and bridge bounds above by the column wall and
|
||||
// flare allowance. The precompile shape-free proof runs before that crossing
|
||||
// plan exists, so conservatively bound every possible column from the resolved
|
||||
// route itself. The center is either the passage anchor or an average of route
|
||||
// samples; in the latter case it lies inside the route's XY bounds.
|
||||
if(passage.controlPoints.size()>=2)
|
||||
{
|
||||
double routeMinimum[3]={std::numeric_limits<double>::infinity(),
|
||||
std::numeric_limits<double>::infinity(),
|
||||
std::numeric_limits<double>::infinity()};
|
||||
double routeMaximum[3]={-std::numeric_limits<double>::infinity(),
|
||||
-std::numeric_limits<double>::infinity(),
|
||||
-std::numeric_limits<double>::infinity()};
|
||||
double maximumBoreRadius=0.0;
|
||||
for(const auto& point:passage.controlPoints)
|
||||
{
|
||||
for(int axis=0;axis<3;++axis)
|
||||
{
|
||||
const double value=static_cast<double>(point.positionSubvoxels[axis])/Q;
|
||||
routeMinimum[axis]=std::min(routeMinimum[axis],value);
|
||||
routeMaximum[axis]=std::max(routeMaximum[axis],value);
|
||||
}
|
||||
maximumBoreRadius=std::max(maximumBoreRadius,
|
||||
static_cast<double>(point.radiusSubvoxels)/Q);
|
||||
}
|
||||
|
||||
double centerMinimumX=routeMinimum[0],centerMaximumX=routeMaximum[0];
|
||||
double centerMinimumY=routeMinimum[1],centerMaximumY=routeMaximum[1];
|
||||
double maximumRouteOffset=std::hypot(
|
||||
routeMaximum[0]-routeMinimum[0],routeMaximum[1]-routeMinimum[1]);
|
||||
const auto upper=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[&passage](const auto& stratum)
|
||||
{return stratum.strateIndex==passage.upperStrateIndex;});
|
||||
if(upper!=layout.strates.end()
|
||||
&&upper->passageCapabilities.usePassageAnchorForColumns)
|
||||
{
|
||||
const auto anchor=std::find_if(upper->anchors.begin(),upper->anchors.end(),
|
||||
[](const auto& value)
|
||||
{return value.kind=="passage_anchor"&&value.values.size()>=2;});
|
||||
if(anchor!=upper->anchors.end())
|
||||
{
|
||||
const double centerX=static_cast<double>(anchor->values[0])/Q;
|
||||
const double centerY=static_cast<double>(anchor->values[1])/Q;
|
||||
centerMinimumX=centerMaximumX=centerX;
|
||||
centerMinimumY=centerMaximumY=centerY;
|
||||
maximumRouteOffset=0.0;
|
||||
for(const auto& point:passage.controlPoints)
|
||||
maximumRouteOffset=std::max(maximumRouteOffset,std::hypot(
|
||||
static_cast<double>(point.positionSubvoxels[0])/Q-centerX,
|
||||
static_cast<double>(point.positionSubvoxels[1])/Q-centerY));
|
||||
}
|
||||
}
|
||||
// The generated maximum column radius is the route offset, bore radius,
|
||||
// and 13.25 m of wall/irregularity/flare allowance.
|
||||
const double columnRadius=maximumRouteOffset+maximumBoreRadius
|
||||
+13.25*voxelsPerMetre;
|
||||
const double columnMinimum[3]={centerMinimumX-columnRadius,
|
||||
centerMinimumY-columnRadius,routeMinimum[2]-6.0*voxelsPerMetre};
|
||||
const double columnMaximum[3]={centerMaximumX+columnRadius,
|
||||
centerMaximumY+columnRadius,routeMaximum[2]+5.0*voxelsPerMetre};
|
||||
bool overlap=true;
|
||||
for(int axis=0;axis<3;++axis)
|
||||
overlap=overlap&&columnMaximum[axis]>=tileMinimum[axis]
|
||||
&&columnMinimum[axis]<=tileMaximum[axis];
|
||||
if(overlap)return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1866,44 +1867,6 @@ namespace VoxelForgeCore
|
||||
static_cast<double>(weighted)/65535.0*sampleVolume});
|
||||
return result;
|
||||
}
|
||||
std::vector<PassageVoidCrossingAuditV1> World::passageVoidCrossings() const
|
||||
{
|
||||
std::vector<PassageVoidCrossingAuditV1> result;
|
||||
if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape)return result;
|
||||
const auto& crossings=impl_->shape->GetPassageVoidCrossings();
|
||||
result.reserve(crossings.size());
|
||||
for(const auto& crossing:crossings)
|
||||
{
|
||||
PassageVoidCrossingAuditV1 value;
|
||||
value.passageIndex=crossing.passageIndex;value.firstSegment=crossing.firstSegment;
|
||||
value.lastSegment=crossing.lastSegment;value.unsupportedSamples=crossing.unsupportedSamples;
|
||||
value.unsupportedLengthMetres=crossing.unsupportedLengthMetres;
|
||||
value.startPointSubvoxels=crossing.startPoint;value.endPointSubvoxels=crossing.endPoint;
|
||||
value.needsColumn=crossing.needsColumn;
|
||||
value.connectedAtStart=crossing.connectedAtStart;
|
||||
value.connectedAtEnd=crossing.connectedAtEnd;
|
||||
value.columnAxisSubvoxels={crossing.centerXSubvoxels,crossing.centerYSubvoxels};
|
||||
value.columnTopZSubvoxels=crossing.topZSubvoxels;
|
||||
value.columnBottomZSubvoxels=crossing.bottomZSubvoxels;
|
||||
value.columnMinimumRadiusSubvoxels=crossing.minimumRadiusSubvoxels;
|
||||
value.columnNominalRadiusSubvoxels=crossing.nominalRadiusSubvoxels;
|
||||
value.columnMaximumRadiusSubvoxels=crossing.maximumRadiusSubvoxels;
|
||||
value.maximumRouteOffsetMetres=crossing.maximumRouteOffsetMetres;
|
||||
value.maximumBoreRadiusMetres=crossing.maximumBoreRadiusMetres;
|
||||
result.push_back(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
bool World::isPassageColumnSample(std::size_t passageIndex,std::size_t segmentIndex,
|
||||
Vec3d position) const
|
||||
{return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape
|
||||
&&impl_->shape->IsResolvedPassageColumnSample(passageIndex,segmentIndex,
|
||||
{position.x,position.y,position.z});}
|
||||
bool World::isPassageShortSpanSample(std::size_t passageIndex,std::size_t segmentIndex,
|
||||
Vec3d position) const
|
||||
{return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape
|
||||
&&impl_->shape->IsResolvedPassageShortSpanSample(passageIndex,segmentIndex,
|
||||
{position.x,position.y,position.z});}
|
||||
std::string World::densityDispatchPathAt(Vec3d position) const
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape
|
||||
?impl_->shape->DensityDispatchPathAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z))
|
||||
@@ -2672,30 +2635,6 @@ namespace VoxelForgeCore
|
||||
RelevantMinimum=std::min(RelevantMinimum,FeatureMinimum);
|
||||
RelevantMaximum=std::max(RelevantMaximum,FeatureMaximum);
|
||||
}
|
||||
// A resolved passage void column can add solid support above or
|
||||
// below the native heightfield's local Z range. Its finite radial
|
||||
// and vertical bounds must participate before the mesh band is clipped.
|
||||
for(const auto& Crossing:tileShape.GetPassageVoidCrossings())
|
||||
{
|
||||
if(!Crossing.needsColumn||Crossing.maximumRadiusSubvoxels<=0
|
||||
||Crossing.topZSubvoxels<=Crossing.bottomZSubvoxels)continue;
|
||||
const double Radius=static_cast<double>(Crossing.maximumRadiusSubvoxels)
|
||||
/static_cast<double>(SubvoxelScale);
|
||||
const double CenterX=static_cast<double>(Crossing.centerXSubvoxels)
|
||||
/static_cast<double>(SubvoxelScale);
|
||||
const double CenterY=static_cast<double>(Crossing.centerYSubvoxels)
|
||||
/static_cast<double>(SubvoxelScale);
|
||||
if(CenterX+Radius<TileMinimum[0]/static_cast<double>(SubvoxelScale)
|
||||
||CenterX-Radius>TileMaximum[0]/static_cast<double>(SubvoxelScale)
|
||||
||CenterY+Radius<TileMinimum[1]/static_cast<double>(SubvoxelScale)
|
||||
||CenterY-Radius>TileMaximum[1]/static_cast<double>(SubvoxelScale))continue;
|
||||
RelevantMinimum=std::min(RelevantMinimum,
|
||||
static_cast<double>(Crossing.bottomZSubvoxels)
|
||||
/static_cast<double>(SubvoxelScale));
|
||||
RelevantMaximum=std::max(RelevantMaximum,
|
||||
static_cast<double>(Crossing.topZSubvoxels)
|
||||
/static_cast<double>(SubvoxelScale));
|
||||
}
|
||||
// The native heightfield band only covers the owner terrain and its
|
||||
// manifest features. Resolved SDF stamps can carve below that band or
|
||||
// add a floor there, so include every overlapping stamp's bounded Z
|
||||
@@ -2804,7 +2743,6 @@ namespace VoxelForgeCore
|
||||
FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope(
|
||||
tileShape,queryMinimum,queryMaximum,
|
||||
options.profilePassageComposer?&out.diagnostics:nullptr,key.lod,
|
||||
options.diagnosticPassageSourceFloorVoxels,
|
||||
options.diagnosticPassageCarveReachMetres);
|
||||
passageFeatureScope.prepareSweptTubeBase(origin,MeshStep,MeshCells,
|
||||
static_cast<std::uint32_t>(world.Seed));
|
||||
@@ -2904,10 +2842,6 @@ namespace VoxelForgeCore
|
||||
{
|
||||
out.diagnostics.passageTileRouteSegmentCandidates=
|
||||
passageFeatureScope.routeSegmentCandidateCount();
|
||||
for(std::size_t passageIndex=0;
|
||||
passageIndex<impl_->resolvedLayout->passages.size();++passageIndex)
|
||||
out.diagnostics.passageTileMouthSegmentCandidates+=
|
||||
passageFeatureScope.mouthSupportSegments(passageIndex).size();
|
||||
}
|
||||
}
|
||||
FGridFillWorkerContext GridFillWorkerContext;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# VoxelForge — read-only design review: stamp tunnels along the curve instead of searching the curve per sample
|
||||
|
||||
You are reviewing a design idea against real code. **Do not edit, create or delete any file, and do not build or run
|
||||
anything.** Read code and reason. Be thorough; mark each claim as [CODE] (with file:line), [MEASURED] (from a report),
|
||||
or [INFERENCE].
|
||||
|
||||
## Context
|
||||
|
||||
VoxelForge Core (`Standalone/Core`) generates a density field: negative = solid, positive = air, 25 cm voxels, 32³ tiles,
|
||||
LOD0–8 clipmap, marching cubes. Same seed must give the same world on every machine and worker count; byte-identity with
|
||||
older versions is NOT required.
|
||||
|
||||
Inter-strate tunnels ("passages") on branch `wip/l111-halfpipe-spline` (HEAD `707bc61`) are a smooth spline swept tube:
|
||||
- a D-shaped walkable floor with a continuous floor height h(s), max grade 15°;
|
||||
- a rounded crest;
|
||||
- an open-air half-pipe where there is no rock;
|
||||
- a thin solid shell ("plain shell rule": interior always air, a thin shell always solid);
|
||||
- smooth-min where tunnels meet;
|
||||
- mouths that ramp into the ground.
|
||||
|
||||
Key code: `Standalone/Core/src/ShapeWorldInternal.h` (`FScopedPassageTileQueries`, `prepareSweptTubeBase`, around
|
||||
lines 374 and 11436), `Standalone/Core/src/VoxelForgeCore.cpp:~2747`. Reports: `Standalone/Research/WorldAssemblerM3/Linux109`
|
||||
… `Linux119` (REPORT.md each).
|
||||
|
||||
Cost today [MEASURED, L117–L119]: the smooth tunnel is 13–22% slower than the coarse L109 tunnel at LOD0–2 readiness.
|
||||
About 95% of the extra cost is in passage-feature tiles. The causes: sweep pairs (sample × curve segment), exact
|
||||
fallbacks that re-scan segments for off-grid queries, segment visits, per-tile prepare, and smooth-min.
|
||||
|
||||
## The idea to review (owner + orchestrator)
|
||||
|
||||
Today each sample "pulls": it searches for the nearest curve segment and computes a distance. Proposed: "push"/stamp.
|
||||
For each tile, walk the curve in rings (frames: position, forward, up, as with an Unreal spline mesh) and write the
|
||||
tube's signed distance into only the lattice samples near each ring. Only a narrow band of about one voxel around the
|
||||
wall needs an exact value; deep rock only needs "rock" and the tunnel interior only needs "air". Off-grid queries
|
||||
(normals/gradients at seams, gameplay point queries) would read the stamped lattice, interpolating, instead of
|
||||
re-searching the curve. Also: accept curve accuracy at one voxel (25 cm) rather than 2.5–10 cm, and build the curve
|
||||
frames once per route rather than per tile.
|
||||
|
||||
## Questions
|
||||
|
||||
1. Is the cost model right? From the code, what does a passage-feature tile do per sample today, and what would stamping
|
||||
replace? Estimate the work ratio (samples touched × ops) for a typical LOD0 tile and for LOD1–2.
|
||||
2. What breaks or gets harder, concretely in this code?
|
||||
- box verdicts/tile skipping (`ClassifyTile`-style proofs that a tile is all solid/air);
|
||||
- seams between tiles: both neighbours must stamp identical values at shared samples, deterministically;
|
||||
- LOD levels (coarser lattices);
|
||||
- off-grid `DensityAtUnclamped` queries and gradients;
|
||||
- min/smooth-min with the rest of the field and with other tunnels;
|
||||
- the floor h(s), half-pipe, shell, mouths;
|
||||
- walk audits;
|
||||
- thread safety.
|
||||
3. Is "write into the lattice" compatible with how density is evaluated (lattice fill vs point evaluation)? If the
|
||||
pipeline is pull-only, what is the smallest change that gets the push benefit? For example, a per-tile sparse
|
||||
band list built by walking rings, then lookups during fill.
|
||||
4. Is there a simpler or cheaper alternative we're missing? For example, an analytic local frame per sample from the
|
||||
spline parameter, or a coarse per-tile segment window that is already almost free.
|
||||
5. Recommendation: the design you would build, in steps, with the evidence bar (what measurement would prove or kill
|
||||
it). Keep it short.
|
||||
|
||||
Write your answer as plain Markdown, at most ~1,200 words.
|
||||
@@ -0,0 +1,53 @@
|
||||
[INFERENCE] **Build a tighter passage cache first.** Ring stamping is promising, but the code already pushes segment contributions into a tile lattice. The main opportunity is reducing coverage, overlap, and off-grid fallback work—not reversing a purely pull-based architecture.
|
||||
|
||||
References below: `H` = `Standalone/Core/src/ShapeWorldInternal.h`; `C` = `Standalone/Core/src/VoxelForgeCore.cpp`; `M` = `Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp`. Report references mean `Standalone/Research/WorldAssemblerM3/LinuxNNN/REPORT.md`.
|
||||
|
||||
**1. Cost model**
|
||||
|
||||
[CODE] `prepareSweptTubeBase` allocates per-passage `(cells+3)³` sample records, including a one-step halo. It walks candidate bounding boxes, lattice samples, then fine segments; individual segment bounds reject some pairs. Accepted pairs compute projection, deterministic square root, radius, floor profile, and potentially shell distance. A final pass flattens the selected tube and caches relief (`H:11436`, `H:11489`, `H:11521`, `H:11659`).
|
||||
|
||||
[CODE] During density evaluation, aligned samples retrieve that cache, then still perform relief weighting, floor shaping, mouth fades, shell composition, and host-field composition. Misses invoke two hierarchy searches: nearest body/floor and nearby shells—not necessarily a linear whole-route scan (`H:1247`, `H:1346`, `H:1427`, `H:1435`).
|
||||
|
||||
[MEASURED] L119 reports 29.40M preparation pairs and 24.46M segment visits across its four LOD0–2 profiles. Preparation fell 59.8% versus L118, while fallbacks increased 55–58%. L117 attributes about 95% of the tile-worker regression to passage-feature tiles. Thus the diagnosis is sound, but those counters do not establish equivalent costs per operation.
|
||||
|
||||
[INFERENCE] For an illustrative straight crossing, assume exposed surface perimeter \(P=16\) m, tile crossing length \(L=32\Delta\), and exact band ±one lattice step. Unique band samples are approximately \(2PL/\Delta^2\):
|
||||
|
||||
| [INFERENCE] Normal tile | LOD0 | LOD1 | LOD2 |
|
||||
|---|---:|---:|---:|
|
||||
| Spacing Δ | 0.25 m | 0.5 m | 1 m |
|
||||
| Band samples | 4,096 | 2,048 | 1,024 |
|
||||
| Fraction of 35³ prepared grid | 9.6% | 4.8% | 2.4% |
|
||||
|
||||
[INFERENCE] These are geometric estimates, not measured “typical tiles”; clipping, floor/outer-shell surfaces, bends, and crossings change them. If today fraction \(f\) of the grid receives \(k\) segment evaluations, and stamping averages \(m\) contributions per band sample, the expensive-work ratio is approximately `(table fraction × m)/(f × k)`, multiplied by relative operation cost. For `f=0.5, k=2, m=2`, that is 19%, 9.6%, 4.8%. Allocation, classification, host evaluation, and composition remain. Dense rings can make \(m\) large and erase this advantage.
|
||||
|
||||
**2. Concrete difficulties**
|
||||
|
||||
- [CODE] **Skipping:** passage-bound overlap currently vetoes a uniform proof and returns `Mixed` (`H:5684`). [INFERENCE] Retain conservative bounds, expanded for approximation and reconstruction support. “No band samples written” proves neither solid nor air: a thin shell can lie between samples.
|
||||
|
||||
- [CODE] **Seams:** cache coordinates derive from integer world coordinates; nearest ties use segment index (`H:11448`, `H:11564`, `H:11640`). [INFERENCE] Generate rings from route-global parameters, never tile-local stepping. Include every contributor affecting shared samples and halos; use stable ordering and arithmetic. Independently clipping ring generation at tile boundaries risks disagreement.
|
||||
|
||||
- [INFERENCE] **LOD:** distinguish 25 cm curve error from lattice spacing. A fixed 25 cm band is insufficient around 1 m cells; cover interpolation corners and gradient support. A 0.75 m shell can disappear at coarse resolution. Independently reconstructed LOD fields also disagree off-grid; define one canonical point-query field.
|
||||
|
||||
- [CODE] **Off-grid:** `sweptSample` rejects non-lattice coordinates; public point queries call `DensityAtUnclamped` without requiring a tile cache (`H:11721`; `C:1875`). Gradients normally use density-grid values, with unclamped fallback (`M:1972`). [INFERENCE] Interpolation needs all eight valid corners, including outside loaded tiles. It changes gradients and zero crossings; interpolating only passage channels also differs from interpolating the fully composed field.
|
||||
|
||||
- [CODE] **Composition:** passage bodies and shells use separate hard minima; final composition includes host/native-floor exceptions (`H:1496–1519`). The floor corner uses smooth-max, implemented through smooth-min (`H:3493`, `H:6940`). [INFERENCE] Do not assume the measured smooth-min cost is tunnel-to-tunnel joining. Preserve separate void/shell channels and composition order. Arbitrary “rock/air” magnitudes can move interpolated crossings and smooth blends.
|
||||
|
||||
- [CODE] **Shape:** floor selection is independent of minimum round distance; the code explicitly warns that minimizing flattened segment distances makes downhill floors jump (`H:1328`). Floor evaluation uses horizontal projection, authored `h(s)`, and XY mouth blending (`H:3448`). Shells have rounded crests and five-metre mouth fades (`H:1461`, `H:3503`). [INFERENCE] A conventional spline-normal D-section changes this geometry. Stamped isolated rings can scallop; rasterize bounded slabs between frames while preserving gravity-relative floors, shell unions, and interior priority.
|
||||
|
||||
- [MEASURED] **Walkability:** L119’s maximum sampled floor-root offset was 7.884 mm; L113 documented voxel-aligned floor ribs. [INFERENCE] A 25 cm curve tolerance does not authorize 25 cm floor error or guarantee ≤15° grade. Audit reconstructed density and meshes, including mouths, overlaps, support, and headroom—not only planner `h(s)`.
|
||||
|
||||
- [CODE] **Threads:** copied worker scopes share prepared data through a shared pointer; active scope is thread-local (`H:353`, `H:11411`; `C:2854`). [INFERENCE] Finish stamping before publication. Parallel ring writes require deterministic reductions; mutable shared lazy caches introduce synchronization and lifetime problems.
|
||||
|
||||
**3. Smallest useful change**
|
||||
|
||||
[CODE] Preparation already precedes `GenerateMesh`, whose fill calls lattice evaluators (`C:2747`, `C:2892`; `M:1488`). Route segments are already prepared globally with 10 cm chord tolerance and an 8 m spacing cap (`H:4601`).
|
||||
|
||||
[INFERENCE] Replace preparation’s broad pair enumeration with conservative slab-to-grid spans or small spatial bins. Keep indexed lookups during pull-based fill. Store body/floor ownership and shell contributions separately; use explicit outside/interior/band states. A dense index plus compact records may beat a sparse hash map. Additional frames can be route-global, but this does not eliminate existing per-tile rasterization.
|
||||
|
||||
**4. Alternatives and recommendation**
|
||||
|
||||
[MEASURED] L115 already reduced coarse candidates 74.1% without reducing fine pairs. L118’s restriction of fallbacks to prepared spans changed output; L116’s 20.8% pair reduction did not recover readiness.
|
||||
|
||||
[INFERENCE] An analytic local frame still needs a parameter lookup, especially near folds. Prefer globally indexed conservative segment bins, shared by preparation and exact point queries.
|
||||
|
||||
[INFERENCE] Build in three steps: instrument unique samples, overlap, misses by caller, memory, and per-LOD time; implement tighter bins/slabs while preserving geometry; then separately trial 25 cm curve tolerance and canonical interpolation. Accept only reproducible near-readiness gains against L119/L109 plus unchanged walk/seam requirements and cross-compiler/worker determinism. Kill the approach if saved distance work becomes rasterization, memory, or reconstruction cost.
|
||||
@@ -0,0 +1,53 @@
|
||||
# VoxelForge — read-only design review: per-block bit flags (masks) to skip work
|
||||
|
||||
You are reviewing an idea against real code. **Do not edit, create or delete any file, and do not build or run
|
||||
anything.** Read code and reason. Be thorough; mark each claim as [CODE] (with file:line), [MEASURED] (from a report),
|
||||
or [INFERENCE].
|
||||
|
||||
## Context
|
||||
|
||||
VoxelForge Core (`Standalone/Core`) generates a density field: negative = solid, positive = air, 25 cm voxels, 32³ tiles,
|
||||
LOD0–8 clipmap, marching cubes. The production mesher for the standalone path is in `Standalone/Source/...`, and Core is
|
||||
also compiled into Unreal. Same seed must give the same world on every machine and worker count; byte-identity with
|
||||
older versions is NOT required.
|
||||
|
||||
Branch `assembler/world` (HEAD `7022d02`'s parent line, currently `4041d4b`) has the smooth inter-strate tunnels.
|
||||
Readiness target: LOD0–2 near terrain in ~1 s on arrival. Measured today: SW ~0.7–0.8 s, VS ~0.9 s, FI/TN ~1.0 s
|
||||
headless. In Linux UE, start→LOD0–2 is ~2.2–2.5 s. At 30 m/s in UE, near coverage is ~40%.
|
||||
|
||||
Reports to consult: `Standalone/Research/WorldAssemblerM3/Linux1*/REPORT.md`, especially:
|
||||
- L77/L87/L117, cost census;
|
||||
- L124, shape gate and per-strate timings;
|
||||
- L125/L126/L128, tunnel prepare: per-segment work, 67–73% pair overlap, support up to 7.9 m;
|
||||
- L128, where an exact one-owner slab scheme halved pairs but got slower because of per-sample ownership tests.
|
||||
|
||||
Tile skipping already exists at tile level: box verdicts / classify, where all-solid or all-air tiles are skipped.
|
||||
|
||||
## The idea (from the owner)
|
||||
|
||||
Keep bit flags per block (1 bit per voxel, or per small brick of e.g. 4³ or 8³ voxels): for example "certainly deep
|
||||
solid", "certainly deep air", "near a surface", "touched by a tunnel", "already evaluated". Use them to skip work in
|
||||
generation and meshing, and possibly elsewhere (collision, streaming, edits, AI/nav, lighting).
|
||||
|
||||
## Questions
|
||||
|
||||
1. Where does time actually go per tile today, and which of that work a mask could skip? Cover density fill,
|
||||
classification, mesher (marching cubes, normals/gradients, skirts, seam refinement), passage prepare, and composition.
|
||||
Use measured census numbers where they exist.
|
||||
2. Which flags are sound? A flag must be a proof, never a guess. A mask "certainly solid" must be derived from a
|
||||
conservative bound (the bound must be the supremum of what the evaluator produces), or the mesher will delete
|
||||
surfaces. Which bounds already exist in the code (box verdicts, interval bounds) that a brick-level mask could reuse?
|
||||
Name the existing hazards. For example, `Identity` means `Sdf >= T`, and `ClassifyTile` proving below-TerrainZ solid
|
||||
would delete 3D caves.
|
||||
3. Granularity: per-voxel bits vs per-brick (4³/8³) vs per-column. What does each cost to build, versus what it
|
||||
saves? Consider SIMD and cache behaviour. Marching cubes needs only cells whose corner signs differ, and gradients
|
||||
need a halo.
|
||||
4. "Already evaluated" or "checked" flags: where would they be correct (memoisation of identical queries), and where
|
||||
would they be wrong (nearest-of-many queries like the tunnel segments)? Is there repeated identical evaluation today,
|
||||
for example halo samples shared between neighbouring tiles, or skirt/seam re-queries?
|
||||
5. Other uses: player edits (diff layer), collision-only server tiles, streaming priority, nav. Which would a mask
|
||||
genuinely help, given the multiplayer rule (replicate seed + layout + diff, never geometry)?
|
||||
6. Recommendation: the one or two mask designs most likely to pay off, with the expected gain (a range, marked
|
||||
inference), the measurement that would prove or kill each, and the risks. Keep it short.
|
||||
|
||||
Write plain Markdown, at most ~1,200 words.
|
||||
@@ -0,0 +1,83 @@
|
||||
[INFERENCE] **Improve the existing brick-proof system; don’t add a general “checked voxel” mask.** The promising savings are avoided field/composition work and passage preparation, rather than marching-cubes classification.
|
||||
|
||||
References below abbreviate these files: [M = production mesher](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp), [S = ShapeWorldInternal.h](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h), [C = VoxelForgeCore.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp), [O = VoxelDensityOpStack.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelDensityOpStack.cpp). L-numbers identify `Standalone/Research/WorldAssemblerM3/LinuxN/REPORT.md`.
|
||||
|
||||
[INFERENCE] Evidence limitation: Linux128 is absent from this checkout. Its supplied 7.9 m support and slower one-owner result remain unverified here. The reports below span different revisions; their percentages must not be presented as today’s census.
|
||||
|
||||
**1. Where time goes**
|
||||
|
||||
[MEASURED] L77’s exclusive FI/TN LOD0–2 worker account:
|
||||
|
||||
| Work | FI | TN |
|
||||
|---|---:|---:|
|
||||
| Total worker CPU/view | 23.02 s | 21.50 s |
|
||||
| Passage composition | 65.9% | 65.4% |
|
||||
| Support | 14.2% | 13.5% |
|
||||
| Base field | 3.2% | 3.3% |
|
||||
| Bands / dispatch | 2.3% / 3.7% | 2.6% / 3.6% |
|
||||
| MC / skirts / refinement, excluding field work | 1.6% / 0.1% / 7.5% | 1.2% / 0.2% / 8.6% |
|
||||
|
||||
[MEASURED] Grid fill occupied 19.058/17.377 worker-seconds, **overlapping** those phase categories. L117’s later census found support zero; its smooth-tunnel regression concentrated about 95% of added tile CPU in passage-feature tiles. Cell classification stayed flat; gradients became cheaper. Thus L77 identifies historical leverage, not current percentages.
|
||||
|
||||
[MEASURED] L125 preparation averaged 514/130/4.1 ms worker work at LOD0/1/2. LOD0 evaluated 3.11–8.49 million segment/sample pairs with 67–72% overlap, although the exact band occupied only 5–7% of grid samples. L126 reduced floor-profile calls 20–23% without useful readiness improvement.
|
||||
|
||||
[CODE] Brick skipping already exists: configurable block verdicts, required-sample masks, and active-block gradient halos (M:646,1053). MC already rejects cells without sign crossings before interpolation/gradients (M:2186). Gradient memoisation exists (M:1956). Skirts issue separate density queries (M:2491); refinement has its own stage (C:2975).
|
||||
|
||||
[INFERENCE] A pre-evaluation proof can save density, composition, and subsequent MC work. A sign mask constructed after density evaluation saves only downstream scanning. Classification itself costs work; no isolated current classification percentage is established by these reports.
|
||||
|
||||
[MEASURED] L124’s shape gate alone took 179–226 ms, with no near-tile admissions before completion. L87 additionally measured a 107–153 ms far-parent gate and no headless collision wait. Tile masks cannot directly remove these gates or explain the supplied UE/headless gap.
|
||||
|
||||
**2. Sound flags and hazards**
|
||||
|
||||
[INFERENCE] For final density \(D\), solid requires an upper bound covering the evaluator’s supremum, \(U<0\); air requires a valid lower bound \(L\ge0\). “Deep” requires an explicit margin. Unknown must stay unknown. A crossing proves an MC-active cell; uniform sampled signs do not prove an entire continuous volume surface-free.
|
||||
|
||||
[CODE] Reusable proofs include:
|
||||
|
||||
- S:5428: halo-expanded box classification.
|
||||
- S:5974: exact surface-column extrema over the MC lattice.
|
||||
- S:6019: floating-island density intervals.
|
||||
- S:5197,6032: native cave intervals.
|
||||
- S:5949: stamp interval composition with outward numerical padding.
|
||||
- O:6998: operation-specific inactivity from SDF lower bounds.
|
||||
|
||||
[CODE] These proofs have guards: passage overlap returns unknown (S:5687); unsupported stamp composition returns unknown (S:5655); stratum crossings are rejected (S:5900). The mesher distinguishes uniform candidates from `bSkipSafe` certificates (M:715,756), and otherwise validates candidates against actual samples (M:1838).
|
||||
|
||||
[CODE] For gated modifiers, `Identity` can mean `Sdf >= T`, not `abs(Sdf) >= T` (O:1496,6998). Noise bounds cannot assume nominal `[-1,1]`: FBM and unclamped cellular bounds are explicitly discussed at O:848,301.
|
||||
|
||||
[INFERENCE] Additional hazards: confusing internal SDF with final density; certifying base terrain before caves, tunnels, shells, stamps or edits; using lattice proofs for off-grid queries; ignoring rounding or gradient halos. A below-`TerrainZ` solid rule applied to 3D caves deletes caves. “Touched by tunnel” should mean conservative candidate overlap; only **proven untouched** authorizes omission.
|
||||
|
||||
**3. Granularity**
|
||||
|
||||
[INFERENCE] Storage per flag for 32³ cells:
|
||||
|
||||
| Granularity | Storage | Construction/tradeoff |
|
||||
|---|---:|---|
|
||||
| Per cell | 4 KiB | 32,768 decisions; expensive unless derived during necessary work |
|
||||
| 4³ bricks | 64 B | 512 proofs; tighter boundaries, more proof/halo overhead |
|
||||
| 8³ bricks | 8 B | 64 proofs; cheaper, more mixed bricks |
|
||||
| XY columns | 128 B | 1,024 decisions; useful mainly for heightfield-specific proofs |
|
||||
|
||||
[INFERENCE] These exclude corner/halo samples, metadata and additional flags. A column bit cannot represent multiple cave intervals. Brick runs preserve contiguous X sampling and SIMD better than scattered per-sample branches; bit packing alone does not establish a speedup.
|
||||
|
||||
[CODE] The existing implementation uses byte arrays and preserves exact samples around active bricks (M:1053–1083). Replacing densities with arbitrary same-sign constants near active cells would corrupt interpolated positions or normals.
|
||||
|
||||
**4. “Already evaluated”**
|
||||
|
||||
[CODE] Passage preparation precedes meshing (C:2747). Its valid sentinel is initialization, not completion: subsequent segments update floor selection, shell minima and round-distance winners, with deterministic tie-breaking (S:11520,11615,11688).
|
||||
|
||||
[INFERENCE] Therefore first-visitor wins is invalid. Memoisation is valid only for identical queries against identical seed/layout/evaluator/edit revision and query semantics; cache the value, not just a bit.
|
||||
|
||||
[MEASURED] L77 counted 8.6–8.9% halo points, without measuring duplicate coordinates. L125 verified 4,900 shared samples across four neighbour pairs and still recorded substantial off-grid skirt/midpoint fallbacks.
|
||||
|
||||
[CODE] Shared-grid reuse and gradient validity caches already exist (M:1031,1956). [INFERENCE] Skirt/seam queries are reuse candidates only when coordinates and semantics actually match; “same voxel” is insufficient.
|
||||
|
||||
**5. Other uses**
|
||||
|
||||
[INFERENCE] Edit-dirty bricks can localize regeneration, with invalidation expanded through normals, seams and collision dependencies. Collision-only servers can skip certified empty regions, but still need boundary geometry. Streaming can prioritize potentially occupied bricks; nav and lighting need clearance/connectivity or visibility data beyond occupancy. Keep masks derived locally from replicated seed + layout + diff, versioned with that state.
|
||||
|
||||
**6. Recommendation**
|
||||
|
||||
- [INFERENCE] **Tune existing 4³/8³ certificates**, caching proofs and emitting contiguous required-sample runs. Plausible incremental near-ready saving: **0–5%**, with regressions possible. Measure proof time, samples avoided, halo expansion, total worker CPU and UE readiness.
|
||||
- [INFERENCE] **Pass required-region information into passage preparation**, rejecting whole segment/brick regions conservatively. Plausible near-ready saving: **0–10%**, highly uncertain because passage overlap currently vetoes proofs. Kill it if pair reduction fails to reduce preparation and end-to-end time.
|
||||
|
||||
[INFERENCE] Both require seam/off-grid validation, adversarial bound checks, edit invalidation tests, and cross-machine/worker determinism. Preserve canonical tie-breaking; old-version byte identity need not constrain a sound redesign.
|
||||
@@ -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,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure the fixed-depth floor and derived centreline against the authored grade."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
LAYOUT = ROOT / "Saved/Linux/l109/audit-layout-final-stamp.json"
|
||||
OUT = ROOT / "Saved/Linux/l109/floor-ramp-grade-final.json"
|
||||
MOUTH_RAMP_METRES = 5.0
|
||||
|
||||
|
||||
def route_m(layout: dict, passage: dict):
|
||||
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
|
||||
controls = [(tuple(float(v) * scale for v in row["position_q"]),
|
||||
float(row["radius_q"]) * scale) for row in passage["control_points"]]
|
||||
lower_q = passage["lower_mouth_q"]
|
||||
player_center_above_floor_q = round(
|
||||
1.13 * 256.0 * 1_000_000.0 / layout["core_voxel_size_um"])
|
||||
lower_index = next((index for index, row in enumerate(passage["control_points"])
|
||||
if row["position_q"][0] == lower_q[0]
|
||||
and row["position_q"][1] == lower_q[1]
|
||||
and row["position_q"][2] - row["radius_q"]
|
||||
+ player_center_above_floor_q == lower_q[2]), None)
|
||||
if lower_index is None:
|
||||
raise RuntimeError(f"{passage['stable_id']}: lower mouth control is unbound")
|
||||
return controls[:lower_index + 1]
|
||||
|
||||
|
||||
def maximum_grade(profile):
|
||||
grades = []
|
||||
for (p0, z0), (p1, z1) in zip(profile, profile[1:]):
|
||||
horizontal = math.hypot(p1[0] - p0[0], p1[1] - p0[1])
|
||||
if horizontal > 1.0e-8:
|
||||
grades.append(math.degrees(math.atan2(abs(z1 - z0), horizontal)))
|
||||
return max(grades, default=0.0)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
layout = json.loads(LAYOUT.read_text(encoding="utf-8"))
|
||||
rows = []
|
||||
for passage in layout["passages"]:
|
||||
route = route_m(layout, passage)
|
||||
segment_lengths = [math.dist(a[0], b[0]) for a, b in zip(route, route[1:])]
|
||||
prefix = [0.0]
|
||||
for length in segment_lengths:
|
||||
prefix.append(prefix[-1] + length)
|
||||
interior_radii = [radius for distance, (_, radius) in zip(prefix, route)
|
||||
if distance >= 10.0 and prefix[-1] - distance >= 10.0]
|
||||
minimum_body_radius = min(interior_radii or [radius for _, radius in route])
|
||||
body_depth = max(0.5, minimum_body_radius - 0.23)
|
||||
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
|
||||
upper_target = max(0.0, route[0][0][2]
|
||||
- passage["upper_mouth_q"][2] * scale + 1.13)
|
||||
lower_target = max(0.0, route[-1][0][2]
|
||||
- passage["lower_mouth_q"][2] * scale + 1.13)
|
||||
floor_profile = []
|
||||
raw_fixed_depth_profile = []
|
||||
maximum_centerline_shift = 0.0
|
||||
for segment_index, (a, b, length) in enumerate(
|
||||
zip(route, route[1:], segment_lengths)):
|
||||
steps = max(1, math.ceil(length / 0.05))
|
||||
for step in range(steps + 1):
|
||||
if floor_profile and step == 0:
|
||||
continue
|
||||
t = step / steps
|
||||
position = tuple(a[0][axis] + (b[0][axis] - a[0][axis]) * t
|
||||
for axis in range(3))
|
||||
radius = a[1] + (b[1] - a[1]) * t
|
||||
along = prefix[segment_index] + length * t
|
||||
mouth_distance = max(0.0, min(along, prefix[-1] - along))
|
||||
u = max(0.0, min(1.0, mouth_distance / MOUTH_RAMP_METRES))
|
||||
ramp = u * u * (3.0 - 2.0 * u)
|
||||
local_radius_depth = max(0.0, radius - 0.23)
|
||||
mouth_target = upper_target if along <= prefix[-1] * 0.5 else lower_target
|
||||
mouth_depth = max(0.0, min(radius, mouth_target))
|
||||
route_depth = mouth_depth + (local_radius_depth - mouth_depth) * ramp
|
||||
tube_depth = min(body_depth, local_radius_depth)
|
||||
effective_center = position[2] - route_depth + tube_depth
|
||||
floor_profile.append((position, effective_center - tube_depth))
|
||||
raw_fixed_depth_profile.append((position, position[2] - tube_depth))
|
||||
maximum_centerline_shift = max(maximum_centerline_shift,
|
||||
abs(effective_center - position[2]))
|
||||
rows.append({
|
||||
"passage": passage["stable_id"],
|
||||
"path_m": prefix[-1],
|
||||
"body_floor_depth_m": body_depth,
|
||||
"mouth_blend_m": MOUTH_RAMP_METRES,
|
||||
"preserved_route_floor_max_grade_deg": maximum_grade(floor_profile),
|
||||
"unshifted_fixed_depth_floor_max_grade_deg": maximum_grade(
|
||||
raw_fixed_depth_profile),
|
||||
"maximum_derived_centerline_shift_m": maximum_centerline_shift,
|
||||
})
|
||||
OUT.write_text(json.dumps({
|
||||
"method": "0.05 m route samples; fixed body depth from the minimum interior radius; "
|
||||
"cubic smoothstep mouth blend",
|
||||
"passages": rows,
|
||||
}, indent=2) + "\n", encoding="utf-8")
|
||||
for row in rows:
|
||||
print(f"[L109FloorRamp] {row['passage']} "
|
||||
f"depth_m={row['body_floor_depth_m']:.3f} "
|
||||
f"preserved_grade_deg={row['preserved_route_floor_max_grade_deg']:.3f} "
|
||||
f"unshifted_fixed_depth_grade_deg={row['unshifted_fixed_depth_floor_max_grade_deg']:.3f} "
|
||||
f"max_center_shift_m={row['maximum_derived_centerline_shift_m']:.3f}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,70 @@
|
||||
# Linux 109 — tunnel floor and shell
|
||||
|
||||
Baseline: `assembler/world` HEAD `95cf8de10723f2dffe7b3dfc116ecf350c7114ae`. Candidate source stamp: `9457b9bed7d57a4a387828961f9dd53632ec602eb5ea8f247b087be6ca5cc312` (evaluator v217).
|
||||
|
||||
## Shape
|
||||
|
||||
The per-tile swept tube now uses a horizontal flat floor, a 0.50 m smooth floor-to-wall corner, and a 0.75 m shell. Body floor depth is fixed per passage at the minimum interior radius minus 0.23 m, capped at the local radius. Five-metre cubic blends ramp both mouths into their ground; the lower mouth follows the matching authored native-cave floor where one exists. The route centreline shifts through radius tapers to keep that floor walkable. A small wall relief remains for naturalness.
|
||||
|
||||
The passage interiors are unioned first and always win over every passage shell, including at coiled crossings. The shell writes solid only where the host field is air; rock remains unchanged. There is no rock classification. Open-air shell and interior samples pass in the owner audit.
|
||||
|
||||
## Removed and retained
|
||||
|
||||
Removed route support restoration, deferred support after later bores, support columns, mouth-support restoration, and their floor-specific capsule checks and profiling records. Replaced the old open-air/column audit gates with the shell audit and removed the no-long-bridge and rock-column gates, which measured the deleted support mechanism. Walkability, capsule, headroom, grade, mouth, naturalness, and ripple gates remain. Floor-ripple probes fully masked by another passage's interior air are skipped only when the overlap is explicit; capsule walkability still has to pass.
|
||||
|
||||
Kept the native Lattice field's explicitly authored below-floor substrate because its spine needs it for walkability; this is part of that native field, not route support restoration. The legacy landmark demo retains its compatibility floor-cap path, which is separate from the resolved route sweep. Native Caves and Lattice fast checks pass.
|
||||
|
||||
## Performance
|
||||
|
||||
Five interleaved L80 pairs per cell, after one warmup for each binary; seconds are median `LOD0–2 near-ready / whole-view-ready`. Baseline binary is from `assembler/world` HEAD. Tile counts matched within each cell.
|
||||
|
||||
| Strate | W8 near | W8 whole | W12 near | W12 whole |
|
||||
|---|---:|---:|---:|---:|
|
||||
| SW | 0.680 → 0.737 (+8.3%) | 1.686 → 1.674 (−0.7%) | 0.700 → 0.761 (+8.8%) | 1.643 → 1.644 (+0.1%) |
|
||||
| FI | 1.455 → 0.911 (−37.4%) | 2.419 → 1.772 (−26.8%) | 1.394 → 0.863 (−38.0%) | 2.288 → 1.664 (−27.3%) |
|
||||
| TN | 1.423 → 0.956 (−32.8%) | 2.310 → 1.778 (−23.0%) | 1.294 → 0.895 (−30.9%) | 2.132 → 1.661 (−22.1%) |
|
||||
| VS | 1.008 → 0.789 (−21.8%) | 1.917 → 1.639 (−14.5%) | 0.854 → 0.738 (−13.5%) | 1.716 → 1.541 (−10.2%) |
|
||||
|
||||
There is no broad regression. SW near-ready is about 0.06 s slower; its whole-view time is flat. **Measured:** SW LOD0–2 skirt triangles rise from 133,894 to 196,602, and summed skirt work rises from 455 to 743 ms at W8 and 650 to 1,044 ms at W12. **Inference:** the added open-air shell surface and skirts explain the small SW near-ready cost. FI, TN, and VS improve at both readiness marks.
|
||||
|
||||
## L77 phase census
|
||||
|
||||
Two samples per case. Entries are `share / estimated worker-s`; estimates use weighted L77 samples times `WorldAssemblerLODWorker` CPU, so they are not direct phase timers.
|
||||
|
||||
| Case | Base passage | Base support | Candidate passage | Candidate support |
|
||||
|---|---:|---:|---:|---:|
|
||||
| FI near | 44.62% / 4.108 | 14.84% / 1.366 | 22.90% / 1.133 | 0% / 0 |
|
||||
| FI whole | 29.31% / 4.874 | 8.08% / 1.344 | 10.59% / 1.206 | 0% / 0 |
|
||||
| TN near | 38.57% / 3.318 | 14.74% / 1.267 | 19.43% / 1.019 | 0% / 0 |
|
||||
| TN whole | 24.48% / 3.824 | 7.59% / 1.186 | 9.25% / 1.074 | 0% / 0 |
|
||||
|
||||
The sampled support share disappears. Baseline estimates were 1.19–1.37 worker-s across these cases, below the 1.6–1.9 s expectation.
|
||||
|
||||
## Audits and determinism
|
||||
|
||||
The full-view all-LOD audit passes: 21,425/21,425 canonical passage centreline samples are open. All three interfaces pass capsule and support checks with zero blocked or unsupported samples, zero grade failures, and both mouths bound (2/2). Capsule samples: 266,175 (0–1), 15,120 (1–2), 64,764 (2–3). The shell audit sampled 89,365 floor points, 8,423 wall points, and 10,527 interior points: zero floor, wall, or interior violations; 22,755 open-air shell samples. Naturalness, mouth profile, and ripple summaries pass 3/3, with negative controls detected. Derived floor grade peaks at 14.49°; fast native Caves headroom is 0.30 m and native Lattice headroom is 2.20 m.
|
||||
|
||||
Fast tier: PASS, 259 PASS checks plus 5 diagnostics across 80 runs. The Clang/GCC hash matrix matches at W1/W8/W12 for the owner four-strata layout and both two-strata canonical recipes. The v217 canonical pins were written for Surface World style 1 and Floating Islands style 2.
|
||||
|
||||
Because Core sources are Unreal-compiled, the Linux VFHostEditor build passed with `-NoUBA -MaxParallelActions=4` (27 actions, 67.4 s). Three FI and three SW `after` arrival traces also passed, each settling its full requested LOD0–2 set. Unreal processes had no `prlimit`.
|
||||
|
||||
## Change metrics
|
||||
|
||||
LOD0–2 camera mesh windows; “moved” means a shared global lattice crossing displaced by more than 0.01 cm. Coverage is the paired edge-classifier coverage.
|
||||
|
||||
| Strate | Added | Removed | Moved | Coverage |
|
||||
|---|---:|---:|---:|---:|
|
||||
| SW | 78,201 | 14,530 | 2,758 | 99.75% |
|
||||
| FI | 37,810 | 42,398 | 78,215 | 98.31% |
|
||||
| TN | 44,694 | 48,925 | 48,816 | 98.29% |
|
||||
| VS | 7,055 | 7,096 | 8,790 | 97.38% |
|
||||
|
||||
## Captures
|
||||
|
||||
84 PNGs: before and after, each with both mouths and 25/50/75% stations for all three passages. Each of the nine open-air sections has three views in each set. The [capture index](captures/INDEX.md) lists every image and flags every **OPEN AIR** view. All 84 source meshes and rendered views contain geometry; minimum visible triangles are 55,420 before and 55,484 after. Six before and seven after images met the pixel floor but were flagged low contrast for review.
|
||||
|
||||
Open-air sections (route distance in metres): `0–1`: 7.226–10.217, 13.456–17.443, 23.174–24.918, 1036.913–1489.321; `1–2`: 101.820–104.499; `2–3`: 7.677–34.747, 368.909–376.660, 393.828–396.510, 444.932–447.654.
|
||||
|
||||
## Failed runs
|
||||
|
||||
Early fast runs found a 0.79 m lower-mouth mismatch at native Caves and loss of the native Lattice route substrate; the mouth now matches the authored cave edge floor, and the explicitly authored Lattice floor field is retained. A sentinel check also initially treated the “no native floor” value as support; this was fixed. The first Unreal build stopped before compilation because UnrealBuildTool could not rotate its log under read-only `~/.config`; redirecting `XDG_CONFIG_HOME` to `/tmp` fixed it. Final audits, timings, hash runs, crossings, captures, build, and six traces passed. No run exceeded 30 minutes.
|
||||
@@ -0,0 +1,511 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture before/after passage mouths, route stations, and exposed shell sections."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from array import array
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
RESEARCH = Path(__file__).resolve().parent
|
||||
CAPTURES = RESEARCH / "captures"
|
||||
SAVED = ROOT / "Saved/Linux/l109/captures"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"before": ROOT / "Saved/Linux/l109/bin-baseline/VoxelForgeCoreHeadless-l109-baseline",
|
||||
"after": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang",
|
||||
}
|
||||
LAYOUTS = {
|
||||
"before": ROOT / "Saved/Linux/l109/audit-layout-baseline.json",
|
||||
"after": ROOT / "Saved/Linux/l109/audit-layout-final-stamp.json",
|
||||
}
|
||||
AUDIT_LOG = ROOT / "Saved/Linux/l109/audit-owner4-final-stamp.log"
|
||||
RENDERERS = {
|
||||
"before": ROOT / "Saved/Linux/l109/bin-baseline/VoxelForgeOffscreenCapture-l109-baseline",
|
||||
"after": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeOffscreenCapture-l109-clang",
|
||||
}
|
||||
SECTION = re.compile(
|
||||
r"^\[PassageOpenAirSection\] id=(\S+) section=(\d+) start_m=([\d.]+) "
|
||||
r"end_m=([\d.]+) samples=(\d+) midpoint_xyz_m=\(([^)]*)\) "
|
||||
r"midpoint_radius_m=([\d.]+)", re.M)
|
||||
DRAWN = re.compile(r"drawn_triangles=(\d+)")
|
||||
QUALITY_METRICS = re.compile(r"stddev_luma=([\d.]+).*coverage_luma_gt12=([\d.]+)")
|
||||
|
||||
|
||||
def vector(text: str) -> tuple[float, float, float]:
|
||||
return tuple(float(value) for value in text.split(","))
|
||||
|
||||
|
||||
def add(a, b):
|
||||
return tuple(x + y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def sub(a, b):
|
||||
return tuple(x - y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def mul(a, scale):
|
||||
return tuple(x * scale for x in a)
|
||||
|
||||
|
||||
def norm(a):
|
||||
length = math.sqrt(sum(x * x for x in a))
|
||||
if length <= 1.0e-9:
|
||||
return (0.0, 1.0, 0.0)
|
||||
return tuple(value / length for value in a)
|
||||
|
||||
|
||||
def angles(camera, target):
|
||||
delta = sub(target, camera)
|
||||
horizontal = math.hypot(delta[0], delta[1])
|
||||
return (math.degrees(math.atan2(delta[0], delta[1])),
|
||||
math.degrees(math.atan2(delta[2], horizontal)))
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def mesh_triangle_count(mesh: Path) -> int:
|
||||
"""Read only the VFMSH headers and count indexed triangles."""
|
||||
with mesh.open("rb") as stream:
|
||||
magic = stream.read(8)
|
||||
materials = magic == b"VFMSH002"
|
||||
if magic not in (b"VFMSH001", b"VFMSH002"):
|
||||
raise RuntimeError(f"unsupported mesh scene header: {mesh}")
|
||||
version, _voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||
if version != (2 if materials else 1):
|
||||
raise RuntimeError(f"mesh scene version mismatch: {mesh}")
|
||||
stream.seek(40, 1)
|
||||
triangles = 0
|
||||
for _ in range(tile_count):
|
||||
stream.seek(16, 1)
|
||||
surface_count, _skirts, vertex_count, index_count, mask_count = struct.unpack(
|
||||
"<5I", stream.read(20))
|
||||
triangles += index_count // 3
|
||||
stream.seek(vertex_count * 24 + (10 * vertex_count if materials else 0)
|
||||
+ index_count * 4 + mask_count, 1)
|
||||
if stream.read(1):
|
||||
raise RuntimeError(f"trailing data in mesh scene: {mesh}")
|
||||
return triangles
|
||||
|
||||
|
||||
def dot(a, b):
|
||||
return sum(x * y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def nearest_surface_anchor(mesh: Path, point, margin_m=200.0) -> dict:
|
||||
"""Return a nearby VFMSH surface vertex and its normal."""
|
||||
best_distance_sq = float("inf")
|
||||
best_position = best_normal = None
|
||||
with mesh.open("rb") as stream:
|
||||
magic = stream.read(8)
|
||||
materials = magic == b"VFMSH002"
|
||||
if magic not in (b"VFMSH001", b"VFMSH002"):
|
||||
raise RuntimeError(f"unsupported mesh scene header: {mesh}")
|
||||
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||
if version != (2 if materials else 1):
|
||||
raise RuntimeError(f"mesh scene version mismatch: {mesh}")
|
||||
stream.seek(40, 1)
|
||||
for _ in range(tile_count):
|
||||
key = struct.unpack("<4i", stream.read(16))
|
||||
surface_count, _skirts, vertex_count, index_count, mask_count = struct.unpack(
|
||||
"<5I", stream.read(20))
|
||||
extent = (32 << key[3]) * voxel_um / 1_000_000.0
|
||||
origin = (key[0] * extent, key[1] * extent, key[2] * extent)
|
||||
if any(point[axis] < origin[axis] - margin_m or
|
||||
point[axis] > origin[axis] + extent + margin_m for axis in range(3)):
|
||||
stream.seek(vertex_count * 24 + (10 * vertex_count if materials else 0)
|
||||
+ index_count * 4 + mask_count, 1)
|
||||
continue
|
||||
positions = array("f")
|
||||
positions.frombytes(stream.read(vertex_count * 12))
|
||||
normals = array("f")
|
||||
normals.frombytes(stream.read(vertex_count * 12))
|
||||
if materials:
|
||||
stream.seek(vertex_count * 10, 1)
|
||||
stream.seek(index_count * 4 + mask_count, 1)
|
||||
if struct.pack("=I", 1) != struct.pack("<I", 1):
|
||||
positions.byteswap()
|
||||
normals.byteswap()
|
||||
for index in range(min(surface_count, vertex_count)):
|
||||
offset = index * 3
|
||||
position = tuple(float(positions[offset + axis]) for axis in range(3))
|
||||
distance_sq = dot(sub(position, point), sub(position, point))
|
||||
if distance_sq >= best_distance_sq:
|
||||
continue
|
||||
raw_normal = tuple(float(normals[offset + axis]) for axis in range(3))
|
||||
if dot(raw_normal, raw_normal) < 1.0e-10:
|
||||
continue
|
||||
best_distance_sq = distance_sq
|
||||
best_position = position
|
||||
best_normal = norm(raw_normal)
|
||||
if best_position is None:
|
||||
raise RuntimeError(f"no nearby surface vertices in mesh scene: {mesh}")
|
||||
return {"position_m": best_position, "normal": best_normal,
|
||||
"distance_m": math.sqrt(best_distance_sq)}
|
||||
|
||||
|
||||
def sanitize(value: str) -> str:
|
||||
return re.sub(r"[^A-Za-z0-9_-]+", "-", value).strip("-").replace("/", "-")
|
||||
|
||||
|
||||
def route_for(layout: dict, passage: dict) -> list[tuple[tuple[float, float, float], float]]:
|
||||
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
|
||||
controls = [(tuple(value * scale for value in row["position_q"]),
|
||||
row["radius_q"] * scale) for row in passage["control_points"]]
|
||||
lower_q = passage["lower_mouth_q"]
|
||||
player_center_above_floor_q = round(
|
||||
1.13 * 256.0 * 1_000_000.0 / layout["core_voxel_size_um"])
|
||||
lower_index = next((index for index, row in enumerate(passage["control_points"])
|
||||
if row["position_q"][0] == lower_q[0]
|
||||
and row["position_q"][1] == lower_q[1]
|
||||
and row["position_q"][2] - row["radius_q"]
|
||||
+ player_center_above_floor_q == lower_q[2]), None)
|
||||
if lower_index is None:
|
||||
raise RuntimeError(f"{passage['stable_id']}: lower mouth control is unbound")
|
||||
result = controls[:lower_index + 1]
|
||||
if len(result) < 2:
|
||||
raise RuntimeError(f"{passage['stable_id']}: incomplete route controls")
|
||||
return result
|
||||
|
||||
|
||||
def route_lengths(route):
|
||||
lengths = [math.dist(a[0], b[0]) for a, b in zip(route, route[1:])]
|
||||
prefix = [0.0]
|
||||
for length in lengths:
|
||||
prefix.append(prefix[-1] + length)
|
||||
return lengths, prefix
|
||||
|
||||
|
||||
def at_distance(route, lengths, prefix, distance):
|
||||
distance = max(0.0, min(distance, prefix[-1]))
|
||||
for index, length in enumerate(lengths):
|
||||
if distance <= prefix[index + 1] or index + 1 == len(lengths):
|
||||
t = 0.0 if length <= 1.0e-9 else (distance - prefix[index]) / length
|
||||
a, b = route[index], route[index + 1]
|
||||
return (tuple(a[0][axis] + (b[0][axis] - a[0][axis]) * t
|
||||
for axis in range(3)),
|
||||
a[1] + (b[1] - a[1]) * t)
|
||||
return route[-1]
|
||||
|
||||
|
||||
def tangent_at(route, lengths, prefix, distance):
|
||||
before = at_distance(route, lengths, prefix, max(0.0, distance - 0.25))[0]
|
||||
after = at_distance(route, lengths, prefix, min(prefix[-1], distance + 0.25))[0]
|
||||
return norm(sub(after, before))
|
||||
|
||||
|
||||
def derived_route(route, passage, layout):
|
||||
"""Apply L109's fixed-depth floor offset to raw layout controls."""
|
||||
lengths, prefix = route_lengths(route)
|
||||
total = prefix[-1]
|
||||
body_radii = [radius for index, (_, radius) in enumerate(route)
|
||||
if prefix[index] >= 10.0 and total - prefix[index] >= 10.0]
|
||||
body_depth = max(0.5, min(body_radii or [radius for _, radius in route]) - 0.23)
|
||||
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
|
||||
upper_z = passage["upper_mouth_q"][2] * scale
|
||||
lower_z = passage["lower_mouth_q"][2] * scale
|
||||
upper_target = max(0.0, route[0][0][2] - upper_z + 1.13)
|
||||
lower_target = max(0.0, route[-1][0][2] - lower_z + 1.13)
|
||||
transformed = []
|
||||
for index, (point, radius) in enumerate(route):
|
||||
local_radius_depth = max(0.0, radius - 0.23)
|
||||
mouth_distance = max(0.0, min(prefix[index], total - prefix[index]))
|
||||
u = max(0.0, min(1.0, mouth_distance / 5.0))
|
||||
ramp = u * u * (3.0 - 2.0 * u)
|
||||
target = upper_target if prefix[index] <= total * 0.5 else lower_target
|
||||
mouth_depth = max(0.0, min(radius, target))
|
||||
route_depth = mouth_depth + (local_radius_depth - mouth_depth) * ramp
|
||||
tube_depth = min(body_depth, local_radius_depth)
|
||||
transformed.append(((point[0], point[1], point[2] - route_depth + tube_depth),
|
||||
radius))
|
||||
return transformed, body_depth
|
||||
|
||||
|
||||
def make_views(layout: dict, audit_sections: list[dict], *, use_derived_shape: bool,
|
||||
include_open_air: bool = True) -> list[dict]:
|
||||
views = []
|
||||
passages = sorted(layout["passages"], key=lambda item: item["stable_id"])
|
||||
for passage in passages:
|
||||
raw_route = route_for(layout, passage)
|
||||
if use_derived_shape:
|
||||
route, body_depth = derived_route(raw_route, passage, layout)
|
||||
else:
|
||||
route = raw_route
|
||||
body_depth = max(point[1] for point in route)
|
||||
def floor_depth(radius: float) -> float:
|
||||
if not use_derived_shape:
|
||||
return radius
|
||||
return min(body_depth, max(0.0, radius - 0.23))
|
||||
lengths, prefix = route_lengths(route)
|
||||
total = prefix[-1]
|
||||
passage_id = passage["stable_id"]
|
||||
safe_id = sanitize(passage_id)
|
||||
|
||||
for end_name, distance, outward_sign in (
|
||||
("upper-mouth", 0.0, -1.0), ("lower-mouth", total, 1.0)):
|
||||
center, radius = at_distance(route, lengths, prefix, distance)
|
||||
tangent = tangent_at(route, lengths, prefix, distance)
|
||||
outward = mul(tangent, outward_sign)
|
||||
floor_z = center[2] - floor_depth(radius)
|
||||
eye = (center[0] + outward[0] * 4.5,
|
||||
center[1] + outward[1] * 4.5,
|
||||
floor_z + 1.55)
|
||||
if end_name == "lower-mouth":
|
||||
eye = add(eye, add(mul(outward, 5.0), (0.0, 0.0, 2.0)))
|
||||
target = (center[0] - outward[0] * 2.0,
|
||||
center[1] - outward[1] * 2.0,
|
||||
floor_z + 1.45)
|
||||
yaw, pitch = angles(eye, target)
|
||||
views.append({"name": f"{safe_id}-{end_name}", "passage": passage_id,
|
||||
"kind": "mouth", "station": end_name,
|
||||
"camera": eye, "target": target, "yaw": yaw, "pitch": pitch,
|
||||
"focus": center, "center_z": center[2]})
|
||||
|
||||
for fraction in (0.25, 0.50, 0.75):
|
||||
distance = total * fraction
|
||||
center, radius = at_distance(route, lengths, prefix, distance)
|
||||
tangent = tangent_at(route, lengths, prefix, distance)
|
||||
depth = floor_depth(radius)
|
||||
floor_z = center[2] - depth
|
||||
horizontal = norm((-tangent[1], tangent[0], 0.0))
|
||||
eye = (center[0] + horizontal[0] * 0.75,
|
||||
center[1] + horizontal[1] * 0.75, floor_z + 1.60)
|
||||
target_center, target_radius = at_distance(route, lengths, prefix,
|
||||
min(total, distance + 14.0))
|
||||
target_depth = floor_depth(target_radius)
|
||||
target = (target_center[0] + horizontal[0] * 2.35,
|
||||
target_center[1] + horizontal[1] * 2.35,
|
||||
target_center[2] - target_depth + 1.80)
|
||||
yaw, pitch = angles(eye, target)
|
||||
views.append({"name": f"{safe_id}-station-{int(fraction * 100)}",
|
||||
"passage": passage_id, "kind": "station",
|
||||
"station": f"{int(fraction * 100)}%", "camera": eye,
|
||||
"target": target, "yaw": yaw, "pitch": pitch,
|
||||
"focus": center, "center_z": center[2],
|
||||
"route_distance_m": distance,
|
||||
"route_length_m": total,
|
||||
"requested_route_distance_m": distance})
|
||||
|
||||
if not include_open_air:
|
||||
continue
|
||||
for section in (item for item in audit_sections if item["passage"] == passage_id):
|
||||
distance = 0.5 * (section["start_m"] + section["end_m"])
|
||||
center, radius = (section["midpoint"], section["radius_m"])
|
||||
tangent = tangent_at(route, lengths, prefix, distance)
|
||||
side = norm((-tangent[1], tangent[0], 0.0))
|
||||
depth = floor_depth(radius)
|
||||
section_id = section["section"]
|
||||
interior_eye = (center[0], center[1], center[2] - depth + 1.25)
|
||||
interior_target = add(interior_eye, mul(tangent, 7.0))
|
||||
side_eye = add(center, add(mul(side, radius + 5.0), (0.0, 0.0, 1.2)))
|
||||
oblique_eye = add(center, add(mul(side, -(radius + 3.5)), (0.0, 0.0, 4.0)))
|
||||
targets = (add(center, (0.0, 0.0, 0.1)),
|
||||
add(center, mul(tangent, 2.0)), interior_target)
|
||||
for view_index, (eye, target) in enumerate(zip(
|
||||
(side_eye, oblique_eye, interior_eye), targets), start=1):
|
||||
yaw, pitch = angles(eye, target)
|
||||
views.append({"name": f"{safe_id}-openair-{section_id}-view-{view_index}",
|
||||
"passage": passage_id, "kind": "open-air",
|
||||
"station": f"section-{section_id}/view-{view_index}",
|
||||
"camera": eye, "target": target, "yaw": yaw,
|
||||
"pitch": pitch, "focus": center,
|
||||
"center_z": center[2], "open_air_section": section_id,
|
||||
"route_distance_m": distance,
|
||||
"requested_route_distance_m": distance,
|
||||
"route_length_m": total})
|
||||
return views
|
||||
|
||||
|
||||
def run_process(name: str, command: list[str], log: Path, *, allow_failure=False) -> str:
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
if result.returncode and not allow_failure:
|
||||
raise RuntimeError(f"{name}: exit={result.returncode} log={log}")
|
||||
print(f"[L109CaptureProcess] name={name} elapsed_s={time.monotonic()-started:.2f} "
|
||||
f"exit={result.returncode} log={log.relative_to(ROOT)}", flush=True)
|
||||
return log.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
|
||||
def capture(variant: str, view: dict, layout: dict) -> dict:
|
||||
variant_saved = SAVED / variant
|
||||
variant_saved.mkdir(parents=True, exist_ok=True)
|
||||
variant_capture = CAPTURES / variant
|
||||
variant_capture.mkdir(parents=True, exist_ok=True)
|
||||
scene = variant_saved / f"{view['name']}.vfmesh"
|
||||
image = variant_capture / f"{view['name']}.png"
|
||||
candidates = [(view["camera"], view["target"], view["focus"],
|
||||
view.get("route_distance_m"), 0.0, "requested")]
|
||||
if variant == "before" and view["kind"] != "mouth":
|
||||
passage = next(item for item in layout["passages"]
|
||||
if item["stable_id"] == view["passage"])
|
||||
route = route_for(layout, passage)
|
||||
lengths, prefix = route_lengths(route)
|
||||
total = prefix[-1]
|
||||
eye_offset = sub(view["camera"], view["focus"])
|
||||
target_offset = sub(view["target"], view["focus"])
|
||||
requested = view["route_distance_m"]
|
||||
for delta in (5, -5, 10, -10, 20, -20, 35, -35, 50, -50, 75, -75,
|
||||
100, -100, 150, -150, 200, -200):
|
||||
distance = max(0.0, min(total, requested + delta))
|
||||
if abs(distance - requested) < 0.01:
|
||||
continue
|
||||
center, _radius = at_distance(route, lengths, prefix, distance)
|
||||
candidates.append((add(center, eye_offset), add(center, target_offset), center,
|
||||
distance, distance - requested, "route-offset"))
|
||||
|
||||
attempt_errors = []
|
||||
for attempt_index, (probe_camera, probe_target, probe_focus, route_distance,
|
||||
route_adjustment, attempt_kind) in enumerate(candidates):
|
||||
suffix = "" if attempt_index == 0 else f"-attempt-{attempt_index:02d}"
|
||||
headless_log = variant_saved / f"{view['name']}{suffix}-mesh.log"
|
||||
render_log = variant_saved / f"{view['name']}{suffix}-render.log"
|
||||
quality_log = variant_saved / f"{view['name']}{suffix}-quality.log"
|
||||
yaw, pitch = angles(probe_camera, probe_target)
|
||||
camera = (*probe_camera, yaw, pitch)
|
||||
export_command = [
|
||||
str(BINARIES[variant]), str(DATA), "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--workers=12",
|
||||
"--clip-radius=3", "--max-clip-level=0", "--max-level=0", "--force-generate",
|
||||
"--recipe=surface-world", f"--camera={fmt(camera)}",
|
||||
"--render-distance-chunks=1", f"--mesh-export={scene}",
|
||||
]
|
||||
export_output = run_process(f"{variant}-{view['name']}{suffix}-mesh", export_command,
|
||||
headless_log)
|
||||
if "status=PASS" not in export_output or not scene.is_file():
|
||||
raise RuntimeError(f"{variant}-{view['name']}: missing successful scene export")
|
||||
source_triangles = mesh_triangle_count(scene)
|
||||
if source_triangles <= 0:
|
||||
attempt_errors.append(f"{attempt_kind}@{route_distance}: empty mesh")
|
||||
if variant == "after" or view["kind"] == "mouth":
|
||||
break
|
||||
continue
|
||||
|
||||
actual_camera, actual_target = probe_camera, probe_target
|
||||
actual_focus = probe_focus
|
||||
surface_distance = None
|
||||
if attempt_kind == "route-offset":
|
||||
anchor = nearest_surface_anchor(scene, probe_focus)
|
||||
surface_distance = anchor["distance_m"]
|
||||
actual_camera = add(anchor["position_m"], mul(anchor["normal"], 12.0))
|
||||
actual_target = anchor["position_m"]
|
||||
actual_focus = anchor["position_m"]
|
||||
actual_yaw, actual_pitch = angles(actual_camera, actual_target)
|
||||
render_command = [
|
||||
str(RENDERERS[variant]), str(scene), str(image), "--size=1920x1080",
|
||||
f"--camera-m={fmt(actual_camera)}", f"--yaw-deg={actual_yaw:.6f}",
|
||||
f"--pitch-deg={actual_pitch:.6f}", "--shade=readable", "--headlamp=on",
|
||||
"--height-tint=on", f"--height-center-m={actual_focus[2]:.6f}",
|
||||
"--height-range-m=32",
|
||||
]
|
||||
render_output = run_process(f"{variant}-{view['name']}{suffix}-render", render_command,
|
||||
render_log)
|
||||
drawn = DRAWN.search(render_output)
|
||||
if drawn is None or int(drawn.group(1)) <= 0 or not image.is_file():
|
||||
attempt_errors.append(f"{attempt_kind}@{route_distance}: no drawn geometry")
|
||||
if variant == "after" or view["kind"] == "mouth":
|
||||
break
|
||||
continue
|
||||
quality = run_process(f"{variant}-{view['name']}{suffix}-quality", [
|
||||
"python3", "-B", str(ROOT / "Standalone/Tools/verify/capture_quality.py"),
|
||||
str(image)], quality_log, allow_failure=True)
|
||||
quality_status = "pass"
|
||||
if "status=PASS" not in quality:
|
||||
metrics = QUALITY_METRICS.search(quality)
|
||||
coverage = float(metrics.group(2)) if metrics else 0.0
|
||||
if coverage >= 0.001:
|
||||
quality_status = "low_contrast_geometry_visible"
|
||||
else:
|
||||
attempt_errors.append(f"{attempt_kind}@{route_distance}: no visible pixel coverage")
|
||||
if variant == "after" or view["kind"] == "mouth":
|
||||
break
|
||||
continue
|
||||
|
||||
row = {**view, "variant": variant, "capture": str(image.relative_to(RESEARCH)),
|
||||
"scene": str(scene.relative_to(ROOT)), "drawn_triangles": int(drawn.group(1)),
|
||||
"source_triangles": source_triangles, "verified": True,
|
||||
"pixel_quality_status": quality_status,
|
||||
"route_distance_actual_m": route_distance,
|
||||
"route_station_adjustment_m": route_adjustment,
|
||||
"fallback_kind": attempt_kind if attempt_kind != "requested" else None,
|
||||
"fallback_surface_distance_m": surface_distance,
|
||||
"camera_actual_m": actual_camera, "target_actual_m": actual_target,
|
||||
"logs": [str(headless_log.relative_to(ROOT)),
|
||||
str(render_log.relative_to(ROOT)),
|
||||
str(quality_log.relative_to(ROOT))]}
|
||||
print("[L109Capture] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
raise RuntimeError(f"{variant}-{view['name']}: no verified image; "
|
||||
+ "; ".join(attempt_errors))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if any(not path.is_file() for path in (*BINARIES.values(), *RENDERERS.values(),
|
||||
*LAYOUTS.values(), AUDIT_LOG)):
|
||||
raise SystemExit("missing L109 capture binary, layout, or audit log")
|
||||
sections = []
|
||||
for passage, section, start, end, samples, midpoint, radius in SECTION.findall(
|
||||
AUDIT_LOG.read_text(encoding="utf-8", errors="replace")):
|
||||
sections.append({"passage": passage, "section": int(section),
|
||||
"start_m": float(start), "end_m": float(end),
|
||||
"samples": int(samples), "midpoint": vector(midpoint),
|
||||
"radius_m": float(radius)})
|
||||
if not sections:
|
||||
raise SystemExit("no open-air sections found in the final passage audit")
|
||||
all_rows = []
|
||||
for variant in ("before", "after"):
|
||||
layout = json.loads(LAYOUTS[variant].read_text(encoding="utf-8"))
|
||||
include_open_air = True
|
||||
views = make_views(layout, sections, use_derived_shape=variant == "after",
|
||||
include_open_air=include_open_air)
|
||||
expected = 5 * len(layout["passages"])
|
||||
if include_open_air:
|
||||
expected += 3 * len(sections)
|
||||
if len(views) != expected:
|
||||
raise RuntimeError(f"unexpected camera count: {len(views)} != {expected}")
|
||||
print(f"[L109CapturePlan] variant={variant} passage_views={5*len(layout['passages'])} "
|
||||
f"open_air_sections={len(sections) if include_open_air else 0} "
|
||||
f"open_air_views={3*len(sections) if include_open_air else 0} "
|
||||
f"total={len(views)}", flush=True)
|
||||
for view in views:
|
||||
all_rows.append(capture(variant, view, layout))
|
||||
CAPTURES.mkdir(parents=True, exist_ok=True)
|
||||
(CAPTURES / "capture-index.json").write_text(
|
||||
json.dumps({"open_air_sections": sections, "captures": all_rows}, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
lines = ["# L109 passage captures", "",
|
||||
"Each PNG was rendered from its own exported mesh. The renderer reported visible "
|
||||
"triangles, and each decoded image met the pixel coverage floor. "
|
||||
"`low_contrast_geometry_visible` marks images that met this floor below the "
|
||||
"preferred contrast target.", ""]
|
||||
for variant in ("before", "after"):
|
||||
lines.extend([f"## {variant.title()}", ""])
|
||||
for row in all_rows:
|
||||
if row["variant"] != variant:
|
||||
continue
|
||||
flag = " **OPEN AIR**" if row["kind"] == "open-air" else ""
|
||||
quality_note = (f"; {row['pixel_quality_status']}"
|
||||
if row["pixel_quality_status"] != "pass" else "")
|
||||
lines.append(f"- [{row['station']}]({variant}/{Path(row['capture']).name}) — "
|
||||
f"{row['passage']}{flag}; {row['drawn_triangles']} visible triangles"
|
||||
f"{quality_note}")
|
||||
lines.append("")
|
||||
(CAPTURES / "INDEX.md").write_text("\n".join(lines), encoding="utf-8")
|
||||
failures = [row for row in all_rows if not row["verified"]]
|
||||
print(f"[L109CaptureSummary] captures={len(all_rows)} verified={len(all_rows)-len(failures)} "
|
||||
f"failed={len(failures)} open_air_sections={len(sections)}", flush=True)
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare LOD0-2 owner mesh edge crossings before and after L109."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import statistics
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l109/crossings"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"before": ROOT / "Saved/Linux/l109/bin-baseline/VoxelForgeCoreHeadless-l109-baseline",
|
||||
"after": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang",
|
||||
}
|
||||
CAMERAS = {
|
||||
"SW": (8.0, 16.0, -372.2626953125),
|
||||
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||
"VS": (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
|
||||
|
||||
def read_exact(stream, size):
|
||||
data = stream.read(size)
|
||||
if len(data) != size:
|
||||
raise RuntimeError("truncated VFMSH export")
|
||||
return data
|
||||
|
||||
|
||||
def crossing_map(path: Path) -> tuple[dict, dict]:
|
||||
crossings = {}
|
||||
meta = {"tiles": 0, "surface_vertices": 0, "classified": 0}
|
||||
with path.open("rb") as stream:
|
||||
magic = read_exact(stream, 8)
|
||||
materials = magic == b"VFMSH002"
|
||||
if magic not in (b"VFMSH001", b"VFMSH002"):
|
||||
raise RuntimeError(f"invalid mesh export {path}")
|
||||
version, voxel_um, tile_count = struct.unpack("<3I", read_exact(stream, 12))
|
||||
if version != (2 if materials else 1):
|
||||
raise RuntimeError("VFMSH version mismatch")
|
||||
read_exact(stream, 40)
|
||||
voxel_m = voxel_um / 1_000_000.0
|
||||
for _ in range(tile_count):
|
||||
key = struct.unpack("<4i", read_exact(stream, 16))
|
||||
surface_count, _skirts, vertex_count, index_count, mask_count = struct.unpack(
|
||||
"<5I", read_exact(stream, 20))
|
||||
positions = struct.iter_unpack("<3f", read_exact(stream, vertex_count * 12))
|
||||
values = list(positions)
|
||||
read_exact(stream, vertex_count * 12) # normals
|
||||
if materials:
|
||||
read_exact(stream, vertex_count * 10)
|
||||
read_exact(stream, index_count * 4 + mask_count)
|
||||
meta["tiles"] += 1
|
||||
meta["surface_vertices"] += min(surface_count, vertex_count)
|
||||
unit = voxel_m * (1 << key[3])
|
||||
for position in values[:min(surface_count, vertex_count)]:
|
||||
grid = tuple(value / unit for value in position)
|
||||
fractions = tuple(abs(value - round(value)) for value in grid)
|
||||
axis = max(range(3), key=fractions.__getitem__)
|
||||
if fractions[axis] < 1.0e-4:
|
||||
continue
|
||||
fixed = [index for index in range(3) if index != axis]
|
||||
if any(fractions[index] > 0.01 for index in fixed):
|
||||
continue
|
||||
edge = [0, 0, 0]
|
||||
for index in fixed:
|
||||
edge[index] = int(round(grid[index]))
|
||||
edge[axis] = math.floor(grid[axis])
|
||||
identity = (key[3], axis, *edge)
|
||||
crossings.setdefault(identity, tuple(float(v) for v in position))
|
||||
meta["classified"] += 1
|
||||
if stream.read(1):
|
||||
raise RuntimeError("trailing data in mesh export")
|
||||
meta["classified_pct"] = (100.0 * meta["classified"] / meta["surface_vertices"]
|
||||
if meta["surface_vertices"] else 0.0)
|
||||
return crossings, meta
|
||||
|
||||
|
||||
def export(strate: str, variant: str) -> Path:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
mesh = OUT / f"{strate.lower()}-{variant}.vfmesh"
|
||||
log = OUT / f"{strate.lower()}-{variant}.log"
|
||||
camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARIES[variant]), str(DATA),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--workers=8", "--clip-radius=3", "--max-clip-level=2", "--max-level=2",
|
||||
"--force-generate", "--recipe=surface-world", f"--camera={camera}",
|
||||
"--render-distance-chunks=0",
|
||||
f"--mesh-export={mesh}",
|
||||
]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1740, check=False)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if result.returncode or not mesh.is_file() or "status=PASS" not in output:
|
||||
raise RuntimeError(f"mesh export failed: {strate}/{variant} exit={result.returncode} {log}")
|
||||
return mesh
|
||||
|
||||
|
||||
def main() -> int:
|
||||
summaries = []
|
||||
for strate in CAMERAS:
|
||||
paths = {variant: export(strate, variant) for variant in ("before", "after")}
|
||||
maps = {variant: crossing_map(path) for variant, path in paths.items()}
|
||||
old, old_meta = maps["before"]
|
||||
new, new_meta = maps["after"]
|
||||
old_keys, new_keys = set(old), set(new)
|
||||
shared = old_keys & new_keys
|
||||
moved_cm = [100.0 * math.dist(old[key], new[key]) for key in shared
|
||||
if math.dist(old[key], new[key]) > 0.0001]
|
||||
row = {
|
||||
"strate": strate, "scope": "camera LOD0-2 mesh window; global lattice edge IDs",
|
||||
"added_crossings": len(new_keys - old_keys),
|
||||
"removed_crossings": len(old_keys - new_keys),
|
||||
"shared_crossings": len(shared), "moved_crossings": len(moved_cm),
|
||||
"mean_moved_cm": statistics.mean(moved_cm) if moved_cm else 0.0,
|
||||
"max_moved_cm": max(moved_cm, default=0.0),
|
||||
"before": old_meta, "after": new_meta,
|
||||
"classified_pair_coverage_pct": min(old_meta["classified_pct"],
|
||||
new_meta["classified_pct"]),
|
||||
"exports": {key: str(value.relative_to(ROOT)) for key, value in paths.items()},
|
||||
}
|
||||
summaries.append(row)
|
||||
print("[L109CrossingDelta] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
(OUT / "summary.json").write_text(
|
||||
json.dumps({"measurement": "per-vertex marching-cubes edge classifier; compare shared global lattice edges; positions >0.01cm count as moved",
|
||||
"strata": summaries}, indent=2) + "\n", encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check Clang/GCC hash identity at W1/W8/W12 and repin v217 owner hashes."""
|
||||
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/l109/hashes"
|
||||
DATASETS = {
|
||||
"owner4": ROOT / "Standalone/data/owner_world.json",
|
||||
"owner2": ROOT / "Standalone/data/regression/owner_world_2strate_l61.json",
|
||||
}
|
||||
BINARIES = {
|
||||
"clang": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang",
|
||||
"gcc": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-gcc",
|
||||
}
|
||||
HASH = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=(\S+)$", re.M)
|
||||
|
||||
|
||||
def run(dataset: str, recipe: str, workers: int, compiler: str) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
label = f"{dataset}-{recipe}-w{workers}-{compiler}"
|
||||
log = OUT / f"{label}.log"
|
||||
layout = OUT / f"{label}.layout.json"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARIES[compiler]), str(DATASETS[dataset]),
|
||||
f"--workers={workers}", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=8", f"--recipe={recipe}",
|
||||
"--stream-hash", f"--resolved-layout-output={layout}",
|
||||
]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
hashes = HASH.findall(output)
|
||||
if result.returncode or len(hashes) != 12:
|
||||
raise SystemExit(f"hash run failed or incomplete: {label}; exit={result.returncode} "
|
||||
f"hashes={len(hashes)} log={log}")
|
||||
row = {"dataset": dataset, "recipe": recipe, "workers": workers,
|
||||
"compiler": compiler, "exit_code": result.returncode,
|
||||
"hashes": hashes, "source_stamp": (STAMP.search(output).group(1)
|
||||
if STAMP.search(output) else None),
|
||||
"elapsed_s": round(time.monotonic() - started, 3),
|
||||
"log": str(log.relative_to(ROOT))}
|
||||
print("[L109HashRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if any(not path.is_file() for path in (*DATASETS.values(), *BINARIES.values())):
|
||||
raise SystemExit("missing L109 hash input")
|
||||
rows = []
|
||||
recipes = {"owner4": ("surface-world",),
|
||||
"owner2": ("surface-world", "floating-islands")}
|
||||
for dataset, dataset_recipes in recipes.items():
|
||||
for recipe in dataset_recipes:
|
||||
for workers in (1, 8, 12):
|
||||
for compiler in ("clang", "gcc"):
|
||||
rows.append(run(dataset, recipe, workers, compiler))
|
||||
(OUT / "runs-partial.json").write_text(
|
||||
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
|
||||
groups = {}
|
||||
for dataset, dataset_recipes in recipes.items():
|
||||
for recipe in dataset_recipes:
|
||||
key = f"{dataset}/{recipe}"
|
||||
group = [row for row in rows if row["dataset"] == dataset
|
||||
and row["recipe"] == recipe]
|
||||
reference = group[0]["hashes"]
|
||||
groups[key] = {"all_worker_compiler_hashes_match": all(
|
||||
row["hashes"] == reference for row in group),
|
||||
"source_stamps": sorted({row["source_stamp"] for row in group}),
|
||||
"runs": [{"workers": row["workers"], "compiler": row["compiler"],
|
||||
"log": row["log"]} for row in group]}
|
||||
if not groups[key]["all_worker_compiler_hashes_match"]:
|
||||
raise SystemExit(f"determinism mismatch: {key}")
|
||||
if dataset == "owner2":
|
||||
canonical = next(row for row in group if row["workers"] == 12
|
||||
and row["compiler"] == "clang")
|
||||
recipe_name = "floating-islands" if recipe == "floating-islands" else "surface-world"
|
||||
style = 1 if recipe_name == "surface-world" else 2
|
||||
baseline = (ROOT / "Standalone/Tools/verify/baselines/owner-hash/canonical"
|
||||
/ f"owner-hash-{recipe_name}-style{style}-v217.baseline")
|
||||
baseline.write_text("\n".join(canonical["hashes"]) + "\n",
|
||||
encoding="utf-8")
|
||||
summary = {"runs": rows, "groups": groups,
|
||||
"owner4_all_four_strata": "owner4/surface-world",
|
||||
"owner2_canonical_pins": ["owner2/surface-world",
|
||||
"owner2/floating-islands"]}
|
||||
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Estimate L77 passage/support worker shares before and after L109."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l109/l77-phase-census"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"base": ROOT / "Saved/Linux/l109/bin-baseline-profile/VoxelForgeCoreHeadless-l109-base-profile",
|
||||
"candidate": ROOT / "Saved/Linux/l109/bin-final-profile/VoxelForgeCoreHeadless-l109-candidate-profile",
|
||||
}
|
||||
CAMERAS = {
|
||||
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||
}
|
||||
WORKER = re.compile(r"^\[WorldAssemblerLODWorker\] (.*)$", re.M)
|
||||
SAMPLE = re.compile(r"^\[L77Sample\] (.*)$", re.M)
|
||||
SAMPLER = re.compile(r"^\[L77Sampler\] action=stop (.*)$", re.M)
|
||||
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
|
||||
|
||||
def run(stratum: str, variant: str, replicate: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
label = f"{stratum.lower()}-{variant}-r{replicate}"
|
||||
log = OUT / f"{label}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[stratum]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARIES[variant]), str(DATA),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=8",
|
||||
f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3",
|
||||
"--linux77-profile",
|
||||
]
|
||||
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)
|
||||
content = log.read_text(encoding="utf-8", errors="replace")
|
||||
sampler_match = SAMPLER.search(content)
|
||||
workers = [dict(FIELDS.findall(match.group(1))) for match in WORKER.finditer(content)]
|
||||
samples = [dict(FIELDS.findall(match.group(1))) for match in SAMPLE.finditer(content)]
|
||||
if result.returncode or sampler_match is None or len(workers) < 9 or not samples:
|
||||
raise SystemExit(f"phase census failed or markers missing: {log}; exit={result.returncode}")
|
||||
sampler = dict(FIELDS.findall(sampler_match.group(1)))
|
||||
if sampler.get("status") != "PASS":
|
||||
raise SystemExit(f"L77 sampler failed: {log}: {sampler}")
|
||||
lod_seconds = {int(row["lod"]): float(row["worker_sum_s"]) for row in workers}
|
||||
|
||||
def estimate(scope: str) -> dict:
|
||||
lods = set(range(3)) if scope == "lod0-2" else set(range(9))
|
||||
eligible = [row for row in samples if int(row["lod"]) in lods]
|
||||
weights = {phase: sum(int(row.get("weight", "1")) for row in eligible
|
||||
if row.get("phase") == phase)
|
||||
for phase in ("passages", "support")}
|
||||
total_weight = sum(int(row.get("weight", "1")) for row in eligible)
|
||||
cpu_seconds = sum(value for lod, value in lod_seconds.items() if lod in lods)
|
||||
result_row = {"worker_cpu_s": round(cpu_seconds, 4),
|
||||
"weighted_samples": total_weight}
|
||||
for phase, weight in weights.items():
|
||||
share = weight / total_weight if total_weight else 0.0
|
||||
result_row[f"{phase}_share_pct"] = round(share * 100.0, 3)
|
||||
result_row[f"{phase}_worker_s_estimate"] = round(cpu_seconds * share, 4)
|
||||
return result_row
|
||||
|
||||
row = {
|
||||
"stratum": stratum, "variant": variant, "replicate": replicate,
|
||||
"exit_code": result.returncode,
|
||||
"sampler": sampler,
|
||||
"lod0_2": estimate("lod0-2"), "whole_view": estimate("whole"),
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
"runner_wall_s": round(time.monotonic() - started, 3),
|
||||
}
|
||||
print("[L109PhaseCensusRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if any(not path.is_file() for path in BINARIES.values()):
|
||||
raise SystemExit("missing L77 profile binary")
|
||||
rows = [run(stratum, variant, replicate)
|
||||
for stratum in ("FI", "TN")
|
||||
for replicate in (1, 2)
|
||||
for variant in ("base", "candidate")]
|
||||
summary = []
|
||||
for stratum in ("FI", "TN"):
|
||||
for variant in ("base", "candidate"):
|
||||
entry = {"stratum": stratum, "variant": variant, "replicates": 2}
|
||||
group = [row for row in rows if row["stratum"] == stratum
|
||||
and row["variant"] == variant]
|
||||
for scope in ("lod0_2", "whole_view"):
|
||||
for key in ("worker_cpu_s", "passages_share_pct", "passages_worker_s_estimate",
|
||||
"support_share_pct", "support_worker_s_estimate"):
|
||||
values = [row[scope][key] for row in group]
|
||||
entry[f"{scope}_{key}_runs"] = values
|
||||
entry[f"{scope}_{key}_median"] = statistics.median(values)
|
||||
summary.append(entry)
|
||||
(OUT / "summary.json").write_text(
|
||||
json.dumps({"runs": rows, "summary": summary}, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Five interleaved L80 readiness pairs for L109, at W8 and W12."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l109/timing"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"base": ROOT / "Saved/Linux/l109/bin-baseline/VoxelForgeCoreHeadless-l109-baseline",
|
||||
"candidate": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-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.compile(r"^\[StreamPipeline\] (.*)$", re.M)
|
||||
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
NEAR = re.compile(
|
||||
r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$",
|
||||
re.M,
|
||||
)
|
||||
|
||||
|
||||
def run(strate: int, workers: int, variant: str, phase: str, replicate: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
label = f"s{strate}-w{workers}-{variant}-{phase}{replicate}"
|
||||
log = OUT / f"{label}.log"
|
||||
camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARIES[variant]), 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",
|
||||
]
|
||||
start = 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_match = PIPELINE.search(content)
|
||||
near_match = NEAR.search(content)
|
||||
if result.returncode or pipeline_match is None or near_match is None:
|
||||
raise SystemExit(f"readiness run failed or trace missing: {log}; exit={result.returncode}")
|
||||
pipeline = dict(FIELDS.findall(pipeline_match.group(1)))
|
||||
row = {
|
||||
"strate": strate, "workers": workers, "variant": variant,
|
||||
"phase": phase, "replicate": replicate, "exit_code": result.returncode,
|
||||
"wall_s": round(time.monotonic() - start, 3),
|
||||
"lod02_ready_s": float(near_match.group(3)),
|
||||
"whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")),
|
||||
"whole_tiles": int(pipeline["whole_tiles"].split("/")[0]),
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L109TimingRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not DATA.is_file() or any(not binary.is_file() for binary in BINARIES.values()):
|
||||
raise SystemExit("missing data or timing binary")
|
||||
rows = []
|
||||
for strate in range(4):
|
||||
for workers in (8, 12):
|
||||
run(strate, workers, "base", "warmup", 0)
|
||||
run(strate, workers, "candidate", "warmup", 0)
|
||||
for replicate in range(1, 6):
|
||||
order = ("base", "candidate") if replicate % 2 else ("candidate", "base")
|
||||
for variant in order:
|
||||
rows.append(run(strate, workers, variant, "measured", replicate))
|
||||
(OUT / "runs-partial.json").write_text(
|
||||
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
|
||||
summary = []
|
||||
for strate in range(4):
|
||||
for workers in (8, 12):
|
||||
item = {"strate": strate, "workers": workers, "pairs": 5}
|
||||
for variant in ("base", "candidate"):
|
||||
group = [row for row in rows if row["strate"] == strate
|
||||
and row["workers"] == workers and row["variant"] == variant]
|
||||
for key in ("lod02_ready_s", "whole_view_ready_s"):
|
||||
values = [row[key] for row in group]
|
||||
item[f"{variant}_{key}"] = values
|
||||
item[f"{variant}_{key}_median"] = statistics.median(values)
|
||||
for key in ("lod02_ready_s", "whole_view_ready_s"):
|
||||
base = item[f"base_{key}_median"]
|
||||
candidate = item[f"candidate_{key}_median"]
|
||||
item[f"{key}_delta_pct"] = round(100 * (candidate / base - 1), 2)
|
||||
summary.append(item)
|
||||
(OUT / "summary.json").write_text(
|
||||
json.dumps({"runs": rows, "summary": summary}, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,484 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run one pinned null-RHI SW/FI arrival for Linux 107."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
HOST = Path("/home/jahni/VFHost")
|
||||
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l109"
|
||||
HOST_SAVED = HOST / "Saved/Linux/l109"
|
||||
EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd")
|
||||
OUT = ROOT / "Saved/Linux/l109/unreal-arrival"
|
||||
SPAWNS_M = {
|
||||
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||
"SW": (25.8369140625, 3.19921875, -200.0),
|
||||
}
|
||||
MOVE_START_S = 20.0
|
||||
MOVE_DURATION_S = 12.0
|
||||
EXIT_S = MOVE_START_S + MOVE_DURATION_S + 5.0
|
||||
RUN_TIMEOUT_S = 1740
|
||||
|
||||
|
||||
def read_events(path: Path) -> list[dict]:
|
||||
events = []
|
||||
if path.is_file():
|
||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
if line.startswith("{"):
|
||||
try:
|
||||
events.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return events
|
||||
|
||||
|
||||
def percentile(values: list[float], fraction: float) -> float | None:
|
||||
if not values:
|
||||
return None
|
||||
ordered = sorted(values)
|
||||
return ordered[min(len(ordered) - 1, int(round((len(ordered) - 1) * fraction)))]
|
||||
|
||||
|
||||
def sample_unreal_peak_rss() -> tuple[int, int] | None:
|
||||
executable = str(EDITOR).encode()
|
||||
project = str(HOST / "VFHost.uproject").encode()
|
||||
peak: tuple[int, int] | None = None
|
||||
for process in Path("/proc").glob("[0-9]*"):
|
||||
try:
|
||||
command_line = (process / "cmdline").read_bytes()
|
||||
if executable not in command_line or project not in command_line:
|
||||
continue
|
||||
status = (process / "status").read_text(encoding="ascii", errors="ignore")
|
||||
match = re.search(r"^VmHWM:\s+(\d+)\s+kB$", status, re.MULTILINE)
|
||||
if match:
|
||||
rss_kib = int(match.group(1))
|
||||
if peak is None or rss_kib > peak[0]:
|
||||
peak = (rss_kib, int(process.name))
|
||||
except (FileNotFoundError, PermissionError, ProcessLookupError, OSError):
|
||||
continue
|
||||
return peak
|
||||
|
||||
|
||||
def endphysics_gap(stdout: Path) -> tuple[float | None, int | None]:
|
||||
release_times: dict[int, dict[int, datetime]] = {}
|
||||
pattern = re.compile(
|
||||
r"^\[(\d{4}\.\d{2}\.\d{2}-\d{2}\.\d{2}\.\d{2}:\d{3})\].*"
|
||||
r"LogTick: tick\s+(\d+)\s+-+\s+Release tick group ([34])")
|
||||
with stdout.open(encoding="utf-8", errors="replace") as stream:
|
||||
for line in stream:
|
||||
match = pattern.match(line)
|
||||
if not match:
|
||||
continue
|
||||
timestamp = datetime.strptime(match.group(1), "%Y.%m.%d-%H.%M.%S:%f")
|
||||
frame = int(match.group(2))
|
||||
group = int(match.group(3))
|
||||
release_times.setdefault(frame, {})[group] = timestamp
|
||||
gaps = [(round((times[4] - times[3]).total_seconds() * 1000.0, 3), frame)
|
||||
for frame, times in release_times.items() if 3 in times and 4 in times]
|
||||
return max(gaps, default=(None, None))
|
||||
|
||||
|
||||
def run(stratum: str, repeat: int, phase: str, ready_depth: int,
|
||||
background_cap: int, worker_reserve: int, helper_background: int,
|
||||
tile_background_low: int, tile_background_normal: int, startup_boost: int,
|
||||
tick_logs: bool, insights: bool) -> dict:
|
||||
label = (f"{phase}-{stratum.lower()}-rd{ready_depth}-bg{background_cap}"
|
||||
f"-reserve{worker_reserve}-helperbg{helper_background}"
|
||||
f"-tilelow{tile_background_low}-tilenormal{tile_background_normal}"
|
||||
f"-ticklog{int(tick_logs)}"
|
||||
f"-boost{startup_boost}-{repeat}")
|
||||
trace = OUT / f"{label}.trace.jsonl"
|
||||
utrace = OUT / f"{label}.utrace"
|
||||
abslog = OUT / f"{label}.log"
|
||||
stdout = OUT / f"{label}.stdout.log"
|
||||
output = HOST_PLUGIN_SAVED / "output" / label
|
||||
userdir = HOST_SAVED / "user" / label
|
||||
zendata = HOST_SAVED / "zen" / label
|
||||
ddc = HOST_SAVED / "DDC"
|
||||
for directory in (OUT, output, userdir, zendata, ddc):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
trace.unlink(missing_ok=True)
|
||||
utrace.unlink(missing_ok=True)
|
||||
spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWNS_M[stratum])
|
||||
command = [
|
||||
str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended",
|
||||
"-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch",
|
||||
"-notraceserver", "-nocrashreports",
|
||||
"-DisablePlugins=AndroidFileServer",
|
||||
"-ddc=NoZenLocalFallback", f"-userdir={userdir}", f"-ZenDataPath={zendata}",
|
||||
f"-LocalDataCachePath={ddc}", "-run=VoxelForgeExplore", "-seed=0",
|
||||
"-archetype=TunnelNetwork", f"-slot={1 if stratum == 'FI' else 0}",
|
||||
"-modes=export", "-gameconfig=1", "-coreworldtest=1",
|
||||
"-ownerfourstrateplan=1", "-corevoxelsizecm=25", "-coreislandtest=0",
|
||||
"-corefallbacktest=0", "-coreofftest=0", "-coreplantest=0",
|
||||
"-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings", "-corehybridmesher=0",
|
||||
f"-out={output}", "-voxel.StreamingCoarseFirst=1",
|
||||
"-voxel.StreamingDynamicNearReserve=1",
|
||||
"-voxel.StreamingLegacyPendingResultCap=0",
|
||||
"-voxel.CoreRegionConcurrentAdmission=1",
|
||||
"-voxel.StreamingResultBacklogCap=256",
|
||||
f"-voxel.StreamingReadyQueueDepth={ready_depth}",
|
||||
f"-voxel.StreamingBackgroundWorkerCap={background_cap}",
|
||||
f"-voxel.StreamingBackgroundWorkerReserve={worker_reserve}",
|
||||
f"-voxel.CoreParallelForBackgroundPriority={helper_background}",
|
||||
f"-voxel.StreamingTileBackgroundLow={tile_background_low}",
|
||||
f"-voxel.StreamingTileBackgroundNormal={tile_background_normal}",
|
||||
f"-voxel.TestLogTickFunctions={int(tick_logs)}",
|
||||
"-voxel.CoreColumnCacheDirectMapped=0", "-voxel.CoreMCGradientMemoization=1",
|
||||
"-voxel.StreamingCollisionLookahead=1",
|
||||
f"-voxel.StreamingStartupApplyBoost={startup_boost}",
|
||||
"-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=140.0",
|
||||
f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}",
|
||||
f"-voxel.TestMoveDurationSeconds={MOVE_DURATION_S:.1f}",
|
||||
f"-voxel.TestExitSeconds={EXIT_S:.1f}", "-voxel.TestMoveDirection=1,0,0",
|
||||
"-voxel.StreamingSchedulerPolicy=3", "-voxel.TestExitOnSteady=0",
|
||||
"-coreworldstreamtrace=1", f"-voxel.StartupTraceSpawnCm={spawn_cm}",
|
||||
"-voxel.StartupTraceUntilEndPlay=1", "-voxel.StartupTraceThroughCrossing=1",
|
||||
"-voxel.StartupTraceMaxTiles=16384", f"-voxel.StartupTraceFile={trace}",
|
||||
f"-abslog={abslog}",
|
||||
]
|
||||
if insights:
|
||||
command += ["-trace=cpu,task,frame", "-statnamedevents",
|
||||
f"-tracefile={utrace}", "-tracefiletrunc"]
|
||||
wrapped = ["taskset", "-c", "0-11", "timeout", f"{RUN_TIMEOUT_S}s", *command]
|
||||
started = time.monotonic()
|
||||
with stdout.open("w", encoding="utf-8") as stream:
|
||||
process = subprocess.Popen(wrapped, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT)
|
||||
peak_rss = None
|
||||
while process.poll() is None:
|
||||
observed = sample_unreal_peak_rss()
|
||||
if observed and (peak_rss is None or observed[0] > peak_rss[0]):
|
||||
peak_rss = observed
|
||||
time.sleep(0.25)
|
||||
exit_code = process.wait()
|
||||
observed = sample_unreal_peak_rss()
|
||||
if observed and (peak_rss is None or observed[0] > peak_rss[0]):
|
||||
peak_rss = observed
|
||||
|
||||
events = read_events(trace)
|
||||
worker_backend_config = next(
|
||||
(e for e in events if e.get("type") == "worker_backend_config"), {})
|
||||
worker_cap_readback = next(
|
||||
(e for e in events if e.get("type") == "streaming_worker_cap_readback"), {})
|
||||
samples = [e for e in events if e.get("type") == "streaming_sample"]
|
||||
arrival = [e for e in samples if float(e.get("t_s", 0.0)) < MOVE_START_S]
|
||||
full = next((e for e in arrival
|
||||
if sum(e.get("desired_by_lod", [0] * 9)[:3]) > 0
|
||||
and sum(e.get("settled_by_lod", [0] * 9)[:3])
|
||||
>= sum(e.get("desired_by_lod", [0] * 9)[:3])), None)
|
||||
first_admission = next((e for e in events if e.get("type") == "scheduler_admission"
|
||||
and e.get("desired_rebuilt") == 1), None)
|
||||
request_events = [e for e in events if e.get("type") == "request"]
|
||||
initial_request_events = [e for e in request_events if e.get("epoch") == 0]
|
||||
tile_by_ordinal = {e.get("request_ordinal"): e for e in events
|
||||
if e.get("type") == "tile"}
|
||||
collision_count = sum(1 for e in events if e.get("type") == "collision_ready"
|
||||
and e.get("level", 9) <= 2
|
||||
and full and e.get("t_s", 1e30) <= full["t_s"])
|
||||
initial_tiles = [tile_by_ordinal[r.get("request_ordinal")] for r in initial_request_events
|
||||
if full and r.get("t_s", 1e30) <= full["t_s"]
|
||||
and r.get("request_ordinal") in tile_by_ordinal
|
||||
and tile_by_ordinal[r["request_ordinal"]].get("level", 9) <= 2]
|
||||
burst_start_s = float(first_admission["t_s"]) if first_admission else -1.0
|
||||
burst_end_s = float(full["t_s"]) if full else -1.0
|
||||
work_by_frame = {int(e.get("frame", -1)): e for e in events
|
||||
if e.get("type") == "game_thread_work_frame"}
|
||||
work_values = [e for e in work_by_frame.values()
|
||||
if burst_start_s <= float(e.get("t_s", -1.0)) <= burst_end_s]
|
||||
arrival_frame_ids = {int(e.get("frame", -1)) for e in work_values}
|
||||
scheduler_by_frame = {int(e.get("frame", -1)): e for e in events
|
||||
if e.get("type") == "scheduler_admission"}
|
||||
scheduler_values = [e for e in events if e.get("type") == "scheduler_admission"
|
||||
and burst_start_s <= float(e.get("t_s", -1.0)) <= burst_end_s]
|
||||
apply_by_frame = {int(e.get("frame", -1)): e for e in events
|
||||
if e.get("type") == "apply_frame"}
|
||||
commandlet_tick_by_frame = {int(e.get("frame", -1)): e for e in events
|
||||
if e.get("type") == "commandlet_world_tick_frame"}
|
||||
garbage_collect_by_frame = {int(e.get("frame", -1)): e for e in events
|
||||
if e.get("type") == "garbage_collect"}
|
||||
# apply_frame N+1 records the interval containing AVoxelWorld::Tick N. Its apply_ms
|
||||
# belongs to Tick N+1, so use apply_by_frame[N] for the same-tick apply breakdown.
|
||||
near_intervals = [apply_by_frame[frame + 1] for frame in arrival_frame_ids
|
||||
if frame + 1 in apply_by_frame]
|
||||
rmc_by_frame: dict[int, dict[str, float]] = {}
|
||||
for event in events:
|
||||
frame = int(event.get("frame", -1))
|
||||
if event.get("type") != "rmc_submit" or frame not in arrival_frame_ids:
|
||||
continue
|
||||
values = rmc_by_frame.setdefault(frame, {
|
||||
"component_acquire_ms": 0.0,
|
||||
"create_section_group_ms": 0.0,
|
||||
"create_sections_ms": 0.0,
|
||||
"collision_handoff_ms": 0.0,
|
||||
})
|
||||
for key in values:
|
||||
values[key] += float(event.get(key, 0.0))
|
||||
collision_completion_by_frame: dict[int, float] = {}
|
||||
for event in events:
|
||||
frame = int(event.get("frame", -1))
|
||||
if event.get("type") == "collision_completion" and frame in arrival_frame_ids:
|
||||
collision_completion_by_frame[frame] = collision_completion_by_frame.get(frame, 0.0) \
|
||||
+ float(event.get("handler_ms", 0.0))
|
||||
near_requests = [r for r in request_events if full and r.get("t_s", 1e30) <= full["t_s"]
|
||||
and r.get("level", 9) <= 2]
|
||||
initial_lod0_counts = Counter(
|
||||
tuple(r.get("tile", ())) for r in near_requests if r.get("level") == 0)
|
||||
repeated_lod0_keys = [
|
||||
{"tile": list(tile), "requests": count}
|
||||
for tile, count in sorted(initial_lod0_counts.items()) if count > 1]
|
||||
initial_scheduler_frames = [e for e in events
|
||||
if e.get("type") == "scheduler_admission" and full
|
||||
and e.get("t_s", 1e30) <= full["t_s"]]
|
||||
worst_interval = max(near_intervals, key=lambda e: float(e.get("frame_ms", 0.0)), default=None)
|
||||
worst_interval_frame_id = int(worst_interval.get("frame", -1)) if worst_interval else -1
|
||||
worst_work_frame_id = worst_interval_frame_id - 1
|
||||
worst_work = work_by_frame.get(worst_work_frame_id, {})
|
||||
worst_apply = apply_by_frame.get(worst_work_frame_id, {})
|
||||
worst_scheduler = scheduler_by_frame.get(worst_work_frame_id, {})
|
||||
worst_rmc = rmc_by_frame.get(worst_work_frame_id, {})
|
||||
worst_collision_completion_ms = collision_completion_by_frame.get(worst_work_frame_id, 0.0)
|
||||
rmc_values = [rmc_by_frame.get(frame, {}) for frame in arrival_frame_ids]
|
||||
apply_values = [apply_by_frame[frame] for frame in arrival_frame_ids
|
||||
if frame in apply_by_frame]
|
||||
commandlet_tick_values = [commandlet_tick_by_frame[frame] for frame in arrival_frame_ids
|
||||
if frame in commandlet_tick_by_frame]
|
||||
garbage_collect_values = [garbage_collect_by_frame[frame] for frame in arrival_frame_ids
|
||||
if frame in garbage_collect_by_frame]
|
||||
|
||||
def work_stat(name: str, fraction: float | None = None) -> float | None:
|
||||
values = [float(e.get(name, 0.0)) for e in work_values]
|
||||
return percentile(values, fraction) if fraction is not None else max(values, default=0.0)
|
||||
|
||||
def scheduler_stat(name: str, fraction: float | None = None) -> float | None:
|
||||
values = [float(e.get(name, 0.0)) for e in scheduler_values]
|
||||
return percentile(values, fraction) if fraction is not None else max(values, default=0.0)
|
||||
|
||||
def rmc_stat(name: str, fraction: float | None = None) -> float | None:
|
||||
values = [float(e.get(name, 0.0)) for e in rmc_values]
|
||||
return percentile(values, fraction) if fraction is not None else max(values, default=0.0)
|
||||
|
||||
def commandlet_stat(name: str, fraction: float | None = None) -> float | None:
|
||||
values = [float(e.get(name, 0.0)) for e in commandlet_tick_values]
|
||||
return percentile(values, fraction) if fraction is not None else max(values, default=0.0)
|
||||
|
||||
row = {
|
||||
"label": label, "stratum": stratum, "repeat": repeat, "phase": phase,
|
||||
"ready_queue_depth": ready_depth,
|
||||
"requested_background_worker_cap": background_cap,
|
||||
"background_worker_cap": worker_cap_readback.get(
|
||||
"streaming_background_worker_cap", background_cap),
|
||||
"requested_background_worker_reserve": worker_reserve,
|
||||
"background_worker_reserve": worker_backend_config.get(
|
||||
"streaming_background_worker_reserve", worker_cap_readback.get(
|
||||
"background_worker_reserve", worker_reserve)),
|
||||
"requested_core_parallel_for_background_priority": helper_background,
|
||||
"core_parallel_for_background_priority": worker_backend_config.get(
|
||||
"core_parallel_for_background_priority", helper_background),
|
||||
"requested_streaming_tile_background_low": tile_background_low,
|
||||
"streaming_tile_background_low": worker_backend_config.get(
|
||||
"streaming_tile_background_low", tile_background_low),
|
||||
"requested_streaming_tile_background_normal": tile_background_normal,
|
||||
"streaming_tile_background_normal": worker_backend_config.get(
|
||||
"streaming_tile_background_normal", tile_background_normal),
|
||||
"effective_tile_slots": worker_backend_config.get("effective_tile_slots"),
|
||||
"background_worker_threads": worker_backend_config.get("taskgraph_background_threads"),
|
||||
"startup_apply_boost": startup_boost, "insights": insights,
|
||||
"engine_tick_logs": tick_logs,
|
||||
"exit_code": exit_code,
|
||||
"launch_wrapper": "taskset -c 0-11 timeout 1740s (no address-space limit)",
|
||||
"peak_rss_kib": peak_rss[0] if peak_rss else None,
|
||||
"peak_rss_mib": round(peak_rss[0] / 1024.0, 1) if peak_rss else None,
|
||||
"peak_rss_pid": peak_rss[1] if peak_rss else None,
|
||||
"endphysics_to_postphysics_gap_ms_max": None,
|
||||
"endphysics_gap_frame": None,
|
||||
"wall_s": round(time.monotonic() - started, 3),
|
||||
"desired_t_s": next((e.get("t_s") for e in events if e.get("type") == "desired_set"), None),
|
||||
"first_admission_t_s": first_admission.get("t_s") if first_admission else None,
|
||||
"lod02_full_settled_t_s": full.get("t_s") if full else None,
|
||||
"near_settle_from_first_admission_s": round(
|
||||
float(full["t_s"]) - float(first_admission["t_s"]), 4)
|
||||
if full and first_admission else None,
|
||||
"desired_by_lod": full.get("desired_by_lod", [])[:3] if full else None,
|
||||
"settled_by_lod": full.get("settled_by_lod", [])[:3] if full else None,
|
||||
"near_admitted_tiles": len(near_requests),
|
||||
"initial_lod0_admissions": sum(initial_lod0_counts.values()),
|
||||
"repeated_lod0_request_keys_before_settle": repeated_lod0_keys,
|
||||
"peak_result_backlog_before_settle": max(
|
||||
(int(e.get("result_backlog", 0)) for e in initial_scheduler_frames), default=0),
|
||||
"last_admission_t_s": max((e.get("t_s", 0.0) for e in near_requests), default=None),
|
||||
"near_tile_events": len(initial_tiles),
|
||||
"near_worker_seconds": round(sum(e.get("worker_task_s", 0.0) for e in initial_tiles), 4),
|
||||
"near_core_seconds": round(sum(e.get("core_generate_s", 0.0) for e in initial_tiles), 4),
|
||||
"collision_ready_by_settle": collision_count,
|
||||
"burst_frame_count": len(near_intervals),
|
||||
"burst_frame_ms_p95": round(sorted(e.get("frame_ms", 0.0) for e in near_intervals)[
|
||||
min(len(near_intervals) - 1, int(.95 * (len(near_intervals) - 1)))], 3)
|
||||
if near_intervals else None,
|
||||
"burst_frame_ms_max": round(max((e.get("frame_ms", 0.0) for e in near_intervals), default=0.0), 3),
|
||||
"burst_full_world_tick_ms_p99": round(commandlet_stat("world_tick_ms", .99) or 0.0, 3),
|
||||
"burst_full_world_tick_ms_max": round(commandlet_stat("world_tick_ms") or 0.0, 3),
|
||||
"burst_world_pre_actor_ms_p99": round(commandlet_stat("world_pre_actor_ms", .99) or 0.0, 3),
|
||||
"burst_world_pre_actor_ms_max": round(commandlet_stat("world_pre_actor_ms") or 0.0, 3),
|
||||
"burst_world_actor_phase_ms_p99": round(commandlet_stat("world_actor_phase_ms", .99) or 0.0, 3),
|
||||
"burst_world_actor_phase_ms_max": round(commandlet_stat("world_actor_phase_ms") or 0.0, 3),
|
||||
"burst_world_post_actor_ms_p99": round(commandlet_stat("world_post_actor_ms", .99) or 0.0, 3),
|
||||
"burst_world_post_actor_ms_max": round(commandlet_stat("world_post_actor_ms") or 0.0, 3),
|
||||
"burst_commandlet_readiness_check_ms_max": round(
|
||||
commandlet_stat("readiness_check_ms") or 0.0, 3),
|
||||
"burst_commandlet_sleep_ms_p99": round(commandlet_stat("sleep_ms", .99) or 0.0, 3),
|
||||
"burst_commandlet_sleep_ms_max": round(commandlet_stat("sleep_ms") or 0.0, 3),
|
||||
"burst_commandlet_loop_ms_max": round(commandlet_stat("loop_ms") or 0.0, 3),
|
||||
"burst_gc_count": len(garbage_collect_values),
|
||||
"burst_gc_duration_ms_max": round(max(
|
||||
(float(e.get("duration_ms", 0.0)) for e in garbage_collect_values), default=0.0), 3),
|
||||
"burst_max_meshes_per_frame": max((e.get("meshes_applied", 0) for e in apply_values), default=0),
|
||||
"burst_max_apply_ms": round(max((e.get("apply_ms", 0.0) for e in apply_values), default=0.0), 3),
|
||||
"burst_max_admit_per_frame": max((sum(e.get("submitted_by_lod", [0] * 9)[:3])
|
||||
for e in scheduler_values), default=0),
|
||||
"burst_frame_timing_count": len(work_values),
|
||||
"burst_active_generation_workers_p95": round(work_stat("active_generation_workers", .95) or 0.0, 3),
|
||||
"burst_active_generation_workers_max": round(work_stat("active_generation_workers") or 0.0, 3),
|
||||
"burst_active_tile_tasks_max": round(work_stat("active_tile_tasks") or 0.0, 3),
|
||||
"burst_world_tick_ms_p99": round(work_stat("tick_ms", .99) or 0.0, 3),
|
||||
"burst_world_tick_ms_max": round(work_stat("tick_ms") or 0.0, 3),
|
||||
"burst_update_chunks_ms_p99": round(work_stat("update_chunks_ms", .99) or 0.0, 3),
|
||||
"burst_update_chunks_ms_max": round(work_stat("update_chunks_ms") or 0.0, 3),
|
||||
"burst_pending_drain_ms_p99": round(work_stat("pending_drain_ms", .99) or 0.0, 3),
|
||||
"burst_pending_drain_ms_max": round(work_stat("pending_drain_ms") or 0.0, 3),
|
||||
"burst_post_apply_callbacks_ms_p99": round(work_stat("post_apply_callbacks_ms", .99) or 0.0, 3),
|
||||
"burst_post_apply_callbacks_ms_max": round(work_stat("post_apply_callbacks_ms") or 0.0, 3),
|
||||
"burst_visibility_reconcile_ms_p99": round(work_stat("visibility_reconcile_ms", .99) or 0.0, 3),
|
||||
"burst_visibility_reconcile_ms_max": round(work_stat("visibility_reconcile_ms") or 0.0, 3),
|
||||
"burst_collision_probe_ms_max": round(work_stat("collision_probe_ms") or 0.0, 3),
|
||||
"burst_visible_apply_probe_ms_max": round(work_stat("visible_apply_probe_ms") or 0.0, 3),
|
||||
"burst_lod0_ready_ms_max": round(work_stat("lod0_ready_ms") or 0.0, 3),
|
||||
"burst_steady_trace_ms_max": round(work_stat("steady_trace_ms") or 0.0, 3),
|
||||
"burst_modification_ms_max": round(work_stat("modification_ms") or 0.0, 3),
|
||||
"burst_lifecycle_audit_ms_p99": round(work_stat("lifecycle_audit_ms", .99) or 0.0, 3),
|
||||
"burst_lifecycle_audit_ms_max": round(work_stat("lifecycle_audit_ms") or 0.0, 3),
|
||||
"burst_collision_gate_ms_p99": round(work_stat("collision_gate_ms", .99) or 0.0, 3),
|
||||
"burst_collision_gate_ms_max": round(work_stat("collision_gate_ms") or 0.0, 3),
|
||||
"burst_unload_ms_p99": round(work_stat("unload_ms", .99) or 0.0, 3),
|
||||
"burst_unload_ms_max": round(work_stat("unload_ms") or 0.0, 3),
|
||||
"burst_trace_json_ms_p99": round(work_stat("trace_json_ms", .99) or 0.0, 3),
|
||||
"burst_trace_json_ms_max": round(work_stat("trace_json_ms") or 0.0, 3),
|
||||
"burst_desired_rebuild_ms_max": round(scheduler_stat("desired_rebuild_ms") or 0.0, 3),
|
||||
"burst_rmc_create_group_ms_p99": round(rmc_stat("create_section_group_ms", .99) or 0.0, 3),
|
||||
"burst_rmc_create_group_ms_max": round(rmc_stat("create_section_group_ms") or 0.0, 3),
|
||||
"burst_rmc_collision_handoff_ms_max": round(rmc_stat("collision_handoff_ms") or 0.0, 3),
|
||||
"burst_collision_completion_ms_max": round(max(
|
||||
collision_completion_by_frame.values(), default=0.0), 3),
|
||||
"worst_frame_breakdown": {
|
||||
"frame": worst_work_frame_id,
|
||||
"world_tick_frame": worst_work_frame_id,
|
||||
"frame_interval_end_frame": worst_interval_frame_id,
|
||||
"frame_interval_ms": round(float(worst_interval.get("frame_ms", 0.0)), 3)
|
||||
if worst_interval else None,
|
||||
"apply_ms": round(float(worst_apply.get("apply_ms", 0.0)), 3)
|
||||
if worst_apply else None,
|
||||
"result_drain_ms": round(float(worst_apply.get("drain_ms", 0.0)), 3)
|
||||
if worst_apply else None,
|
||||
"outside_world_tick_ms": round(max(0.0, float(worst_interval.get("frame_ms", 0.0))
|
||||
- float(worst_work.get("tick_ms", 0.0))), 3)
|
||||
if worst_interval else None,
|
||||
"outside_full_world_tick_ms": round(max(0.0, float(worst_interval.get("frame_ms", 0.0))
|
||||
- float(commandlet_tick_by_frame.get(worst_work_frame_id, {}).get("world_tick_ms", 0.0))), 3)
|
||||
if worst_interval else None,
|
||||
"full_world_tick_ms": round(float(commandlet_tick_by_frame.get(
|
||||
worst_work_frame_id, {}).get("world_tick_ms", 0.0)), 3),
|
||||
"world_pre_actor_ms": round(float(commandlet_tick_by_frame.get(
|
||||
worst_work_frame_id, {}).get("world_pre_actor_ms", 0.0)), 3),
|
||||
"world_actor_phase_ms": round(float(commandlet_tick_by_frame.get(
|
||||
worst_work_frame_id, {}).get("world_actor_phase_ms", 0.0)), 3),
|
||||
"world_post_actor_ms": round(float(commandlet_tick_by_frame.get(
|
||||
worst_work_frame_id, {}).get("world_post_actor_ms", 0.0)), 3),
|
||||
"commandlet_readiness_check_ms": round(float(commandlet_tick_by_frame.get(
|
||||
worst_work_frame_id, {}).get("readiness_check_ms", 0.0)), 3),
|
||||
"commandlet_sleep_ms": round(float(commandlet_tick_by_frame.get(
|
||||
worst_work_frame_id, {}).get("sleep_ms", 0.0)), 3),
|
||||
"commandlet_loop_ms": round(float(commandlet_tick_by_frame.get(
|
||||
worst_work_frame_id, {}).get("loop_ms", 0.0)), 3),
|
||||
"gc_duration_ms": round(float(garbage_collect_by_frame.get(
|
||||
worst_work_frame_id, {}).get("duration_ms", 0.0)), 3),
|
||||
"world_tick_ms": round(float(worst_work.get("tick_ms", 0.0)), 3),
|
||||
"active_generation_workers": int(worst_work.get("active_generation_workers", 0)),
|
||||
"active_tile_tasks": int(worst_work.get("active_tile_tasks", 0)),
|
||||
"collision_gate_ms": round(float(worst_work.get("collision_gate_ms", 0.0)), 3),
|
||||
"update_chunks_ms": round(float(worst_work.get("update_chunks_ms", 0.0)), 3),
|
||||
"desired_rebuild_ms": round(float(worst_scheduler.get("desired_rebuild_ms", 0.0)), 3),
|
||||
"pending_drain_ms": round(float(worst_work.get("pending_drain_ms", 0.0)), 3),
|
||||
"post_apply_callbacks_ms": round(float(worst_work.get("post_apply_callbacks_ms", 0.0)), 3),
|
||||
"collision_probe_ms": round(float(worst_work.get("collision_probe_ms", 0.0)), 3),
|
||||
"visibility_reconcile_ms": round(float(worst_work.get("visibility_reconcile_ms", 0.0)), 3),
|
||||
"visible_apply_probe_ms": round(float(worst_work.get("visible_apply_probe_ms", 0.0)), 3),
|
||||
"lod0_ready_ms": round(float(worst_work.get("lod0_ready_ms", 0.0)), 3),
|
||||
"unload_ms": round(float(worst_work.get("unload_ms", 0.0)), 3),
|
||||
"steady_trace_ms": round(float(worst_work.get("steady_trace_ms", 0.0)), 3),
|
||||
"modification_ms": round(float(worst_work.get("modification_ms", 0.0)), 3),
|
||||
"lifecycle_audit_ms": round(float(worst_work.get("lifecycle_audit_ms", 0.0)), 3),
|
||||
"trace_json_ms": round(float(worst_work.get("trace_json_ms", 0.0)), 3),
|
||||
"trace_archive_serialize_ms": round(float(
|
||||
worst_work.get("trace_archive_serialize_ms", 0.0)), 3),
|
||||
"collision_completion_ms": round(worst_collision_completion_ms, 3),
|
||||
**{key: round(float(worst_rmc.get(key, 0.0)), 3)
|
||||
for key in ("component_acquire_ms", "create_section_group_ms",
|
||||
"create_sections_ms", "collision_handoff_ms")},
|
||||
},
|
||||
"trace": str(trace.relative_to(ROOT)), "utrace": str(utrace.relative_to(ROOT)),
|
||||
"abslog": str(abslog.relative_to(ROOT)), "stdout": str(stdout.relative_to(ROOT)),
|
||||
}
|
||||
stdout_text = stdout.read_text(encoding="utf-8", errors="replace")
|
||||
gap_ms, gap_frame = endphysics_gap(stdout)
|
||||
row["endphysics_to_postphysics_gap_ms_max"] = gap_ms
|
||||
row["endphysics_gap_frame"] = gap_frame
|
||||
failure = re.search(r"\[VoxelForgeCoreWorldTest\] failed: ([^\r\n]+)", stdout_text)
|
||||
if failure:
|
||||
row["world_test_failure"] = failure.group(1)
|
||||
with (OUT / "runs.jsonl").open("a", encoding="utf-8") as stream:
|
||||
stream.write(json.dumps(row, sort_keys=True) + "\n")
|
||||
(OUT / f"{label}.summary.json").write_text(json.dumps(row, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
print("[L109UnrealArrival] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
if exit_code:
|
||||
raise SystemExit(f"Unreal commandlet failed: {stdout}; exit={exit_code}")
|
||||
if not full or len(arrival) < 8:
|
||||
raise SystemExit(f"Unreal arrival trace incomplete: {stdout}; exit={exit_code}")
|
||||
if insights and (not utrace.is_file() or utrace.stat().st_size < 1024):
|
||||
raise SystemExit(f"Unreal Insights trace missing or empty: {utrace}")
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--stratum", choices=("FI", "SW"), required=True)
|
||||
parser.add_argument("--repeat", type=int, default=1)
|
||||
parser.add_argument("--phase", choices=("probe", "before", "after", "snapshot"), default="probe")
|
||||
parser.add_argument("--ready-depth", type=int, default=64)
|
||||
parser.add_argument("--background-cap", type=int, choices=(0, 1), default=1)
|
||||
parser.add_argument("--worker-reserve", type=int, choices=(0, 1, 2, 3), default=0)
|
||||
parser.add_argument("--helper-background", type=int, choices=(0, 1), default=1)
|
||||
parser.add_argument("--tile-background-low", type=int, choices=(0, 1), default=1)
|
||||
parser.add_argument("--tile-background-normal", type=int, choices=(0, 1), default=0)
|
||||
parser.add_argument("--startup-boost", type=int, choices=(0, 1), default=1)
|
||||
parser.add_argument("--tick-logs", action="store_true")
|
||||
parser.add_argument("--insights", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if not EDITOR.is_file():
|
||||
raise SystemExit(f"missing Unreal commandlet executable: {EDITOR}")
|
||||
run(args.stratum, args.repeat, args.phase, args.ready_depth,
|
||||
args.background_cap, args.worker_reserve, args.helper_background,
|
||||
args.tile_background_low, args.tile_background_normal, args.startup_boost,
|
||||
args.tick_logs, args.insights)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,93 @@
|
||||
# L109 passage captures
|
||||
|
||||
Each PNG was rendered from its own exported mesh. The renderer reported visible triangles, and each decoded image met the pixel coverage floor. `low_contrast_geometry_visible` marks images that met this floor below the preferred contrast target.
|
||||
|
||||
## Before
|
||||
|
||||
- [upper-mouth](before/passage-0-1-0-upper-mouth.png) — passage/0-1/0; 188440 visible triangles
|
||||
- [lower-mouth](before/passage-0-1-0-lower-mouth.png) — passage/0-1/0; 564520 visible triangles
|
||||
- [25%](before/passage-0-1-0-station-25.png) — passage/0-1/0; 234197 visible triangles
|
||||
- [50%](before/passage-0-1-0-station-50.png) — passage/0-1/0; 315900 visible triangles
|
||||
- [75%](before/passage-0-1-0-station-75.png) — passage/0-1/0; 256359 visible triangles
|
||||
- [section-1/view-1](before/passage-0-1-0-openair-1-view-1.png) — passage/0-1/0 **OPEN AIR**; 211462 visible triangles
|
||||
- [section-1/view-2](before/passage-0-1-0-openair-1-view-2.png) — passage/0-1/0 **OPEN AIR**; 208255 visible triangles
|
||||
- [section-1/view-3](before/passage-0-1-0-openair-1-view-3.png) — passage/0-1/0 **OPEN AIR**; 221239 visible triangles
|
||||
- [section-2/view-1](before/passage-0-1-0-openair-2-view-1.png) — passage/0-1/0 **OPEN AIR**; 218664 visible triangles
|
||||
- [section-2/view-2](before/passage-0-1-0-openair-2-view-2.png) — passage/0-1/0 **OPEN AIR**; 223817 visible triangles
|
||||
- [section-2/view-3](before/passage-0-1-0-openair-2-view-3.png) — passage/0-1/0 **OPEN AIR**; 221239 visible triangles
|
||||
- [section-3/view-1](before/passage-0-1-0-openair-3-view-1.png) — passage/0-1/0 **OPEN AIR**; 224005 visible triangles
|
||||
- [section-3/view-2](before/passage-0-1-0-openair-3-view-2.png) — passage/0-1/0 **OPEN AIR**; 230599 visible triangles; low_contrast_geometry_visible
|
||||
- [section-3/view-3](before/passage-0-1-0-openair-3-view-3.png) — passage/0-1/0 **OPEN AIR**; 229291 visible triangles
|
||||
- [section-4/view-1](before/passage-0-1-0-openair-4-view-1.png) — passage/0-1/0 **OPEN AIR**; 407353 visible triangles
|
||||
- [section-4/view-2](before/passage-0-1-0-openair-4-view-2.png) — passage/0-1/0 **OPEN AIR**; 406732 visible triangles
|
||||
- [section-4/view-3](before/passage-0-1-0-openair-4-view-3.png) — passage/0-1/0 **OPEN AIR**; 363878 visible triangles
|
||||
- [upper-mouth](before/passage-1-2-0-upper-mouth.png) — passage/1-2/0; 576595 visible triangles
|
||||
- [lower-mouth](before/passage-1-2-0-lower-mouth.png) — passage/1-2/0; 555286 visible triangles
|
||||
- [25%](before/passage-1-2-0-station-25.png) — passage/1-2/0; 667602 visible triangles
|
||||
- [50%](before/passage-1-2-0-station-50.png) — passage/1-2/0; 602756 visible triangles
|
||||
- [75%](before/passage-1-2-0-station-75.png) — passage/1-2/0; 620502 visible triangles
|
||||
- [section-1/view-1](before/passage-1-2-0-openair-1-view-1.png) — passage/1-2/0 **OPEN AIR**; 636438 visible triangles
|
||||
- [section-1/view-2](before/passage-1-2-0-openair-1-view-2.png) — passage/1-2/0 **OPEN AIR**; 593960 visible triangles
|
||||
- [section-1/view-3](before/passage-1-2-0-openair-1-view-3.png) — passage/1-2/0 **OPEN AIR**; 622040 visible triangles
|
||||
- [upper-mouth](before/passage-2-3-0-upper-mouth.png) — passage/2-3/0; 59568 visible triangles
|
||||
- [lower-mouth](before/passage-2-3-0-lower-mouth.png) — passage/2-3/0; 294430 visible triangles; low_contrast_geometry_visible
|
||||
- [25%](before/passage-2-3-0-station-25.png) — passage/2-3/0; 55420 visible triangles; low_contrast_geometry_visible
|
||||
- [50%](before/passage-2-3-0-station-50.png) — passage/2-3/0; 458134 visible triangles
|
||||
- [75%](before/passage-2-3-0-station-75.png) — passage/2-3/0; 475978 visible triangles
|
||||
- [section-1/view-1](before/passage-2-3-0-openair-1-view-1.png) — passage/2-3/0 **OPEN AIR**; 104612 visible triangles
|
||||
- [section-1/view-2](before/passage-2-3-0-openair-1-view-2.png) — passage/2-3/0 **OPEN AIR**; 81078 visible triangles
|
||||
- [section-1/view-3](before/passage-2-3-0-openair-1-view-3.png) — passage/2-3/0 **OPEN AIR**; 94962 visible triangles; low_contrast_geometry_visible
|
||||
- [section-2/view-1](before/passage-2-3-0-openair-2-view-1.png) — passage/2-3/0 **OPEN AIR**; 476052 visible triangles
|
||||
- [section-2/view-2](before/passage-2-3-0-openair-2-view-2.png) — passage/2-3/0 **OPEN AIR**; 479198 visible triangles
|
||||
- [section-2/view-3](before/passage-2-3-0-openair-2-view-3.png) — passage/2-3/0 **OPEN AIR**; 479198 visible triangles
|
||||
- [section-3/view-1](before/passage-2-3-0-openair-3-view-1.png) — passage/2-3/0 **OPEN AIR**; 485580 visible triangles
|
||||
- [section-3/view-2](before/passage-2-3-0-openair-3-view-2.png) — passage/2-3/0 **OPEN AIR**; 478866 visible triangles
|
||||
- [section-3/view-3](before/passage-2-3-0-openair-3-view-3.png) — passage/2-3/0 **OPEN AIR**; 490304 visible triangles; low_contrast_geometry_visible
|
||||
- [section-4/view-1](before/passage-2-3-0-openair-4-view-1.png) — passage/2-3/0 **OPEN AIR**; 475662 visible triangles
|
||||
- [section-4/view-2](before/passage-2-3-0-openair-4-view-2.png) — passage/2-3/0 **OPEN AIR**; 496128 visible triangles
|
||||
- [section-4/view-3](before/passage-2-3-0-openair-4-view-3.png) — passage/2-3/0 **OPEN AIR**; 496508 visible triangles; low_contrast_geometry_visible
|
||||
|
||||
## After
|
||||
|
||||
- [upper-mouth](after/passage-0-1-0-upper-mouth.png) — passage/0-1/0; 253282 visible triangles
|
||||
- [lower-mouth](after/passage-0-1-0-lower-mouth.png) — passage/0-1/0; 559326 visible triangles
|
||||
- [25%](after/passage-0-1-0-station-25.png) — passage/0-1/0; 145630 visible triangles
|
||||
- [50%](after/passage-0-1-0-station-50.png) — passage/0-1/0; 430386 visible triangles
|
||||
- [75%](after/passage-0-1-0-station-75.png) — passage/0-1/0; 294374 visible triangles
|
||||
- [section-1/view-1](after/passage-0-1-0-openair-1-view-1.png) — passage/0-1/0 **OPEN AIR**; 289074 visible triangles
|
||||
- [section-1/view-2](after/passage-0-1-0-openair-1-view-2.png) — passage/0-1/0 **OPEN AIR**; 298057 visible triangles
|
||||
- [section-1/view-3](after/passage-0-1-0-openair-1-view-3.png) — passage/0-1/0 **OPEN AIR**; 322309 visible triangles
|
||||
- [section-2/view-1](after/passage-0-1-0-openair-2-view-1.png) — passage/0-1/0 **OPEN AIR**; 283642 visible triangles
|
||||
- [section-2/view-2](after/passage-0-1-0-openair-2-view-2.png) — passage/0-1/0 **OPEN AIR**; 316771 visible triangles
|
||||
- [section-2/view-3](after/passage-0-1-0-openair-2-view-3.png) — passage/0-1/0 **OPEN AIR**; 322309 visible triangles
|
||||
- [section-3/view-1](after/passage-0-1-0-openair-3-view-1.png) — passage/0-1/0 **OPEN AIR**; 262299 visible triangles
|
||||
- [section-3/view-2](after/passage-0-1-0-openair-3-view-2.png) — passage/0-1/0 **OPEN AIR**; 310919 visible triangles; low_contrast_geometry_visible
|
||||
- [section-3/view-3](after/passage-0-1-0-openair-3-view-3.png) — passage/0-1/0 **OPEN AIR**; 363071 visible triangles
|
||||
- [section-4/view-1](after/passage-0-1-0-openair-4-view-1.png) — passage/0-1/0 **OPEN AIR**; 378007 visible triangles
|
||||
- [section-4/view-2](after/passage-0-1-0-openair-4-view-2.png) — passage/0-1/0 **OPEN AIR**; 471132 visible triangles
|
||||
- [section-4/view-3](after/passage-0-1-0-openair-4-view-3.png) — passage/0-1/0 **OPEN AIR**; 382187 visible triangles
|
||||
- [upper-mouth](after/passage-1-2-0-upper-mouth.png) — passage/1-2/0; 570417 visible triangles; low_contrast_geometry_visible
|
||||
- [lower-mouth](after/passage-1-2-0-lower-mouth.png) — passage/1-2/0; 511318 visible triangles
|
||||
- [25%](after/passage-1-2-0-station-25.png) — passage/1-2/0; 661032 visible triangles
|
||||
- [50%](after/passage-1-2-0-station-50.png) — passage/1-2/0; 602996 visible triangles
|
||||
- [75%](after/passage-1-2-0-station-75.png) — passage/1-2/0; 615520 visible triangles
|
||||
- [section-1/view-1](after/passage-1-2-0-openair-1-view-1.png) — passage/1-2/0 **OPEN AIR**; 629052 visible triangles
|
||||
- [section-1/view-2](after/passage-1-2-0-openair-1-view-2.png) — passage/1-2/0 **OPEN AIR**; 588414 visible triangles
|
||||
- [section-1/view-3](after/passage-1-2-0-openair-1-view-3.png) — passage/1-2/0 **OPEN AIR**; 616234 visible triangles
|
||||
- [upper-mouth](after/passage-2-3-0-upper-mouth.png) — passage/2-3/0; 59562 visible triangles
|
||||
- [lower-mouth](after/passage-2-3-0-lower-mouth.png) — passage/2-3/0; 294522 visible triangles; low_contrast_geometry_visible
|
||||
- [25%](after/passage-2-3-0-station-25.png) — passage/2-3/0; 55484 visible triangles; low_contrast_geometry_visible
|
||||
- [50%](after/passage-2-3-0-station-50.png) — passage/2-3/0; 457754 visible triangles
|
||||
- [75%](after/passage-2-3-0-station-75.png) — passage/2-3/0; 475706 visible triangles
|
||||
- [section-1/view-1](after/passage-2-3-0-openair-1-view-1.png) — passage/2-3/0 **OPEN AIR**; 104612 visible triangles
|
||||
- [section-1/view-2](after/passage-2-3-0-openair-1-view-2.png) — passage/2-3/0 **OPEN AIR**; 81078 visible triangles
|
||||
- [section-1/view-3](after/passage-2-3-0-openair-1-view-3.png) — passage/2-3/0 **OPEN AIR**; 94962 visible triangles; low_contrast_geometry_visible
|
||||
- [section-2/view-1](after/passage-2-3-0-openair-2-view-1.png) — passage/2-3/0 **OPEN AIR**; 475688 visible triangles
|
||||
- [section-2/view-2](after/passage-2-3-0-openair-2-view-2.png) — passage/2-3/0 **OPEN AIR**; 479134 visible triangles
|
||||
- [section-2/view-3](after/passage-2-3-0-openair-2-view-3.png) — passage/2-3/0 **OPEN AIR**; 479134 visible triangles
|
||||
- [section-3/view-1](after/passage-2-3-0-openair-3-view-1.png) — passage/2-3/0 **OPEN AIR**; 485216 visible triangles
|
||||
- [section-3/view-2](after/passage-2-3-0-openair-3-view-2.png) — passage/2-3/0 **OPEN AIR**; 479198 visible triangles
|
||||
- [section-3/view-3](after/passage-2-3-0-openair-3-view-3.png) — passage/2-3/0 **OPEN AIR**; 490436 visible triangles; low_contrast_geometry_visible
|
||||
- [section-4/view-1](after/passage-2-3-0-openair-4-view-1.png) — passage/2-3/0 **OPEN AIR**; 475298 visible triangles
|
||||
- [section-4/view-2](after/passage-2-3-0-openair-4-view-2.png) — passage/2-3/0 **OPEN AIR**; 495856 visible triangles
|
||||
- [section-4/view-3](after/passage-2-3-0-openair-4-view-3.png) — passage/2-3/0 **OPEN AIR**; 496236 visible triangles; low_contrast_geometry_visible
|
||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 493 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 523 KiB |
|
After Width: | Height: | Size: 505 KiB |
|
After Width: | Height: | Size: 528 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 513 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 324 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 86 KiB |