diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a8762dc..521428e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2299,16 +2299,55 @@ results; the code path is not promoted to legacy Maze/VerticalShafts generation. rooms, or tunnels. The Unreal adapter preserves the established passage planner, then resolves authored stamps and cavern graph records, then finalizes the layout. Stable names are required and are scoped by strate in Core; landmarks may target a stamp, room, or tunnel by that stable name. -Core profile heightfields are restricted to SurfaceWorld. Native caves require the TunnelNetwork or -Underwater archetype; native lattice requires Maze or VerticalShafts. Core recipe/version fields -enter the resolved layout identity when selected. An imported manifest cannot be mixed with a -per-strate profiled heightfield. +Core profile heightfields are restricted to SurfaceWorld. Native caves require the TunnelNetwork, +Underwater, or VerticalShafts archetype; native lattice requires Maze or VerticalShafts. Core +recipe/version fields enter the resolved layout identity when selected. An imported manifest cannot +be mixed with a per-strate profiled heightfield. The checked-out Core API contains stamp vocabularies V1, V2, and V3. Unreal uses V1 for the original stamp kinds and V3 for monolith/platform motifs, then combines and strictly validates the result. Research17 names V2, but the source API and motif vocabulary require V3; source was followed. Research27 water remains out of scope for U1. +### 8.56 Native Vertical Shafts graph field — World Assembler M3 L139 + +Vertical Shafts uses the same `PlanResolvedCavernNetworkV1` and `Core.Caves.PrimitiveField` as +Tunnel Network. There is no separate shaft planner. The existing room/tunnel shapes do not express +a capped bore spanning the strate height, so the shared cave field gains a shaft-cylinder record +kind; resolved records then add access tunnels, shaft cross-connects, and internal ledges. +`Core.Lattice.PrimitiveField` is not used: its L58 VerticalShafts route read as a corkscrew, so it +does not satisfy this archetype's open-shaft identity. + +The basal owner strate has an arrival mouth from passage 2–3 and deliberately has no departure +mouth. Here the primordial law's route is the arrival mouth to a walkable terminal landing above the +bedrock seal. The shared planner extends its walk-only spine to that terminal with the same grade +limit used for a two-mouth cave; it does not invent a second passage. Route landings and connectors +are built as part of the graph. Shaft bores and their ledges are a separate exploration tier: an +open vertical drop or a climb between ledges is not walk-only traversal. Climb and mine remain +reporting tiers; they never relax the walk-only guarantee. + +`FVerticalShaftParams` mapping: + +| Field | Core behavior | +|---|---| +| `ShaftSpacing` | Planner room spacing, converting owner voxels to integer micrometres. | +| `ShaftDensity` | `roomDensityQ16`; seeds shaft placement, with the route's initial shaft guaranteed when nonzero. | +| `ShaftMinRadius`, `ShaftMaxRadius` | Minimum and maximum shaft radii. | +| `CrossConnectChance` | `tunnelDensityQ16` for cross-shaft links; generic cave branches are disabled in shaft mode. | +| `ConnectorRadius` | Fixed minimum/maximum radius for access and connector bores. | +| `LedgeSpacing`, `LedgeDepth` | Shaft shelf spacing and intrusion depth; zero spacing disables ledges, while zero depth leaves them nonintrusive. | +| `SurfaceRoughness` | Shaft-wall roughness amplitude. Frequency is derived as `max(0.5 m, 4 owner voxels)` because the legacy params have no frequency field. | +| `BoundarySealThickness`, `BaseDensity` | Continue to feed the resolved top and basal boundary bands through `sealAndDensity`, not the planner settings. | + +L139 increments `WorldAssemblyEvaluatorVersion` to 237 and extends the versioned cavern graph +vocabulary for shaft cylinders. The fast-tier dispatch report binds S3 to +`Core.Caves.PrimitiveField` / `Caves-native`. The 16-seed composed acceptance gate is not green: +seed 2 fails the passage-shell audit at the incoming mouth. Three geometry variants failed that +same gate, so the remaining issue is recorded for a new design premise rather than another local +patch. L139 does not claim promoted shaft generation; its L80 run is recorded as characterization +of the current worktree. See +[`Standalone/Research/WorldAssemblerM3/Linux139/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux139/REPORT.md). + The shared VoxelForge custom asset version is 2: version 1 records the tunnel-floor slope migration; version 2 adds load-time conversion for inter-strate gap, strate height, and transition width. The saved strate-layout width is used, currently 8 m. Old packages are converted in memory and marked diff --git a/CODEMAP.md b/CODEMAP.md index 21f31f1..a302ea0 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -174,7 +174,7 @@ remains `0`, and lateral regions remain gated off. | `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. | | `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. | | `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV3`: physical layout-chunk width, versioned micrometre intervals, owned ceiling/content/bedrock bands, honored transition settings, deterministic per-column transition bands, recipe anchors, director-planned passages, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built once from the director-resolved plan and consumed by bounds planning, Core dispatch, composition, and validation. §8.40–8.42, §8.46. | -| `ResolvedLayoutStratePassageCapabilitiesV1::{hasNativeCaveField,hasNativeLatticeField}`, `IsNativeLatticeFieldStratum`, `HasNativeGraphField` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `Standalone/Core/src/ResolvedLayoutInternal.h` | Explicit per-strate native graph capability; `Core.Lattice.PrimitiveField` is restricted to Maze/VerticalShafts recipe version 1. Lattice seeds include world seed, strate, recipe version and stable feature ID; explicit polyline records keep their endpoints and receive conservative bounds. §8.54. | +| `ResolvedLayoutStratePassageCapabilitiesV1::{hasNativeCaveField,hasNativeLatticeField}`, `IsNativeLatticeFieldStratum`, `HasNativeGraphField` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `Standalone/Core/src/ResolvedLayoutInternal.h` | Explicit per-strate native graph capability; `Core.Lattice.PrimitiveField` remains Maze/VerticalShafts recipe version 1, and `Core.Caves.PrimitiveField` also accepts VerticalShafts in L139. Lattice seeds include world seed, strate, recipe version and stable feature ID; explicit polyline records keep their endpoints and receive conservative bounds. §8.54, §8.56. | | `FCavernRuntimeFeature::FSegmentNode`, `ApplyResolvedNativeCavesField`, `ClassifyNativeCaveFieldBox` | `Standalone/Core/src/ShapeWorldInternal.h` | A deterministic nested segment BVH accelerates native graph distance and conservative interval evaluation; the outer graph BVH still supplies tile candidates and protects `isTileShapeFree`. The 0.4 m feature-union width is separate from the floor fillet width. §8.54. | | `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. | | `ApplyOwnerFourStratePlan` | `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp` | BuildHost-only transient fixed-slot plan from the four staged owner strate assets; leaves the authored settings and strate packages untouched. | @@ -1352,6 +1352,20 @@ tunnel-slope version in `VoxelStrateDefinition.cpp`, plus the standalone `VoxelF in `VoxelForgeExploreCommandlet.cpp`. `GetStrateForChunk` still receives layout-chunk Z, not a Core tile index. See `Standalone/Research/WorldAssemblerM3/Unreal1/REPORT.md` for BuildHost evidence. +### 3.38 Native Vertical Shafts graph field — World Assembler M3 L139 + +| Symbol | Canonical file | Role | +|---|---|---| +| `ResolvedCavernPlannerSettingsV1::verticalShaftMode`, basal terminal route planning | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h` | Extends shared `PlanResolvedCavernNetworkV1` for an arrival-only basal strate; its walk-only spine terminates at a landing above bedrock. Shaft mode disables generic branches and uses dedicated shaft nodes. | +| `ResolvedCavernRoomV1::shapeKind=1`, `ApplyResolvedNativeCavesField` | `Standalone/Core/src/ShapeWorldInternal.h` | Evaluates capped vertical shaft cylinders with shelves alongside shared access and cross-connect tunnels. Uses `Core.Caves.PrimitiveField`, not the L58 lattice spiral. | +| VerticalShafts settings mapping | `Standalone/Source/WorldAssemblyPrototype.h` | Maps the 11 legacy params into shared spacing, room density/radii, bore radius, cross-connect density, ledges, roughness, and existing boundary seal/density bands. | +| `IsNativeCaveFieldStratum`, S3 binding and `FCavesFieldAdapter` | `Standalone/Core/src/ShapeWorldInternal.h`, `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` | Reports `VerticalShafts` as `Core.Caves.PrimitiveField` v1 / `Caves-native`; leaves the legacy generator archetype identity intact. | +| `RunL139SeedSweep.py`, `RunL139Determinism.py`, `RunL139Captures.py`, `RunL139WholeLayerCapture.py`, `RunL139FinalTiming.py` | `Standalone/Research/WorldAssemblerM3/Linux139/` | Reproducible composed 16-seed audit, Clang/GCC W1/W8/W12 identity check, five before/after eye-height views plus density-cutaway and plan-projection outside views, and L80 characterization. The seed-2 passage-shell failure blocks promotion. | + +See ARCHITECTURE §8.56 and +[`Standalone/Research/WorldAssemblerM3/Linux139/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux139/REPORT.md) +for the planner semantics, field mapping, audit result, captures, and outstanding shell failure. + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/CURRENT-STATE.md b/CURRENT-STATE.md index 0ad5435..5fc63a0 100644 --- a/CURRENT-STATE.md +++ b/CURRENT-STATE.md @@ -1,18 +1,19 @@ # 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. +As of 2026-11-01. L139 is being developed on `wip/l134-cave-planner`. Current-state claims below point to their code or measured report; L139's Vertical Shafts implementation has not passed composed acceptance and is not promoted. ## 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). +- **Core:** evaluator version **237** in the L139 worktree. Unreal's `AVoxelWorld::bUseVoxelForgeCore` remains 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), [L139 report](Standalone/Research/WorldAssemblerM3/Linux139/REPORT.md). +- **Verified owner4 generator dispatch:** S0 SurfaceWorld binds `SurfaceWorld-native`; S1 FloatingIslands remains `compiled-analytic/FloatingIslands`; S2 TunnelNetwork binds `Core.Caves.PrimitiveField`; and the L139 fast-tier report binds S3 VerticalShafts to `Core.Caves.PrimitiveField` / `Caves-native`. This proves dispatch, not the full shaft gate: the composed 16-seed sweep fails the passage-shell audit at seed 2. [Dispatch](Standalone/Core/src/ShapeWorldInternal.h), [adapter recipe checks](Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h), [L139 report](Standalone/Research/WorldAssemblerM3/Linux139/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 L165/L166 incremental desired-set path now defaults on (`voxel.StreamingIncrementalDesiredSet=1`): Core clipmap keys, overlay membership, and priority order update incrementally, with same-center Core keys cached. No separate overlay or priority incremental switches are required. Audits stay off (`StreamingIncrementalDesiredAuditEvery=0`, `StreamingIncrementalDesiredMaterializationAuditEvery=0`, mismatch injection `=0`). L157 early admission and apply-cap controls stay off (`StreamingFastTravelWaitForAllCriticalTiles=1`, `StreamingFastTravelApplyBudgetMilliseconds=0`). L160 ahead-first and same-center rebuild suppression stay off (`StreamingFastTravelForwardFirst=0`, `StreamingRebuildOnCoreRegionCompletion=1`). Its separate Core desired-cache switch stays off (`StreamingCoreDesiredCacheByCenter=0`); the new incremental path enables its own same-center Core cache. The far-apply lane is Unreal-only. [CVar source](Source/VoxelForge/Private/VoxelWorld.cpp), [L121 report](Standalone/Research/WorldAssemblerM3/Linux121/REPORT.md), [architecture](ARCHITECTURE.md). -- **Determinism:** proven on Linux with Clang and GCC at W1/W8/W12. L131's unchanged-output comparison covers owner4's SW/FI/TN/VS views in 24 compiler/worker cases; the canonical owner4/owner2 recipe matrix also passes. Windows determinism is unverified. [L131 checks](Standalone/Research/WorldAssemblerM3/Linux131/REPORT.md), [v227 canonical hashes](Standalone/Tools/verify/baselines/owner-hash/canonical/). +- **Streaming:** arrival uses a bounded startup apply burst and a far-result supplement; dynamic near reserve and incremental desired sets are default-on. L172 made `voxel.StreamingFastTravelWaitForAllCriticalTiles=0` and `voxel.StreamingFastTravelApplyBudgetMilliseconds=1.0` the defaults. Forward-first and same-center rebuild suppression remain off; audit CVars remain off. The far-apply lane is Unreal-only. [L172 report](Standalone/Research/WorldAssemblerM3/Linux172/REPORT.md), [CVar source](Source/VoxelForge/Private/VoxelWorld.cpp), [architecture](ARCHITECTURE.md). +- **Determinism:** L131's unchanged-output comparison covers owner4 SW/FI/TN/VS views in 24 compiler/worker cases, and the canonical owner4/owner2 recipe matrix passes. The L139 Tunnel Network and Vertical Shafts cave plans matched across Clang/GCC W1/W8/W12 (six cases); their composed passage sweep still fails seed 2. Windows determinism is unverified. [L131 checks](Standalone/Research/WorldAssemblerM3/Linux131/REPORT.md), [L139 checks](Standalone/Research/WorldAssemblerM3/Linux139/REPORT.md), [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). +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 latest complete L131 measurements; L139 characterization is in the [L139 report](Standalone/Research/WorldAssemblerM3/Linux139/REPORT.md) and [timing summary](Saved/Linux/l139/timing/summary.json). 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 | |---|---:|---:| @@ -28,17 +29,18 @@ The latest recorded Linux Unreal start-to-LOD0–2 values are L127's one run per ## 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). +- L139 Vertical Shafts is not accepted: the composed audit's seed 2 fails the incoming passage shell after three geometry variants failed that same gate. L80 characterization and the Linux host build passed, but neither changes the composed acceptance result. See [L139 report](Standalone/Research/WorldAssemblerM3/Linux139/REPORT.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`. +- Build standalone Clang or GCC with [`Standalone/BuildLinux.sh`](Standalone/BuildLinux.sh), for example `bash Standalone/BuildLinux.sh --compiler=clang++-19 --variant=l139-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/l139 --overall-timeout 1740`. It includes the four-strate owner density-dispatch report; per-run logs and the summary stay under `Saved/Linux/l139`. +- The L139 v237 canonical Surface World and Floating Islands owner-hash baselines are in `Standalone/Tools/verify/baselines/owner-hash/canonical/`; both values match v236. 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. The L139 run recorded the L137 A/A noise floor of ±1%. Each headless run uses `taskset -c 0-11`, `prlimit --as=6442450944`, and a 1,740 s timeout. See [`RunL139FinalTiming.py`](Standalone/Research/WorldAssemblerM3/Linux139/RunL139FinalTiming.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/l139/ubt.log`. Run Unreal without `prlimit`. ## Closed experiments diff --git a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp index d275a7b..5e66412 100644 --- a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp +++ b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp @@ -144,13 +144,15 @@ bool ConfigureCoreGeometryStratum(const FStrateSlot& Slot, ECaveGeneratorType Ge return true; case EVoxelForgeCoreGeneratorRecipe::NativeCaves: if (GeneratorType != ECaveGeneratorType::TunnelNetwork - && GeneratorType != ECaveGeneratorType::Underwater) + && GeneratorType != ECaveGeneratorType::Underwater + && GeneratorType != ECaveGeneratorType::VerticalShafts) { - OutError = FString::Printf(TEXT("Strate %d selects NativeCaves but its generator archetype is not TunnelNetwork/Underwater."), + OutError = FString::Printf(TEXT("Strate %d selects NativeCaves but its generator archetype is not TunnelNetwork/Underwater/VerticalShafts."), Slot.StrateIndex); return false; } - Item.generatorKind = "Caves"; + Item.generatorKind = GeneratorType == ECaveGeneratorType::VerticalShafts + ? "VerticalShafts" : "Caves"; Item.recipeId = "Core.Caves.PrimitiveField"; Item.recipeVersion = 1; Item.boundDispatchPath = "Core native cave graph uses resolved room and tunnel records"; diff --git a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h index 6b61bb9..24c68ab 100644 --- a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h +++ b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h @@ -119,6 +119,48 @@ namespace VoxelForgeCore::PassageSpline return std::lerp(plannedFloor,receivingFloor,blend); } + inline void BlendEndpointFloorToRoute(std::span controls, + std::vector& floorProfile,std::size_t endpoint, + std::span> receivingRoute,double unitsPerMetre, + double playerCenterAboveFloorMetres=1.13,double maximumGradeDegrees=12.0) + { + if(controls.size()!=floorProfile.size()||endpoint>=floorProfile.size() + ||receivingRoute.empty()||!(unitsPerMetre>0.0) + ||!(maximumGradeDegrees>0.0&&maximumGradeDegrees<45.0))return; + const double maximumGrade=std::tan(maximumGradeDegrees + *3.14159265358979323846/180.0); + const double routeReach=controls[endpoint][3]/unitsPerMetre+0.5; + constexpr double MouthProfileBlendDistanceMetres=10.0; + double routeHeight=0.0,routeDistanceSquared=0.0; + if(!ClosestRouteHeightAtXY(receivingRoute,controls[endpoint][0], + controls[endpoint][1],routeHeight,routeDistanceSquared) + ||routeDistanceSquared>std::pow(routeReach*unitsPerMetre,2.0))return; + floorProfile[endpoint]=routeHeight + -playerCenterAboveFloorMetres*unitsPerMetre; + double pathDistance=0.0; + for(std::size_t step=1;step0&&step>endpoint)break; + const std::size_t index=endpoint==0?step:endpoint-step; + const std::size_t previous=endpoint==0?index-1:index+1; + const double dx=(controls[index][0]-controls[previous][0])/unitsPerMetre; + const double dy=(controls[index][1]-controls[previous][1])/unitsPerMetre; + const double horizontal=std::hypot(dx,dy); + pathDistance+=horizontal; + double localRouteHeight=0.0,localRouteDistanceSquared=0.0; + const bool followsRoute=pathDistance<=MouthProfileBlendDistanceMetres + &&ClosestRouteHeightAtXY(receivingRoute, + controls[index][0],controls[index][1],localRouteHeight,localRouteDistanceSquared) + &&localRouteDistanceSquared<=std::pow(routeReach*unitsPerMetre,2.0); + double target=floorProfile[index]; + if(followsRoute) + target=localRouteHeight-playerCenterAboveFloorMetres*unitsPerMetre; + const double maximumStep=maximumGrade*horizontal*unitsPerMetre; + floorProfile[index]=std::clamp(target, + floorProfile[previous]-maximumStep,floorProfile[previous]+maximumStep); + } + } + inline std::vector BuildSegments(std::span controls, std::size_t spanCount,double spacingMetres,double unitsPerMetre=1.0, std::span plannedFloorProfile={}, diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 422cdf2..0400ef9 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -138,6 +138,9 @@ namespace VoxelForgeCore bool passageComposerProfiled = false; std::uint64_t passageTileFeatureCount = 0; std::uint64_t passageTileRouteSegmentCandidates = 0; + std::uint64_t nativeCaveFieldSamples = 0; + std::uint64_t cavernRoomCandidates = 0; + std::uint64_t cavernEdgeCandidates = 0; std::uint64_t resolvedBoundaryBandQueries = 0; std::uint64_t passageComposerQueries = 0; std::uint64_t passageIterations = 0; @@ -496,8 +499,20 @@ namespace VoxelForgeCore // 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. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 227; + // 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; + // 231 carries passage tangents and floor slopes through cave portal lead-ins and keeps + // floor backing local to the walk footprint, with depth measured along the floor normal; + // 233 bounds passage ring jitter to the authored placement disk; 234 aligns route mouth + // anchors with capsule foot height and carries support through the SurfaceWorld mouth; + // 235 plans the cave backbone as a sampled curve with both passage mouth tangents and + // preserves open cave lanes through passage shell overlaps; 236 binds passage mouths to + // the planned graph route, preserves its walk corridor through shell overlaps, and keeps + // boundary-blended host rock from widening the authored passage wall. + // v238 plans one resolved passage spline with explicit landing chambers. + // v239 plans their floor planes as the passage's continuous floor profile. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 239; enum class FieldChannel : std::uint32_t { @@ -970,6 +985,8 @@ namespace VoxelForgeCore inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV6 = 6; inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV7 = 7; inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1; + inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV2 = 2; + inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV3 = 3; inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1; enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict }; @@ -1047,9 +1064,18 @@ namespace VoxelForgeCore { std::string stableId; std::int32_t strateIndex = -1; + // 0 = floor-backed room; 1 = a true vertical cylindrical shaft. The + // shaft form is part of the shared cave graph so other recipes can reuse + // the same graph, indexing, interval and density machinery. + std::uint8_t shapeKind = 0; std::array centerMicrometres{}; std::array 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 floorGradientQ16{}; std::int64_t smoothWidthMicrometres = 0; // Optional feature-to-feature union width. Zero preserves the historical // behavior where smoothWidthMicrometres also controls the graph union. @@ -1057,6 +1083,11 @@ namespace VoxelForgeCore std::int64_t irregularityAmplitudeMicrometres = 0; std::int64_t irregularityWavelengthMicrometres = 0; std::uint64_t irregularitySeed = 0; + // Used only by shapeKind=1. floorMicrometres is the shaft top; the + // bottom is center.z - halfExtents.z. Ledges face one cardinal side. + std::int64_t ledgeSpacingMicrometres = 0; + std::int64_t ledgeDepthMicrometres = 0; + std::uint8_t ledgeSide = 0; std::array boundsMinimumMicrometres{}; std::array boundsMaximumMicrometres{}; }; @@ -1069,6 +1100,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, 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> polylineMicrometres; @@ -1085,6 +1121,43 @@ namespace VoxelForgeCore std::array 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; + // Vertical Shafts uses this same planner and graph field. It binds its + // incoming passage and resolves a bedrock terminal when no departure + // mouth exists, then adds shaft records beside the walk-only backbone. + bool verticalShaftMode = false; + std::int64_t ledgeSpacingMicrometres = 8000000; + std::int64_t ledgeDepthMicrometres = 1000000; + }; + // Immutable material-only deposits. The capsule and its conservative bounds are in // micrometres; bounds include irregularity and the blend envelope. struct ResolvedLayoutOreVeinRequestV1 @@ -1211,7 +1284,8 @@ namespace VoxelForgeCore inline bool IsNativeCaveFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept { - return stratum.generatorKind=="Caves" + return (stratum.generatorKind=="Caves"||stratum.generatorKind=="TunnelNetwork" + ||stratum.generatorKind=="VerticalShafts") &&stratum.recipeId=="Core.Caves.PrimitiveField" &&stratum.recipeVersion==1; } @@ -1252,6 +1326,16 @@ namespace VoxelForgeCore std::int64_t radiusSubvoxels = 0; }; + struct ResolvedLayoutPassageLandingChamberV1 + { + // The chamber floor plane passes through this point. Gradient is dz/dx, + // dz/dy in Q16 so the chamber continues the passage's authored h(s) grade. + std::array floorCenterSubvoxels{}; + std::array floorGradientQ16{}; + std::int64_t radiusSubvoxels = 0; + std::int64_t clearHeightSubvoxels = 0; + }; + struct ResolvedLayoutPassageV1 { std::string stableId; @@ -1266,6 +1350,12 @@ namespace VoxelForgeCore std::int64_t midRadiusSubvoxels = 0; // Derived from the mouth binding; the main descent is monotone through this point. std::size_t lowerMouthControlIndex = 0; + ResolvedLayoutPassageLandingChamberV1 upperLandingChamber; + ResolvedLayoutPassageLandingChamberV1 lowerLandingChamber; + // When true, each control point's position.z - radius is the planned + // floor h(s), and both chamber planes continue that same endpoint profile. + // False preserves older resolved snapshots' runtime floor composition. + bool floorProfileIncludesLandingChambers = false; std::vector controlPoints; }; @@ -1381,6 +1471,17 @@ namespace VoxelForgeCore std::span rooms, std::span 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& rooms, + std::vector& edges, + std::string& error); VFCORE_API bool ResolveResolvedLayoutOreVeinsV1( ResolvedLayoutSnapshotV3& layout, std::int32_t seed, std::span requests, diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index d4fea3d..96322ee 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -4,6 +4,7 @@ #include "VoxelForgeCore/VoxelForgeCore.h" #include +#include #include #include #include @@ -650,7 +651,23 @@ namespace <<"],\"lower_mouth_q\":["<(room.shapeKind) + <<",\"ledge_spacing_um\":"<(room.ledgeSide); if(room.unionWidthMicrometres>0) out<<",\"union_width_um\":"<(std::llround( @@ -1225,19 +1255,117 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, if(!insideContent(*upper,passage.upperMouthSubvoxels) ||!insideContent(*lower,passage.lowerMouthSubvoxels)) add("passage_mouth_content",passage.stableId+" mouth is outside its owned content band"); + const auto validateLandingChamber=[&]( + const ResolvedLayoutPassageLandingChamberV1& chamber, + const ResolvedLayoutStratumV2& host, + const std::array& mouth,const char* side) + { + const double playerCenterHeight=(side==std::string_view("upper") + &&host.generatorKind=="SurfaceWorld")?0.98:1.13; + const std::int64_t playerCenterHeightQ=static_cast( + std::llround(playerCenterHeight*WorldAssemblySubvoxelsPerVoxel + *1000000.0/std::max(1,coreVoxelSizeMicrometres))); + if(chamber.floorCenterSubvoxels[0]!=mouth[0] + ||chamber.floorCenterSubvoxels[1]!=mouth[1] + ||chamber.floorCenterSubvoxels[2]+playerCenterHeightQ!=mouth[2] + ||chamber.radiusSubvoxels<=0 + ||chamber.clearHeightSubvoxels<=0 + ||chamber.radiusSubvoxels( + std::llround(2.2*WorldAssemblySubvoxelsPerVoxel*1000000.0 + /std::max(1,coreVoxelSizeMicrometres)))) + { + add("passage_landing_chamber",passage.stableId+" "+side + +" landing chamber has invalid floor, radius, or capsule clearance"); + return; + } + const std::uint64_t gradientSquared=static_cast( + chamber.floorGradientQ16[0]*chamber.floorGradientQ16[0] + +chamber.floorGradientQ16[1]*chamber.floorGradientQ16[1]); + const std::int64_t gradientMagnitude=static_cast( + PassageIsqrt(gradientSquared)); + if(gradientMagnitude>17560) + add("passage_landing_grade",passage.stableId+" "+side + +" landing chamber floor exceeds 15 degrees"); + const std::int64_t planeReachQ=static_cast( + (chamber.radiusSubvoxels*gradientMagnitude+65535)/65536); + const std::int64_t shellQ=static_cast(std::llround( + 0.75*WorldAssemblySubvoxelsPerVoxel*1000000.0 + /std::max(1,coreVoxelSizeMicrometres))); + const auto qToUm=[&](std::int64_t q) + {return q*static_cast(coreVoxelSizeMicrometres) + /WorldAssemblySubvoxelsPerVoxel;}; + if(!host.content.presentInCurrentField + ||qToUm(chamber.floorCenterSubvoxels[2]-planeReachQ-shellQ) + host.content.topMicrometres) + add("passage_landing_content",passage.stableId+" "+side + +" landing chamber or plain shell leaves its host content band"); + }; + if(upper&&lower) + { + validateLandingChamber(passage.upperLandingChamber,*upper, + passage.upperMouthSubvoxels,"upper"); + validateLandingChamber(passage.lowerLandingChamber,*lower, + passage.lowerMouthSubvoxels,"lower"); + } if(passage.controlPoints.size()<2)continue; const std::int64_t PlayerCenterAboveFloorQ=coreVoxelSizeMicrometres>0 ?static_cast(std::llround(1.13*WorldAssemblySubvoxelsPerVoxel *1000000.0/coreVoxelSizeMicrometres)):1157; - const auto floorProfile=[&](const ResolvedLayoutPassagePointV1& p) - {return p.positionSubvoxels[2]-p.radiusSubvoxels+PlayerCenterAboveFloorQ;}; + const std::int64_t SurfacePlayerCenterAboveFloorQ=coreVoxelSizeMicrometres>0 + ?static_cast(std::llround(0.98*WorldAssemblySubvoxelsPerVoxel + *1000000.0/coreVoxelSizeMicrometres)):901; + std::vector centerHeightOffsets(passage.controlPoints.size(), + PlayerCenterAboveFloorQ); + const auto& upperMouthPoint=passage.controlPoints.front(); + const bool hasSurfaceRoute=upper->generatorKind=="SurfaceWorld" + &&upperMouthPoint.positionSubvoxels[0]==passage.upperMouthSubvoxels[0] + &&upperMouthPoint.positionSubvoxels[1]==passage.upperMouthSubvoxels[1] + &&upperMouthPoint.positionSubvoxels[2]-upperMouthPoint.radiusSubvoxels + +SurfacePlayerCenterAboveFloorQ==passage.upperMouthSubvoxels[2]; + if(hasSurfaceRoute) + { + const std::int64_t qPerMetre=static_cast(std::llround( + static_cast(WorldAssemblySubvoxelsPerVoxel)*1000000.0 + /std::max(1,coreVoxelSizeMicrometres))); + const std::int64_t blendLength=std::max(8*qPerMetre, + 2*passage.mouthRadiusSubvoxels); + std::int64_t distance=0; + for(std::size_t i=0;i0) + { + const auto& a=passage.controlPoints[i-1].positionSubvoxels; + const auto& b=passage.controlPoints[i].positionSubvoxels; + const auto dx=static_cast(std::abs(b[0]-a[0])); + const auto dy=static_cast(std::abs(b[1]-a[1])); + const auto square=dx*dx+dy*dy; + auto length=PassageIsqrt(square); + if(length*length(length); + } + const double t=std::clamp(static_cast(distance) + /std::max(1,blendLength),0.0,1.0); + const double smooth=t*t*(3.0-2.0*t); + centerHeightOffsets[i]=SurfacePlayerCenterAboveFloorQ + +static_cast(std::llround( + (PlayerCenterAboveFloorQ-SurfacePlayerCenterAboveFloorQ)*smooth)); + } + } + const auto floorProfile=[&](std::size_t index, + const ResolvedLayoutPassagePointV1& p) + {return p.positionSubvoxels[2]-p.radiusSubvoxels + +centerHeightOffsets[index];}; std::size_t lowerMouthControlIndex=passage.controlPoints.size(); for(std::size_t i=1;i(dx*dx+dy*dy)); - const std::int64_t signedDz=floorProfile(point)-floorProfile(previous); + const std::int64_t signedDz=floorProfile(i,point) + -floorProfile(i-1,previous); const std::int64_t dz=std::abs(signedDz); const bool landingExtension=i>lowerMouthControlIndex; if((landingExtension?dz*65536>static_cast(horizontal)*17560 @@ -1485,9 +1619,11 @@ 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 + &&cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV3) add("cavern_graph_vocabulary_version", - "cavern graph records require vocabulary version 1"); + "cavern graph records require a supported vocabulary version"); if(cavernGraphVocabularyVersion!=0) { if(cavernRooms.empty()||cavernEdges.empty()) @@ -1518,6 +1654,43 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, ||(room.irregularityAmplitudeMicrometres>0 &&room.irregularityWavelengthMicrometres<=0)) add("cavern_room_parameters",room.stableId+" has invalid or unbounded parameters"); + const auto* roomStratum=findStratum(room.strateIndex); + if(room.shapeKind>1) + add("cavern_room_shape",room.stableId+" has an unknown shape kind"); + if(room.shapeKind==1) + { + const bool verticalShaftStratum=roomStratum + &&roomStratum->generatorKind=="VerticalShafts" + &&IsNativeCaveFieldStratum(*roomStratum); + if(cavernGraphVocabularyVersion100000000 + ||room.ledgeDepthMicrometres<0 + ||room.ledgeDepthMicrometres>=room.halfExtentsMicrometres[0] + ||room.ledgeSide>3 + ||room.floorMicrometres + !=room.centerMicrometres[2]+room.halfExtentsMicrometres[2] + ||!room.floorProfileStableId.empty() + ||room.floorGradientQ16[0]!=0||room.floorGradientQ16[1]!=0) + add("cavern_shaft_parameters",room.stableId+ + " has invalid native shaft bounds, ledges, or recipe binding"); + } + else if(room.ledgeSpacingMicrometres!=0||room.ledgeDepthMicrometres!=0 + ||room.ledgeSide!=0) + add("cavern_room_shape",room.stableId+ + " carries shaft parameters on an ordinary room"); + const std::int64_t gradientSquared=static_cast(room.floorGradientQ16[0]) + *room.floorGradientQ16[0]+static_cast(room.floorGradientQ16[1]) + *room.floorGradientQ16[1]; + if((room.floorProfileStableId.empty() + &&room.floorProfileDistanceMicrometres!=-1) + ||(!room.floorProfileStableId.empty() + &&room.floorProfileDistanceMicrometres<0) + ||gradientSquared>static_cast(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) @@ -1558,6 +1731,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 low=edge.controlPointsMicrometres[0]; std::array high=low; if(nativeLattice&&!edge.polylineMicrometres.empty()) @@ -1587,11 +1769,37 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, if(room!=cavernRooms.end()) { const auto& endpoint=edge.controlPointsMicrometres.back(); + bool validRoomEndpoint=false; + if(room->shapeKind==1) + { + const std::int64_t shaftBottom=room->centerMicrometres[2] + -room->halfExtentsMicrometres[2]; + const std::int64_t shaftTop=room->centerMicrometres[2] + +room->halfExtentsMicrometres[2]; + const bool insideShaft=endpoint[2]>=shaftBottom + &&endpoint[2]<=shaftTop; + bool onLanding=room->ledgeSpacingMicrometres==0; + if(room->ledgeSpacingMicrometres>0) + { + const std::int64_t ledgeIndex=(shaftTop-endpoint[2] + +room->ledgeSpacingMicrometres/2) + /room->ledgeSpacingMicrometres; + const std::int64_t ledgeZ=shaftTop + -ledgeIndex*room->ledgeSpacingMicrometres; + const std::int64_t maximumLedge=(shaftTop-shaftBottom-1000000) + /room->ledgeSpacingMicrometres; + onLanding=ledgeIndex>=1&&ledgeIndex<=maximumLedge + &&std::abs(endpoint[2]-ledgeZ)<=500000; + } + validRoomEndpoint=insideShaft&&onLanding; + } + else validRoomEndpoint= + std::abs(endpoint[2]-room->floorMicrometres)<=500000; if(endpoint[0]centerMicrometres[0]-room->halfExtentsMicrometres[0] ||endpoint[0]>room->centerMicrometres[0]+room->halfExtentsMicrometres[0] ||endpoint[1]centerMicrometres[1]-room->halfExtentsMicrometres[1] ||endpoint[1]>room->centerMicrometres[1]+room->halfExtentsMicrometres[1] - ||std::abs(endpoint[2]-room->floorMicrometres)>500000) + ||!validRoomEndpoint) { add("cavern_edge_portal",edge.stableId+" endpoint_um=(" +std::to_string(endpoint[0])+","+std::to_string(endpoint[1])+","+ @@ -1613,6 +1821,124 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, add("cavern_edge_polyline",edge.stableId+ " has no bounded resolve-time polyline with matching endpoints"); } + std::vector 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 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) + floorProfileStartDistanceMicrometres, + b->floorProfileEndDistanceMicrometres); + }); + const auto endpointZ=[](const ResolvedCavernEdgeV1& edge,bool atMinimum) + { + const bool forward=edge.floorProfileStartDistanceMicrometres + (edge.floorProfileEndDistanceMicrometres + -edge.floorProfileStartDistanceMicrometres); + if(span==0.0)return std::numeric_limits::infinity(); + const bool forward=edge.floorProfileStartDistanceMicrometres + 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.00001) + add("cavern_floor_profile_joint",profileId+" between "+previous->stableId + +" and "+edge->stableId+" has distance gap=" + +std::to_string(low-previousHigh)+" um height step=" + +std::to_string(endpointZ(*previous,false)-endpointZ(*edge,true)) + +" um slope step="+std::to_string( + endpointSlope(*previous,false)-slope0) + +" ("+std::to_string(endpointSlope(*previous,false))+" vs " + +std::to_string(slope0)+") spans_um=(" + +std::to_string(previous->floorProfileEndDistanceMicrometres + -previous->floorProfileStartDistanceMicrometres)+","+ + std::to_string(edge->floorProfileEndDistanceMicrometres + -edge->floorProfileStartDistanceMicrometres)+")"); + } + 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(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 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()) { @@ -1738,10 +2064,27 @@ std::optional ResolvedLayoutSnapshotV3::FromJson( p.style=static_cast(LayoutInt(v,"style")); p.placementRadiusSubvoxels=LayoutInt(v,"placement_radius_q"); p.mouthRadiusSubvoxels=LayoutInt(v,"mouth_radius_q");p.midRadiusSubvoxels=LayoutInt(v,"mid_radius_q"); + p.floorProfileIncludesLandingChambers= + LayoutBool(v,"floor_profile_includes_landing_chambers"); const auto& upper=v.Get("upper_mouth_q").AsArray(); const auto& lower=v.Get("lower_mouth_q").AsArray(); for(std::size_t axis=0;axis<3&&axis 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[axis].Integer()); room.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um"); room.unionWidthMicrometres=LayoutInt(v,"union_width_um"); room.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um"); room.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um"); room.irregularitySeed=static_cast(LayoutInt(v,"irregularity_seed")); + room.shapeKind=static_cast(LayoutInt(v,"shape_kind")); + room.ledgeSpacingMicrometres=LayoutInt(v,"ledge_spacing_um"); + room.ledgeDepthMicrometres=LayoutInt(v,"ledge_depth_um"); + room.ledgeSide=static_cast(LayoutInt(v,"ledge_side")); s.cavernRooms.push_back(std::move(room)); } for(const auto& v:root.Get("cavern_edges").AsArray()) @@ -1821,6 +2174,13 @@ std::optional ResolvedLayoutSnapshotV3::FromJson( edge.startAnchorStableId=LayoutString(v,"start_anchor_stable_id"); edge.endRoomStableId=LayoutString(v,"end_room_stable_id"); edge.strateIndex=static_cast(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& rooms, + std::vector& edges,std::string& error) +{ + rooms.clear();edges.clear();error.clear(); + const auto plannerStarted=std::chrono::steady_clock::now(); + 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.maximumRoomRadiusMicrometres100000000 + ||settings.destinationRadiusMicrometres100000000 + ||settings.floorSupportDepthMicrometres<250000 + ||settings.floorSupportDepthMicrometres>2000000 + ||settings.maximumTunnelLengthMicrometres<=0 + ||settings.minimumTunnelRadiusMicrometres<0 + ||settings.maximumTunnelRadiusMicrometres10000000 + ||settings.irregularityAmplitudeMicrometres<0 + ||settings.irregularityAmplitudeMicrometres>10000000 + ||(settings.irregularityAmplitudeMicrometres>0 + &&settings.irregularityWavelengthMicrometres<=0) + ||(settings.ledgeSpacingMicrometres<0 + ||settings.ledgeSpacingMicrometres>100000000) + ||settings.ledgeDepthMicrometres<0 + ||settings.ledgeDepthMicrometres>5000000) + {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.upperStrateIndexstableId)) + arrivalPassage=&passage; + if(passage.upperStrateIndex==strateIndex&&passage.lowerStrateIndex>strateIndex + &&(!departurePassage||passage.stableIdstableId)) + departurePassage=&passage; + } + const bool terminalShaftRoute=settings.verticalShaftMode + &&stratum->generatorKind=="VerticalShafts"&&!departurePassage; + if(!arrivalPassage||(!departurePassage&&!terminalShaftRoute) + ||(settings.verticalShaftMode&&!terminalShaftRoute)) + {error="cavern planner needs an incoming mouth and either an outgoing mouth or a Vertical Shafts basal terminal";return false;} + if(layout.coreVoxelSizeMicrometres==0) + {error="cavern planner requires a nonzero Core voxel size";return false;} + + using FPoint=std::array; + const std::string routePrefix=(terminalShaftRoute?"vertical-shafts/strate-" + :"tunnel-network/strate-")+std::to_string(strateIndex)+"/"; + 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 PortalLeadLength=6000000; + 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(dx*dx+dy*dy); + return static_cast(integerSqrt(squared)); + }; + const auto qToMicrometres=[&](std::int64_t q) + {return q*static_cast(layout.coreVoxelSizeMicrometres) + /WorldAssemblySubvoxelsPerVoxel;}; + constexpr std::int64_t LandingConnectorBoreRadius=2000000; + const std::int64_t mouthBoreRadius=LandingConnectorBoreRadius; + const auto chooseMouthRouteTangent=[&](const ResolvedLayoutPassageV1& passage, + std::size_t mouthIndex,bool lowerSide, + const std::array& authoredRouteTangentQ16, + std::int64_t authoredFloorSlopeQ16,const FPoint& mouthFloorPoint, + std::int64_t chamberRadiusMicrometres) + { + constexpr std::array,16> DirectionsQ16={{ + {{65536,0}},{{60547,25080}},{{46341,46341}},{{25080,60547}}, + {{0,65536}},{{-25080,60547}},{{-46341,46341}},{{-60547,25080}}, + {{-65536,0}},{{-60547,-25080}},{{-46341,-46341}},{{-25080,-60547}}, + {{0,-65536}},{{25080,-60547}},{{46341,-46341}},{{60547,-25080}}}}; + const auto scoreDirection=[&](const std::array& outwardQ16) + { + const std::size_t segmentCount=passage.controlPoints.size()>1 + ?passage.controlPoints.size()-1:0; + std::int64_t worst=std::numeric_limits::max(); + bool sampled=false; + const std::int64_t projectedSlopeQ16=(authoredFloorSlopeQ16 + *(outwardQ16[0]*authoredRouteTangentQ16[0] + +outwardQ16[1]*authoredRouteTangentQ16[1]) + /(65536ll*65536ll)); + constexpr std::int64_t MouthClearanceProbeExtent=48000000; + const std::int64_t firstClearanceProbe=std::max( + 2000000,chamberRadiusMicrometres+1000000); + for(std::int64_t distance=firstClearanceProbe; + distance<=MouthClearanceProbeExtent;distance+=2000000) + { + const std::int64_t x=mouthFloorPoint[0] + +outwardQ16[0]*distance/65536; + const std::int64_t y=mouthFloorPoint[1] + +outwardQ16[1]*distance/65536; + const std::int64_t caveFloor=mouthFloorPoint[2] + +projectedSlopeQ16*distance/65536; + const std::int64_t caveCeiling=caveFloor+PlannedBoreHeight; + for(std::size_t segmentIndex=0;segmentIndex=mouthIndex)continue; + if(!lowerSide&&segmentIndex<2)continue; + const auto& a=passage.controlPoints[segmentIndex]; + const auto& b=passage.controlPoints[segmentIndex+1]; + const std::int64_t ax=qToMicrometres(a.positionSubvoxels[0]); + const std::int64_t ay=qToMicrometres(a.positionSubvoxels[1]); + const std::int64_t az=qToMicrometres(a.positionSubvoxels[2]); + const std::int64_t bx=qToMicrometres(b.positionSubvoxels[0]); + const std::int64_t by=qToMicrometres(b.positionSubvoxels[1]); + const std::int64_t bz=qToMicrometres(b.positionSubvoxels[2]); + const std::int64_t dx=bx-ax,dy=by-ay; + const double lengthSquared=static_cast(dx)*dx + +static_cast(dy)*dy; + const double projection=lengthSquared>0.0 + ?std::clamp((static_cast(x-ax)*dx + +static_cast(y-ay)*dy)/lengthSquared,0.0,1.0):0.0; + const std::int64_t tQ16=static_cast(std::llround( + projection*65536.0)); + const std::int64_t closestX=ax+dx*tQ16/65536; + const std::int64_t closestY=ay+dy*tQ16/65536; + const std::int64_t horizontalDistance=static_cast( + integerSqrt(static_cast((x-closestX)*(x-closestX) + +(y-closestY)*(y-closestY)))); + const std::int64_t passageCenterZ=qToMicrometres( + a.positionSubvoxels[2]) + +(qToMicrometres(b.positionSubvoxels[2]) + -qToMicrometres(a.positionSubvoxels[2]))*tQ16/65536; + const std::int64_t passageRadius=qToMicrometres(a.radiusSubvoxels) + +(qToMicrometres(b.radiusSubvoxels) + -qToMicrometres(a.radiusSubvoxels))*tQ16/65536; + const std::int64_t horizontalGap=horizontalDistance + -passageRadius-mouthBoreRadius; + const std::int64_t passageFloor=passageCenterZ-passageRadius; + const std::int64_t passageCeiling=passageCenterZ+passageRadius; + const std::int64_t verticalGap=caveFloor>passageCeiling + ?caveFloor-passageCeiling + :(passageFloor>caveCeiling?passageFloor-caveCeiling + :-std::min(caveCeiling-passageFloor, + passageCeiling-caveFloor)); + const std::int64_t separation=horizontalGap>=0&&verticalGap>=0 + ?static_cast(integerSqrt(static_cast( + horizontalGap*horizontalGap+verticalGap*verticalGap))) + :std::max(horizontalGap,verticalGap); + worst=std::min(worst,separation);sampled=true; + } + } + return sampled?worst:std::numeric_limits::max(); + }; + + // Keep the authored tangent whenever it clears the other passage spans. + // If the passage folds back over the mouth, choose the outward direction + // with the greatest 3D clearance while preserving deterministic tie order. + const std::array authoredOutwardQ16={ + lowerSide?authoredRouteTangentQ16[0]:-authoredRouteTangentQ16[0], + lowerSide?authoredRouteTangentQ16[1]:-authoredRouteTangentQ16[1]}; + const std::int64_t authoredScore=scoreDirection(authoredOutwardQ16); + if(authoredScore>=0)return authoredRouteTangentQ16; + std::array bestOutwardQ16=authoredOutwardQ16; + std::int64_t bestScore=authoredScore; + std::int64_t bestAlignment=authoredOutwardQ16[0]*authoredOutwardQ16[0] + +authoredOutwardQ16[1]*authoredOutwardQ16[1]; + for(const auto& candidate:DirectionsQ16) + { + const std::int64_t score=scoreDirection(candidate); + const std::int64_t alignment=candidate[0]*authoredOutwardQ16[0] + +candidate[1]*authoredOutwardQ16[1]; + if(score>bestScore||(score==bestScore&&alignment>bestAlignment)) + {bestScore=score;bestAlignment=alignment;bestOutwardQ16=candidate;} + } + std::printf("[CavernMouthClearancePlan] passage=%s side=%s authored_clearance_m=%.3f selected_clearance_m=%.3f outward_q16=(%lld,%lld)\n", + passage.stableId.c_str(),lowerSide?"lower":"upper", + authoredScore/1.0e6,bestScore/1.0e6, + static_cast(bestOutwardQ16[0]), + static_cast(bestOutwardQ16[1])); + return std::array{ + lowerSide?bestOutwardQ16[0]:-bestOutwardQ16[0], + lowerSide?bestOutwardQ16[1]:-bestOutwardQ16[1]}; + }; + const auto mouthInnerPoint=[&](const ResolvedLayoutPassageV1& passage, + bool lowerSide,FPoint& point,std::array& routeTangentQ16, + std::int64_t& floorSlopeQ16) + { + const auto& chamber=lowerSide?passage.lowerLandingChamber + :passage.upperLandingChamber; + for(std::size_t axis=0;axis<3;++axis) + point[axis]=qToMicrometres(chamber.floorCenterSubvoxels[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; + // The lower arrival continues from the previous passage point through its + // mouth. The upper departure approaches its mouth from the cave and then + // continues toward control point 1, so the two route tangents have opposite + // signs even though both portal offsets lie on the cave side. + const std::int64_t direction=lowerSide?1:-1; + const std::int64_t tx=direction*(passage.controlPoints[mouthIndex].positionSubvoxels[0] + -passage.controlPoints[tangentIndex].positionSubvoxels[0]); + const std::int64_t ty=direction*(passage.controlPoints[mouthIndex].positionSubvoxels[1] + -passage.controlPoints[tangentIndex].positionSubvoxels[1]); + const auto tangentLength=static_cast(integerSqrt( + static_cast(tx*tx+ty*ty))); + if(tangentLength<=0)return false; + routeTangentQ16={tx*65536/tangentLength,ty*65536/tangentLength}; + const auto& mouthControl=passage.controlPoints[mouthIndex]; + const std::int64_t mouthFloor=chamber.floorCenterSubvoxels[2]; + point[2]=qToMicrometres(mouthFloor); + floorSlopeQ16=(chamber.floorGradientQ16[0]*routeTangentQ16[0] + +chamber.floorGradientQ16[1]*routeTangentQ16[1])/65536; + const auto authoredRouteTangentQ16=routeTangentQ16; + routeTangentQ16=chooseMouthRouteTangent(passage,mouthIndex,lowerSide, + authoredRouteTangentQ16,floorSlopeQ16,point, + qToMicrometres(chamber.radiusSubvoxels)); + floorSlopeQ16=(chamber.floorGradientQ16[0]*routeTangentQ16[0] + +chamber.floorGradientQ16[1]*routeTangentQ16[1])/65536; + const std::int64_t offsetDirection=lowerSide?1:-1; + const std::int64_t portOffset=std::max(0, + qToMicrometres(chamber.radiusSubvoxels)-LandingConnectorBoreRadius); + point[0]+=offsetDirection*routeTangentQ16[0]*portOffset/65536; + point[1]+=offsetDirection*routeTangentQ16[1]*portOffset/65536; + point[2]=qToMicrometres(mouthFloor) + +(chamber.floorGradientQ16[0]*(point[0] + -qToMicrometres(chamber.floorCenterSubvoxels[0])) + +chamber.floorGradientQ16[1]*(point[1] + -qToMicrometres(chamber.floorCenterSubvoxels[1])))/65536; + return true; + }; + FPoint routeStart{},routeEnd{}; + std::array arrivalTangentQ16{},departureTangentQ16{}; + std::int64_t arrivalFloorSlopeQ16=0,departureFloorSlopeQ16=0; + if(!mouthInnerPoint(*arrivalPassage,true,routeStart,arrivalTangentQ16, + arrivalFloorSlopeQ16)) + {error="cavern passage mouth is missing a usable horizontal route tangent";return false;} + if(!terminalShaftRoute + &&!mouthInnerPoint(*departurePassage,false,routeEnd,departureTangentQ16, + departureFloorSlopeQ16)) + {error="cavern passage mouth is missing a usable horizontal route tangent";return false;} + if(terminalShaftRoute) + { + departureTangentQ16=arrivalTangentQ16; + routeEnd[2]=stratum->content.bottomMicrometres+3000000; + departureFloorSlopeQ16=0; + } + + std::int64_t dx=routeEnd[0]-routeStart[0]; + std::int64_t dy=routeEnd[1]-routeStart[1]; + const std::int64_t dz=routeEnd[2]-routeStart[2]; + const std::int64_t horizontalBias=std::clamp( + settings.tunnelHorizontalBiasQ16,0,65536); + const std::int64_t planGradeDenominator=MaximumWalkGradeDenominator + +horizontalBias/32768; + const std::int64_t requiredLength=(std::abs(dz)*planGradeDenominator*130)/100+1000; + if(terminalShaftRoute) + { + // The basal stratum has no next mouth by design. The walk-only spine starts + // at its incoming passage and ends on a supported landing above bedrock. + // Optional open shafts are side features with separate climb/fall reporting. + // Normalize by the truncated Q16 tangent magnitude: dividing by 65536 + // shortens non-cardinal vectors enough to miss the strict grade-run bound. + const std::uint64_t tangentSquared=static_cast( + departureTangentQ16[0]*departureTangentQ16[0] + +departureTangentQ16[1]*departureTangentQ16[1]); + const std::int64_t tangentMagnitudeQ16=static_cast( + integerSqrt(tangentSquared)); + if(tangentMagnitudeQ16<=0) + {error="cavern planner terminal tangent has zero horizontal length";return false;} + const std::int64_t terminalProjectionLength=requiredLength+1000; + for(std::size_t axis=0;axis<2;++axis) + routeEnd[axis]=routeStart[axis] + +departureTangentQ16[axis]*terminalProjectionLength/tangentMagnitudeQ16; + dx=routeEnd[0]-routeStart[0];dy=routeEnd[1]-routeStart[1]; + } + std::int64_t pathAxisX=dx,pathAxisY=dy; + FPoint routeLeadStart=routeStart,routeLeadEnd=routeEnd; + for(std::size_t axis=0;axis<2;++axis) + { + routeLeadStart[axis]+=arrivalTangentQ16[axis]*PortalLeadLength/65536; + routeLeadEnd[axis]-=departureTangentQ16[axis]*PortalLeadLength/65536; + } + routeLeadStart[2]+=arrivalFloorSlopeQ16*PortalLeadLength/65536; + routeLeadEnd[2]-=departureFloorSlopeQ16*PortalLeadLength/65536; + const std::int64_t mouthTangentDot=arrivalTangentQ16[0]*departureTangentQ16[0] + +arrivalTangentQ16[1]*departureTangentQ16[1]; + const auto makeRoute=[&](std::int64_t handleLength, + std::int64_t lateralOffsetMicrometres=0) + { + if(mouthTangentDot<0) + { + // A cubic with opposed portal tangents folds back over itself when its + // handles are lengthened to satisfy the walking grade. Make the loop + // explicit: leave both mouths on their authored tangents, carry two + // parallel legs on separate lanes, and turn through cavern rooms. + constexpr std::int64_t LaneSeparation=30000000; + const std::array sideQ16={ + -arrivalTangentQ16[1],arrivalTangentQ16[0]}; + FPoint outTip=routeLeadStart,shiftedTip{},returnTip{},terminal=routeLeadEnd; + for(std::size_t axis=0;axis<2;++axis) + { + outTip[axis]+=arrivalTangentQ16[axis]*handleLength/65536; + shiftedTip[axis]=outTip[axis]+sideQ16[axis] + *(LaneSeparation+lateralOffsetMicrometres)/65536; + } + outTip[2]+=arrivalFloorSlopeQ16*handleLength/65536; + shiftedTip[2]=outTip[2]; + terminal=routeLeadEnd; + const std::int64_t departureTangentLengthSquared= + departureTangentQ16[0]*departureTangentQ16[0] + +departureTangentQ16[1]*departureTangentQ16[1]; + const std::int64_t toTerminalX=terminal[0]-shiftedTip[0]; + const std::int64_t toTerminalY=terminal[1]-shiftedTip[1]; + const std::int64_t returnLength=std::max(1000000, + (toTerminalX*departureTangentQ16[0] + +toTerminalY*departureTangentQ16[1])*65536 + /departureTangentLengthSquared); + for(std::size_t axis=0;axis<2;++axis) + returnTip[axis]=shiftedTip[axis] + +departureTangentQ16[axis]*returnLength/65536; + returnTip[2]=routeEnd[2] + -departureFloorSlopeQ16*(returnLength+PortalLeadLength)/65536; + return std::vector{routeStart,routeLeadStart,outTip, + shiftedTip,returnTip,routeLeadEnd,routeEnd}; + } + FPoint controlStart=routeLeadStart,controlEnd=routeLeadEnd; + for(std::size_t axis=0;axis<2;++axis) + { + controlStart[axis]+=arrivalTangentQ16[axis]*handleLength/65536; + controlEnd[axis]-=departureTangentQ16[axis]*handleLength/65536; + } + controlStart[2]+=arrivalFloorSlopeQ16*handleLength/65536; + controlEnd[2]-=departureFloorSlopeQ16*handleLength/65536; + + std::vector points; + points.reserve(35); + points.push_back(routeStart); + points.push_back(routeLeadStart); + constexpr std::int64_t CurveParameterOne=std::int64_t{1}<<20; + constexpr std::int64_t CurveSamples=32; + const auto interpolate=[=](const FPoint& a,const FPoint& b,std::int64_t t) + { + FPoint point{}; + for(std::size_t axis=0;axis<3;++axis) + point[axis]=a[axis]+(b[axis]-a[axis])*t/CurveParameterOne; + return point; + }; + const std::int64_t pathAxisLength=static_cast(integerSqrt( + static_cast(pathAxisX*pathAxisX+pathAxisY*pathAxisY))); + const std::array pathSideQ16=pathAxisLength>0 + ?std::array{{-pathAxisY*65536/pathAxisLength, + pathAxisX*65536/pathAxisLength}} + :std::array{{-arrivalTangentQ16[1],arrivalTangentQ16[0]}}; + for(std::int64_t sample=1;sample<=CurveSamples;++sample) + { + const std::int64_t t=sample*CurveParameterOne/CurveSamples; + const FPoint a=interpolate(routeLeadStart,controlStart,t); + const FPoint b=interpolate(controlStart,controlEnd,t); + const FPoint c=interpolate(controlEnd,routeLeadEnd,t); + const FPoint d=interpolate(a,b,t); + const FPoint e=interpolate(b,c,t); + FPoint point=interpolate(d,e,t); + if(lateralOffsetMicrometres!=0) + { + const std::int64_t one=CurveParameterOne-t; + const std::int64_t t2=t*t/CurveParameterOne; + const std::int64_t one2=one*one/CurveParameterOne; + const std::int64_t envelope=std::min(CurveParameterOne, + 16*t2*one2/CurveParameterOne); + const std::int64_t offset=lateralOffsetMicrometres*envelope + /CurveParameterOne; + for(std::size_t axis=0;axis<2;++axis) + point[axis]+=pathSideQ16[axis]*offset/65536; + } + points.push_back(point); + } + points.push_back(routeEnd); + return points; + }; + const auto routeLength=[&](const std::vector& points) + { + std::int64_t length=0; + for(std::size_t i=1;i a{},b{}; + double radius=0.0; + }; + std::vector routePassageClearanceSpans; + const auto appendPassageClearanceSpans=[&]( + const ResolvedLayoutPassageV1& passage,bool lowerMouth) + { + if(passage.controlPoints.size()<2)return; + std::vector controls; + controls.reserve(passage.controlPoints.size()); + for(const auto& point:passage.controlPoints) + controls.push_back({static_cast(qToMicrometres( + point.positionSubvoxels[0])), + static_cast(qToMicrometres(point.positionSubvoxels[1])), + static_cast(qToMicrometres(point.positionSubvoxels[2])), + static_cast(qToMicrometres(point.radiusSubvoxels))}); + const auto segments=VoxelForgeCore::PassageSpline::BuildSegments(controls, + controls.size()-1,2.0,1000000.0); + const double totalLength=segments.empty()?0.0: + segments.back().alongMetres+segments.back().lengthMetres; + constexpr double PortalExclusionMetres=10.0; + for(const auto& segment:segments) + { + const double midpoint=segment.alongMetres+segment.lengthMetres*0.5; + if((lowerMouth&&totalLength-midpoint& p0, + const std::array& p1,const std::array& q0, + const std::array& q1) + { + constexpr double Epsilon=1.0e-12; + std::array u{},v{},w{}; + double a=0.0,b=0.0,c=0.0,d=0.0,e=0.0; + for(std::size_t axis=0;axis<3;++axis) + { + u[axis]=p1[axis]-p0[axis];v[axis]=q1[axis]-q0[axis]; + w[axis]=p0[axis]-q0[axis]; + a+=u[axis]*u[axis];b+=u[axis]*v[axis];c+=v[axis]*v[axis]; + d+=u[axis]*w[axis];e+=v[axis]*w[axis]; + } + double sNumerator=0.0,sDenominator=a*c-b*b; + double tNumerator=0.0,tDenominator=sDenominator; + if(a<=Epsilon&&c<=Epsilon)return std::sqrt(w[0]*w[0]+w[1]*w[1]+w[2]*w[2]); + if(a<=Epsilon) + {sNumerator=0.0;sDenominator=1.0;tNumerator=e;tDenominator=c;} + else if(c<=Epsilon) + {tNumerator=0.0;tDenominator=1.0;sNumerator=-d;sDenominator=a;} + else + { + if(sDenominatorsDenominator) + {sNumerator=sDenominator;tNumerator=e+b;tDenominator=c;} + } + } + if(tNumerator<0.0) + { + tNumerator=0.0; + if(-d<0.0)sNumerator=0.0; + else if(-d>a)sNumerator=sDenominator; + else {sNumerator=-d;sDenominator=a;} + } + else if(tNumerator>tDenominator) + { + tNumerator=tDenominator; + if(-d+b<0.0)sNumerator=0.0; + else if(-d+b>a)sNumerator=sDenominator; + else {sNumerator=-d+b;sDenominator=a;} + } + const double s=std::abs(sNumerator)& points) + { + if(routePassageClearanceSpans.empty()||points.size()<2) + return std::numeric_limits::infinity(); + const double totalLength=routeLength(points)/1.0e6; + constexpr double PortalExclusionMetres=10.0; + double minimumClearance=std::numeric_limits::infinity(); + double along=0.0; + for(std::size_t index=1;index=PortalExclusionMetres + &&totalLength-midpoint>=PortalExclusionMetres) + { + const std::array a={static_cast(points[index-1][0]), + static_cast(points[index-1][1]), + static_cast(points[index-1][2])}; + const std::array b={static_cast(points[index][0]), + static_cast(points[index][1]), + static_cast(points[index][2])}; + for(const auto& span:routePassageClearanceSpans) + minimumClearance=std::min(minimumClearance, + segmentDistanceMicrometres(a,b,span.a,span.b)/1.0e6 + -span.radius/1.0e6-mouthBoreRadius/1.0e6); + } + along+=segmentLength; + } + return minimumClearance; + }; + std::int64_t handleLength=10000000; + std::int64_t handleLow=handleLength; + std::int64_t handleHigh=std::max(handleLength,requiredLength/2); + auto routePoints=makeRoute(handleHigh); + while(routeLength(routePoints)(handleHigh*2,1000000000ll); + routePoints=makeRoute(handleHigh); + } + if(routeLength(makeRoute(handleLow))>=requiredLength) + handleHigh=handleLow; + else + { + for(int attempt=0;attempt<24&&handleHigh-handleLow>1;++attempt) + { + const std::int64_t middle=handleLow+(handleHigh-handleLow)/2; + if(routeLength(makeRoute(middle))>=requiredLength)handleHigh=middle; + else handleLow=middle; + } + } + handleLength=handleHigh; + routePoints=makeRoute(handleLength); + double plannedPassageClearanceMetres=routePassageClearance(routePoints); + std::int64_t selectedLateralOffsetMicrometres=0; + if(plannedPassageClearanceMetres<0.25) + { + constexpr std::array LateralOffsetsMicrometres={ + 20000000,-20000000,40000000,-40000000, + 80000000,-80000000,160000000,-160000000}; + for(const std::int64_t offset:LateralOffsetsMicrometres) + { + auto candidate=makeRoute(handleLength,offset); + if(routeLength(candidate)plannedPassageClearanceMetres+1.0e-6) + { + routePoints=std::move(candidate); + plannedPassageClearanceMetres=clearance; + selectedLateralOffsetMicrometres=offset; + } + } + } + const auto routeTangentAlignment=[&](std::size_t from,std::size_t to, + const std::array& tangent) + { + const double tangentLength=std::hypot(static_cast(tangent[0]), + static_cast(tangent[1])); + const double dx=static_cast(routePoints[to][0]-routePoints[from][0]); + const double dy=static_cast(routePoints[to][1]-routePoints[from][1]); + const double segmentLength=std::hypot(dx,dy); + return tangentLength>0.0&&segmentLength>0.0 + ?(dx*tangent[0]+dy*tangent[1])/(tangentLength*segmentLength):0.0; + }; + const double plannedMouthTangentAlignment=std::min( + routeTangentAlignment(1,2,arrivalTangentQ16), + routeTangentAlignment(routePoints.size()-2,routePoints.size()-1, + departureTangentQ16)); + const std::int64_t totalRouteLength=routeLength(routePoints); + if(totalRouteLength<=0||totalRouteLength cumulative(routePoints.size(),0); + for(std::size_t i=1;i(distance,0,totalRouteLength); + const std::int64_t u=static_cast(static_cast(distance) + *ProfileOne/totalRouteLength); + const std::int64_t u2=u*u/ProfileOne; + const std::int64_t u3=u2*u/ProfileOne; + return std::array{ + 2*u3-3*u2+ProfileOne,u3-2*u2+u,-2*u3+3*u2,u3-u2, + 6*u2-6*u,3*u2-4*u+ProfileOne,-6*u2+6*u,3*u2-2*u}; + }; + const auto profileSlopeAt=[&](std::int64_t distance, + std::int64_t startSlope,std::int64_t endSlope) + { + const auto basis=profileBasis(distance); + const std::int64_t endpointDerivative=basis[4]*routeStart[2] + +basis[6]*routeEnd[2]; + const std::int64_t tangentDerivative=basis[5]*startSlope + +basis[7]*endSlope; + return roundedDivide(roundedDivide(endpointDerivative,ProfileOne)*65536, + totalRouteLength) + +roundedDivide(tangentDerivative,ProfileOne); + }; + const std::int64_t averageFloorSlopeQ16=roundedDivide( + (routeEnd[2]-routeStart[2])*65536,totalRouteLength); + const std::int64_t authoredArrivalFloorSlopeQ16=arrivalFloorSlopeQ16; + const std::int64_t authoredDepartureFloorSlopeQ16=departureFloorSlopeQ16; + const std::int64_t maximumProfileSlopeQ16=10813; // 0.165, below the 1:5 walk grade. + const auto profileSlopeBlendSafe=[&](std::int64_t blendQ16) + { + const std::int64_t startSlope=averageFloorSlopeQ16 + +(authoredArrivalFloorSlopeQ16-averageFloorSlopeQ16)*blendQ16/65536; + const std::int64_t endSlope=averageFloorSlopeQ16 + +(authoredDepartureFloorSlopeQ16-averageFloorSlopeQ16)*blendQ16/65536; + for(std::int64_t sample=0;sample<=128;++sample) + { + const std::int64_t distance=totalRouteLength*sample/128; + if(std::abs(profileSlopeAt(distance,startSlope,endSlope)) + >maximumProfileSlopeQ16)return false; + } + return true; + }; + std::int64_t slopeBlendLow=0,slopeBlendHigh=65536; + for(int attempt=0;attempt<17;++attempt) + { + const std::int64_t middle=(slopeBlendLow+slopeBlendHigh+1)/2; + if(profileSlopeBlendSafe(middle))slopeBlendLow=middle; + else slopeBlendHigh=middle-1; + } + arrivalFloorSlopeQ16=averageFloorSlopeQ16 + +(authoredArrivalFloorSlopeQ16-averageFloorSlopeQ16)*slopeBlendLow/65536; + departureFloorSlopeQ16=averageFloorSlopeQ16 + +(authoredDepartureFloorSlopeQ16-averageFloorSlopeQ16)*slopeBlendLow/65536; + const auto routeFloorAt=[&](std::int64_t distance) + { + const auto basis=profileBasis(distance); + const std::int64_t positionTerms=basis[0]*routeStart[2] + +basis[2]*routeEnd[2]; + const std::int64_t startTangentDistance=basis[1]*totalRouteLength/ProfileOne; + const std::int64_t endTangentDistance=basis[3]*totalRouteLength/ProfileOne; + const std::int64_t tangentTerms=(startTangentDistance*arrivalFloorSlopeQ16 + +endTangentDistance*departureFloorSlopeQ16)/65536; + return roundedDivide(positionTerms,ProfileOne)+tangentTerms; + }; + const auto routeFloorSlopeQ16At=[&](std::int64_t distance) + {return profileSlopeAt(distance,arrivalFloorSlopeQ16,departureFloorSlopeQ16);}; + const auto routeFloorGradientAt=[&](std::int64_t distance) + { + distance=std::clamp(distance,0,totalRouteLength); + std::size_t segment=1; + while(segmentcumulative[segment])++segment; + const auto unitTangent=[&](std::size_t index) + { + const double tx=static_cast(routePoints[index][0] + -routePoints[index-1][0]); + const double ty=static_cast(routePoints[index][1] + -routePoints[index-1][1]); + const double length=std::hypot(tx,ty); + return length>0.0?std::array{tx/length,ty/length} + :std::array{0.0,0.0}; + }; + auto tangent=unitTangent(segment); + if(segment+11.0e-12){tangent[0]/=length;tangent[1]/=length;} + } + const std::int64_t profileSlope=routeFloorSlopeQ16At(distance); + return std::array{ + static_cast(std::llround(tangent[0]*profileSlope)), + static_cast(std::llround(tangent[1]*profileSlope))}; + }; + const auto pointAt=[&](std::int64_t distance) + { + distance=std::clamp(distance,0,totalRouteLength); + std::size_t segment=1; + while(segmentcumulative[segment])++segment; + const std::int64_t startDistance=cumulative[segment-1]; + const std::int64_t segmentLength=std::max(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(settings.roomSpacingMicrometres, + 4000000),settings.maximumTunnelLengthMicrometres); + spacing=std::max(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 nodeDistances; + for(std::int64_t distance=spacing;distance(beforeX), + static_cast(beforeY)); + const double afterLength=std::hypot(static_cast(afterX), + static_cast(afterY)); + if(beforeLength<=0.0||afterLength<=0.0)continue; + const double alignment=(static_cast(beforeX)*afterX + +static_cast(beforeY)*afterY)/(beforeLength*afterLength); + if(alignment<0.95||std::min(beforeLength,afterLength) profileSpacedNodeDistances; + profileSpacedNodeDistances.reserve(nodeDistances.size()); + for(const std::int64_t distance:nodeDistances) + if(distance>=MinimumProfileSpanMicrometres + &&(profileSpacedNodeDistances.empty() + ||distance-profileSpacedNodeDistances.back() + >=MinimumProfileSpanMicrometres)) + profileSpacedNodeDistances.push_back(distance); + while(!profileSpacedNodeDistances.empty() + &&totalRouteLength-profileSpacedNodeDistances.back() + 256) + {error="cavern planner would exceed the 256-node bounded backbone";return false;} + + const auto clampQ16=[](std::int32_t value) + {return std::clamp(value,0,65536);}; + const std::int64_t smooth=std::clamp( + std::max(1500000,settings.smoothWidthMicrometres), + 50000,10000000); + const std::int64_t irregularity=std::clamp( + settings.irregularityAmplitudeMicrometres,0,500000); + const std::int64_t routeBoreRadius=std::clamp( + std::max(MinimumBoreRadius,settings.minimumTunnelRadiusMicrometres), + MinimumBoreRadius,MaximumBoreRadius); + const std::int64_t maximumRouteBoreRadius=std::clamp( + std::max(routeBoreRadius,settings.maximumTunnelRadiusMicrometres), + routeBoreRadius,MaximumBoreRadius); + const auto selectedBoreRadius=[&](const std::string& stableId) + { + const std::uint64_t span=static_cast( + maximumRouteBoreRadius-routeBoreRadius); + return routeBoreRadius+(span==0?0:static_cast( + LayoutStampSeed(seed,strateIndex,stableId)%(span+1))); + }; + const auto fractional=[](std::int64_t value,std::int64_t q16) + {return value*std::clamp(q16,0,65536)/65536;}; + struct FRoomNode + { + std::string id; + FPoint point{}; + std::string floorProfileStableId; + std::int64_t floorProfileDistanceMicrometres=-1; + std::array floorGradientQ16{}; + std::int64_t radiusMicrometres=0; + bool destination=false; + }; + std::vector nodes;nodes.reserve(nodeDistances.size()+1); + 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( + 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(hash%static_cast(baseRoomSpan+1)); + nodes.push_back(std::move(node)); + } + if(terminalShaftRoute) + { + FRoomNode terminal;terminal.id=routePrefix+"basal-landing"; + terminal.point=routeEnd;terminal.floorProfileStableId=backboneFloorProfileId; + terminal.floorProfileDistanceMicrometres=totalRouteLength; + terminal.floorGradientQ16=routeFloorGradientAt(totalRouteLength); + terminal.radiusMicrometres=settings.destinationRadiusMicrometres; + terminal.destination=true;nodes.push_back(std::move(terminal)); + } + const auto makeCompound=[&](const FRoomNode& node,bool destination, + std::vector& 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( + variety*2/65536); + const std::size_t primitiveCount=2+static_cast( + 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(primitiveCount); + for(std::int64_t component=0;component(componentHash%22001); + const std::int64_t componentRadius=std::max(500000, + fractional(radius,componentScaleQ16)); + const std::int64_t axisScaleQ16=component==0?65536 + :47000+static_cast((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(500000, + fractional(componentRadius,heightRatio)); + const std::int64_t floorCut=floorCutBase + +(floorRange==0?0:static_cast(componentHash%( + static_cast(floorRange+1)))); + const std::int64_t offsetMagnitude=component==0?0 + :fractional(radius,12000+static_cast(componentHash%11001)); + const std::int64_t routeLengthXY=std::max(1, + static_cast(integerSqrt(static_cast( + 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,0}; + // Keep the local floor plane on the backbone profile at the route point, + // then place each offset primitive from that plane at its own XY centre. + room.floorMicrometres=node.point[2] + +(static_cast(room.floorGradientQ16[0]) + *(room.centerMicrometres[0]-node.point[0]) + +static_cast(room.floorGradientQ16[1]) + *(room.centerMicrometres[1]-node.point[1]))/65536; + room.centerMicrometres[2]=room.floorMicrometres+fractional(halfZ,floorCut); + if(component>0) + { + const std::int64_t headroom=std::max(0, + halfZ-fractional(halfZ,floorCut)); + const std::int64_t jitterRange=std::min(headroom,halfZ/5); + const std::int64_t ceilingJitter=jitterRange>0 + ?static_cast((componentHash>>32) + %static_cast(jitterRange+1)):0; + room.centerMicrometres[2]+=ceilingJitter; + } + 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 profileSpan=profileEndDistance-profileStartDistance; + const std::int64_t startSlope=routeFloorSlopeQ16At(profileStartDistance); + const std::int64_t endSlope=routeFloorSlopeQ16At(profileEndDistance); + edge.controlPointsMicrometres[0][2]=floorStart; + edge.controlPointsMicrometres[1][2]=floorStart + +roundedDivide(startSlope*profileSpan,3*65536); + edge.controlPointsMicrometres[2][2]=floorEnd + -roundedDivide(endSlope*profileSpan,3*65536); + edge.controlPointsMicrometres[3][2]=floorEnd; + } + return edge; + }; + struct FPlannedShaftLanding + { + FRoomNode node; + FPoint point{}; + }; + std::vector shaftLandings; + std::size_t plannedShaftCount=0; + if(terminalShaftRoute&&settings.roomDensityQ16>0) + { + const std::int64_t ledgeSpacing=std::max(0, + settings.ledgeSpacingMicrometres); + const std::int64_t ledgeDepth=std::clamp( + settings.ledgeDepthMicrometres,0,5000000); + bool emittedShaft=false; + const std::int64_t routeLengthXY=std::max(1, + static_cast(integerSqrt(static_cast( + pathAxisX*pathAxisX+pathAxisY*pathAxisY)))); + const std::int64_t sideX=-pathAxisY*65536/routeLengthXY; + const std::int64_t sideY=pathAxisX*65536/routeLengthXY; + const auto selectedRadius=[&](const std::string& stableId) + { + const std::int64_t span=settings.maximumRoomRadiusMicrometres + -settings.minimumRoomRadiusMicrometres; + return settings.minimumRoomRadiusMicrometres+(span==0?0: + static_cast(LayoutStampSeed(seed,strateIndex,stableId) + %static_cast(span+1))); + }; + for(std::size_t nodeIndex=1;nodeIndex+1(hash&0xffffu) + >=settings.roomDensityQ16)continue; + const std::int64_t radius=selectedRadius(parent.id+"/shaft-radius"); + const std::int64_t top=parent.point[2] + +(ledgeSpacing>0?ledgeSpacing:1000000); + const std::int64_t minimumTop=stratum->content.bottomMicrometres+4000000; + if(top>stratum->content.topMicrometres-1000000 + ||topcontent.bottomMicrometres+1000000,top-200000000); + if(ledgeSpacing>0&&top-bottom=0?1:-1)*(positiveSide?1:-1); + const std::int64_t sideYSign=(sideY>=0?1:-1)*(positiveSide?1:-1); + const bool useX=std::abs(sideX)>=std::abs(sideY); + const std::uint8_t outwardSide=useX + ?static_cast(sideXSign>0?0:2) + :static_cast(sideYSign>0?1:3); + const std::array,4> cardinal={{{1,0},{0,1}, + {-1,0},{0,-1}}}; + const auto outward=cardinal[outwardSide]; + const std::int64_t offset=std::max( + parent.radiusMicrometres+radius+3000000, + settings.roomSpacingMicrometres/2); + FPlannedShaftLanding landing;landing.node.id=routePrefix+"shaft-" + +std::to_string(shaftLandings.size()); + landing.node.point={parent.point[0]+outward[0]*offset, + parent.point[1]+outward[1]*offset,parent.point[2]}; + landing.node.radiusMicrometres=radius; + const std::int64_t landingZ=top-(ledgeSpacing>0?ledgeSpacing:1000000); + const std::array toParent={-outward[0],-outward[1]}; + const std::uint8_t ledgeSide=static_cast( + (outwardSide+2)%4); + ResolvedCavernRoomV1 shaft;shaft.stableId=landing.node.id+"/primitive-0"; + shaft.strateIndex=strateIndex;shaft.shapeKind=1; + shaft.centerMicrometres={landing.node.point[0],landing.node.point[1], + (top+bottom)/2}; + shaft.halfExtentsMicrometres={radius,radius,(top-bottom)/2}; + shaft.floorMicrometres=top;shaft.smoothWidthMicrometres=smooth; + shaft.ledgeSpacingMicrometres=ledgeSpacing; + shaft.ledgeDepthMicrometres=ledgeDepth;shaft.ledgeSide=ledgeSide; + shaft.irregularityAmplitudeMicrometres=irregularity; + shaft.irregularityWavelengthMicrometres= + settings.irregularityWavelengthMicrometres; + shaft.irregularitySeed=LayoutStampSeed(seed,strateIndex,shaft.stableId); + rooms.push_back(std::move(shaft)); + landing.point={landing.node.point[0] + +toParent[0]*(radius-ledgeDepth/2), + landing.node.point[1]+toParent[1]*(radius-ledgeDepth/2),landingZ}; + landing.node.point=landing.point; + const std::string accessId=routePrefix+"shaft-access-" + +std::to_string(shaftLandings.size()); + auto access=makeEdge(accessId,componentRoomId(parent),parent.point, + landing.node,routeBoreRadius,-1,-1); + // The connector's own floor SDF retains its tread. It must not enter + // the descending backbone's finite floor-cap arbitration, where its + // nearby but independent floor could suppress backbone support. + access.floorSupportDepthMicrometres=0; + const std::int64_t run=distance2d(parent.point,landing.point); + const std::int64_t rise=std::abs(parent.point[2]-landing.point[2]); + if(run<=0||rise*MaximumWalkGradeDenominator>run) + { + rooms.pop_back(); + continue; + } + edges.push_back(std::move(access)); + shaftLandings.push_back(std::move(landing)); + emittedShaft=true; + } + plannedShaftCount=shaftLandings.size(); + for(std::size_t index=1;index(hash&0xffffu)>=settings.tunnelDensityQ16) + continue; + const auto& from=shaftLandings[index-1]; + const auto& to=shaftLandings[index]; + const std::int64_t run=distance2d(from.point,to.point); + const std::int64_t rise=std::abs(from.point[2]-to.point[2]); + if(run<=0||run>settings.maximumTunnelLengthMicrometres + ||rise*MaximumWalkGradeDenominator>run)continue; + auto link=makeEdge(routePrefix+"shaft-cross-"+std::to_string(index-1), + componentRoomId(from.node),from.point,to.node,routeBoreRadius,-1,-1); + link.floorSupportDepthMicrometres=0; + edges.push_back(std::move(link)); + } + } + + const auto arrivalPoint=routeStart; + FPoint previousPoint=arrivalPoint; + std::int64_t previousProfileDistance=0; + std::string previousAnchor=arrivalPassage->stableId+"/lower-landing-chamber"; + for(std::size_t nodeIndex=0;nodeIndexstableId+"/upper-landing-chamber", + routeEnd,nodes.back(), + std::min(selectedBoreRadius(departureEdgeId),LandingConnectorBoreRadius), + totalRouteLength,nodes.back().floorProfileDistanceMicrometres); + edges.push_back(std::move(edge)); + } + + 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::clamp(settings.originRoomMaximumConnections,0,32)); + std::size_t branchCount=0; + for(std::size_t nodeIndex=1;!terminalShaftRoute&&nodeIndex+1(branchHash&0xffffu)>=branchThreshold)continue; + const std::int64_t branchLength=std::max( + 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((branchHash>>17) + %static_cast(baseRoomSpan+1)); + const std::int64_t pathAxisLength=std::max(1, + static_cast(integerSqrt(static_cast( + pathAxisX*pathAxisX+pathAxisY*pathAxisY)))); + branch.point={parent.point[0]+(-pathAxisY)*branchLength/pathAxisLength*direction, + parent.point[1]+pathAxisX*branchLength/pathAxisLength*direction, + parent.point[2]}; + // A side branch leaves the backbone laterally, so it has its own local flat + // floor and must not reference the parent's one-dimensional h(s) profile. + makeCompound(branch,false,rooms); + const std::string branchEdgeId=routePrefix+"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 mouth-to-terminal 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(integerSqrt( + static_cast((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) + { + std::printf("[CavernPlannerGradeReject] edge=%s run_m=%.3f rise_m=%.3f grade_deg=%.3f start=(%.3f,%.3f,%.3f) end=(%.3f,%.3f,%.3f)\n", + edge.stableId.c_str(),run/1.0e6,rise/1.0e6, + std::atan2(static_cast(rise),static_cast(run)) + *180.0/3.14159265358979323846, + a[0]/1.0e6,a[1]/1.0e6,a[2]/1.0e6, + b[0]/1.0e6,b[1]/1.0e6,b[2]/1.0e6); + 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(rise),static_cast(run)) + *180.0/3.14159265358979323846); + } + const double plannerMilliseconds=std::chrono::duration( + std::chrono::steady_clock::now()-plannerStarted).count(); + std::printf("[CavernPlannerV1] strate=%d seed=%d route=%s->%s route_nodes=%zu room_primitives=%zu shafts=%zu edges=%zu branches=%zu route_horizontal_m=%.3f floor_delta_m=%.3f max_grade_deg=%.3f mouth_tangent_min_dot=%.3f grade_limit_deg=11.310 clearance_radius_m=%.3f clearance_height_m=%.3f destination_radius_m=%.3f plan_ms=%.3f status=PASS\n", + strateIndex,seed,arrivalPassage->stableId.c_str(), + departurePassage?departurePassage->stableId.c_str():"basal-terminal", + nodes.size(),rooms.size(),plannedShaftCount,edges.size(),branchCount,totalRouteLength/1.0e6, + std::abs(dz)/1.0e6,maximumGradeDegrees,plannedMouthTangentAlignment, + routeBoreRadius/1.0e6,PlannedBoreHeight/1.0e6, + settings.destinationRadiusMicrometres/1.0e6,plannerMilliseconds); + return true; +} + bool ResolveResolvedLayoutOreVeinsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed, std::span requests,std::string& error) { @@ -2651,6 +4208,8 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t /layout.coreVoxelSizeMicrometres)); if(qPerMetre<=0){error="passage planner could not resolve fixed-point metres";return false;} const std::int64_t playerCenterAboveFloorQ=static_cast(std::llround(1.13*qPerMetre)); + const std::int64_t surfacePlayerCenterAboveFloorQ=static_cast( + std::llround(0.98*qPerMetre)); const std::array,16> directions{{ {{65536,0}},{{60547,25080}},{{46341,46341}},{{25080,60547}}, {{0,65536}},{{-25080,60547}},{{-46341,46341}},{{-60547,25080}}, @@ -2704,6 +4263,38 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t out={to->values[0]-from->values[0],to->values[1]-from->values[1]}; return out[0]!=0||out[1]!=0; }; + const auto plannedMouthFloor=[&](const ResolvedLayoutStratumV2& host, + const std::array& mouth,bool upperSide, + std::int64_t playerCenterAboveFloorQ,std::int64_t mouthRadiusQ) + { + std::int64_t result=mouth[2]-playerCenterAboveFloorQ; + const bool receivesRoute=upperSide + ?host.generatorKind=="SurfaceWorld"||host.generatorKind=="TunnelNetwork" + :host.generatorKind=="TunnelNetwork"||host.generatorKind=="VerticalShafts"; + if(!receivesRoute)return result; + std::vector> route; + for(const auto& anchor:host.anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + route.push_back({static_cast(anchor.values[0]), + static_cast(anchor.values[1]), + static_cast(anchor.values[2])}); + if(host.generatorKind=="TunnelNetwork"&&upperSide&&route.size()>2) + route.erase(route.begin(),route.end()-2); + else if(!upperSide&&(host.generatorKind=="TunnelNetwork" + ||host.generatorKind=="VerticalShafts")&&route.size()>2) + { + route.resize(2); + } + double routeHeight=0.0,distanceSquared=0.0; + const double reach=static_cast(mouthRadiusQ)/qPerMetre+0.5; + if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route, + static_cast(mouth[0]),static_cast(mouth[1]), + routeHeight,distanceSquared) + &&distanceSquared<=std::pow(reach*qPerMetre,2.0)) + result=static_cast(std::llround(routeHeight + -playerCenterAboveFloorQ)); + return result; + }; const auto radialWithin=[](const std::array& p,std::int64_t radius) {return radius>0&&p[0]*p[0]+p[1]*p[1]<=radius*radius;}; const auto reachValue=[](std::uint64_t hash,std::int64_t lo,std::int64_t hi) @@ -2801,6 +4392,8 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t ?(lowerMouth=lowerArrivalPad->positionSubvoxels,true) :connection==0&&routeAnchor(lower,false,lowerMouth); const bool surfaceGroundDeparture=hasUpperRoute&&upperIsSurfaceWorld; + const std::int64_t upperPlayerCenterAboveFloorQ=surfaceGroundDeparture + ?surfacePlayerCenterAboveFloorQ:playerCenterAboveFloorQ; const ResolvedLayoutAnchorV2* passageAnchor=nullptr; if(connection==0) for(const auto& anchor:upper.anchors) @@ -2842,11 +4435,76 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t reach/static_cast(qPerMetre)); } } + // Reserve each landing envelope inside its host band before building h(s). + // SurfaceWorld can raise its floor by at most the 4.5 m headroom allowance, + // so include that bounded rise as well as the grade reach and floor shell. + constexpr std::int64_t MaximumLandingGradeQ16=17560; + const std::int64_t landingRadius=mouthRadius+3*qPerMetre; + const std::int64_t landingClearHeight=static_cast( + std::llround(4.5*qPerMetre)); + const std::int64_t landingShell=static_cast( + std::llround(0.75*qPerMetre)); + const std::int64_t landingPlaneReach=(landingRadius + *MaximumLandingGradeQ16+65535)/65536; + const std::array upperMouthBeforeBandFit=upperMouth; + const std::array lowerMouthBeforeBandFit=lowerMouth; + const auto fitMouth=[&](std::array& mouth, + const ResolvedLayoutStratumV2& host,std::int64_t playerHeightQ, + const char* side) + { + if(!host.content.presentInCurrentField)return true; + const std::int64_t minimumFloor=contentZQ(host.content.bottomMicrometres) + +landingPlaneReach+landingShell; + const std::int64_t nativeFloorRise=host.generatorKind=="SurfaceWorld" + ?landingClearHeight:0; + const std::int64_t maximumFloor=contentZQ(host.content.topMicrometres) + -landingPlaneReach-landingShell-landingClearHeight-nativeFloorRise; + if(maximumFloor(qPerMetre)); + } + return true; + }; + if(!fitMouth(upperMouth,upper,upperPlayerCenterAboveFloorQ,"upper") + ||!fitMouth(lowerMouth,lower,playerCenterAboveFloorQ,"lower"))return false; + const auto bindFittedMouthAnchor=[&]( + const ResolvedLayoutStratumV2& host, + const std::array& before, + const std::array& after,const char* side) + { + if(before==after)return; + auto mutableHost=std::find_if(layout.strates.begin(),layout.strates.end(), + [&](const auto& stratum) + {return stratum.strateIndex==host.strateIndex;}); + if(mutableHost!=layout.strates.end()) + for(auto& anchor:mutableHost->anchors) + if((anchor.kind=="passage_arrival" + ||anchor.kind=="passage_departure") + &&anchor.values.size()>=3 + &&anchor.values[0]==after[0] + &&anchor.values[1]==after[1]) + { + anchor.values[2]=after[2]; + std::printf("[PassageLandingChamberArrivalBinding] id=%s side=%s anchor=%s status=PASS\n", + passage.stableId.c_str(),side,anchor.stableId.c_str()); + break; + } + }; + bindFittedMouthAnchor(upper,upperMouthBeforeBandFit,upperMouth,"upper"); + bindFittedMouthAnchor(lower,lowerMouthBeforeBandFit,lowerMouth,"lower"); if(!radialWithin(upperMouth,placementRadius)||!radialWithin(lowerMouth,placementRadius)) {error=passage.stableId+" route mouth is outside the resolved PassageConfig.DistanceMax radius";return false;} passage.upperMouthSubvoxels=upperMouth;passage.lowerMouthSubvoxels=lowerMouth; - constexpr std::int64_t LandingExtensionQ=8; std::array upperTangent{},lowerTangent{}; const bool hasUpperTangent=upperDeparturePad ?(upperTangent={upperDeparturePad->approachDirectionQ16[0], @@ -2855,18 +4513,28 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const bool hasPassageArrival=std::any_of(lower.anchors.begin(),lower.anchors.end(), [](const auto& anchor){return anchor.kind=="passage_arrival" &&anchor.values.size()>=3;}); + // Authored pad direction wins; otherwise native cave route points still + // define the receiving tangent when their first point is passage_arrival. const bool hasLowerTangent=lowerArrivalPad ?(lowerTangent={lowerArrivalPad->approachDirectionQ16[0], lowerArrivalPad->approachDirectionQ16[1]},true) - :hasLowerRoute&&!hasPassageArrival&&routeTangent(lower,false,lowerTangent); + :hasLowerRoute + &&(!hasPassageArrival||IsNativeCaveFieldStratum(lower)) + &&routeTangent(lower,false,lowerTangent); const bool profiledSurfaceJoin=hasLowerTangent &&std::any_of(lower.anchors.begin(),lower.anchors.end(), [](const auto& anchor){return anchor.kind=="heightfield_profile" &&anchor.values.size()>=2&&anchor.values[0]>0;}); + const std::int64_t upperPlannedFloor=plannedMouthFloor(upper,upperMouth, + true,upperPlayerCenterAboveFloorQ,mouthRadius); + const std::int64_t lowerPlannedFloor=plannedMouthFloor(lower,lowerMouth, + false,playerCenterAboveFloorQ,mouthRadius); const std::int64_t outerRingRadius=std::max(1, placementRadius-mouthRadius-qPerMetre/2); const std::uint32_t pathStyle=anchoredSpiral?1u:style; + const bool generatedWormEntry=!hasUpperRoute&&!hasUpperTangent + &&!anchoredSpiral&&pathStyle==1u; std::int64_t ringRadius=outerRingRadius; if(pathStyle==1u) ringRadius=std::min(outerRingRadius, @@ -2880,6 +4548,8 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t std::max(0,cfg?cfg->cascadeLedgeSubvoxels:0) *static_cast(cfg?cfg->cascadeSteps:4)), mouthRadius+2*qPerMetre,outerRingRadius); + if(generatedWormEntry) + ringRadius=outerRingRadius; if(surfaceGroundDeparture) { const double departureRadius=std::hypot(static_cast(upperMouth[0]), @@ -2906,8 +4576,6 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t // Authored tangents and island anchors already determine the phase. // A generated mouth in a native cave uses its radial approach; an // island landing uses the tangent fit below to ease off the crater rim. - const bool generatedIslandWormEntry=!hasUpperRoute&&!hasUpperTangent - &&!anchoredSpiral&&upper.generatorKind=="FloatingIslands"; if(hasUpperRoute||(anchoredIslandPassage&&!anchoredSpiral) ||(!hasUpperRoute&&!anchoredIslandPassage&&!anchoredSpiral &&upper.generatorKind!="FloatingIslands")) @@ -3031,11 +4699,15 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t centerX=static_cast(passageAnchor->values[0]); centerY=static_cast(passageAnchor->values[1]); } + const double placementBudget=std::max(0.0, + static_cast(placementRadius) + -std::hypot(centerX,centerY)-2.0); + radius=std::clamp(radius,0.0,placementBudget); return std::array{ static_cast(std::llround(centerX+radius*directions[directionIndex][0]/65536.0)), static_cast(std::llround(centerY+radius*directions[directionIndex][1]/65536.0))}; }; - if(generatedIslandWormEntry) + if(generatedWormEntry) { struct FPhaseScore { @@ -3050,7 +4722,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const std::uint32_t hashedPhase=phase; const int hashedDirection=direction; const std::int64_t entryVerticalDistance=std::abs( - lowerMouth[2]-upperMouth[2]); + lowerPlannedFloor-upperPlannedFloor); const std::int64_t entryRouteGradeQ16=profiledSurfaceJoin?8000 :style==0&&anchoredIslandPassage&&!anchoredSpiral?15500 :TargetGradeQ16; @@ -3160,7 +4832,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const double tangentAlignment=(entryX*tangentX+entryY*tangentY) /(entryLength*tangentLength); const std::int64_t firstAxisZ=upperMouth[2]+mouthRadius - -playerCenterAboveFloorQ; + -upperPlayerCenterAboveFloorQ; const std::int64_t secondAxisZ=firstAxisZ+PassageRoundRatio( lowerMouth[2]-upperMouth[2],static_cast(entryDistance), static_cast(routeLength)); @@ -3273,20 +4945,33 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t double bestClearance=-std::numeric_limits::infinity(); for(std::size_t index=0;indexrouteLength; - const bool stableLengthTie=selected - &&candidate.routeLength==selected->routeLength - &&(candidate.clearance>selected->clearance+1.0e-6 - ||(std::abs(candidate.clearance-selected->clearance)<=1.0e-6 - &&candidate.tangent>selected->tangent)); - if(shorter||stableLengthTie)selected=&candidate; + if(candidate.clearance+ClearanceToleranceMetresselected->tangent+1.0e-6; + const bool tangentTie=selected + &&std::abs(candidate.tangent-selected->tangent)<=1.0e-6; + const bool stableTangentTie=tangentTie&&( + candidate.routeLengthrouteLength + ||(candidate.routeLength==selected->routeLength + &&candidate.clearance>selected->clearance+1.0e-6)); + if(smoother||stableTangentTie)selected=&candidate; } + if(!selected) + for(std::size_t index=0;indexselected->clearance + ||(candidate.clearance==selected->clearance + &&candidate.routeLengthrouteLength)) + selected=&candidate; + } if(selected) { phase=selected->phase; @@ -3402,10 +5087,70 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t pushXY(point); } } - pushXY(firstRing); + if(generatedWormEntry) + { + // Join the portal lead to the winding route with a circular arc whose + // radius remains wider than the bore. A short quadratic corner still + // makes the passage sweep swell on the inside of this turn. + const auto nextRing=ringPoint(static_cast( + (static_cast(phase)+direction+16)%16)); + const double incomingX=static_cast(firstRing[0]-upperMouth[0]); + const double incomingY=static_cast(firstRing[1]-upperMouth[1]); + const double outgoingX=static_cast(nextRing[0]-firstRing[0]); + const double outgoingY=static_cast(nextRing[1]-firstRing[1]); + const double incomingLength=std::hypot(incomingX,incomingY); + const double outgoingLength=std::hypot(outgoingX,outgoingY); + const double turnCross=incomingX*outgoingY-incomingY*outgoingX; + const double turnDot=incomingX*outgoingX+incomingY*outgoingY; + const double turnAngle=std::atan2(turnCross,turnDot); + const double halfTangent=std::tan(std::abs(turnAngle)*0.5); + if(incomingLength>1.0e-6&&outgoingLength>1.0e-6 + &&std::abs(halfTangent)>1.0e-6) + { + const double maximumCut=0.80*std::min(incomingLength,outgoingLength); + const double requestedRadius=std::max(mouthRadius,midRadius) + +0.75*qPerMetre; + const double filletRadius=std::min(static_cast(requestedRadius), + maximumCut/std::abs(halfTangent)); + const double cut=filletRadius*std::abs(halfTangent); + const double incomingUnitX=incomingX/incomingLength; + const double incomingUnitY=incomingY/incomingLength; + const double outgoingUnitX=outgoingX/outgoingLength; + const double outgoingUnitY=outgoingY/outgoingLength; + const std::array start={ + firstRing[0]-incomingUnitX*cut, + firstRing[1]-incomingUnitY*cut}; + const std::array end={ + firstRing[0]+outgoingUnitX*cut, + firstRing[1]+outgoingUnitY*cut}; + const double turnSign=turnCross>=0.0?1.0:-1.0; + const std::array center={ + start[0]-turnSign*incomingUnitY*filletRadius, + start[1]+turnSign*incomingUnitX*filletRadius}; + const double startAngle=std::atan2(start[1]-center[1], + start[0]-center[0]); + const int arcSegments=std::max(1,static_cast(std::ceil( + filletRadius*std::abs(turnAngle)/(0.75*qPerMetre)))); + pushXY({static_cast(std::llround(start[0])), + static_cast(std::llround(start[1]))}); + for(int sample=1;sample<=arcSegments;++sample) + { + const double t=static_cast(sample)/arcSegments; + const double angle=startAngle+turnAngle*t; + const std::array point=sample==arcSegments?end: + std::array{center[0]+filletRadius*std::cos(angle), + center[1]+filletRadius*std::sin(angle)}; + pushXY({static_cast(std::llround(point[0])), + static_cast(std::llround(point[1]))}); + } + } + else pushXY(firstRing); + } + else pushXY(firstRing); std::uint64_t totalHorizontal=0;std::uint32_t loops=0; const std::int64_t verticalDistance=hasAnchorHelix - ?std::abs(lowerMouth[2]-helixBedrockZ):std::abs(lowerMouth[2]-upperMouth[2]); + ?std::abs(lowerPlannedFloor-(helixBedrockZ-playerCenterAboveFloorQ)) + :std::abs(lowerPlannedFloor-upperPlannedFloor); auto measurePath=[&]() { std::uint64_t total=0; @@ -3538,39 +5283,6 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t } pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath(); std::size_t lowerMouthIndex=xy.size()-1; - std::vector> lowerLandingPoints; - if(hasLowerTangent) - { - std::array previousRoutePoint=lowerMouth; - std::int64_t remainingQ=LandingExtensionQ*qPerMetre; - bool skippedMouth=false; - for(const auto& anchor:lower.anchors) - if(anchor.kind=="route_point"&&anchor.values.size()>=3) - { - const std::array routePoint{{anchor.values[0], - anchor.values[1],anchor.values[2]}}; - if(!skippedMouth){skippedMouth=true;continue;} - if(remainingQ<=0)break; - const std::int64_t dx=routePoint[0]-previousRoutePoint[0]; - const std::int64_t dy=routePoint[1]-previousRoutePoint[1]; - const std::uint64_t segmentLength=distanceQ(dx,dy); - if(segmentLength==0)continue; - const std::int64_t advance=std::min(remainingQ, - static_cast(segmentLength)); - std::array landingPoint{}; - for(std::size_t axis=0;axis<3;++axis) - landingPoint[axis]=previousRoutePoint[axis]+PassageRoundRatio( - routePoint[axis]-previousRoutePoint[axis],advance, - static_cast(segmentLength)); - if(!radialWithin(landingPoint,placementRadius))break; - lowerLandingPoints.push_back(landingPoint); - pushXY({landingPoint[0],landingPoint[1]}); - previousRoutePoint=landingPoint;remainingQ-=advance; - if(advance(segmentLength))break; - } - if(lowerLandingPoints.empty()) - {error=passage.stableId+" lower route has no landing length inside PassageConfig.DistanceMax";return false;} - } if(VoxelForgeCore::WorldAssemblyEvaluatorVersion>=188 &&style==0&&anchoredIslandPassage&&!anchoredSpiral&&xy.size()>4) { @@ -3613,8 +5325,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t std::max(8*qPerMetre,2*mouthRadius)); for(std::size_t i=0;ilowerMouthIndex&&i+1>=xy.size()-1))?mouthRadius:midRadius; + std::int64_t radius=(i==0||i==lowerMouthIndex)?mouthRadius:midRadius; if(surfaceGroundDeparture&&i(cumulative[i]) @@ -3623,6 +5334,20 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t radius=surfacePortalRadius+static_cast(std::llround( (midRadius-surfacePortalRadius)*smooth)); } + else if(i( + std::max(UpperMouthRadiusBlendLengthMetres*qPerMetre, + 2*mouthRadius)); + if(cumulative[i](cumulative[i])/blendLength; + const double smooth=t*t*(3.0-2.0*t); + radius=mouthRadius+static_cast(std::llround( + (midRadius-mouthRadius)*smooth)); + } + } // Grow the lower bore into its mouth over fixed route distance. Assigning // the full mouth radius to the penultimate point creates a segment-length // flare whose cubic-detrended profile can read as a short-period wall beat. @@ -3642,9 +5367,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t } } std::int64_t playerZ=0; - if(i>lowerMouthIndex) - playerZ=lowerLandingPoints[i-lowerMouthIndex-1][2]; - else if(hasAnchorHelix&&i<=helixEndIndex) + if(hasAnchorHelix&&i<=helixEndIndex) playerZ=upperMouth[2]+PassageRoundRatio(helixBedrockZ-upperMouth[2], static_cast(cumulative[i]), static_cast(helixHorizontal)); @@ -3658,12 +5381,23 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t static_cast(segmentLength)); } ResolvedLayoutPassagePointV1 point; - point.positionSubvoxels={xy[i][0],xy[i][1],playerZ+radius-playerCenterAboveFloorQ}; + std::int64_t pointPlayerCenterAboveFloorQ=playerCenterAboveFloorQ; + if(surfaceGroundDeparture) + { + const double t=std::clamp(static_cast(cumulative[i]) + /surfaceMouthBlendLength,0.0,1.0); + const double smooth=t*t*(3.0-2.0*t); + pointPlayerCenterAboveFloorQ=upperPlayerCenterAboveFloorQ + +static_cast(std::llround( + (playerCenterAboveFloorQ-upperPlayerCenterAboveFloorQ)*smooth)); + } + point.positionSubvoxels={xy[i][0],xy[i][1], + playerZ+radius-pointPlayerCenterAboveFloorQ}; point.radiusSubvoxels=radius;passage.controlPoints.push_back(point); } if(surfaceGroundDeparture) std::printf("[ResolvedSurfaceWorldDepartureMouth] passage=%s ground_floor_z_m=%.3f portal_radius_m=%.3f tunnel_radius_m=%.3f blend_length_m=%.3f departure_xy_m=(%.3f,%.3f) first_tunnel_xy_m=(%.3f,%.3f) first_tunnel_floor_z_m=%.3f first_tunnel_radius_m=%.3f\n", - passage.stableId.c_str(),(upperMouth[2]-static_cast(std::llround(0.88*qPerMetre)))/static_cast(qPerMetre), + passage.stableId.c_str(),(upperMouth[2]-upperPlayerCenterAboveFloorQ)/static_cast(qPerMetre), surfacePortalRadius/static_cast(qPerMetre), midRadius/static_cast(qPerMetre), surfaceMouthBlendLength/static_cast(qPerMetre), @@ -3676,15 +5410,198 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t passage.controlPoints.size()>1? (passage.controlPoints[1].positionSubvoxels[2] -passage.controlPoints[1].radiusSubvoxels+playerCenterAboveFloorQ - -static_cast(std::llround(0.88*qPerMetre))) + -static_cast(std::llround(1.13*qPerMetre))) /static_cast(qPerMetre):0.0, passage.controlPoints.size()>1?passage.controlPoints[1].radiusSubvoxels /static_cast(qPerMetre):0.0); passage.controlPoints.front().positionSubvoxels[2]=upperMouth[2] - +passage.controlPoints.front().radiusSubvoxels-playerCenterAboveFloorQ; + +passage.controlPoints.front().radiusSubvoxels-upperPlayerCenterAboveFloorQ; passage.controlPoints[lowerMouthIndex].positionSubvoxels[2]=lowerMouth[2] +passage.controlPoints[lowerMouthIndex].radiusSubvoxels -playerCenterAboveFloorQ; + std::vector chamberFloorControls; + std::vector chamberFloorProfile; + chamberFloorControls.reserve(passage.controlPoints.size()); + chamberFloorProfile.reserve(passage.controlPoints.size()); + for(const auto& point:passage.controlPoints) + { + chamberFloorControls.push_back({ + static_cast(point.positionSubvoxels[0]), + static_cast(point.positionSubvoxels[1]), + static_cast(point.positionSubvoxels[2]), + static_cast(point.radiusSubvoxels)}); + chamberFloorProfile.push_back(static_cast( + point.positionSubvoxels[2]-point.radiusSubvoxels)); + } + const auto routePoints=[&](const ResolvedLayoutStratumV2& host) + { + std::vector> route; + for(const auto& anchor:host.anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + route.push_back({static_cast(anchor.values[0]), + static_cast(anchor.values[1]), + static_cast(anchor.values[2])}); + return route; + }; + if(upper.generatorKind=="TunnelNetwork" + ||upper.generatorKind=="SurfaceWorld") + { + auto route=routePoints(upper); + if(upper.generatorKind=="TunnelNetwork"&&route.size()>2) + route.erase(route.begin(),route.end()-2); + double routeHeight=0.0,distanceSquared=0.0; + if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route, + chamberFloorControls.front()[0],chamberFloorControls.front()[1], + routeHeight,distanceSquared) + &&distanceSquared<=std::pow(chamberFloorControls.front()[3] + +2.0*qPerMetre,2.0)) + VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute( + chamberFloorControls,chamberFloorProfile,0,route,qPerMetre, + upper.generatorKind=="SurfaceWorld"?0.98:1.13); + } + if(lower.generatorKind=="TunnelNetwork" + ||lower.generatorKind=="VerticalShafts") + { + auto route=routePoints(lower); + if(route.size()>2)route.resize(2); + double routeHeight=0.0,distanceSquared=0.0; + if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route, + chamberFloorControls[lowerMouthIndex][0], + chamberFloorControls[lowerMouthIndex][1], + routeHeight,distanceSquared) + &&distanceSquared<=std::pow( + chamberFloorControls[lowerMouthIndex][3] + +2.0*qPerMetre,2.0)) + VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute( + chamberFloorControls,chamberFloorProfile, + lowerMouthIndex,route,qPerMetre); + } + // Commit the endpoint route fit into h(s) before chamber records are + // resolved. The passage field and both landing planes then consume the + // same deterministic profile instead of blending the chamber in later. + for(std::size_t i=0;i( + std::llround(chamberFloorProfile[i])) + +passage.controlPoints[i].radiusSubvoxels; + const auto resolveLandingChamber=[&]( + ResolvedLayoutPassageLandingChamberV1& chamber, + const std::array& mouth,std::size_t endpointIndex, + std::size_t neighborIndex,const ResolvedLayoutStratumV2& host, + const char* side) + { + chamber.floorCenterSubvoxels=mouth; + chamber.radiusSubvoxels=mouthRadius+3*qPerMetre; + chamber.clearHeightSubvoxels=static_cast( + std::llround(4.5*qPerMetre)); + const auto& endpoint=passage.controlPoints[endpointIndex]; + const auto& neighbor=passage.controlPoints[neighborIndex]; + const std::int64_t endpointFloor=static_cast( + std::llround(chamberFloorProfile[endpointIndex])); + const std::int64_t neighborFloor=static_cast( + std::llround(chamberFloorProfile[neighborIndex])); + chamber.floorCenterSubvoxels[2]=endpointFloor; + const std::int64_t dx=endpoint.positionSubvoxels[0] + -neighbor.positionSubvoxels[0]; + const std::int64_t dy=endpoint.positionSubvoxels[1] + -neighbor.positionSubvoxels[1]; + const std::int64_t dz=endpointFloor-neighborFloor; + const std::int64_t horizontalSquared=dx*dx+dy*dy; + if(horizontalSquared<=0) + {error=passage.stableId+" "+side+" landing chamber has no endpoint tangent";return false;} + chamber.floorGradientQ16={ + PassageRoundRatio(dx<0?-dz:dz,std::abs(dx)*65536, + horizontalSquared), + PassageRoundRatio(dy<0?-dz:dz,std::abs(dy)*65536, + horizontalSquared)}; + if(host.generatorKind=="SurfaceWorld"&&side[0]=='u') + { + // The surface route enters the outer half of this chamber. Fit + // its landing plane to that receiving route, while keeping its + // center height equal to the passage's planned h(s) endpoint. + const auto surfaceRoute=routePoints(host); + const double radiusSquared=static_cast( + chamber.radiusSubvoxels)*chamber.radiusSubvoxels; + double farthestDistanceSquared=0.0; + std::array farthestPoint{}; + for(const auto& routePoint:surfaceRoute) + { + const double routeDx=routePoint[0]-mouth[0]; + const double routeDy=routePoint[1]-mouth[1]; + const double distanceSquared=routeDx*routeDx+routeDy*routeDy; + if(distanceSquared<=radiusSquared + &&distanceSquared>farthestDistanceSquared) + { + farthestDistanceSquared=distanceSquared; + farthestPoint=routePoint; + } + } + double mouthRouteHeight=0.0,mouthRouteDistanceSquared=0.0; + if(farthestDistanceSquared>0.0 + &&VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY( + surfaceRoute,static_cast(mouth[0]), + static_cast(mouth[1]),mouthRouteHeight, + mouthRouteDistanceSquared)) + { + const std::int64_t surfaceDx=static_cast( + std::llround(farthestPoint[0]-mouth[0])); + const std::int64_t surfaceDy=static_cast( + std::llround(farthestPoint[1]-mouth[1])); + const std::int64_t surfaceDz=static_cast( + std::llround(farthestPoint[2]-mouthRouteHeight)); + const std::int64_t surfaceHorizontalSquared=surfaceDx*surfaceDx + +surfaceDy*surfaceDy; + if(surfaceHorizontalSquared>0) + chamber.floorGradientQ16={ + PassageRoundRatio(surfaceDx<0?-surfaceDz:surfaceDz, + std::abs(surfaceDx)*65536, + surfaceHorizontalSquared), + PassageRoundRatio(surfaceDy<0?-surfaceDz:surfaceDz, + std::abs(surfaceDy)*65536, + surfaceHorizontalSquared)}; + } + } + const std::uint64_t gradientSquared=static_cast( + chamber.floorGradientQ16[0]*chamber.floorGradientQ16[0] + +chamber.floorGradientQ16[1]*chamber.floorGradientQ16[1]); + const std::int64_t gradientMagnitude=static_cast( + PassageIsqrt(gradientSquared)); + constexpr std::int64_t MaximumLandingGradeQ16=17560; + if(gradientMagnitude>MaximumLandingGradeQ16) + {error=passage.stableId+" "+side+" landing chamber floor exceeds 15 degrees";return false;} + const std::int64_t planeReachQ=PassageRoundRatio( + chamber.radiusSubvoxels,gradientMagnitude,65536); + const std::int64_t shellQ=static_cast( + std::llround(0.75*qPerMetre)); + const auto qToUm=[&](std::int64_t q) + {return q*static_cast(layout.coreVoxelSizeMicrometres) + /WorldAssemblySubvoxelsPerVoxel;}; + if(!host.content.presentInCurrentField + ||qToUm(chamber.floorCenterSubvoxels[2]-planeReachQ-shellQ) + host.content.topMicrometres) + { + error=passage.stableId+" "+side + +" landing chamber does not fit inside its host content band"; + return false; + } + return true; + }; + if(!resolveLandingChamber(passage.upperLandingChamber,upperMouth,0,1, + upper,"upper") + ||!resolveLandingChamber(passage.lowerLandingChamber,lowerMouth, + lowerMouthIndex,lowerMouthIndex-1,lower,"lower")) + return false; + passage.floorProfileIncludesLandingChambers=true; + std::printf("[PassageLandingChamberPlan] id=%s upper_radius_m=%.3f lower_radius_m=%.3f clear_height_m=4.500 upper_floor_grade_q16=(%lld,%lld) lower_floor_grade_q16=(%lld,%lld) status=PASS\n", + passage.stableId.c_str(), + passage.upperLandingChamber.radiusSubvoxels/static_cast(qPerMetre), + passage.lowerLandingChamber.radiusSubvoxels/static_cast(qPerMetre), + static_cast(passage.upperLandingChamber.floorGradientQ16[0]), + static_cast(passage.upperLandingChamber.floorGradientQ16[1]), + static_cast(passage.lowerLandingChamber.floorGradientQ16[0]), + static_cast(passage.lowerLandingChamber.floorGradientQ16[1])); layout.passages.push_back(std::move(passage)); } } @@ -3712,17 +5629,22 @@ ResolvedLayoutValidatorAuditV3 AuditResolvedLayoutValidatorV3() else result.failures.push_back({"negative_control_accepted",std::move(name)+" was accepted by strict validator"}); }; auto base=ResolvedLayoutSnapshotV3{};base.worldIdentity="synthetic";base.ownerVoxelSizeMicrometres=125000; - base.coreVoxelSizeMicrometres=250000;base.strates={make("a",0,10000000,0),make("b",1,-2000000,-12000000)}; + base.coreVoxelSizeMicrometres=250000;base.strates={make("a",0,30000000,0),make("b",1,-2000000,-40000000)}; ResolvedLayoutPassageConfigV1 passageConfig;passageConfig.upperStrateIndex=0;passageConfig.connections=1; passageConfig.mouthRadiusSubvoxels=1280;passageConfig.midRadiusSubvoxels=1280; - passageConfig.reachMinimumSubvoxels=2048;passageConfig.reachMaximumSubvoxels=2048; + passageConfig.reachMinimumSubvoxels=9216;passageConfig.reachMaximumSubvoxels=9216; passageConfig.distanceMinimumSubvoxels=1024;passageConfig.distanceMaximumSubvoxels=10240; std::string planError; if(!PlanResolvedLayoutPassagesV1(base,19,{passageConfig},planError)) result.failures.push_back({"synthetic_passage_plan_failed",planError}); {std::vector issues;std::string error; if(!base.validate(ResolvedLayoutValidationModeV3::strict,issues,error)) - result.failures.push_back({"positive_control_rejected","valid ceiling/content/bedrock layout was rejected"});} + { + for(const auto& issue:issues) + std::printf("[ResolvedLayoutValidatorPositiveControlIssue] code=%s detail=%s\n", + issue.code.c_str(),issue.detail.c_str()); + result.failures.push_back({"positive_control_rejected","valid ceiling/content/bedrock layout was rejected"}); + }} auto missingPassage=base;missingPassage.passages.clear();expectReject("missing interface passage",missingPassage); auto outsidePassage=base; if(!outsidePassage.passages.empty()) diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 768fb94..60fec55 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -308,6 +308,14 @@ public: stampIndices_=shape.BuildStampTileList(minimumSubvoxels_,maximumSubvoxels_); cavernFeatureIndices_=shape.BuildCavernFeatureTileList( minimumSubvoxels_,maximumSubvoxels_); + if(profileStats_) + for(const std::uint32_t index:cavernFeatureIndices_) + { + if(index>=shape.CavernRuntime.size())continue; + const auto& feature=shape.CavernRuntime[index]; + if(feature.room)++profileStats_->cavernRoomCandidates; + else if(feature.edge)++profileStats_->cavernEdgeCandidates; + } passageRoutes_.assign(shape.ResolvedLayout ?shape.ResolvedLayout->passages.size():0,0); passageRouteSegments_.resize(passageRoutes_.size()); @@ -324,10 +332,13 @@ public: candidate.maximumSubvoxels=bounds.maximumSubvoxels; candidate.sweepMinimumSubvoxels=bounds.minimumSubvoxels; candidate.sweepMaximumSubvoxels=bounds.maximumSubvoxels; + if(passageRoutes_[feature.passageIndex]==0) + routePassageIndices_.push_back(feature.passageIndex); passageRoutes_[feature.passageIndex]=1; passageRouteSegments_[feature.passageIndex].push_back(candidate); ++routeSegmentCandidateCount_; } + std::sort(routePassageIndices_.begin(),routePassageIndices_.end()); for(auto& candidates:passageRouteSegments_) { std::sort(candidates.begin(),candidates.end(),[](const auto& a,const auto& b) @@ -343,6 +354,7 @@ public: maximumSubvoxels_(source.maximumSubvoxels_),featureIndices_(source.featureIndices_), stampIndices_(source.stampIndices_),cavernFeatureIndices_(source.cavernFeatureIndices_), passageRoutes_(source.passageRoutes_), + routePassageIndices_(source.routePassageIndices_), passageRouteSegments_(source.passageRouteSegments_), previous_(ActivePassageTileQueries),profileStats_(source.profileStats_), profileLod_(source.profileLod_), @@ -366,7 +378,37 @@ public: bool hasSweptTubeBase() const noexcept{return passageSweepData_!=nullptr;} void copySweptTubeRuntimeCounts() const noexcept { - if(!ownsPassageSweepDiagnostics_||!profileStats_||!passageSweepData_)return; + if(!profileStats_||!passageSweepData_)return; + const auto merge=[&](std::atomic& target, + std::uint64_t value) + { + if(value)target.fetch_add(value,std::memory_order_relaxed); + }; + merge(passageSweepData_->lookups,localSweepLookups_); + merge(passageSweepData_->cacheHits,localSweepCacheHits_); + merge(passageSweepData_->baseInfluenceHits,localSweepBaseInfluenceHits_); + merge(passageSweepData_->outOfInfluenceHits,localSweepOutOfInfluenceHits_); + merge(passageSweepData_->missingSamples,localSweepMissingSamples_); + merge(passageSweepData_->offGridMisses,localSweepOffGridMisses_); + merge(passageSweepData_->unpreparedMisses,localSweepUnpreparedMisses_); + for(std::size_t caller=0;callermissingLookupsByCaller[caller], + localMissingLookupsByCaller_[caller]); + merge(passageSweepData_->exactRouteFallbacksByCaller[caller], + localExactRouteFallbacksByCaller_[caller]); + merge(passageSweepData_->featureBoundSkipsByCaller[caller], + localFeatureBoundSkipsByCaller_[caller]); + merge(passageSweepData_->tileFreeSkipsByCaller[caller], + localTileFreeSkipsByCaller_[caller]); + merge(passageSweepData_->verticalBoundSkipsByCaller[caller], + localVerticalBoundSkipsByCaller_[caller]); + merge(passageSweepData_->negativeReachSkipsByCaller[caller], + localNegativeReachSkipsByCaller_[caller]); + merge(passageSweepData_->positiveReachSkipsByCaller[caller], + localPositiveReachSkipsByCaller_[caller]); + } + if(!ownsPassageSweepDiagnostics_)return; profileStats_->passageSweepLookups=passageSweepData_->lookups.load( std::memory_order_relaxed); profileStats_->passageSweepCacheHits=passageSweepData_->cacheHits.load( @@ -412,8 +454,7 @@ public: const auto caller=static_cast( FStandaloneShapeWorld::CurrentPassageDensityQueryCaller()); if(callerexactRouteFallbacksByCaller[caller].fetch_add( - 1,std::memory_order_relaxed); + ++localExactRouteFallbacksByCaller_[caller]; } void recordFeatureBoundSkip() const noexcept { @@ -421,8 +462,7 @@ public: const auto caller=static_cast( FStandaloneShapeWorld::CurrentPassageDensityQueryCaller()); if(callerfeatureBoundSkipsByCaller[caller].fetch_add( - 1,std::memory_order_relaxed); + ++localFeatureBoundSkipsByCaller_[caller]; } enum class ESweepMissSkipReason : std::uint8_t {TileFree,VerticalBound,NegativeReach,PositiveReach}; @@ -432,19 +472,17 @@ public: const auto caller=static_cast( FStandaloneShapeWorld::CurrentPassageDensityQueryCaller()); if(caller>=PassageDensityQueryCallerCount)return; - std::atomic* counts=nullptr; switch(reason) { case ESweepMissSkipReason::TileFree: - counts=&passageSweepData_->tileFreeSkipsByCaller[caller];break; + ++localTileFreeSkipsByCaller_[caller];break; case ESweepMissSkipReason::VerticalBound: - counts=&passageSweepData_->verticalBoundSkipsByCaller[caller];break; + ++localVerticalBoundSkipsByCaller_[caller];break; case ESweepMissSkipReason::NegativeReach: - counts=&passageSweepData_->negativeReachSkipsByCaller[caller];break; + ++localNegativeReachSkipsByCaller_[caller];break; case ESweepMissSkipReason::PositiveReach: - counts=&passageSweepData_->positiveReachSkipsByCaller[caller];break; + ++localPositiveReachSkipsByCaller_[caller];break; } - if(counts)counts->fetch_add(1,std::memory_order_relaxed); } static const FScopedPassageTileQueries* CurrentFor( const FStandaloneShapeWorld& shape) @@ -454,6 +492,8 @@ public: } bool hasRouteFeature(std::size_t passageIndex) const {return passageIndex& routePassageIndices() const noexcept + {return routePassageIndices_;} bool routeFeatureMayAffect(std::size_t passageIndex,double qx,double qy, double qz) const { @@ -493,6 +533,7 @@ public: std::array minimumSubvoxels_,maximumSubvoxels_; std::vector featureIndices_,stampIndices_,cavernFeatureIndices_; std::vector passageRoutes_; + std::vector routePassageIndices_; std::vector> passageRouteSegments_; std::size_t routeSegmentCandidateCount_=0; bool mayContainResolvedBoundaryBand_=true,mayContainTransitionPassageFade_=true; @@ -502,6 +543,15 @@ public: double passageCarveReachMetres_=0.0; std::shared_ptr passageSweepData_; bool ownsPassageSweepDiagnostics_=true; + mutable std::uint64_t localSweepLookups_=0,localSweepCacheHits_=0, + localSweepBaseInfluenceHits_=0,localSweepOutOfInfluenceHits_=0, + localSweepMissingSamples_=0,localSweepOffGridMisses_=0, + localSweepUnpreparedMisses_=0; + mutable std::array + localMissingLookupsByCaller_{},localExactRouteFallbacksByCaller_{}, + localFeatureBoundSkipsByCaller_{},localTileFreeSkipsByCaller_{}, + localVerticalBoundSkipsByCaller_{},localNegativeReachSkipsByCaller_{}, + localPositiveReachSkipsByCaller_{}; }; double GetCoreVoxelSizeCm() const @@ -1190,6 +1240,21 @@ public: VoxelForgeCore::Linux77Profiler::EPhase::Passage); if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X) ||!std::isfinite(Y)||!std::isfinite(Z))return density; + const auto* passageTileQueries=ActivePassageTileQueries; + const bool passageTileScopeMatches=passageTileQueries + &&passageTileQueries->matches(*this); + auto* passageProfile=passageTileScopeMatches + ?passageTileQueries->profileStats():nullptr; + if(passageProfile)++passageProfile->passageComposerQueries; + if(passageProfile&&nativeTunnelFieldDensity + !=std::numeric_limits::lowest()) + ++passageProfile->nativeCaveFieldSamples; + // The tile feature list is a conservative broad phase. If no passage route + // can affect this tile, none of the per-sample mouth, shell, or relief work + // below can change the field. + if(passageTileScopeMatches + &&passageTileQueries->routePassageIndices().empty())return density; + constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); constexpr double FloorShellThicknessMetres=VF_PassageShellThicknessMetres; constexpr double WallReliefMetres=VF_PassageWallReliefMetres; @@ -1200,17 +1265,11 @@ public: const double qx=static_cast(X)*Q,qy=static_cast(Y)*Q, qz=static_cast(Z)*Q; const double shellThickness=FloorShellThicknessMetres*qPerMetre; - const auto* passageTileQueries=ActivePassageTileQueries; - const bool passageTileScopeMatches=passageTileQueries - &&passageTileQueries->matches(*this); - auto* passageProfile=passageTileScopeMatches - ?passageTileQueries->profileStats():nullptr; - if(passageProfile)++passageProfile->passageComposerQueries; - struct FNearestTubeSample { double signedDistance=std::numeric_limits::infinity(); double shellSignedDistance=std::numeric_limits::infinity(); + double floorShellSignedDistance=std::numeric_limits::infinity(); double roundSignedDistance=std::numeric_limits::infinity(); double centerZ=0.0,floorCenterZ=0.0,radius=0.0,floorDepth=0.0; double t=0.0,floorT=0.0; @@ -1220,22 +1279,96 @@ public: }; double unionDistance=std::numeric_limits::infinity(); double unionShellDistance=std::numeric_limits::infinity(); + double unionFloorShellDistance=std::numeric_limits::infinity(); + double unionLandingChamberFloorShellDistance= + std::numeric_limits::infinity(); + double plannedTunnelPassageShellDistance=std::numeric_limits::infinity(); const std::uint32_t reliefSeed=static_cast(Owner?Owner->Seed:0); const auto* queryStratum=FindBoundStratumForManagerZ(Z); - for(std::size_t passageIndex=0;passageIndexpassages.size(); - ++passageIndex) + const bool plannedTunnelStratum=queryStratum&&queryStratum->Resolved + &&(queryStratum->Resolved->generatorKind=="TunnelNetwork" + ||queryStratum->Resolved->generatorKind=="VerticalShafts") + &&queryStratum->Resolved->recipeId=="Core.Caves.PrimitiveField" + &&queryStratum->Resolved->recipeVersion==1; + const auto* routePassageIndices=passageTileScopeMatches + ?&passageTileQueries->routePassageIndices():nullptr; + const std::size_t routeCount=passageTileScopeMatches + ?routePassageIndices->size():ResolvedLayout->passages.size(); + for(std::size_t routeOffset=0;routeOffsetpassages[passageIndex]; if(passage.controlPoints.size()<2)continue; - if(queryStratum&&queryStratum->Resolved + const bool surfaceDepartureLanding=queryStratum&&queryStratum->Resolved + &&queryStratum->Resolved->lowerInterfaceCapabilities.surfaceToTunnelLandingSupport + &&passage.upperStrateIndex==queryStratum->StrateIndex; + const auto floorThicknessWithLanding=[&](double floorThickness, + double smoothFloorBlend,double alongMetres) + { + if(!surfaceDepartureLanding)return floorThickness; + const double fade=std::clamp(1.0-alongMetres/8.0,0.0,1.0); + const double smoothFade=fade*fade*(3.0-2.0*fade); + const double landingThickness=0.50*qPerMetre + *smoothFloorBlend*smoothFade; + return std::max(floorThickness,landingThickness); + }; + const bool passageTargetsNativeCaveField=queryStratum&&queryStratum->Resolved &&queryStratum->Resolved->passageCapabilities.hasNativeCaveField - &&passage.lowerStrateIndex==queryStratum->StrateIndex) - continue; + &&passage.lowerStrateIndex==queryStratum->StrateIndex; + const bool passageTargetsPlannedTunnelNetwork=plannedTunnelStratum; + const double mouthShellBlendMetres=passageTargetsPlannedTunnelNetwork + ?2.0:MouthShellBlendMetres; + // Legacy native cave fields own the tunnel bore on their lower-strate + // boundary. The shared passage composer still owns its landing chamber. if(passageProfile)++passageProfile->passageIterations; - if(passageTileScopeMatches&&!passageTileQueries->hasRouteFeature(passageIndex)) - continue; const auto& constants=PassageComposerConstants[passageIndex]; + const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1* landingChamber=nullptr; + if(queryStratum&&queryStratum->Resolved) + { + if(queryStratum->StrateIndex==passage.upperStrateIndex) + landingChamber=&passage.upperLandingChamber; + else if(queryStratum->StrateIndex==passage.lowerStrateIndex) + landingChamber=&passage.lowerLandingChamber; + } + if(landingChamber&&landingChamber->radiusSubvoxels>0 + &&landingChamber->clearHeightSubvoxels>0) + { + const double centerX=static_cast( + landingChamber->floorCenterSubvoxels[0]); + const double centerY=static_cast( + landingChamber->floorCenterSubvoxels[1]); + const double floorCenterZ=static_cast( + landingChamber->floorCenterSubvoxels[2]) + +(landingChamber->floorGradientQ16[0]*(qx-centerX) + +landingChamber->floorGradientQ16[1]*(qy-centerY))/65536.0; + const double radialDistance=DeterministicSqrt( + (qx-centerX)*(qx-centerX)+(qy-centerY)*(qy-centerY)) + -landingChamber->radiusSubvoxels; + const double floorDistance=floorCenterZ-qz; + const double ceilingDistance=qz-(floorCenterZ + +landingChamber->clearHeightSubvoxels); + const double chamberDistance=std::max({radialDistance, + floorDistance,ceilingDistance}); + const double chamberShellDistance=std::max(-chamberDistance, + chamberDistance-shellThickness); + const double chamberFloorShellDistance=std::max( + std::max(-floorDistance,floorDistance-shellThickness), + radialDistance); + unionDistance=std::min(unionDistance,chamberDistance); + unionShellDistance=std::min(unionShellDistance,chamberShellDistance); + unionFloorShellDistance=std::min(unionFloorShellDistance, + chamberFloorShellDistance); + unionLandingChamberFloorShellDistance=std::min( + unionLandingChamberFloorShellDistance,chamberFloorShellDistance); + if(passageTargetsPlannedTunnelNetwork) + plannedTunnelPassageShellDistance=std::min( + plannedTunnelPassageShellDistance,chamberShellDistance); + } + if(passageTargetsNativeCaveField&&!passageTargetsPlannedTunnelNetwork) + continue; + FNearestTubeSample nearest; const bool usePreparedSweep=passageTileScopeMatches &&passageTileQueries->hasSweptTubeBase(); @@ -1256,6 +1389,20 @@ public: nearest.floorT=swept->floorT; nearest.valid=true; nearest.swept=swept; + if(!swept->hasExactContribution) + { + // The prepared sentinel is farther from the route than the + // maximum body, relief, shell, and blend support. Preserve its + // exact flat distance in the passage union; its wall and floor + // shell bounds are positive, so the rest of the composer has + // no effect for this passage sample. + const double outsideDistance=PassageFlatBottomDistance( + static_cast(swept->roundSignedDistance), + swept->floorCenterZSubvoxels,qz, + swept->floorDepthSubvoxels,qPerMetre); + unionDistance=std::min(unionDistance,outsideDistance); + continue; + } } else { @@ -1381,7 +1528,7 @@ public: const double mouthDistanceMetres=std::max(0.0,std::min(alongMetres, constants.totalPathLengthMetres-alongMetres)); const double shellFade=std::clamp(mouthDistanceMetres/ - MouthShellBlendMetres,0.0,1.0); + mouthShellBlendMetres,0.0,1.0); const double smoothShellFade=shellFade*shellFade *(3.0-2.0*shellFade); const double floorDistanceMetres=(floorCenterZ-floorDepth-qz) @@ -1399,12 +1546,16 @@ public: const double pastMouthMetres=std::max(0.0,alongMetres -constants.totalPathLengthMetres); const double extensionFade=std::clamp(1.0 - -pastMouthMetres/MouthShellBlendMetres,0.0,1.0); + -pastMouthMetres/mouthShellBlendMetres,0.0,1.0); floorExtensionFade=extensionFade*extensionFade *(3.0-2.0*extensionFade); } - const double floorShellThickness=smoothFloorBlend*shellThickness - *floorExtensionFade*smoothShellFade; + const double floorShellThickness=floorThicknessWithLanding( + smoothFloorBlend*(shellThickness + +(passageTargetsPlannedTunnelNetwork?0.25*qPerMetre:0.0)) + *floorExtensionFade + *smoothShellFade, + smoothFloorBlend,alongMetres); const double wallShellDistance=PassageRoundedWallShellDistance( flatDistance,wallElevationMetres*qPerMetre, OpenAirSideWallHeightMetres*qPerMetre,mouthWallThickness); @@ -1412,8 +1563,14 @@ public: static_cast(flatDistance)-floorShellThickness); const double localShellDistance=std::min(wallShellDistance, floorShellDistance); + if(passageTargetsPlannedTunnelNetwork + &&mouthDistanceMetres>=mouthShellBlendMetres) + plannedTunnelPassageShellDistance=std::min( + plannedTunnelPassageShellDistance,localShellDistance); nearest.shellSignedDistance=std::min(nearest.shellSignedDistance, localShellDistance); + nearest.floorShellSignedDistance=std::min( + nearest.floorShellSignedDistance,floorShellDistance); }; double shellFloorSearchBoundSq=-1.0; VisitNearestPassageSegmentRange(passageIndex,0,lastIndex-1, @@ -1430,6 +1587,14 @@ public: const double crestFade=std::clamp((OpenAirSideWallHeightMetres -wallElevationMetres)/0.25,0.0,1.0); const double smoothCrestFade=crestFade*crestFade*(3.0-2.0*crestFade); + const auto& segment=PassageSegmentData[passageIndex][nearest.segmentIndex]; + const double alongMetres=segment.authoredAlongMetres + +nearest.t*segment.authoredLengthMetres; + const double mouthDistanceMetres=std::max(0.0,std::min(alongMetres, + constants.totalPathLengthMetres-alongMetres)); + const double shellFade=std::clamp(mouthDistanceMetres/ + mouthShellBlendMetres,0.0,1.0); + const double smoothShellFade=shellFade*shellFade*(3.0-2.0*shellFade); const double wallReliefWeight=(0.05+0.95*(1.0-verticalWallAlignment)) *smoothCrestFade; float relief=0.0f; @@ -1448,15 +1613,7 @@ public: const double shapedDistance=PassageFlatBottomDistance( static_cast(roughRoundDistance),nearest.floorCenterZ,qz, nearest.floorDepth,qPerMetre); - const auto& segment=PassageSegmentData[passageIndex][nearest.segmentIndex]; const auto& floorSegment=PassageSegmentData[passageIndex][nearest.floorSegmentIndex]; - const double alongMetres=segment.authoredAlongMetres - +nearest.t*segment.authoredLengthMetres; - const double mouthDistanceMetres=std::max(0.0,std::min(alongMetres, - constants.totalPathLengthMetres-alongMetres)); - const double shellFade=std::clamp(mouthDistanceMetres/ - MouthShellBlendMetres,0.0,1.0); - const double smoothShellFade=shellFade*shellFade*(3.0-2.0*shellFade); // Ease both wall and floor shell into the host ground over five metres. // The floor shell remains below the floor plane throughout that blend. const double floorDistanceMetres=(nearest.floorCenterZ @@ -1474,18 +1631,26 @@ public: const double pastMouthMetres=std::max(0.0,floorAlongMetres -constants.totalPathLengthMetres); const double extensionFade=std::clamp(1.0 - -pastMouthMetres/MouthShellBlendMetres,0.0,1.0); + -pastMouthMetres/mouthShellBlendMetres,0.0,1.0); floorExtensionFade=extensionFade*extensionFade *(3.0-2.0*extensionFade); } - const double floorShellThickness=smoothFloorBlend*shellThickness - *floorExtensionFade*smoothShellFade; + const double floorShellThickness=floorThicknessWithLanding( + smoothFloorBlend*(shellThickness + +(passageTargetsPlannedTunnelNetwork?0.25*qPerMetre:0.0)) + *floorExtensionFade + *smoothShellFade, + smoothFloorBlend,alongMetres); const double wallShellDistance=PassageRoundedWallShellDistance( shapedDistance,wallElevationMetres*qPerMetre, OpenAirSideWallHeightMetres*qPerMetre,mouthWallThickness); const double floorShellDistance=std::max(-static_cast(shapedDistance), static_cast(shapedDistance)-floorShellThickness); + nearest.floorShellSignedDistance=std::min( + nearest.floorShellSignedDistance,floorShellDistance); unionDistance=std::min(unionDistance,static_cast(shapedDistance)); + unionFloorShellDistance=std::min(unionFloorShellDistance, + nearest.floorShellSignedDistance); const double localShellDistance=std::min(wallShellDistance, floorShellDistance); unionShellDistance=std::min(unionShellDistance, @@ -1494,12 +1659,232 @@ public: if(!std::isfinite(unionDistance))return density; const float passageAirDistance=static_cast(-unionDistance/Q); + // Keep the landing floor shell where it is exposed to the chamber. At + // the tunnel joint, the passage void must be able to open through that + // floor shell so its own continuous floor and shell define the throat. + if(unionLandingChamberFloorShellDistance<0.0&&unionDistance>0.0) + return static_cast(unionLandingChamberFloorShellDistance/Q); + const auto plannedPortalWalkCorridorContains=[&]() + { + if(!queryStratum||!queryStratum->Resolved + ||(queryStratum->Resolved->generatorKind!="TunnelNetwork" + &&queryStratum->Resolved->generatorKind!="VerticalShafts") + ||queryStratum->Resolved->recipeId!="Core.Caves.PrimitiveField" + ||queryStratum->Resolved->recipeVersion!=1)return false; + const bool terminalShaftRoute= + queryStratum->Resolved->generatorKind=="VerticalShafts"; + const VoxelForgeCore::ResolvedLayoutAnchorV2* first=nullptr; + const VoxelForgeCore::ResolvedLayoutAnchorV2* second=nullptr; + const VoxelForgeCore::ResolvedLayoutAnchorV2* third=nullptr; + const VoxelForgeCore::ResolvedLayoutAnchorV2* lastThree=nullptr; + const VoxelForgeCore::ResolvedLayoutAnchorV2* lastTwo=nullptr; + const VoxelForgeCore::ResolvedLayoutAnchorV2* last=nullptr; + std::size_t routeCount=0; + for(const auto& anchor:queryStratum->Resolved->anchors) + { + if(anchor.kind!="route_point"||anchor.values.size()<3)continue; + if(routeCount==0)first=&anchor; + else if(routeCount==1)second=&anchor; + else if(routeCount==2)third=&anchor; + lastThree=lastTwo;lastTwo=last;last=&anchor; + ++routeCount; + } + if(routeCount<6||!first||!second||!third||!lastThree||!lastTwo||!last + ||!first->stableId.ends_with("000-arrival") + ||!second->stableId.ends_with("000-edge-start"))return false; + if(terminalShaftRoute) + { + if(!last->stableId.ends_with("999-basal-terminal"))return false; + } + else if(!lastTwo->stableId.ends_with("998-departure-inner") + ||!last->stableId.ends_with("999-departure"))return false; + const double metresPerSubvoxel=1.0/qPerMetre; + const auto position=[&](const VoxelForgeCore::ResolvedLayoutAnchorV2& anchor) + { + return std::array{anchor.values[0]*metresPerSubvoxel, + anchor.values[1]*metresPerSubvoxel, + anchor.values[2]*metresPerSubvoxel}; + }; + const double queryX=X*Q/qPerMetre; + const double queryY=Y*Q/qPerMetre; + const double queryZ=Z*Q/qPerMetre; + const auto nearRouteSegment=[&](const VoxelForgeCore::ResolvedLayoutAnchorV2& start, + const VoxelForgeCore::ResolvedLayoutAnchorV2& end,double limit,bool fromDeparture, + double distanceToDepartureAfterEnd) + { + const auto a=position(start),b=position(end); + const double dx=b[0]-a[0],dy=b[1]-a[1]; + const double horizontalLength=std::hypot(dx,dy); + if(horizontalLength<=1.0e-9)return false; + const double t=std::clamp(((queryX-a[0])*dx + +(queryY-a[1])*dy)/(horizontalLength*horizontalLength),0.0,1.0); + const double along=fromDeparture + ?distanceToDepartureAfterEnd+(1.0-t)*horizontalLength + :distanceToDepartureAfterEnd+t*horizontalLength; + if(along>limit)return false; + const double routeX=a[0]+t*dx,routeY=a[1]+t*dy; + if(std::hypot(queryX-routeX,queryY-routeY)>0.80)return false; + const double routeFloor=std::lerp(a[2],b[2],t)-1.13; + return queryZ>=routeFloor-0.30&&queryZ<=routeFloor+2.25; + }; + // The connected layer route begins inside the landing chamber. Let + // its air open the chamber's plain side shell through the chamber + // radius plus the shell thickness; the condition below remains + // limited to samples where the tunnel/chamber shell is present. + constexpr double PortalWalkLeadMetres=9.0; + const auto arrival=position(*first),arrivalInner=position(*second); + const double arrivalInnerDistance=std::hypot( + arrivalInner[0]-arrival[0],arrivalInner[1]-arrival[1]); + if(nearRouteSegment(*first,*second,PortalWalkLeadMetres,false,0.0) + ||nearRouteSegment(*second,*third,PortalWalkLeadMetres,false, + arrivalInnerDistance))return true; + if(terminalShaftRoute)return false; + const auto departureInner=position(*lastTwo),departure=position(*last); + const double departureTailDistance=std::hypot( + departure[0]-departureInner[0],departure[1]-departureInner[1]); + return nearRouteSegment(*lastThree,*lastTwo,PortalWalkLeadMetres,true, + departureTailDistance) + ||nearRouteSegment(*lastTwo,*last,PortalWalkLeadMetres,true,0.0); + }; + const auto connectedSurfaceLandingShellContains=[&]() + { + if(!queryStratum||!queryStratum->Resolved + ||queryStratum->Resolved->generatorKind!="SurfaceWorld")return false; + const double queryX=qx/qPerMetre,queryY=qy/qPerMetre,queryZ=qz/qPerMetre; + constexpr double ChamberShellThicknessMetres=0.75; + constexpr double RouteClearanceMetres=1.25; + constexpr double RouteHeightClearanceMetres=1.5; + constexpr double PortalWalkLeadMetres=9.0; + for(const auto& passage:ResolvedLayout->passages) + { + if(passage.upperStrateIndex!=queryStratum->StrateIndex)continue; + const auto& chamber=passage.upperLandingChamber; + if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0)continue; + const double cx=static_cast(chamber.floorCenterSubvoxels[0])/qPerMetre; + const double cy=static_cast(chamber.floorCenterSubvoxels[1])/qPerMetre; + const double floorZ=static_cast(chamber.floorCenterSubvoxels[2])/qPerMetre + +chamber.floorGradientQ16[0]/65536.0*(queryX-cx) + +chamber.floorGradientQ16[1]/65536.0*(queryY-cy); + const double radialDistance=std::hypot(queryX-cx,queryY-cy) + -static_cast(chamber.radiusSubvoxels)/qPerMetre; + const double floorDistance=floorZ-queryZ; + const double ceilingDistance=queryZ-(floorZ + +static_cast(chamber.clearHeightSubvoxels)/qPerMetre); + const double chamberDistance=std::max({radialDistance, + floorDistance,ceilingDistance}); + const double chamberShellDistance=std::max(-chamberDistance, + chamberDistance-ChamberShellThicknessMetres); + if(chamberDistance<=0.0||chamberShellDistance>=0.0)continue; + + std::vector route; + for(const auto& anchor:queryStratum->Resolved->anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + route.push_back(&anchor); + if(route.size()<2)continue; + const double mouthZ=static_cast(passage.upperMouthSubvoxels[2]) + /qPerMetre; + std::size_t endpoint=route.size(); + double endpointDistanceSquared=1.0; + for(std::size_t index=0;indexvalues[0]/qPerMetre; + const double y=route[index]->values[1]/qPerMetre; + const double z=route[index]->values[2]/qPerMetre; + const double dx=x-cx,dy=y-cy; + const double distanceSquared=dx*dx+dy*dy; + if(distanceSquared routeDistance(route.size(),0.0); + for(std::size_t index=1;indexvalues[0]-route[index-1]->values[0]) + /qPerMetre; + const double dy=(route[index]->values[1]-route[index-1]->values[1]) + /qPerMetre; + routeDistance[index]=routeDistance[index-1]+std::hypot(dx,dy); + } + for(std::size_t index=1;indexPortalWalkLeadMetres)continue; + const double routeX=ax+t*dx,routeY=ay+t*dy; + const double routeZ=az+t*(b.values[2]-a.values[2])/qPerMetre; + if(std::hypot(queryX-routeX,queryY-routeY)<=RouteClearanceMetres + &&std::abs(queryZ-routeZ)<=RouteHeightClearanceMetres)return true; + } + } + return false; + }; + if(plannedTunnelStratum + &&nativeTunnelFieldDensity!=std::numeric_limits::lowest() + &&nativeTunnelFieldDensity<0.0f) + { + const double metresPerVoxel=Q/qPerMetre; + const double queryZMetres=Z*metresPerVoxel; + const double contentBottomMetres=static_cast( + queryStratum->Resolved->content.bottomMicrometres)/1000000.0; + const double contentTopMetres=static_cast( + queryStratum->Resolved->content.topMicrometres)/1000000.0; + const double boundaryDistance=std::min( + std::abs(queryZMetres-contentBottomMetres), + std::abs(queryZMetres-contentTopMetres)); + const double boundaryJoinBand=std::max(2.0, + static_cast(queryStratum->Resolved->transitionWidthMicrometres) + /2000000.0); + if(boundaryDistance<=boundaryJoinBand + &&std::abs(static_cast(passageAirDistance)*metresPerVoxel) + <=FloorShellThicknessMetres) + // A neighboring stratum can be open at a boundary sample even + // though the native graph owns solid host rock there. Keep the + // authored passage signed distance through that transition band; + // otherwise the neighboring field can widen its wall by a whole + // shell thickness. Native graph air still wins above via the + // positive nativeTunnelFieldDensity path. + return passageAirDistance; + } // Air is the union of the native graph and passage voids. Keeping the // signed passage distance outside the void as well gives marching cubes // a real negative distance through the floor instead of the terrain's // saturated deep-rock value. + if(nativeTunnelFieldDensity!=std::numeric_limits::lowest() + &&plannedTunnelPassageShellDistance<0.0&&unionDistance>0.0) + { + if(nativeTunnelFieldDensity>=0.0f&&plannedPortalWalkCorridorContains()) + return std::max(nativeTunnelFieldDensity,passageAirDistance); + return std::min(density, + static_cast(plannedTunnelPassageShellDistance/Q)); + } if(nativeTunnelFieldDensity>=0.0f) + { + // The planner's passage shell remains authoritative through native + // cave voids after the explicit mouth fade has ended. Keep this scoped + // to the planner-crossing passage shell so nearby portal joints stay open. + if(plannedTunnelPassageShellDistance<0.0&&unionDistance>0.0) + return std::min(nativeTunnelFieldDensity, + static_cast(plannedTunnelPassageShellDistance/Q)); + constexpr float NativeCaveAirMarginVoxels=0.75f; + if(unionDistance>0.0 + &&nativeTunnelFieldDensity(unionFloorShellDistance/Q)); return std::max(nativeTunnelFieldDensity,passageAirDistance); + } if(nativeFloorSupport!=std::numeric_limits::lowest() &&nativeFloorSupport<0.0f) return std::min(nativeFloorSupport,unionDistance<=0.0 @@ -1507,20 +1892,24 @@ public: if(nativeTunnelFieldDensity!=std::numeric_limits::lowest()) return std::max(density,passageAirDistance); if(unionDistance>0.0&&unionShellDistance<0.0&&density>=0.0f) + { + if(connectedSurfaceLandingShellContains()) + return std::max(density,passageAirDistance); return static_cast(unionShellDistance/Q); + } return std::max(density,passageAirDistance); } float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const { float graphDensity=0.0f; - float nativeLatticeFloorSupport=std::numeric_limits::lowest(); + float nativeGraphFloorSupport=std::numeric_limits::lowest(); { VoxelForgeCore::Linux77Profiler::FScopedPhase l77GraphPhase( VoxelForgeCore::Linux77Profiler::EPhase::CompositionGraph); FScopedLegacyFieldTermTimer TermTimer(ELegacyFieldTerm::ComposerCavern); graphDensity=ApplyResolvedCavernGraph(density,X,Y,Z, - &nativeLatticeFloorSupport); + &nativeGraphFloorSupport); } const auto* bound=FindBoundStratumForManagerZ(Z); const bool liminalRouteComposition=bound&&bound->Resolved @@ -1578,7 +1967,7 @@ public: const float nativeTunnelFieldDensity=nativeTunnelField ?graphDensity:std::numeric_limits::lowest(); stamped=ApplyResolvedStratePassageCarves(stamped,X,Y,Z, - nativeTunnelFieldDensity,nativeLatticeFloorSupport); + nativeTunnelFieldDensity,nativeGraphFloorSupport); } if(bTraceTnComposition) std::printf("[L75TNCompositionTrace] input=%.9g graph=%.9g boundary=%.9g passage=%.9g stamp=%.9g native=1\n", @@ -1654,6 +2043,26 @@ public: } if(overlaps(low,high))return true; } + const auto chamberOverlaps=[&]( + const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) + { + const double cx=static_cast(chamber.floorCenterSubvoxels[0])/Q; + const double cy=static_cast(chamber.floorCenterSubvoxels[1])/Q; + const double cz=static_cast(chamber.floorCenterSubvoxels[2])/Q; + const double radius=static_cast(chamber.radiusSubvoxels)/Q; + const double floorExtent=radius*std::hypot( + static_cast(chamber.floorGradientQ16[0])/65536.0, + static_cast(chamber.floorGradientQ16[1])/65536.0); + const double extent=reachCoreVoxels+0.01; + const double low[3]={cx-radius-extent,cy-radius-extent, + cz-floorExtent-extent}; + const double high[3]={cx+radius+extent,cy+radius+extent, + cz+floorExtent+static_cast( + chamber.clearHeightSubvoxels)/Q+extent}; + return overlaps(low,high); + }; + if(chamberOverlaps(passage.upperLandingChamber) + ||chamberOverlaps(passage.lowerLandingChamber))return true; } return false; } @@ -2275,6 +2684,10 @@ public: SurfaceDistanceQ=std::make_unique_for_overwrite(static_cast(NodeCount)); Substage=Clock::now(); BuildPackedRockIntervals(Stats.RockIntervalLines); + const auto criticalAirInitialStart=Clock::now(); + const uint64 criticalAirVoxelsBeforeDetail=ApplyCriticalAirPackedSigns(); + const double criticalAirInitialSeconds= + std::chrono::duration(Clock::now()-criticalAirInitialStart).count(); Stats.PackedSignBuildSeconds=std::chrono::duration(Clock::now()-Substage).count(); // Native density queries for non-heightfield strates use the exact interval // evaluator. Build those native intervals before preparing the tunnel sweeps. @@ -2297,8 +2710,16 @@ public: ApplyDetailBand(Stats.DetailBandLines); Stats.DetailApplySeconds=std::chrono::duration(Clock::now()-Substage).count(); Substage=Clock::now(); + const uint64 criticalAirVoxelsAfterDetail=ApplyCriticalAirPackedSigns(); + const double criticalAirRepairSeconds= + std::chrono::duration(Clock::now()-Substage).count(); + Substage=Clock::now(); Stats.SurfaceProxyNodes+=CompleteSignEdgeDistances(); Stats.SignEdgeDistanceSeconds=std::chrono::duration(Clock::now()-Substage).count(); + Substage=Clock::now(); + const uint64 criticalAirVoxelsAfterSignRepair=ApplyCriticalAirPackedSigns(); + const double criticalAirSignEdgeRepairSeconds= + std::chrono::duration(Clock::now()-Substage).count(); Stats.SurfaceSeconds=std::chrono::duration(Clock::now()-Stage).count(); // Route validation is world-wide. Detail regions reuse the same authored route data and // do not need to repeat the 5,642-point world audit for every entering tile group. @@ -2330,8 +2751,13 @@ public: Stats.CandidateMergeLines.WallSeconds*1000.0,Stats.CandidateMergeLines.WorkerSeconds*1000.0, Stats.CandidateMergeSeconds*1000.0,Stats.DetailBandLines.WallSeconds*1000.0, Stats.DetailBandLines.WorkerSeconds*1000.0); - std::printf("[ShapeSubstage] detail_apply=%.3fms sign_edge_distances=%.3fms passage_validation=%.3fms shared_field_primitives=%.3fms far_parents=%.3fms\n", - Stats.DetailApplySeconds*1000.0,Stats.SignEdgeDistanceSeconds*1000.0, + std::printf("[ShapeSubstage] detail_apply=%.3fms critical_air_sign_repair=%.3fms critical_air_voxels=%llu/%llu/%llu sign_edge_distances=%.3fms passage_validation=%.3fms shared_field_primitives=%.3fms far_parents=%.3fms\n", + Stats.DetailApplySeconds*1000.0,criticalAirInitialSeconds*1000.0 + +criticalAirRepairSeconds*1000.0+criticalAirSignEdgeRepairSeconds*1000.0, + static_cast(criticalAirVoxelsBeforeDetail), + static_cast(criticalAirVoxelsAfterDetail), + static_cast(criticalAirVoxelsAfterSignRepair), + Stats.SignEdgeDistanceSeconds*1000.0, Stats.PassageValidationSeconds*1000.0,Stats.SharedFieldSeconds*1000.0, Stats.FarShapeSeconds*1000.0); std::printf("[ShapeBreakdown] owner_shapes=%.3fms owner_shape_worker=%.3fms owner_bins=%.3fms packed_signs=%.3fms passage_prep=%.3fms candidate_sweeps=%.3fms visual_detail_prep=%.3fms detail_band=%.3fms sign_edge_repair=%.3fms route_validation=%.3fms far_parent_shapes=%.3fms far_shared_primitives=%.3fms far_bins=%.3fms\n", @@ -3403,12 +3829,24 @@ private: { double floorDepthMetres=0.0,totalPathLengthMetres=0.0; double authoredTotalPathLengthMetres=0.0; + double upperMouthPlayerCenterHeightMetres=1.13; std::size_t routeProfileSegmentCount=0; double upperMouthX=0.0,upperMouthY=0.0; double upperMouthPlannedFloor=0.0; double upperMouthTangentX=0.0,upperMouthTangentY=0.0; std::vector> upperMouthReceivingRoute; + double lowerMouthX=0.0,lowerMouthY=0.0; + double lowerMouthPlannedFloor=0.0; + double lowerMouthTangentX=0.0,lowerMouthTangentY=0.0; + std::vector> lowerMouthReceivingRoute; + std::array upperLandingFloorCenterSubvoxels{}; + std::array upperLandingFloorGradient{}; + double upperLandingRadiusSubvoxels=0.0; + std::array lowerLandingFloorCenterSubvoxels{}; + std::array lowerLandingFloorGradient{}; + double lowerLandingRadiusSubvoxels=0.0; std::size_t routeEndControlIndex=0; + bool floorProfileIncludesLandingChambers=false; }; struct FPassageSegmentData { @@ -3432,6 +3870,49 @@ private: std::max(0.0,radiusSubvoxels-0.23*qPerMetre)); } + static double BlendPassageFloorToLandingChambers( + const FPassageComposerConstants& constants, + const FPassageSegmentData& segment,double floorT, + double queryXSubvoxels,double queryYSubvoxels,double plannedFloor, + double qPerMetre) + { + if(qPerMetre<=0.0||!(constants.totalPathLengthMetres>0.0)) + return plannedFloor; + const double alongMetres=segment.authoredAlongMetres + +floorT*segment.authoredLengthMetres; + constexpr double LandingFloorBlendMetres=12.0; + const auto floorWeight=[&](double distanceMetres,double radiusSubvoxels) + { + if(!(radiusSubvoxels>0.0))return 0.0; + const double radiusMetres=radiusSubvoxels/qPerMetre; + if(distanceMetres<=radiusMetres)return 1.0; + if(distanceMetres>=radiusMetres+LandingFloorBlendMetres)return 0.0; + const double t=(distanceMetres-radiusMetres)/LandingFloorBlendMetres; + return 1.0-t*t*(3.0-2.0*t); + }; + const double upperWeight=floorWeight(std::max(0.0,alongMetres), + constants.upperLandingRadiusSubvoxels); + const double lowerWeight=floorWeight(std::max(0.0, + constants.totalPathLengthMetres-alongMetres), + constants.lowerLandingRadiusSubvoxels); + if(upperWeight<=0.0&&lowerWeight<=0.0)return plannedFloor; + const auto floorAt=[&](const std::array& center, + const std::array& gradient) + { + return center[2]+gradient[0]*(queryXSubvoxels-center[0]) + +gradient[1]*(queryYSubvoxels-center[1]); + }; + const double upperFloor=floorAt(constants.upperLandingFloorCenterSubvoxels, + constants.upperLandingFloorGradient); + const double lowerFloor=floorAt(constants.lowerLandingFloorCenterSubvoxels, + constants.lowerLandingFloorGradient); + const double totalWeight=upperWeight+lowerWeight; + if(totalWeight<=1.0) + return plannedFloor*(1.0-totalWeight) + +upperFloor*upperWeight+lowerFloor*lowerWeight; + return (upperFloor*upperWeight+lowerFloor*lowerWeight)/totalWeight; + } + static double PassageFlatFloorCenterZSubvoxels( const FPassageComposerConstants& constants, const std::vector& segments, @@ -3467,14 +3948,26 @@ private: +profileT*profileSegment.authoredFloorZDelta; } } - const double mouthBlendFloor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY( + if(constants.floorProfileIncludesLandingChambers) + return plannedFloor+floorDepthSubvoxels; + const double upperBlendFloor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY( constants.upperMouthReceivingRoute,queryXSubvoxels,queryYSubvoxels, plannedFloor,constants.upperMouthX,constants.upperMouthY, constants.upperMouthPlannedFloor, constants.upperMouthTangentX,constants.upperMouthTangentY, + VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres, + constants.upperMouthPlayerCenterHeightMetres, + qPerMetre); + const double lowerBlendFloor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY( + constants.lowerMouthReceivingRoute,queryXSubvoxels,queryYSubvoxels, + upperBlendFloor,constants.lowerMouthX,constants.lowerMouthY, + constants.lowerMouthPlannedFloor, + -constants.lowerMouthTangentX,-constants.lowerMouthTangentY, VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,1.13, qPerMetre); - return mouthBlendFloor+floorDepthSubvoxels; + const double landingFloor=BlendPassageFloorToLandingChambers(constants, + segment,floorT,queryXSubvoxels,queryYSubvoxels,lowerBlendFloor,qPerMetre); + return landingFloor+floorDepthSubvoxels; } static float PassageFlatBottomDistance(float roundDistanceSubvoxels, @@ -3578,15 +4071,18 @@ private: const VoxelForgeCore::ResolvedCavernRoomV1* room=nullptr; const VoxelForgeCore::ResolvedCavernEdgeV1* edge=nullptr; bool nativeGraph=false; + std::uint8_t roomShapeKind=0; + std::uint8_t ledgeSide=0; std::array boundsMinimumMicrometres{},boundsMaximumMicrometres{}; std::array centerMetres{},halfExtentsMetres{}; + double ledgeSpacingMetres=0.0,ledgeDepthMetres=0.0; struct FSegment { std::array start{},end{},minimum{},maximum{}; }; struct FSegmentNode { - std::array minimum{},maximum{}; + std::array minimum{},maximum{},minimumNormalized{},maximumNormalized{}; std::size_t first=0,end=0; int left=-1,right=-1; }; @@ -3596,6 +4092,7 @@ private: double minimumFloorMetres=std::numeric_limits::infinity(); double maximumFloorMetres=-std::numeric_limits::infinity(); double maximumSegmentGrade=0.0; + std::array floorGradient{}; std::array centerVoxels{},halfExtentsVoxels{}; std::array,4> controlPointsVoxels{}; float floorVoxels=0.0f; @@ -4039,8 +4536,12 @@ private: { CavernRuntime.clear();CavernSpatialNodes.clear();CavernSpatialOrder.clear(); if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0 - ||ResolvedLayout->cavernGraphVocabularyVersion - !=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1)return; + ||(ResolvedLayout->cavernGraphVocabularyVersion + !=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1 + &&ResolvedLayout->cavernGraphVocabularyVersion + !=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV2 + &&ResolvedLayout->cavernGraphVocabularyVersion + !=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV3))return; const auto isNativeGraphStrate=[this](std::int32_t strateIndex) { const auto found=std::find_if(ResolvedLayout->strates.begin(), @@ -4057,6 +4558,10 @@ private: { FCavernRuntimeFeature runtime;runtime.room=&room; runtime.nativeGraph=isNativeGraphStrate(room.strateIndex); + runtime.roomShapeKind=room.shapeKind; + runtime.ledgeSide=room.ledgeSide; + runtime.ledgeSpacingMetres=room.ledgeSpacingMicrometres/1000000.0; + runtime.ledgeDepthMetres=room.ledgeDepthMicrometres/1000000.0; runtime.boundsMinimumMicrometres=room.boundsMinimumMicrometres; runtime.boundsMaximumMicrometres=room.boundsMaximumMicrometres; for(std::size_t axis=0;axis<3;++axis) @@ -4070,6 +4575,8 @@ private: } runtime.floorVoxels=static_cast( static_cast(room.floorMicrometres)/micronsPerVoxel); + runtime.floorGradient={room.floorGradientQ16[0]/65536.0, + room.floorGradientQ16[1]/65536.0}; runtime.smoothWidthVoxels=static_cast( static_cast(room.smoothWidthMicrometres)/micronsPerVoxel); runtime.irregularityAmplitudeVoxels=static_cast( @@ -4176,6 +4683,16 @@ private: return nodeIndex; }; buildSegmentNode(buildSegmentNode,0,runtime.segmentOrder.size()); + const double radius=edge.boreRadiusMicrometres/1000000.0; + const double height=edge.boreHeightMicrometres/1000000.0; + if(radius>0.0&&height>0.0) + for(auto& node:runtime.segmentNodes) + for(std::size_t axis=0;axis<3;++axis) + { + const double scale=axis==2?height:radius; + node.minimumNormalized[axis]=node.minimum[axis]/scale; + node.maximumNormalized[axis]=node.maximum[axis]/scale; + } } CavernRuntime.push_back(runtime); } @@ -4625,6 +5142,31 @@ private: passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1); auto& constants=PassageComposerConstants[passageIndex]; constants.routeEndControlIndex=routeEnd; + constants.floorProfileIncludesLandingChambers= + passage.floorProfileIncludesLandingChambers; + for(std::size_t axis=0;axis<3;++axis) + { + constants.upperLandingFloorCenterSubvoxels[axis]=static_cast( + passage.upperLandingChamber.floorCenterSubvoxels[axis]); + constants.lowerLandingFloorCenterSubvoxels[axis]=static_cast( + passage.lowerLandingChamber.floorCenterSubvoxels[axis]); + } + constants.upperLandingFloorGradient={ + passage.upperLandingChamber.floorGradientQ16[0]/65536.0, + passage.upperLandingChamber.floorGradientQ16[1]/65536.0}; + constants.lowerLandingFloorGradient={ + passage.lowerLandingChamber.floorGradientQ16[0]/65536.0, + passage.lowerLandingChamber.floorGradientQ16[1]/65536.0}; + constants.upperLandingRadiusSubvoxels=static_cast( + passage.upperLandingChamber.radiusSubvoxels); + constants.lowerLandingRadiusSubvoxels=static_cast( + passage.lowerLandingChamber.radiusSubvoxels); + const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&](const auto& stratum) + {return stratum.strateIndex==passage.upperStrateIndex;}); + if(upperStratum!=ResolvedLayout->strates.end() + &&upperStratum->generatorKind=="SurfaceWorld") + constants.upperMouthPlayerCenterHeightMetres=0.98; using FRawPassageSegment=VoxelForgeCore::PassageSpline::FSegment; std::vector controls; @@ -4642,13 +5184,13 @@ private: } const double authoredUpperMouthFloorDepth=std::max(0.0, static_cast(passage.controlPoints.front().positionSubvoxels[2] - -passage.upperMouthSubvoxels[2])/qPerMetre+1.13); + -passage.upperMouthSubvoxels[2])/qPerMetre + +constants.upperMouthPlayerCenterHeightMetres); double upperMouthFloorDepth=authoredUpperMouthFloorDepth; - const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), - ResolvedLayout->strates.end(),[&](const auto& stratum) - {return stratum.strateIndex==passage.upperStrateIndex;}); if(upperStratum!=ResolvedLayout->strates.end() - &&upperStratum->generatorKind=="FloatingIslands") + &&(upperStratum->generatorKind=="FloatingIslands" + ||upperStratum->generatorKind=="TunnelNetwork" + ||upperStratum->generatorKind=="SurfaceWorld")) { std::vector> route; for(const auto& anchor:upperStratum->anchors) @@ -4656,16 +5198,85 @@ private: route.push_back({static_cast(anchor.values[0]), static_cast(anchor.values[1]), static_cast(anchor.values[2])}); + if(upperStratum->generatorKind=="TunnelNetwork"&&route.size()>2) + route.erase(route.begin(),route.end()-2); constants.upperMouthReceivingRoute=route; double routeHeight=0.0,distanceSquared=0.0; if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route, controls.front()[0],controls.front()[1],routeHeight,distanceSquared) &&distanceSquared<=std::pow(controls.front()[3]+2.0*qPerMetre,2.0)) + { upperMouthFloorDepth=std::max(upperMouthFloorDepth, - std::max(0.0,(controls.front()[2]-routeHeight)/qPerMetre+1.13)); + std::max(0.0,(controls.front()[2]-routeHeight)/qPerMetre + +constants.upperMouthPlayerCenterHeightMetres)); + if(!passage.floorProfileIncludesLandingChambers + &&(upperStratum->generatorKind=="TunnelNetwork" + ||upperStratum->generatorKind=="SurfaceWorld") + ) + VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute( + controls,plannedFloorProfile,0,route,qPerMetre, + constants.upperMouthPlayerCenterHeightMetres); + } } controls.front()[3]=std::max(controls.front()[3], upperMouthFloorDepth*qPerMetre); + if(upperStratum!=ResolvedLayout->strates.end() + &&(upperStratum->generatorKind=="FloatingIslands" + ||upperStratum->generatorKind=="TunnelNetwork" + ||upperStratum->generatorKind=="SurfaceWorld")) + for(const auto& anchor:upperStratum->anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + constants.upperMouthReceivingRoute.push_back({ + static_cast(anchor.values[0]), + static_cast(anchor.values[1]), + static_cast(anchor.values[2])}); + if(upperStratum!=ResolvedLayout->strates.end() + &&upperStratum->generatorKind=="TunnelNetwork" + &&constants.upperMouthReceivingRoute.size()>2) + constants.upperMouthReceivingRoute.erase( + constants.upperMouthReceivingRoute.begin(), + constants.upperMouthReceivingRoute.end()-2); + + const auto lowerStratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&](const auto& stratum) + {return stratum.strateIndex==passage.lowerStrateIndex;}); + if(lowerStratum!=ResolvedLayout->strates.end() + &&(lowerStratum->generatorKind=="TunnelNetwork" + ||lowerStratum->generatorKind=="VerticalShafts") + &&routeEndanchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + constants.lowerMouthReceivingRoute.push_back({ + static_cast(anchor.values[0]), + static_cast(anchor.values[1]), + static_cast(anchor.values[2])}); + if(constants.lowerMouthReceivingRoute.size()>2) + constants.lowerMouthReceivingRoute.resize(2); + if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY( + constants.lowerMouthReceivingRoute,controls[routeEnd][0], + controls[routeEnd][1],routeHeight,distanceSquared) + &&distanceSquared<=std::pow(controls[routeEnd][3] + +2.0*qPerMetre,2.0)) + { + if(!passage.floorProfileIncludesLandingChambers) + VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute( + controls,plannedFloorProfile,routeEnd, + constants.lowerMouthReceivingRoute,qPerMetre); + constants.lowerMouthX=controls[routeEnd][0]; + constants.lowerMouthY=controls[routeEnd][1]; + constants.lowerMouthPlannedFloor=plannedFloorProfile[routeEnd]; + if(routeEnd>0) + { + constants.lowerMouthTangentX=controls[routeEnd][0] + -controls[routeEnd-1][0]; + constants.lowerMouthTangentY=controls[routeEnd][1] + -controls[routeEnd-1][1]; + } + } + else constants.lowerMouthReceivingRoute.clear(); + } const auto sampledSegments=VoxelForgeCore::PassageSpline::BuildSegments( controls,controls.size()-1,PassageSegmentSpacingMetres,qPerMetre, plannedFloorProfile,PassageSegmentChordErrorMetres); @@ -4701,6 +5312,26 @@ private: constants.upperMouthTangentY=sampledSegments.front().b[1] -sampledSegments.front().a[1]; } + const std::string lowerMouthAnchor=passage.stableId+"/lower-landing-chamber"; + const std::string upperMouthAnchor=passage.stableId+"/upper-landing-chamber"; + for(const auto& edge:ResolvedLayout->cavernEdges) + { + if(edge.controlPointsMicrometres.size()<2)continue; + if(edge.startAnchorStableId==lowerMouthAnchor) + { + constants.lowerMouthTangentX=edge.controlPointsMicrometres[1][0] + -edge.controlPointsMicrometres[0][0]; + constants.lowerMouthTangentY=edge.controlPointsMicrometres[1][1] + -edge.controlPointsMicrometres[0][1]; + } + if(edge.startAnchorStableId==upperMouthAnchor) + { + constants.upperMouthTangentX=edge.controlPointsMicrometres[0][0] + -edge.controlPointsMicrometres[1][0]; + constants.upperMouthTangentY=edge.controlPointsMicrometres[0][1] + -edge.controlPointsMicrometres[1][1]; + } + } std::vector rawSegments=sampledSegments; std::vector> routeRadiusSamples; routeRadiusSamples.push_back({0.0,controls.front()[3]/qPerMetre}); @@ -4867,6 +5498,40 @@ private: std::ceil((localMaximum[axis]+localExtent)*Q)); } } + const auto includeLandingChamber=[&]( + const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) + { + const double cx=static_cast(chamber.floorCenterSubvoxels[0])/Q; + const double cy=static_cast(chamber.floorCenterSubvoxels[1])/Q; + const double cz=static_cast(chamber.floorCenterSubvoxels[2])/Q; + const double radius=static_cast(chamber.radiusSubvoxels)/Q; + const double gx=static_cast(chamber.floorGradientQ16[0])/65536.0; + const double gy=static_cast(chamber.floorGradientQ16[1])/65536.0; + const double floorExtent=radius*std::hypot(gx,gy); + const double height=static_cast( + chamber.clearHeightSubvoxels)/Q; + const double extent=requiredReachCoreVoxels+0.01; + const std::array minimum{ + cx-radius-extent,cy-radius-extent,cz-floorExtent-extent}; + const std::array maximum{ + cx+radius+extent,cy+radius+extent, + cz+floorExtent+height+extent}; + FPassageProofBound proof; + for(std::size_t axis=0;axis<3;++axis) + { + routeMinimum[axis]=std::min(routeMinimum[axis],minimum[axis]); + routeMaximum[axis]=std::max(routeMaximum[axis],maximum[axis]); + proof.minimum[axis]=minimum[axis]; + proof.maximum[axis]=maximum[axis]; + } + PassageProofBounds.push_back(proof); + }; + if(segmentIndex==0) + includeLandingChamber( + ResolvedLayout->passages[passageIndex].upperLandingChamber); + if(segmentEndIndex==segmentCount) + includeLandingChamber( + ResolvedLayout->passages[passageIndex].lowerLandingChamber); const double routeExtent=maximumRadiusSubvoxels/Q +requiredReachCoreVoxels+0.01; const double tileExtent=routeExtent; @@ -5326,11 +5991,7 @@ public: FMetricInterval caveVoid{std::numeric_limits::infinity(), std::numeric_limits::infinity(),true}; bool haveFeature=false; - double latticeFloorSupportLowerBound=std::numeric_limits::infinity(); - const auto* resolvedStratum=FindResolvedLayoutStratum(strateIndex); - const bool nativeLattice=resolvedStratum - &&VoxelForgeCore::IsNativeLatticeFieldStratum(*resolvedStratum) - &&resolvedStratum->passageCapabilities.hasNativeLatticeField; + double nativeFloorSupportLowerBound=std::numeric_limits::infinity(); for(const std::uint32_t index:candidates) { if(index>=CavernRuntime.size())continue; @@ -5341,6 +6002,11 @@ public: FMetricInterval featureVoid; if(feature.room) { + // Shaft wall and ledge SDFs are evaluated exactly per sample. + // Until a conservative interval bound for their carved shelves is + // available, keep intersecting boxes Mixed instead of proving a + // potentially false uniform sign. + if(feature.roomShapeKind==1)return {0.0,0.0,false}; double normalizedMinimumSquared=0.0,normalizedMaximumSquared=0.0; double meanRadius=0.0; for(std::size_t axis=0;axis<3;++axis) @@ -5360,7 +6026,18 @@ public: (std::sqrt(normalizedMinimumSquared)-1.0)*meanRadius, (std::sqrt(normalizedMaximumSquared)-1.0)*meanRadius,true}; const double floorZ=feature.room->floorMicrometres/1000000.0; - const FMetricInterval floor={floorZ-hi[2],floorZ-lo[2],true}; + const double centerX=feature.room->centerMicrometres[0]/1000000.0; + const double centerY=feature.room->centerMicrometres[1]/1000000.0; + const double gx=feature.room->floorGradientQ16[0]/65536.0; + const double gy=feature.room->floorGradientQ16[1]/65536.0; + const double x0=lo[0]-centerX,x1=hi[0]-centerX; + const double y0=lo[1]-centerY,y1=hi[1]-centerY; + const double gradientMinimum=(gx>=0.0?gx*x0:gx*x1) + +(gy>=0.0?gy*y0:gy*y1); + const double gradientMaximum=(gx>=0.0?gx*x1:gx*x0) + +(gy>=0.0?gy*y1:gy*y0); + const FMetricInterval floor={floorZ-hi[2]+gradientMinimum, + floorZ-lo[2]+gradientMaximum,true}; const double width=feature.room->smoothWidthMicrometres/1000000.0; featureVoid={SmoothMaxMetres(ellipsoid.minimum,floor.minimum,width), SmoothMaxMetres(ellipsoid.maximum,floor.maximum,width),true}; @@ -5482,21 +6159,19 @@ public: SmoothMaxMetres(tube.maximum,floor.maximum,width),true}; } else return {0.0,0.0,false}; - if(nativeLattice) + if(feature.edge) { - const double supportDepth=feature.edge - ?feature.edge->floorSupportDepthMicrometres/1000000.0:0.0; - const double floorMinimum=feature.edge - ?feature.minimumFloorMetres:std::numeric_limits::infinity(); - const double floorMaximum=feature.edge - ?feature.maximumFloorMetres:-std::numeric_limits::infinity(); + const double supportDepth= + feature.edge->floorSupportDepthMicrometres/1000000.0; + const double floorMinimum=feature.minimumFloorMetres; + const double floorMaximum=feature.maximumFloorMetres; const bool overlapsXY=hi[0]>=feature.boundsMinimumMicrometres[0]/1.0e6 &&lo[0]<=feature.boundsMaximumMicrometres[0]/1.0e6 &&hi[1]>=feature.boundsMinimumMicrometres[1]/1.0e6 &&lo[1]<=feature.boundsMaximumMicrometres[1]/1.0e6; if(supportDepth>0.0&&overlapsXY &&hi[2]>=floorMinimum-supportDepth&&lo[2]<=floorMaximum) - latticeFloorSupportLowerBound=std::min(latticeFloorSupportLowerBound, + nativeFloorSupportLowerBound=std::min(nativeFloorSupportLowerBound, std::max(-supportDepth,lo[2]-floorMaximum)); } if(!std::isfinite(featureVoid.minimum)||!std::isfinite(featureVoid.maximum)) @@ -5525,11 +6200,11 @@ public: result.minimum=std::max(HostRockMetres,-caveVoid.maximum); result.maximum=std::max(HostRockMetres,-caveVoid.minimum); } - // The final lattice field owns an extra support cap after feature union. + // Native graph edges may own a support cap after feature union. // Lower only the interval minimum when a box could touch that cap; the // untouched maximum remains a sound bound when only part of the box does. - if(std::isfinite(latticeFloorSupportLowerBound)) - result.minimum=std::min(result.minimum,latticeFloorSupportLowerBound); + if(std::isfinite(nativeFloorSupportLowerBound)) + result.minimum=std::min(result.minimum,nativeFloorSupportLowerBound); // Cover floating-point rounding in interval endpoints and the field's fixed-point // coordinate conversion while keeping the interval tight compared with one micron. constexpr double OutwardPaddingMetres=1.0e-6; @@ -7825,8 +8500,6 @@ private: *nativeFloorSupport=std::numeric_limits::lowest(); (void)density; const double metresPerVoxel=Owner->CoreVoxelSizeMicrons/1000000.0; - const std::array point={X*metresPerVoxel,Y*metresPerVoxel, - Z*metresPerVoxel}; std::vector localCandidates; const std::vector* candidates=nullptr; if(scoped)candidates=&tileQueries->cavernFeatureCandidates(); @@ -7840,11 +8513,22 @@ private: localCandidates=BuildCavernFeatureTileList(coordinate,coordinate); candidates=&localCandidates; } + // No cave feature overlaps this scoped tile (or unscoped point), so the + // native field is exactly its constant host-rock value. Avoid per-sample + // coordinate conversion and the empty feature-composition path. + constexpr double HostRockMetres=-32.0; + if(candidates->empty()) + return static_cast(HostRockMetres/metresPerVoxel); + const std::array point={X*metresPerVoxel,Y*metresPerVoxel, + Z*metresPerVoxel}; double caveVoid=std::numeric_limits::infinity(); - double latticeFloorSupportMinimum=std::numeric_limits::infinity(); - const bool nativeLattice=bound.Resolved - &&VoxelForgeCore::IsNativeLatticeFieldStratum(*bound.Resolved) - &&bound.Resolved->passageCapabilities.hasNativeLatticeField; + double nativeFloorSupportMinimum=std::numeric_limits::infinity(); + double floorSupportScore=std::numeric_limits::infinity(); + double floorSupportDepth=0.0,floorSupportFloor=0.0,floorSupportGrade=0.0; + const std::string* floorSupportEdgeId=nullptr; + const std::string* floorSupportProfileId=nullptr; + double lowestOpenEdgeFloor=std::numeric_limits::infinity(); + const std::string* lowestOpenEdgeProfileId=nullptr; for(const std::uint32_t index:*candidates) { if(index>=CavernRuntime.size())continue; @@ -7852,9 +8536,86 @@ private: const std::int32_t strateIndex=feature.room?feature.room->strateIndex :feature.edge?feature.edge->strateIndex:-1; if(strateIndex!=bound.StrateIndex)continue; + // Tile culling keeps the candidate list small, but a tile can still + // contain millions of sample points outside a feature's authored + // bounds. Reject those points before evaluating room SDFs or walking + // an edge's segment BVH. Keep a 1 mm guard for float voxel-coordinate + // rounding; the serialized bounds already include all SDF, union, + // irregularity, bore, and floor-support reach. + constexpr double FeatureBoundsGuardMetres=0.001; + bool insideFeatureBounds=true; + for(std::size_t axis=0;axis<3;++axis) + { + const double minimum=feature.boundsMinimumMicrometres[axis]/1000000.0 + -FeatureBoundsGuardMetres; + const double maximum=feature.boundsMaximumMicrometres[axis]/1000000.0 + +FeatureBoundsGuardMetres; + if(point[axis]maximum) + {insideFeatureBounds=false;break;} + } + if(!insideFeatureBounds)continue; double featureVoid=std::numeric_limits::infinity(); + double edgeFloor=0.0; if(feature.room) { + if(feature.roomShapeKind==1) + { + const double radius=feature.halfExtentsMetres[0]; + const double bottom=feature.centerMetres[2]-feature.halfExtentsMetres[2]; + const double top=feature.centerMetres[2]+feature.halfExtentsMetres[2]; + const double radial=std::hypot(point[0]-feature.centerMetres[0], + point[1]-feature.centerMetres[1]); + const double radialQ=radial-radius; + const double verticalQ=std::max(bottom-point[2],point[2]-top); + double shaftDistance=std::min(std::max(radialQ,verticalQ),0.0) + +std::hypot(std::max(radialQ,0.0),std::max(verticalQ,0.0)); + if(feature.irregularityAmplitudeVoxels>0.0f) + shaftDistance+=StampValueNoise( + X-feature.centerVoxels[0],Y-feature.centerVoxels[1], + Z-feature.centerVoxels[2],feature.irregularityWavelengthVoxels, + feature.irregularitySeed) + *feature.irregularityAmplitudeVoxels*metresPerVoxel; + + // A cardinal ledge faces the walkable backbone. Shelves are + // cut out of the otherwise open cylinder, leaving most of the + // shaft cross-section clear at every interval. + if(feature.ledgeSpacingMetres>0.0&&feature.ledgeDepthMetres>0.0) + { + const long long nearest=static_cast(std::llround( + (top-point[2])/feature.ledgeSpacingMetres)); + const long long maximum=static_cast(std::floor( + (top-bottom-1000000.0)/feature.ledgeSpacingMetres)); + if(nearest>=1&&nearest<=maximum) + { + const double ledgeZ=top-nearest*feature.ledgeSpacingMetres; + constexpr double LedgeThicknessMetres=0.6; + constexpr double LedgeFilletMetres=0.1; + const double dx=point[0]-feature.centerMetres[0]; + const double dy=point[1]-feature.centerMetres[1]; + const std::array,4> sides={{{1.0,0.0}, + {0.0,1.0},{-1.0,0.0},{0.0,-1.0}}}; + const auto side=sides[feature.ledgeSide%4]; + const double radialLocal=dx*side[0]+dy*side[1]; + const double tangentLocal=-dx*side[1]+dy*side[0]; + const double halfDepth=feature.ledgeDepthMetres*0.5; + const double halfWidth=radius*0.45; + const double halfThickness=LedgeThicknessMetres*0.5; + const double qx=std::abs(radialLocal-(radius-halfDepth))-halfDepth; + const double qy=std::abs(tangentLocal)-halfWidth; + const double qz=std::abs(point[2]-(ledgeZ-halfThickness)) + -halfThickness; + const double outside=std::hypot(std::max(qx,0.0), + std::max(qy,0.0),std::max(qz,0.0)); + const double platformDistance=outside + +std::min(std::max(qx,std::max(qy,qz)),0.0) + -LedgeFilletMetres; + shaftDistance=std::max(shaftDistance,-platformDistance); + } + } + featureVoid=shaftDistance; + } + else + { double normalizedSquared=0.0,meanRadius=0.0; for(std::size_t axis=0;axis<3;++axis) { @@ -7863,10 +8624,22 @@ private: const double delta=(point[axis]-feature.centerMetres[axis])/radius; normalizedSquared+=delta*delta;meanRadius+=radius/3.0; } - const double ellipsoid=(std::sqrt(normalizedSquared)-1.0)*meanRadius; - const double floor=feature.room->floorMicrometres/1000000.0-point[2]; + double ellipsoid=(std::sqrt(normalizedSquared)-1.0)*meanRadius; + if(feature.irregularityAmplitudeVoxels>0.0f) + { + const float fade=SmoothUnit((Z-feature.floorVoxels)/0.8f); + ellipsoid+=StampValueNoise( + X-feature.centerVoxels[0],Y-feature.centerVoxels[1], + Z-feature.centerVoxels[2], + feature.irregularityWavelengthVoxels,feature.irregularitySeed) + *feature.irregularityAmplitudeVoxels*metresPerVoxel*fade; + } + const double floor=feature.room->floorMicrometres/1000000.0-point[2] + +feature.floorGradient[0]*(point[0]-feature.centerMetres[0]) + +feature.floorGradient[1]*(point[1]-feature.centerMetres[1]); const double width=feature.room->smoothWidthMicrometres/1000000.0; featureVoid=SmoothMaxMetres(ellipsoid,floor,width); + } } else if(feature.edge&&!feature.segments.empty()) { @@ -7901,7 +8674,17 @@ private: } const double normalized=std::sqrt(radialSquared/(radius*radius) +vertical*vertical/(height*height)); - const double distance=(normalized-1.0)*std::min(radius,height); + double distance=(normalized-1.0)*std::min(radius,height); + if(feature.irregularityAmplitudeVoxels>0.0f) + { + const float floorVoxels=static_cast(nearestFloor/metresPerVoxel); + const float fade=SmoothUnit((Z-floorVoxels)/0.8f); + distance+=StampValueNoise( + X-feature.controlPointsVoxels[0][0], + Y-feature.controlPointsVoxels[0][1],Z-floorVoxels, + feature.irregularityWavelengthVoxels,feature.irregularitySeed) + *feature.irregularityAmplitudeVoxels*metresPerVoxel*fade; + } const bool clearlyCloser=distance normalizedPoint={ + point[0]/radius,point[1]/radius,point[2]/height}; const auto nodeLowerBound=[&](const FCavernRuntimeFeature::FSegmentNode& node) { double gapSquared=0.0; for(std::size_t axis=0;axis<3;++axis) { - const double scale=axis==2?height:radius; - const double q=point[axis]/scale; - const double low=node.minimum[axis]/scale; - const double high=node.maximum[axis]/scale; + const double q=normalizedPoint[axis]; + const double low=node.minimumNormalized[axis]; + const double high=node.maximumNormalized[axis]; const double gap=qhigh?q-high:0.0; gapSquared+=gap*gap; } @@ -7955,17 +8739,69 @@ private: } else for(std::uint32_t segment=0;segmentfloorSupportDepthMicrometres/1000000.0; - if(nativeLattice&&supportDepth>0.0&&nearestRadialfloorSupportDepthMicrometres/1000000.0; + constexpr double MinimumBackingDepthMetres=0.65; + constexpr double FloorSupportFootprintRadiusMetres=0.95; + // Portal edges join a wider authored passage whose floor spans the + // full bore. Carry support across that shared mouth footprint; + // ordinary backbone and branch edges retain the narrow support + // strip that prevents nearby routes from sealing one another. + const bool portalMouthEdge=feature.edge->startAnchorStableId.ends_with( + "-mouth"); + const double floorSupportRadius=portalMouthEdge?radius: + std::min(radius,FloorSupportFootprintRadiusMetres); + const double candidateNormalDepth=(nearestFloor-point[2]) + /std::sqrt(1.0+nearestGrade*nearestGrade); + if(candidateSupportDepth>=MinimumBackingDepthMetres + &&candidateNormalDepth>=MinimumBackingDepthMetres + &&candidateNormalDepth<=candidateSupportDepth + &&nearestRadialfloorProfileStableId.empty(); + const bool sameProfile=floorSupportProfileId&&hasProfile + &&*floorSupportProfileId==feature.edge->floorProfileStableId; + if(!floorSupportProfileId||sameProfile) + { + // For a shared backbone, support follows the closest + // horizontal centerline. Vertical separation must not make + // a nearby branch take support away from this path. + if(!floorSupportEdgeId||candidateScorestableId<*floorSupportEdgeId)))) + { + floorSupportScore=candidateScore; + floorSupportDepth=candidateSupportDepth; + floorSupportFloor=nearestFloor; + floorSupportGrade=nearestGrade; + floorSupportEdgeId=&feature.edge->stableId; + } + if(hasProfile) + floorSupportProfileId=&feature.edge->floorProfileStableId; + } + } + edgeFloor=nearestFloor; const double floor=(nearestFloor-point[2]) /std::sqrt(1.0+nearestGrade*nearestGrade); const double width=feature.edge->smoothWidthMicrometres/1000000.0; featureVoid=SmoothMaxMetres(tube,floor,width); } if(!std::isfinite(featureVoid))continue; + if(feature.edge&&feature.edge->floorSupportDepthMicrometres>=650000 + &&featureVoid<0.0) + { + if(edgeFloorfloorProfileStableId + <(lowestOpenEdgeProfileId?*lowestOpenEdgeProfileId:std::string{}))) + { + lowestOpenEdgeFloor=edgeFloor; + lowestOpenEdgeProfileId=&feature.edge->floorProfileStableId; + } + } if(!std::isfinite(caveVoid))caveVoid=featureVoid; else caveVoid=SmoothMinMetres(caveVoid,featureVoid, feature.room @@ -7976,21 +8812,34 @@ private: ?feature.edge->unionWidthMicrometres :feature.edge->smoothWidthMicrometres)/1000000.0); } + const double floorSupportNormalDepth=(floorSupportFloor-point[2]) + /std::sqrt(1.0+floorSupportGrade*floorSupportGrade); + if(std::isfinite(floorSupportScore)&&point[2](latticeFloorSupportMinimum/metresPerVoxel); + // Preserve each native graph edge's planned floor backing after smooth + // unions. The record's finite depth keeps independent routes from sealing + // one another when they pass at different elevations. + // Backing from a higher route edge must not seal a lower overlapping bore. + // Room voids and same-level route joins still retain their authored support. + const bool sameContinuousFloorProfile=floorSupportProfileId + &&lowestOpenEdgeProfileId&&!floorSupportProfileId->empty() + &&*floorSupportProfileId==*lowestOpenEdgeProfileId; + const bool sealsOpenLowerEdge=std::isfinite(lowestOpenEdgeFloor) + &&lowestOpenEdgeFloorlowestOpenEdgeFloor&&!sameContinuousFloorProfile; + if(std::isfinite(nativeFloorSupportMinimum)&&!sealsOpenLowerEdge) + composed=std::min(composed,nativeFloorSupportMinimum); + if(nativeFloorSupport&&std::isfinite(nativeFloorSupportMinimum)) + *nativeFloorSupport=static_cast(nativeFloorSupportMinimum/metresPerVoxel); return static_cast(composed/metresPerVoxel); } @@ -9968,6 +10817,32 @@ private: },&Timing,nullptr,16); } + uint64 ApplyCriticalAirPackedSigns() + { + const int32 NX=Size[0],NY=Size[1]; + std::array workerVoxels{}; + ParallelLines(static_cast(NX)*NY,[&](size_t Line,int32 Worker) + { + const int32 X=Min[0]+static_cast(Line%NX); + const int32 Y=Min[1]+static_cast(Line/NX); + const size_t Base=Line*WordsPerZ; + for(const uint32 Id:GetLineCandidates(2,X,Y)) + { + const FPrimitive& primitive=Primitives[Id]; + if(!primitive.bCarve||!primitive.bCritical)continue; + const int32 lo=std::max(primitive.Min[2],Min[2]); + const int32 hi=std::min(primitive.Max[2],Max[2]); + for(int32 Z=lo;Z<=hi;++Z) + { + if(!ShapeContains(primitive,X,Y,Z))continue; + FillBits(RockWords,Base,Z-Min[2],Z-Min[2],false); + ++workerVoxels[static_cast(Worker)]; + } + } + },nullptr,nullptr,16); + return std::accumulate(workerVoxels.begin(),workerVoxels.end(),uint64{0}); + } + void BuildCandidateBands(FParallelTiming& AxisTiming,FParallelTiming& MergeTiming, double& MergeSeconds) { @@ -11711,7 +12586,8 @@ private: const int32 VZ=static_cast(std::lround(Z)); const bool bCritical=IsInsideCriticalAir(VX,VY,VZ); if(bCritical){++RouteCritical;++SegmentCritical;} - if(IsRock(VX,VY,VZ)) + const bool bRock=IsRock(VX,VY,VZ); + if(bRock) { ++Stats.WalkFailures;++RouteFailures;++SegmentFailures; if(RouteFailures==1) @@ -11925,7 +12801,7 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa ?shape_->ResolvedLayout->coreVoxelSizeMicrometres:250000); const double shellThickness=VF_PassageShellThicknessMetres*qPerMetre; const double stampedOutsideDistance=sweep->influenceSubvoxels+1.0; - constexpr double MouthShellBlendMetres=5.0; + constexpr double DefaultMouthShellBlendMetres=5.0; const double wallReliefShell=0.41*qPerMetre; const double floorDetailShell=1.0*qPerMetre; if(profile) @@ -11950,6 +12826,19 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa { const auto& candidates=passageRouteSegments_[passageIndex]; if(candidates.empty())continue; + double mouthShellBlendMetres=DefaultMouthShellBlendMetres; + if(shape_->ResolvedLayout&&passageIndexResolvedLayout->passages.size()) + { + const auto& passage=shape_->ResolvedLayout->passages[passageIndex]; + const auto lower=std::find_if(shape_->ResolvedLayout->strates.begin(), + shape_->ResolvedLayout->strates.end(),[&](const auto& stratum) + {return stratum.strateIndex==passage.lowerStrateIndex;}); + if(lower!=shape_->ResolvedLayout->strates.end() + &&lower->generatorKind=="TunnelNetwork" + &&lower->recipeId=="Core.Caves.PrimitiveField" + &&lower->recipeVersion==1) + mouthShellBlendMetres=2.0; + } const std::int32_t sweepIndex=static_cast(sweep->passages.size()); sweep->passageToSweep[passageIndex]=sweepIndex; auto& passageSamples=sweep->passages.emplace_back(); @@ -12103,7 +12992,7 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa const double mouthDistanceMetres=std::max(0.0,std::min(alongMetres, constants.totalPathLengthMetres-alongMetres)); const double shellFade=std::clamp(mouthDistanceMetres/ - MouthShellBlendMetres,0.0,1.0); + mouthShellBlendMetres,0.0,1.0); const double smoothShellFade=shellFade*shellFade *(3.0-2.0*shellFade); const double floorDistanceMetres=(floorCenterZ-floorDepth-qz)/qPerMetre; @@ -12120,7 +13009,7 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa const double pastMouthMetres=std::max(0.0,alongMetres -constants.totalPathLengthMetres); const double extensionFade=std::clamp(1.0 - -pastMouthMetres/MouthShellBlendMetres,0.0,1.0); + -pastMouthMetres/mouthShellBlendMetres,0.0,1.0); floorExtensionFade=extensionFade*extensionFade *(3.0-2.0*extensionFade); } @@ -12289,28 +13178,30 @@ inline const FStandaloneShapeWorld::FScopedPassageTileQueries::FPassageSweepSamp FStandaloneShapeWorld::FScopedPassageTileQueries::sweptSample( std::size_t passageIndex,double qx,double qy,double qz) const { - const auto sweep=passageSweepData_; + // This scope retains the sweep through the full query; avoid a shared_ptr + // refcount operation for every sample lookup. + auto* sweep=passageSweepData_.get(); if(!sweep)return nullptr; - sweep->lookups.fetch_add(1,std::memory_order_relaxed); + if(profileStats_)++localSweepLookups_; const auto recordMissing=[&]() { - sweep->missingSamples.fetch_add(1,std::memory_order_relaxed); + if(profileStats_)++localSweepMissingSamples_; if(!profileStats_)return; const auto caller=static_cast( FStandaloneShapeWorld::CurrentPassageDensityQueryCaller()); if(callermissingLookupsByCaller[caller].fetch_add(1,std::memory_order_relaxed); + ++localMissingLookupsByCaller_[caller]; }; if(passageIndex>=sweep->passageToSweep.size()) { - sweep->unpreparedMisses.fetch_add(1,std::memory_order_relaxed); + if(profileStats_)++localSweepUnpreparedMisses_; recordMissing(); return nullptr; } const std::int32_t passageSweepIndex=sweep->passageToSweep[passageIndex]; if(passageSweepIndex<0||static_cast(passageSweepIndex)>=sweep->passages.size()) { - sweep->unpreparedMisses.fetch_add(1,std::memory_order_relaxed); + if(profileStats_)++localSweepUnpreparedMisses_; recordMissing(); return nullptr; } @@ -12324,7 +13215,7 @@ FStandaloneShapeWorld::FScopedPassageTileQueries::sweptSample( if(rounded<0||rounded>=sweep->dimension ||std::abs(coordinate-static_cast(rounded))>1.0e-5) { - sweep->offGridMisses.fetch_add(1,std::memory_order_relaxed); + if(profileStats_)++localSweepOffGridMisses_; recordMissing(); return nullptr; } @@ -12336,13 +13227,16 @@ FStandaloneShapeWorld::FScopedPassageTileQueries::sweptSample( const auto& samples=sweep->passages[static_cast(passageSweepIndex)].samples; if(index>=samples.size()||!samples[index].valid) { - sweep->unpreparedMisses.fetch_add(1,std::memory_order_relaxed); + if(profileStats_)++localSweepUnpreparedMisses_; recordMissing(); return nullptr; } - sweep->cacheHits.fetch_add(1,std::memory_order_relaxed); - if(samples[index].signedDistance<=sweep->influenceSubvoxels) - sweep->baseInfluenceHits.fetch_add(1,std::memory_order_relaxed); - else sweep->outOfInfluenceHits.fetch_add(1,std::memory_order_relaxed); + if(profileStats_) + { + ++localSweepCacheHits_; + if(samples[index].signedDistance<=sweep->influenceSubvoxels) + ++localSweepBaseInfluenceHits_; + else ++localSweepOutOfInfluenceHits_; + } return &samples[index]; } diff --git a/Standalone/Research/WorldAssemblerM3/Design49/README.md b/Standalone/Research/WorldAssemblerM3/Design49/README.md new file mode 100644 index 0000000..cb4051b --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Design49/README.md @@ -0,0 +1,33 @@ +# Design49 — flat landing pad at tunnel ends (owner idea, reviewed 2026-11-02) + +Owner: "the end of the tunnel should be flat; flatten the arrival with a fade-out that blends to the surrounding +density; a cheap stamp applied at arrival, no matter what's there." + +A read-only design review (Claude Opus 5.5, medium effort) of `wip/l134-cave-planner` recommends a **clamp, not a +lerp**. Each tunnel end gets a level disc at the tunnel's end floor P. Out to a blend radius, the native ground is +squeezed between two gentle cones (12°) starting at P: a lower one that fills solid, an upper one that carves air. +Inside the disc the floor is exactly flat; outside it, the slope is never steeper than max(cone, native), with no +overhang or cliff. The full note is in the session transcript; key points: + +- **Placement:** the last step of `ApplyResolvedStrateComposition`, after the passage composer, on both the native and + non-native branches. The fill is masked by the tube-only distance (bore stays open). It replaces the landing-chamber + box, shell and floor-shell special cases that caused L152–L156. +- **Formula** (metres; ×cos α for 1-Lipschitz). d = max(0, r−R), t = tan α. + - Fill: `Lo = P − t·d`, `F = max(z−Lo, (Lo−Ds)−z, r−(R+B))`. + - Air: `Hi = P + t·d`, `A = min(z−Hi, (Hi+H)−z, (R+B)−r)`. + - Result: `c = max(min(native, max(F, −tubeDist)), A)`. + - Hard min/max inside R; smooth-min width ramps 0 → 0.5 m over the first metre outside it. + - Why not lerp: native density saturates at about ±4.125 voxels (`DetailSupremum`), so a lerp makes cliffs and + films. +- **Parameters:** α = 12°, R = max(mouthRadius + 1 m, 3 m), B = min(8 m, |Δh_ring|/t + 1 m), H = 3 m, Ds = 1.25 m. +- **Planner:** + - P = h(end), with h′ = 0 over the last R. + - Sample the edge ring; move P into the feasible band or take the next deterministic site. + - Rewrite route heights inside R+B to the clamped ground. + - Content-band check including P−tB−Ds and P+tB+H. +- **Bounds:** the pad influence box goes into the proof bounds, the fallback overlap test, the **tile feature + bounds** (critical: the composer early-returns on an empty route list) and the proof-miss sampler. +- **Audits:** new `LandingPadAudit` (flatness ±2 cm, support, headroom, 36 radial blend transects ≤ 15° with one + crossing) plus negative controls (pad off, 2% tilt, B halved, bore mask removed). +- **Risks:** caves under pads (connectivity), low ceilings, shaft footprints, island edges and their air gap, liminal + decks; off-route edges on non-heightfield layers are a metric only. diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux134/REPORT.md new file mode 100644 index 0000000..2b7afbf --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux134/REPORT.md @@ -0,0 +1,26 @@ +# Linux134 — Tunnel Network cave planner + +`TunnelNetwork` dispatches native `Core.Caves.PrimitiveField` v1. Seed 0 is strict-valid: 30 compound rooms, 72 room primitives, 31 edges, 3 branches, and a destination chamber. Its 27-point backbone spans 400.620 m horizontally with a 59.021 m floor change and an 8.381° maximum grade. + +Design 4 gives backbone rooms and edges one continuous floor profile `h(s)`. Edge curves follow that profile; each room primitive is placed at its own profiled floor. Side branches keep independent flat floors. Overlap backing is capped at the lower floor. This removes the descending-joint capsule blockage. The incoming passage remains fully open: 21,425/21,425 samples. + +The final-source fast tier passes in 370.8 s, including owner passage, shell, floor, ripple and negative-control audits. The default composed route audit passes: 0 blocked, 0 unsupported, 0 grade failures across 51,975 cave-route capsule samples; blocked-route controls detect their controls. The tile seam audit also passes (4 pairs, 4,900 shared samples, 0 mismatches). + +Seed 2's cave route passes independently (0 blocked, 0 unsupported, 0 grade failures), but its full composed audit still fails at `passage/1-2/0`: 22 unsupported samples, first at `(15.153, 5.830, -754.680) m`. Seed 1's cave route has 24 blocked samples at the arrival mouth, beginning near `(8.380, -12.618, -864.420) m` on `passage/2-3/0`. Explicit seeds 0 and 3 produce no sweepable route candidates. The default seed is the only fully green composed audit among these runs. + +Clang and GCC W1/W8/W12 match all 13 pipeline hash lines at the cave camera; each route audit passes. Seven legacy/planner cutaway pairs cover the arrival room, two backbone rooms, two tunnels, destination chamber, and departure room. All planner views render; several legacy cutaways show sparse geometry. Review them in `Saved/Linux/l134/captures/COMPARE.html`. + +L80 timing used five interleaved samples per binary at W8/W12. Values below are the planner median change versus `assembler/world`; each cell is whole-view ready / LOD0–2 ready. + +| Strate | W8 | W12 | +|---|---:|---:| +| 0 | -0.70% / +1.52% | +0.97% / +3.50% | +| 1 | +3.81% / +8.38% | +2.41% / +6.19% | +| 2 | +3.38% / +12.90% | +3.98% / +10.26% | +| 3 | +7.97% / +13.54% | +1.93% / +7.67% | + +The VFHost Linux Development build succeeds with `-NoUBA -MaxParallelActions=4`. Final Clang/GCC source stamp: `f3b69ea0ce186c855643702a88e0d0732c81c18ec93c17d1824c25210e6408a3`. Canonical owner hashes are pinned at v230. + +Logs and timing data are under `Saved/Linux/l134`; the timing summary is `timing/v230-summary.json`, the final fast log is `verify-fast-final-source.log`, and the host build log is `ubt.log`. + +The separate legacy `--audit-native-caves` signature check still fails its mouth offset (it expects 1.00 m; the route contract is 1.13 m) and ripple span (1.8878 m against its 1.25 m cap). Its center-air, floor-solid, and shell probes pass; the composed owner route audit above is green. diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/arrival-room-assembler-world.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/arrival-room-assembler-world.png new file mode 100644 index 0000000..1dd29fd Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/arrival-room-assembler-world.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/arrival-room-planner.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/arrival-room-planner.png new file mode 100644 index 0000000..7452f5e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/arrival-room-planner.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/backbone-room-a-assembler-world.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/backbone-room-a-assembler-world.png new file mode 100644 index 0000000..7e15d37 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/backbone-room-a-assembler-world.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/backbone-room-a-planner.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/backbone-room-a-planner.png new file mode 100644 index 0000000..fc9ae19 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/backbone-room-a-planner.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/backbone-room-b-assembler-world.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/backbone-room-b-assembler-world.png new file mode 100644 index 0000000..4585e9b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/backbone-room-b-assembler-world.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/backbone-room-b-planner.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/backbone-room-b-planner.png new file mode 100644 index 0000000..c41bc88 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/backbone-room-b-planner.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/departure-room-assembler-world.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/departure-room-assembler-world.png new file mode 100644 index 0000000..73c1323 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/departure-room-assembler-world.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/departure-room-planner.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/departure-room-planner.png new file mode 100644 index 0000000..b976f99 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/departure-room-planner.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/destination-chamber-assembler-world.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/destination-chamber-assembler-world.png new file mode 100644 index 0000000..1b1714b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/destination-chamber-assembler-world.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/destination-chamber-planner.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/destination-chamber-planner.png new file mode 100644 index 0000000..85aa0e0 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/destination-chamber-planner.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/tunnel-a-assembler-world.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/tunnel-a-assembler-world.png new file mode 100644 index 0000000..1157bb4 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/tunnel-a-assembler-world.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/tunnel-a-planner.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/tunnel-a-planner.png new file mode 100644 index 0000000..f12dcf9 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/tunnel-a-planner.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/tunnel-b-assembler-world.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/tunnel-b-assembler-world.png new file mode 100644 index 0000000..794aeb3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/tunnel-b-assembler-world.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/captures/tunnel-b-planner.png b/Standalone/Research/WorldAssemblerM3/Linux134/captures/tunnel-b-planner.png new file mode 100644 index 0000000..1652068 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux134/captures/tunnel-b-planner.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/layout-seed0-final-gate.json b/Standalone/Research/WorldAssemblerM3/Linux134/layout-seed0-final-gate.json new file mode 100644 index 0000000..c7f9a96 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux134/layout-seed0-final-gate.json @@ -0,0 +1 @@ +{"schema_version":3,"world_identity":"owner-world/seed/0","owner_voxel_size_um":125000,"core_voxel_size_um":250000,"strate_layout_chunk_size_um":8000000,"strate_layout_chunk_width_subvoxels":8192,"strates":[{"stable_id":"owner-world/seed/0/strate/0/generator/SurfaceWorld","order":0,"strate_index":0,"generator_kind":"SurfaceWorld","recipe_id":"SurfaceWorld.Heightfield2D","recipe_version":2,"interior_bottom_um":-504000000,"interior_top_um":8000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":7500000,"top_um":8000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-503500000,"top_um":7500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-504000000,"top_um":-503500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":1,"transition_width_um":8000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":2000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"BuildSurfaceStratumPlan uses resolved layout chunk width from ResolvedLayoutSnapshotV3","query_dispatch_path":"NativeSurfaceDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[{"kind":"cap_profile","stable_id":"biome/4097","units":"1/256 Core voxel","values":[-18022,0,64000,1536,0]},{"kind":"route_point","stable_id":"route/0","units":"1/256 Core voxel","values":[8192,24576,-381223,1024]},{"kind":"route_point","stable_id":"route/1","units":"1/256 Core voxel","values":[8192,24320,-381254,1024]},{"kind":"route_point","stable_id":"route/2","units":"1/256 Core voxel","values":[8192,24064,-381284,1024]},{"kind":"route_point","stable_id":"route/3","units":"1/256 Core voxel","values":[8192,23808,-381302,1024]},{"kind":"route_point","stable_id":"route/4","units":"1/256 Core voxel","values":[8192,23552,-381304,1024]},{"kind":"route_point","stable_id":"route/5","units":"1/256 Core voxel","values":[8192,23296,-381291,1024]},{"kind":"route_point","stable_id":"route/6","units":"1/256 Core voxel","values":[8192,23040,-381270,1024]},{"kind":"route_point","stable_id":"route/7","units":"1/256 Core voxel","values":[8192,22784,-381242,1024]},{"kind":"route_point","stable_id":"route/8","units":"1/256 Core voxel","values":[8192,22528,-381218,1024]},{"kind":"route_point","stable_id":"route/9","units":"1/256 Core voxel","values":[8192,22272,-381192,1024]},{"kind":"route_point","stable_id":"route/10","units":"1/256 Core voxel","values":[8192,22016,-381161,1024]},{"kind":"route_point","stable_id":"route/11","units":"1/256 Core voxel","values":[8192,21760,-381118,1024]},{"kind":"route_point","stable_id":"route/12","units":"1/256 Core voxel","values":[8192,21504,-381122,1024]},{"kind":"route_point","stable_id":"route/13","units":"1/256 Core voxel","values":[8192,21248,-381155,1024]},{"kind":"route_point","stable_id":"route/14","units":"1/256 Core voxel","values":[8192,20992,-381183,1024]},{"kind":"route_point","stable_id":"route/15","units":"1/256 Core voxel","values":[8192,20736,-381207,1024]},{"kind":"route_point","stable_id":"route/16","units":"1/256 Core voxel","values":[8192,20480,-381219,1024]},{"kind":"route_point","stable_id":"route/17","units":"1/256 Core voxel","values":[8192,20224,-381220,1024]},{"kind":"route_point","stable_id":"route/18","units":"1/256 Core voxel","values":[8192,19968,-381202,1024]},{"kind":"route_point","stable_id":"route/19","units":"1/256 Core voxel","values":[8192,19712,-381167,1024]},{"kind":"route_point","stable_id":"route/20","units":"1/256 Core voxel","values":[8192,19456,-381130,1024]},{"kind":"route_point","stable_id":"route/21","units":"1/256 Core voxel","values":[8192,19200,-381101,1024]},{"kind":"route_point","stable_id":"route/22","units":"1/256 Core voxel","values":[8192,18944,-381072,1024]},{"kind":"route_point","stable_id":"route/23","units":"1/256 Core voxel","values":[8192,18688,-381092,1024]},{"kind":"route_point","stable_id":"route/24","units":"1/256 Core voxel","values":[8192,18432,-381149,1024]},{"kind":"route_point","stable_id":"route/25","units":"1/256 Core voxel","values":[8192,18176,-381200,1024]},{"kind":"route_point","stable_id":"route/26","units":"1/256 Core voxel","values":[8192,17920,-381237,1024]},{"kind":"route_point","stable_id":"route/27","units":"1/256 Core voxel","values":[8192,17664,-381260,1024]},{"kind":"route_point","stable_id":"route/28","units":"1/256 Core voxel","values":[8192,17408,-381262,1024]},{"kind":"route_point","stable_id":"route/29","units":"1/256 Core voxel","values":[8192,17152,-381263,1024]},{"kind":"route_point","stable_id":"route/30","units":"1/256 Core voxel","values":[8192,16896,-381248,1024]},{"kind":"route_point","stable_id":"route/31","units":"1/256 Core voxel","values":[8192,16640,-381216,1024]},{"kind":"route_point","stable_id":"route/32","units":"1/256 Core voxel","values":[8192,16384,-381197,1024]}]},{"stable_id":"owner-world/seed/0/strate/1/generator/FloatingIslands","order":1,"strate_index":1,"generator_kind":"FloatingIslands","recipe_id":"FloatingIslands.ClusterField","recipe_version":0,"interior_bottom_um":-744000000,"interior_top_um":-504000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":-504500000,"top_um":-504000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-743500000,"top_um":-504500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-744000000,"top_um":-743500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":3,"transition_width_um":24000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":6000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"BuildFloatingIslandStratumPlan uses resolved layout chunk width from ResolvedLayoutSnapshotV3","query_dispatch_path":"NativeFloatingIslandDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[]},{"stable_id":"owner-world/seed/0/strate/2/generator/TunnelNetwork","order":2,"strate_index":2,"generator_kind":"TunnelNetwork","recipe_id":"Core.Caves.PrimitiveField","recipe_version":1,"interior_bottom_um":-872000000,"interior_top_um":-744000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":-744500000,"top_um":-744000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-871500000,"top_um":-744500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-872000000,"top_um":-871500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":2,"transition_width_um":16000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":4000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"resolved manager slot scaled by ResolvedLayoutSnapshotV3 chunk width","query_dispatch_path":"legacy Core route selects manager chunk by floor(CoreZ / resolved layout chunk width)","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[{"kind":"route_point","stable_id":"tunnel-network/route/000-arrival","units":"1/256 Core voxel","values":[19626,-14541,-770431]},{"kind":"route_point","stable_id":"tunnel-network/route/000-edge-start","units":"1/256 Core voxel","values":[22439,-15775,-770431]},{"kind":"route_point","stable_id":"tunnel-network/route/1","units":"1/256 Core voxel","values":[17078,-28949,-772600]},{"kind":"route_point","stable_id":"tunnel-network/route/2","units":"1/256 Core voxel","values":[11717,-42123,-774768]},{"kind":"route_point","stable_id":"tunnel-network/route/3","units":"1/256 Core voxel","values":[6356,-55297,-776937]},{"kind":"route_point","stable_id":"tunnel-network/route/4","units":"1/256 Core voxel","values":[995,-68470,-779106]},{"kind":"route_point","stable_id":"tunnel-network/route/5","units":"1/256 Core voxel","values":[-4366,-81644,-781275]},{"kind":"route_point","stable_id":"tunnel-network/route/6","units":"1/256 Core voxel","values":[-9726,-94818,-783443]},{"kind":"route_point","stable_id":"tunnel-network/route/7","units":"1/256 Core voxel","values":[-15087,-107991,-785612]},{"kind":"route_point","stable_id":"tunnel-network/route/8","units":"1/256 Core voxel","values":[-12857,-92984,-787926]},{"kind":"route_point","stable_id":"tunnel-network/route/9","units":"1/256 Core voxel","values":[-10626,-77977,-790239]},{"kind":"route_point","stable_id":"tunnel-network/route/10","units":"1/256 Core voxel","values":[-8395,-62969,-792553]},{"kind":"route_point","stable_id":"tunnel-network/route/11","units":"1/256 Core voxel","values":[-6165,-47962,-794866]},{"kind":"route_point","stable_id":"tunnel-network/route/12","units":"1/256 Core voxel","values":[-3934,-32955,-797180]},{"kind":"route_point","stable_id":"tunnel-network/route/13","units":"1/256 Core voxel","values":[-1703,-17947,-799493]},{"kind":"route_point","stable_id":"tunnel-network/route/14","units":"1/256 Core voxel","values":[-588,-10444,-800650]},{"kind":"route_point","stable_id":"tunnel-network/route/15","units":"1/256 Core voxel","values":[527,-2940,-801807]},{"kind":"route_point","stable_id":"tunnel-network/route/16","units":"1/256 Core voxel","values":[2758,12067,-804120]},{"kind":"route_point","stable_id":"tunnel-network/route/17","units":"1/256 Core voxel","values":[4989,27075,-806434]},{"kind":"route_point","stable_id":"tunnel-network/route/18","units":"1/256 Core voxel","values":[7219,42082,-808747]},{"kind":"route_point","stable_id":"tunnel-network/route/19","units":"1/256 Core voxel","values":[9450,57089,-811061]},{"kind":"route_point","stable_id":"tunnel-network/route/20","units":"1/256 Core voxel","values":[11681,72097,-813374]},{"kind":"route_point","stable_id":"tunnel-network/route/21","units":"1/256 Core voxel","values":[13911,87104,-815688]},{"kind":"route_point","stable_id":"tunnel-network/route/22","units":"1/256 Core voxel","values":[8550,73930,-817857]},{"kind":"route_point","stable_id":"tunnel-network/route/23","units":"1/256 Core voxel","values":[3190,60757,-820025]},{"kind":"route_point","stable_id":"tunnel-network/route/24","units":"1/256 Core voxel","values":[-2171,47583,-822194]},{"kind":"route_point","stable_id":"tunnel-network/route/25","units":"1/256 Core voxel","values":[-7532,34409,-824363]},{"kind":"route_point","stable_id":"tunnel-network/route/26","units":"1/256 Core voxel","values":[-12893,21236,-826532]},{"kind":"route_point","stable_id":"tunnel-network/route/27","units":"1/256 Core voxel","values":[-18254,8062,-828700]},{"kind":"route_point","stable_id":"tunnel-network/route/998-departure-inner","units":"1/256 Core voxel","values":[-23615,-5112,-830869]},{"kind":"route_point","stable_id":"tunnel-network/route/999-departure","units":"1/256 Core voxel","values":[-20544,-5190,-830869]}]},{"stable_id":"owner-world/seed/0/strate/3/generator/VerticalShafts","order":3,"strate_index":3,"generator_kind":"VerticalShafts","recipe_id":"legacy.VerticalShafts","recipe_version":0,"interior_bottom_um":-1000000000,"interior_top_um":-872000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":-872500000,"top_um":-872000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-999500000,"top_um":-872500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-1000000000,"top_um":-999500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":2,"transition_width_um":16000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":4000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"resolved manager slot scaled by ResolvedLayoutSnapshotV3 chunk width","query_dispatch_path":"legacy Core route selects manager chunk by floor(CoreZ / resolved layout chunk width)","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[]}],"passages":[{"stable_id":"passage/0-1/0","upper_strate_index":0,"lower_strate_index":1,"style":1,"placement_radius_q":38400,"upper_mouth_q":[8192,16384,-381197],"lower_mouth_q":[26457,3276,-754688],"mouth_radius_q":4608,"mid_radius_q":3584,"control_points":[{"position_q":[8192,16384,-380818],"radius_q":1536},{"position_q":[917,-21635,-385895],"radius_q":3584},{"position_q":[-7484,-19826,-387477],"radius_q":3584},{"position_q":[-14728,-15190,-389060],"radius_q":3584},{"position_q":[-19949,-8188,-390668],"radius_q":3584},{"position_q":[-22342,455,-392319],"radius_q":3584},{"position_q":[-21299,9658,-394024],"radius_q":3584},{"position_q":[-16638,18010,-395785],"radius_q":3584},{"position_q":[-8834,23996,-397595],"radius_q":3584},{"position_q":[917,26365,-399442],"radius_q":3584},{"position_q":[10885,24520,-401308],"radius_q":3584},{"position_q":[19213,18751,-403173],"radius_q":3584},{"position_q":[24397,10181,-405017],"radius_q":3584},{"position_q":[25658,455,-406822],"radius_q":3584},{"position_q":[23047,-8711,-408577],"radius_q":3584},{"position_q":[17303,-15931,-410275],"radius_q":3584},{"position_q":[9535,-20350,-411920],"radius_q":3584},{"position_q":[1023,-22825,-413552],"radius_q":3584},{"position_q":[-7513,-20559,-415177],"radius_q":3584},{"position_q":[-14529,-15504,-416769],"radius_q":3584},{"position_q":[-19292,-8367,-418349],"radius_q":3584},{"position_q":[-21267,48,-419940],"radius_q":3584},{"position_q":[-20089,8793,-421564],"radius_q":3584},{"position_q":[-15656,16727,-423237],"radius_q":3584},{"position_q":[-8310,22578,-424966],"radius_q":3584},{"position_q":[1023,25175,-426749],"radius_q":3584},{"position_q":[10856,23787,-428577],"radius_q":3584},{"position_q":[19412,18437,-430435],"radius_q":3584},{"position_q":[25054,10002,-432303],"radius_q":3584},{"position_q":[26733,48,-434161],"radius_q":3584},{"position_q":[24257,-9576,-435990],"radius_q":3584},{"position_q":[18285,-17214,-437775],"radius_q":3584},{"position_q":[10059,-21768,-439506],"radius_q":3584},{"position_q":[956,-24440,-441252],"radius_q":3584},{"position_q":[-7955,-21879,-442959],"radius_q":3584},{"position_q":[-15028,-16351,-444611],"radius_q":3584},{"position_q":[-19496,-8840,-446220],"radius_q":3584},{"position_q":[-20998,-368,-447804],"radius_q":3584},{"position_q":[-19445,8082,-449386],"radius_q":3584},{"position_q":[-14956,15543,-450988],"radius_q":3584},{"position_q":[-7904,21020,-452632],"radius_q":3584},{"position_q":[956,23560,-454329],"radius_q":3584},{"position_q":[10414,22467,-456081],"radius_q":3584},{"position_q":[18913,17590,-457885],"radius_q":3584},{"position_q":[24850,9530,-459728],"radius_q":3584},{"position_q":[27002,-368,-461592],"radius_q":3584},{"position_q":[24901,-10287,-463459],"radius_q":3584},{"position_q":[18986,-18398,-465307],"radius_q":3584},{"position_q":[10465,-23326,-467119],"radius_q":3584},{"position_q":[727,-25966,-468976],"radius_q":3584},{"position_q":[-8660,-23382,-470768],"radius_q":3584},{"position_q":[-16053,-17501,-472507],"radius_q":3584},{"position_q":[-20498,-9513,-474190],"radius_q":3584},{"position_q":[-21647,-721,-475822],"radius_q":3584},{"position_q":[-19618,7706,-477418],"radius_q":3584},{"position_q":[-14808,14814,-478997],"radius_q":3584},{"position_q":[-7780,19814,-480585],"radius_q":3584},{"position_q":[727,22034,-482204],"radius_q":3584},{"position_q":[9709,20964,-483869],"radius_q":3584},{"position_q":[17888,16440,-485589],"radius_q":3584},{"position_q":[23848,8856,-487365],"radius_q":3584},{"position_q":[26353,-721,-489187],"radius_q":3584},{"position_q":[24728,-10663,-491041],"radius_q":3584},{"position_q":[19133,-19128,-492909],"radius_q":3584},{"position_q":[10589,-24532,-494770],"radius_q":3584},{"position_q":[375,-26910,-496701],"radius_q":3584},{"position_q":[-9413,-24583,-498553],"radius_q":3584},{"position_q":[-17272,-18600,-500371],"radius_q":3584},{"position_q":[-21974,-10210,-502141],"radius_q":3584},{"position_q":[-23021,-953,-503856],"radius_q":3584},{"position_q":[-20591,7731,-505516],"radius_q":3584},{"position_q":[-15315,14737,-507131],"radius_q":3584},{"position_q":[-8029,19336,-508717],"radius_q":3584},{"position_q":[375,21090,-510297],"radius_q":3584},{"position_q":[8956,19763,-511896],"radius_q":3584},{"position_q":[16669,15341,-513532],"radius_q":3584},{"position_q":[22372,8159,-515220],"radius_q":3584},{"position_q":[24979,-953,-516965],"radius_q":3584},{"position_q":[23755,-10638,-518762],"radius_q":3584},{"position_q":[18626,-19204,-520600],"radius_q":3584},{"position_q":[10340,-25010,-522462],"radius_q":3584},{"position_q":[-40,-26968,-524407],"radius_q":3584},{"position_q":[-10007,-25083,-526274],"radius_q":3584},{"position_q":[-18304,-19287,-528137],"radius_q":3584},{"position_q":[-23449,-10719,-529977],"radius_q":3584},{"position_q":[-24682,-1023,-531776],"radius_q":3584},{"position_q":[-22073,8103,-533523],"radius_q":3584},{"position_q":[-16359,15296,-535214],"radius_q":3584},{"position_q":[-8631,19716,-536853],"radius_q":3584},{"position_q":[-40,21032,-538453],"radius_q":3584},{"position_q":[8363,19263,-540034],"radius_q":3584},{"position_q":[15637,14654,-541619],"radius_q":3584},{"position_q":[20897,7650,-543232],"radius_q":3584},{"position_q":[23318,-1023,-544889],"radius_q":3584},{"position_q":[22273,-10266,-546601],"radius_q":3584},{"position_q":[17582,-18646,-548369],"radius_q":3584},{"position_q":[9738,-24629,-550185],"radius_q":3584},{"position_q":[-449,-26134,-552081],"radius_q":3584},{"position_q":[-10304,-24712,-553914],"radius_q":3584},{"position_q":[-18851,-19323,-555774],"radius_q":3584},{"position_q":[-24459,-10866,-557642],"radius_q":3584},{"position_q":[-26099,-920,-559497],"radius_q":3584},{"position_q":[-23601,8670,-561322],"radius_q":3584},{"position_q":[-17638,16268,-563100],"radius_q":3584},{"position_q":[-9446,20800,-564823],"radius_q":3584},{"position_q":[-449,21866,-566491],"radius_q":3584},{"position_q":[8065,19634,-568111],"radius_q":3584},{"position_q":[15090,14619,-569700],"radius_q":3584},{"position_q":[19887,7503,-571280],"radius_q":3584},{"position_q":[21901,-920,-572874],"radius_q":3584},{"position_q":[20745,-9699,-574504],"radius_q":3584},{"position_q":[16304,-17673,-576184],"radius_q":3584},{"position_q":[8923,-23546,-577920],"radius_q":3584},{"position_q":[-781,-24695,-579719],"radius_q":3584},{"position_q":[-10277,-23587,-581479],"radius_q":3584},{"position_q":[-18792,-18673,-583289],"radius_q":3584},{"position_q":[-24714,-10575,-585135],"radius_q":3584},{"position_q":[-26829,-662,-587001],"radius_q":3584},{"position_q":[-24690,9242,-588867],"radius_q":3584},{"position_q":[-18759,17316,-590711],"radius_q":3584},{"position_q":[-10254,22207,-592517],"radius_q":3584},{"position_q":[-781,23305,-594272],"radius_q":3584},{"position_q":[8092,20759,-595972],"radius_q":3584},{"position_q":[15149,15268,-597618],"radius_q":3584},{"position_q":[19632,7794,-599222],"radius_q":3584},{"position_q":[21171,-662,-600804],"radius_q":3584},{"position_q":[19656,-9127,-602387],"radius_q":3584},{"position_q":[15182,-16625,-603994],"radius_q":3584},{"position_q":[8115,-22139,-605644],"radius_q":3584},{"position_q":[-982,-23132,-607329],"radius_q":3584},{"position_q":[-10003,-22072,-609001],"radius_q":3584},{"position_q":[-18216,-17526,-610729],"radius_q":3584},{"position_q":[-24186,-9903,-612512],"radius_q":3584},{"position_q":[-26670,-292,-614339],"radius_q":3584},{"position_q":[-25005,9660,-616196],"radius_q":3584},{"position_q":[-19376,18103,-618064],"radius_q":3584},{"position_q":[-10823,23468,-619923],"radius_q":3584},{"position_q":[-982,24868,-621753],"radius_q":3584},{"position_q":[8366,22274,-623538],"radius_q":3584},{"position_q":[15725,16415,-625270],"radius_q":3584},{"position_q":[20160,8466,-626946],"radius_q":3584},{"position_q":[21330,-292,-628572],"radius_q":3584},{"position_q":[19340,-8709,-630164],"radius_q":3584},{"position_q":[14565,-15838,-631744],"radius_q":3584},{"position_q":[7546,-20878,-633335],"radius_q":3584},{"position_q":[-1016,-21949,-634923],"radius_q":3584},{"position_q":[-9624,-20652,-636525],"radius_q":3584},{"position_q":[-17377,-16233,-638168],"radius_q":3584},{"position_q":[-23110,-9024,-639864],"radius_q":3584},{"position_q":[-25720,128,-641615],"radius_q":3584},{"position_q":[-24470,9843,-643418],"radius_q":3584},{"position_q":[-19300,18412,-645261],"radius_q":3584},{"position_q":[-10984,24192,-647125],"radius_q":3584},{"position_q":[-1016,26051,-648991],"radius_q":3584},{"position_q":[8746,23694,-650840],"radius_q":3584},{"position_q":[16564,17708,-652653],"radius_q":3584},{"position_q":[21236,9346,-654416],"radius_q":3584},{"position_q":[22280,128,-656123],"radius_q":3584},{"position_q":[19876,-8526,-657777],"radius_q":3584},{"position_q":[14641,-15529,-659386],"radius_q":3584},{"position_q":[7386,-20154,-660970],"radius_q":3584},{"position_q":[-878,-21498,-662511],"radius_q":3584},{"position_q":[-9285,-19770,-664091],"radius_q":3584},{"position_q":[-16591,-15186,-665679],"radius_q":3584},{"position_q":[-21892,-8178,-667296],"radius_q":3584},{"position_q":[-24338,526,-668961],"radius_q":3584},{"position_q":[-23288,9809,-670681],"radius_q":3584},{"position_q":[-18566,18214,-672455],"radius_q":3584},{"position_q":[-10682,24194,-674277],"radius_q":3584},{"position_q":[-878,26502,-676131],"radius_q":3584},{"position_q":[9084,24576,-677999],"radius_q":3584},{"position_q":[17350,18755,-679860],"radius_q":3584},{"position_q":[22454,10191,-681695],"radius_q":3584},{"position_q":[23662,526,-683488],"radius_q":3584},{"position_q":[21058,-8560,-685228],"radius_q":3584},{"position_q":[15375,-15727,-686911],"radius_q":3584},{"position_q":[7687,-20152,-688544],"radius_q":3584},{"position_q":[-592,-21868,-690101],"radius_q":3584},{"position_q":[-9086,-19670,-691716],"radius_q":3584},{"position_q":[-16122,-14694,-693302],"radius_q":3584},{"position_q":[-20954,-7599,-694882],"radius_q":3584},{"position_q":[-23007,835,-696480],"radius_q":3584},{"position_q":[-21871,9649,-698116],"radius_q":3584},{"position_q":[-17419,17662,-699803],"radius_q":3584},{"position_q":[-10003,23555,-701547],"radius_q":3584},{"position_q":[-592,26132,-703343],"radius_q":3584},{"position_q":[9283,24675,-705181],"radius_q":3584},{"position_q":[17819,19247,-707043],"radius_q":3584},{"position_q":[23392,10770,-708910],"radius_q":3584},{"position_q":[24993,835,-710762],"radius_q":3584},{"position_q":[22475,-8720,-712581],"radius_q":3584},{"position_q":[16523,-16280,-714353],"radius_q":3584},{"position_q":[8366,-20791,-716069],"radius_q":3584},{"position_q":[-206,-22859,-717692],"radius_q":3584},{"position_q":[-9043,-20329,-719384],"radius_q":3584},{"position_q":[-16086,-14877,-721023],"radius_q":3584},{"position_q":[-20586,-7439,-722624],"radius_q":3584},{"position_q":[-22163,1003,-724205],"radius_q":3584},{"position_q":[-20684,9485,-725789],"radius_q":3584},{"position_q":[-16224,17021,-727401],"radius_q":3584},{"position_q":[-9140,22572,-729058],"radius_q":3584},{"position_q":[-206,25141,-730769],"radius_q":3584},{"position_q":[9327,24016,-732536],"radius_q":3584},{"position_q":[17855,19064,-734352],"radius_q":3584},{"position_q":[23760,10930,-736202],"radius_q":3584},{"position_q":[25837,1003,-738069],"radius_q":3584},{"position_q":[23662,-8884,-739932],"radius_q":3584},{"position_q":[17717,-16920,-741772],"radius_q":3584},{"position_q":[9229,-21774,-743572],"radius_q":3584},{"position_q":[215,-24070,-745285],"radius_q":3584},{"position_q":[26457,3276,-751237],"radius_q":4608}]},{"stable_id":"passage/1-2/0","upper_strate_index":1,"lower_strate_index":2,"style":1,"placement_radius_q":24576,"upper_mouth_q":[19626,-14541,-749774],"lower_mouth_q":[19626,-14541,-771455],"mouth_radius_q":4608,"mid_radius_q":3072,"control_points":[{"position_q":[19626,-14541,-746323],"radius_q":4608},{"position_q":[11028,-10341,-748134],"radius_q":4608},{"position_q":[14628,-5642,-750790],"radius_q":3072},{"position_q":[16189,228,-751940],"radius_q":3072},{"position_q":[15303,6377,-753115],"radius_q":3072},{"position_q":[11982,11751,-754311],"radius_q":3072},{"position_q":[6728,15364,-755517],"radius_q":3072},{"position_q":[459,16542,-756725],"radius_q":3072},{"position_q":[-5703,15102,-757922],"radius_q":3072},{"position_q":[-10695,11381,-759100],"radius_q":3072},{"position_q":[-13754,6115,-760253],"radius_q":3072},{"position_q":[-14531,228,-761377],"radius_q":3072},{"position_q":[-13079,-5380,-762473],"radius_q":3072},{"position_q":[-9740,-9971,-763547],"radius_q":3072},{"position_q":[-5028,-13017,-764609],"radius_q":3072},{"position_q":[459,-14178,-765670],"radius_q":3072},{"position_q":[6053,-13279,-766743],"radius_q":3072},{"position_q":[11518,-10983,-767571],"radius_q":3365},{"position_q":[19626,-14541,-768004],"radius_q":4608}]},{"stable_id":"passage/2-3/0","upper_strate_index":2,"lower_strate_index":3,"style":1,"placement_radius_q":24576,"upper_mouth_q":[-20544,-5190,-831893],"lower_mouth_q":[-20544,-5190,-921962],"mouth_radius_q":4608,"mid_radius_q":3072,"control_points":[{"position_q":[-20544,-5190,-828442],"radius_q":4608},{"position_q":[-13079,-5380,-829802],"radius_q":4608},{"position_q":[-9740,-9971,-832372],"radius_q":3072},{"position_q":[-5028,-13017,-833394],"radius_q":3072},{"position_q":[459,-14178,-834416],"radius_q":3072},{"position_q":[6053,-13279,-835448],"radius_q":3072},{"position_q":[11028,-10341,-836500],"radius_q":3072},{"position_q":[14628,-5642,-837578],"radius_q":3072},{"position_q":[16189,228,-838685],"radius_q":3072},{"position_q":[15303,6377,-839816],"radius_q":3072},{"position_q":[11982,11751,-840967],"radius_q":3072},{"position_q":[6728,15364,-842128],"radius_q":3072},{"position_q":[459,16542,-843290],"radius_q":3072},{"position_q":[-5703,15102,-844443],"radius_q":3072},{"position_q":[-10695,11381,-845577],"radius_q":3072},{"position_q":[-13754,6115,-846687],"radius_q":3072},{"position_q":[-14531,228,-847768],"radius_q":3072},{"position_q":[-12750,-5469,-848855],"radius_q":3072},{"position_q":[-9640,-10128,-849876],"radius_q":3072},{"position_q":[-5042,-13384,-850902],"radius_q":3072},{"position_q":[511,-14773,-851945],"radius_q":3072},{"position_q":[6316,-13988,-853012],"radius_q":3072},{"position_q":[11518,-10983,-854106],"radius_q":3072},{"position_q":[15233,-6074,-855227],"radius_q":3072},{"position_q":[16726,24,-856371],"radius_q":3072},{"position_q":[15631,6287,-857529],"radius_q":3072},{"position_q":[12082,11594,-858692],"radius_q":3072},{"position_q":[6714,14998,-859850],"radius_q":3072},{"position_q":[511,15947,-860993],"radius_q":3072},{"position_q":[-5441,14393,-862113],"radius_q":3072},{"position_q":[-10204,10739,-863207],"radius_q":3072},{"position_q":[-13149,5682,-864273],"radius_q":3072},{"position_q":[-13994,24,-865315],"radius_q":3072},{"position_q":[-12852,-5706,-866379],"radius_q":3072},{"position_q":[-9890,-10552,-867414],"radius_q":3072},{"position_q":[-5263,-14044,-868469],"radius_q":3072},{"position_q":[478,-15580,-869552],"radius_q":3072},{"position_q":[6519,-14767,-870662],"radius_q":3072},{"position_q":[11869,-11575,-871797],"radius_q":3072},{"position_q":[15555,-6429,-872950],"radius_q":3072},{"position_q":[16861,-184,-874112],"radius_q":3072},{"position_q":[15529,6051,-875273],"radius_q":3072},{"position_q":[11833,11171,-876423],"radius_q":3072},{"position_q":[6493,14338,-877554],"radius_q":3072},{"position_q":[478,15140,-878660],"radius_q":3072},{"position_q":[-5238,13614,-879737],"radius_q":3072},{"position_q":[-9854,10147,-880789],"radius_q":3072},{"position_q":[-12827,5327,-881821],"radius_q":3072},{"position_q":[-13859,-184,-882842],"radius_q":3072},{"position_q":[-13353,-6043,-883913],"radius_q":3072},{"position_q":[-10402,-11127,-884984],"radius_q":3072},{"position_q":[-5616,-14795,-886082],"radius_q":3072},{"position_q":[363,-16343,-887207],"radius_q":3072},{"position_q":[6581,-15370,-888353],"radius_q":3072},{"position_q":[11942,-11940,-889513],"radius_q":3072},{"position_q":[15468,-6618,-890675],"radius_q":3072},{"position_q":[16536,-361,-891832],"radius_q":3072},{"position_q":[15028,5714,-892972],"radius_q":3072},{"position_q":[11320,10596,-894088],"radius_q":3072},{"position_q":[6140,13586,-895178],"radius_q":3072},{"position_q":[363,14377,-896240],"radius_q":3072},{"position_q":[-5176,13011,-897279],"radius_q":3072},{"position_q":[-9780,9783,-898303],"radius_q":3072},{"position_q":[-12913,5139,-899324],"radius_q":3072},{"position_q":[-14184,-361,-900352],"radius_q":3072},{"position_q":[-14091,-6391,-901450],"radius_q":3072},{"position_q":[-11012,-11676,-902564],"radius_q":3072},{"position_q":[-5992,-15396,-903703],"radius_q":3072},{"position_q":[187,-16815,-904857],"radius_q":3072},{"position_q":[6456,-15609,-906020],"radius_q":3072},{"position_q":[11689,-11978,-907180],"radius_q":3072},{"position_q":[14982,-6605,-908328],"radius_q":3072},{"position_q":[15849,-476,-909456],"radius_q":3072},{"position_q":[14290,5365,-910557],"radius_q":3072},{"position_q":[10710,10047,-911630],"radius_q":3072},{"position_q":[5764,12986,-912678],"radius_q":3072},{"position_q":[187,13905,-913708],"radius_q":3072},{"position_q":[-5301,12772,-914729],"radius_q":3072},{"position_q":[-10034,9744,-915752],"radius_q":3072},{"position_q":[-13400,5152,-916789],"radius_q":3072},{"position_q":[-14871,-476,-917848],"radius_q":3073},{"position_q":[-14828,-6646,-918159],"radius_q":3886},{"position_q":[-20544,-5190,-918511],"radius_q":4608}]}],"cavern_graph_vocabulary_version":1,"cavern_rooms":[{"stable_id":"tunnel-network/strate-2/room-0/primitive-0","strate_index":2,"center_um":[16677955,-28270663,-752557115],"half_extents_um":[4387063,4387063,3729030],"floor_um":-755491928,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2496338057380503,"bounds_min_um":[10540892,-34407726,-758036145],"bounds_max_um":[22815018,-22133600,-747078085]},{"stable_id":"tunnel-network/strate-2/room-0/primitive-1","strate_index":2,"center_um":[17020975,-26789220,-753339029],"half_extents_um":[2966903,2546513,2521885],"floor_um":-755491928,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1298697264498033,"bounds_min_um":[12304072,-31085733,-757610914],"bounds_max_um":[21737878,-22492707,-749067144]},{"stable_id":"tunnel-network/strate-2/room-0/primitive-2","strate_index":2,"center_um":[16450136,-29254574,-752994543],"half_extents_um":[2993025,3957768,3364126],"floor_um":-755491928,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1371456813936030,"bounds_min_um":[11707111,-34962342,-758108669],"bounds_max_um":[21193161,-23546806,-747880417]},{"stable_id":"tunnel-network/strate-2/room-1/primitive-0","strate_index":2,"center_um":[11442663,-41135576,-755226186],"half_extents_um":[3654710,3654710,3106525],"floor_um":-757609833,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":234826507654047,"bounds_min_um":[6037953,-46540286,-760082711],"bounds_max_um":[16847373,-35730866,-750369661]},{"stable_id":"tunnel-network/strate-2/room-1/primitive-1","strate_index":2,"center_um":[11594838,-40478358,-755656092],"half_extents_um":[2756758,2146566,2343261],"floor_um":-757609833,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7709850420954135,"bounds_min_um":[7088080,-44374924,-759749353],"bounds_max_um":[16101596,-36581792,-751562831]},{"stable_id":"tunnel-network/strate-2/room-1/primitive-2","strate_index":2,"center_um":[11266058,-41898301,-755492594],"half_extents_um":[2900022,2484772,2465036],"floor_um":-757609833,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8413310229855003,"bounds_min_um":[6616036,-46133073,-759707630],"bounds_max_um":[15916080,-37663529,-751277558]},{"stable_id":"tunnel-network/strate-2/room-10/primitive-0","strate_index":2,"center_um":[-6020215,-46837832,-775511224],"half_extents_um":[2387849,2387849,2029686],"floor_um":-777236531,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6716714120628840,"bounds_min_um":[-10158064,-50975681,-779290910],"bounds_max_um":[-1882366,-42699983,-771731538]},{"stable_id":"tunnel-network/strate-2/room-10/primitive-1","strate_index":2,"center_um":[-5872692,-46200705,-775741796],"half_extents_um":[1826203,1962098,1667795],"floor_um":-777236531,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":949841926828163,"bounds_min_um":[-9448895,-49912803,-779159591],"bounds_max_um":[-2296489,-42488607,-772324001]},{"stable_id":"tunnel-network/strate-2/room-11/primitive-0","strate_index":2,"center_um":[-3841819,-32182255,-777631544],"half_extents_um":[2861697,2861697,2432459],"floor_um":-779495825,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8857025401709256,"bounds_min_um":[-8453516,-36793952,-781814003],"bounds_max_um":[769878,-27570558,-773449085]},{"stable_id":"tunnel-network/strate-2/room-11/primitive-1","strate_index":2,"center_um":[-3658549,-31390746,-777838391],"half_extents_um":[2142648,1726483,1821263],"floor_um":-779495825,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2648511638984815,"bounds_min_um":[-7551197,-34867229,-781409654],"bounds_max_um":[234099,-27914263,-774267128]},{"stable_id":"tunnel-network/strate-2/room-11/primitive-2","strate_index":2,"center_um":[-4066656,-33153284,-778160634],"half_extents_um":[2084659,2032332,1771972],"floor_um":-779495825,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3923835578665842,"bounds_min_um":[-7901315,-36935616,-781682606],"bounds_max_um":[-231997,-29370952,-774638662]},{"stable_id":"tunnel-network/strate-2/room-12/primitive-0","strate_index":2,"center_um":[-1663423,-17526678,-778680904],"half_extents_um":[4903164,4903164,4167719],"floor_um":-781755118,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":974453298205719,"bounds_min_um":[-8316587,-24179842,-784598623],"bounds_max_um":[4989741,-10873514,-772763185]},{"stable_id":"tunnel-network/strate-2/room-12/primitive-1","strate_index":2,"center_um":[-1344755,-16150408,-779107843],"half_extents_um":[3265060,2532244,2775320],"floor_um":-781755118,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4287246203240359,"bounds_min_um":[-6359815,-20432652,-783633163],"bounds_max_um":[3670305,-11868164,-774582523]},{"stable_id":"tunnel-network/strate-2/room-13/primitive-0","strate_index":2,"center_um":[-574225,-10198889,-778588700],"half_extents_um":[6750000,6750000,5737541],"floor_um":-782884764,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4237475639875859,"bounds_min_um":[-9074225,-18698889,-786076241],"bounds_max_um":[7925775,-1698889,-771101159]},{"stable_id":"tunnel-network/strate-2/room-13/primitive-1","strate_index":2,"center_um":[-162640,-8421330,-778752776],"half_extents_um":[5324214,4346640,4525614],"floor_um":-782884764,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4079198432664629,"bounds_min_um":[-7236854,-14517970,-785028390],"bounds_max_um":[6911574,-2324690,-772477162]},{"stable_id":"tunnel-network/strate-2/room-14/primitive-0","strate_index":2,"center_um":[514974,-2871100,-782190765],"half_extents_um":[2513484,2513484,2136476],"floor_um":-784014411,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5513777901835640,"bounds_min_um":[-3748510,-7134584,-786077241],"bounds_max_um":[4778458,1392384,-778304289]},{"stable_id":"tunnel-network/strate-2/room-14/primitive-1","strate_index":2,"center_um":[708827,-2033881,-782363850],"half_extents_um":[1548535,2147599,1825472],"floor_um":-784014411,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4736343399867725,"bounds_min_um":[-2589708,-5931480,-785939322],"bounds_max_um":[4007362,1863718,-778788378]},{"stable_id":"tunnel-network/strate-2/room-15/primitive-0","strate_index":2,"center_um":[2693370,11784477,-784624347],"half_extents_um":[2568657,2568657,2183374],"floor_um":-786273704,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7277496228914001,"bounds_min_um":[-1625287,7465820,-788557721],"bounds_max_um":[7012027,16103134,-780690973]},{"stable_id":"tunnel-network/strate-2/room-15/primitive-1","strate_index":2,"center_um":[2825663,12355828,-784400364],"half_extents_um":[2161775,2315655,1968320],"floor_um":-786273704,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3986507348598952,"bounds_min_um":[-1086112,8290173,-788118684],"bounds_max_um":[6737438,16421483,-780682044]},{"stable_id":"tunnel-network/strate-2/room-16/primitive-0","strate_index":2,"center_um":[4871766,26440054,-787036556],"half_extents_um":[2218825,2218825,1886014],"floor_um":-788532998,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":619731278775179,"bounds_min_um":[902941,22471229,-790672570],"bounds_max_um":[8840591,30408879,-783400542]},{"stable_id":"tunnel-network/strate-2/room-16/primitive-1","strate_index":2,"center_um":[4994168,26968687,-787098831],"half_extents_um":[1879209,1675534,1597339],"floor_um":-788532998,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1702248410764664,"bounds_min_um":[1364959,23543153,-790446170],"bounds_max_um":[8623377,30394221,-783751492]},{"stable_id":"tunnel-network/strate-2/room-16/primitive-2","strate_index":2,"center_um":[4768977,25996124,-787103674],"half_extents_um":[2034374,1842627,1729230],"floor_um":-788532998,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5564952308868066,"bounds_min_um":[984603,22403497,-790582904],"bounds_max_um":[8553351,29588751,-783624444]},{"stable_id":"tunnel-network/strate-2/room-17/primitive-0","strate_index":2,"center_um":[7050163,41095632,-787158865],"half_extents_um":[4887035,4887035,4154009],"floor_um":-790792291,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8507542348025464,"bounds_min_um":[413128,34458597,-793062874],"bounds_max_um":[13687198,47732667,-781254856]},{"stable_id":"tunnel-network/strate-2/room-17/primitive-1","strate_index":2,"center_um":[7424471,42712199,-787452738],"half_extents_um":[4308519,3695270,3662267],"floor_um":-790792291,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":137070891032852,"bounds_min_um":[1365952,37266929,-792865005],"bounds_max_um":[13482990,48157469,-782040471]},{"stable_id":"tunnel-network/strate-2/room-17/primitive-2","strate_index":2,"center_um":[6826928,40131519,-788274898],"half_extents_um":[2696970,3720085,3162094],"floor_um":-790792291,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2276262411431109,"bounds_min_um":[2379958,34661434,-793186992],"bounds_max_um":[11273898,45601604,-783362804]},{"stable_id":"tunnel-network/strate-2/room-18/primitive-0","strate_index":2,"center_um":[9228559,55751209,-789503179],"half_extents_um":[4802420,4802420,4082086],"floor_um":-793051584,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4360877377612790,"bounds_min_um":[2676139,49198789,-795335265],"bounds_max_um":[15780979,62303629,-783671093]},{"stable_id":"tunnel-network/strate-2/room-18/primitive-1","strate_index":2,"center_um":[9502049,56932361,-789787541],"half_extents_um":[4092565,3247399,3478705],"floor_um":-793051584,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5597263948205663,"bounds_min_um":[3659484,51934962,-795016246],"bounds_max_um":[15344614,61929760,-784558836]},{"stable_id":"tunnel-network/strate-2/room-19/primitive-0","strate_index":2,"center_um":[11406955,70406787,-792668680],"half_extents_um":[3472456,3472456,2951608],"floor_um":-795310877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8346153217617005,"bounds_min_um":[6184499,65184331,-797370288],"bounds_max_um":[16629411,75629243,-787967072]},{"stable_id":"tunnel-network/strate-2/room-19/primitive-1","strate_index":2,"center_um":[11596697,71226250,-793261802],"half_extents_um":[2431662,2355598,2066927],"floor_um":-795310877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4360072178059799,"bounds_min_um":[7415035,67120652,-797078729],"bounds_max_um":[15778359,75331848,-789444875]},{"stable_id":"tunnel-network/strate-2/room-19/primitive-2","strate_index":2,"center_um":[11170897,69387297,-793090793],"half_extents_um":[2727321,2347079,2318239],"floor_um":-795310877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1461371555216243,"bounds_min_um":[6693576,65290218,-797159032],"bounds_max_um":[15648218,73484376,-789022554]},{"stable_id":"tunnel-network/strate-2/room-19/side-1/primitive-0","strate_index":2,"center_um":[13211571,78200589,-793479112],"half_extents_um":[2809826,2809826,2388369],"floor_um":-795310877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4071746333343996,"bounds_min_um":[8651745,73640763,-797617481],"bounds_max_um":[17771397,82760415,-789340743]},{"stable_id":"tunnel-network/strate-2/room-19/side-1/primitive-1","strate_index":2,"center_um":[13377214,78915974,-793937459],"half_extents_um":[1876747,1603207,1595246],"floor_um":-795310877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3953725205313751,"bounds_min_um":[9750467,75562767,-797282705],"bounds_max_um":[17003961,82269181,-790592213]},{"stable_id":"tunnel-network/strate-2/room-2/primitive-0","strate_index":2,"center_um":[6207370,-54000489,-756467282],"half_extents_um":[4425839,4425839,3761990],"floor_um":-759727738,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6192132064124667,"bounds_min_um":[31531,-60176328,-761979272],"bounds_max_um":[12383209,-47824650,-750955292]},{"stable_id":"tunnel-network/strate-2/room-2/primitive-1","strate_index":2,"center_um":[6392659,-53200258,-756792314],"half_extents_um":[4020641,2949099,3417569],"floor_um":-759727738,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2990344898337824,"bounds_min_um":[622018,-57899357,-761959883],"bounds_max_um":[12163300,-48501159,-751624745]},{"stable_id":"tunnel-network/strate-2/room-2/primitive-2","strate_index":2,"center_um":[5871403,-55451471,-757369489],"half_extents_um":[2488155,3301955,2806681],"floor_um":-759727738,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1276700820642230,"bounds_min_um":[1633248,-60503426,-761926170],"bounds_max_um":[10109558,-50399516,-752812808]},{"stable_id":"tunnel-network/strate-2/room-20/primitive-0","strate_index":2,"center_um":[13585352,85062365,-795021947],"half_extents_um":[4264406,4264406,3624771],"floor_um":-797570171,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3969348325447,"bounds_min_um":[7570946,79047959,-800396718],"bounds_max_um":[19599758,91076771,-789647176]},{"stable_id":"tunnel-network/strate-2/room-20/primitive-1","strate_index":2,"center_um":[13886798,86364256,-795328785],"half_extents_um":[2534435,3397287,2887714],"floor_um":-797570171,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7891806891759534,"bounds_min_um":[9602363,81216969,-799966499],"bounds_max_um":[18171233,91511543,-790691071]},{"stable_id":"tunnel-network/strate-2/room-20/primitive-2","strate_index":2,"center_um":[13403152,84275475,-794482316],"half_extents_um":[4015969,3449752,3413598],"floor_um":-797570171,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8495668019456799,"bounds_min_um":[7637183,79075723,-799645914],"bounds_max_um":[19169121,89475227,-789318718]},{"stable_id":"tunnel-network/strate-2/room-21/primitive-0","strate_index":2,"center_um":[8350060,72197453,-796679491],"half_extents_um":[4178024,4178024,3551345],"floor_um":-799688076,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8353780683996427,"bounds_min_um":[2422036,66269429,-801980836],"bounds_max_um":[14278084,78125477,-791378146]},{"stable_id":"tunnel-network/strate-2/room-21/primitive-1","strate_index":2,"center_um":[8546833,73047281,-796393638],"half_extents_um":[4034901,3040336,3429690],"floor_um":-799688076,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6275606997446609,"bounds_min_um":[2761932,68256945,-801573328],"bounds_max_um":[14331734,77837617,-791213948]},{"stable_id":"tunnel-network/strate-2/room-22/primitive-0","strate_index":2,"center_um":[3114767,59332540,-799940824],"half_extents_um":[3002189,3002189,2551878],"floor_um":-801805981,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4725229704537249,"bounds_min_um":[-1637422,54580351,-804242702],"bounds_max_um":[7866956,64084729,-795638946]},{"stable_id":"tunnel-network/strate-2/room-22/primitive-1","strate_index":2,"center_um":[3318887,60214097,-799893458],"half_extents_um":[1929253,2362089,2007790],"floor_um":-801805981,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2396133457513924,"bounds_min_um":[-360366,56102008,-803651248],"bounds_max_um":[6998140,64326186,-796135668]},{"stable_id":"tunnel-network/strate-2/room-23/primitive-0","strate_index":2,"center_um":[-2120525,46467627,-800779997],"half_extents_um":[4857437,4857437,4128851],"floor_um":-803923886,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5610586952733464,"bounds_min_um":[-8727962,39860190,-806658848],"bounds_max_um":[4486912,53075064,-794901146]},{"stable_id":"tunnel-network/strate-2/room-23/primitive-1","strate_index":2,"center_um":[-1761492,48018225,-801022107],"half_extents_um":[3810511,3727772,3238957],"floor_um":-803923886,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5164566143628108,"bounds_min_um":[-7322003,42540453,-806011064],"bounds_max_um":[3799019,53495997,-796033150]},{"stable_id":"tunnel-network/strate-2/room-24/primitive-0","strate_index":2,"center_um":[-7355818,33602715,-803832820],"half_extents_um":[3113529,3113529,2646518],"floor_um":-806041791,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":998271559690284,"bounds_min_um":[-12219347,28739186,-808229338],"bounds_max_um":[-2492289,38466244,-799436302]},{"stable_id":"tunnel-network/strate-2/room-24/primitive-1","strate_index":2,"center_um":[-7154770,34471005,-804596690],"half_extents_um":[2007431,1484070,1706328],"floor_um":-806041791,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":170759264075404,"bounds_min_um":[-10912201,31236935,-808053018],"bounds_max_um":[-3397339,37705075,-801140362]},{"stable_id":"tunnel-network/strate-2/room-24/side-2/primitive-0","strate_index":2,"center_um":[-9160434,25808913,-803619374],"half_extents_um":[3926542,3926542,3337584],"floor_um":-806041791,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7408076088762940,"bounds_min_um":[-14836976,20132371,-808706958],"bounds_max_um":[-3483892,31485455,-798531790]},{"stable_id":"tunnel-network/strate-2/room-24/side-2/primitive-1","strate_index":2,"center_um":[-8911672,26883271,-803115311],"half_extents_um":[3345687,3770645,3205071],"floor_um":-806041791,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6480493913793558,"bounds_min_um":[-14007359,21362626,-808070382],"bounds_max_um":[-3815985,32403916,-798160240]},{"stable_id":"tunnel-network/strate-2/room-25/primitive-0","strate_index":2,"center_um":[-12591110,20737802,-805663741],"half_extents_um":[3886887,3886887,3303877],"floor_um":-808159696,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6676136965576361,"bounds_min_um":[-18227997,15100915,-810717618],"bounds_max_um":[-6954223,26374689,-800609864]},{"stable_id":"tunnel-network/strate-2/room-25/primitive-1","strate_index":2,"center_um":[-12362052,21727061,-805367148],"half_extents_um":[3658012,3236315,3109332],"floor_um":-808159696,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4861500673895666,"bounds_min_um":[-17770064,16740746,-810226480],"bounds_max_um":[-6954040,26713376,-800507816]},{"stable_id":"tunnel-network/strate-2/room-25/primitive-2","strate_index":2,"center_um":[-12839139,19666610,-805409435],"half_extents_um":[3735292,3338542,3175020],"floor_um":-808159696,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6799134419770337,"bounds_min_um":[-18324431,14578068,-810334455],"bounds_max_um":[-7353847,24755152,-800484415]},{"stable_id":"tunnel-network/strate-2/room-26/primitive-0","strate_index":2,"center_um":[-17826403,7872889,-807466708],"half_extents_um":[4174468,4174468,3548323],"floor_um":-810277601,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1552637404666204,"bounds_min_um":[-23750871,1948421,-812765031],"bounds_max_um":[-11901935,13797357,-802168385]},{"stable_id":"tunnel-network/strate-2/room-26/primitive-1","strate_index":2,"center_um":[-17609466,8809803,-807391856],"half_extents_um":[3922353,3817255,3334023],"floor_um":-810277601,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1658728811274312,"bounds_min_um":[-23281819,3242548,-812475879],"bounds_max_um":[-11937113,14377058,-802307833]},{"stable_id":"tunnel-network/strate-2/room-3/primitive-0","strate_index":2,"center_um":[972078,-66865401,-759832470],"half_extents_um":[2749441,2749441,2337041],"floor_um":-761845643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2640905445479701,"bounds_min_um":[-3527363,-71364842,-763919511],"bounds_max_um":[5471519,-62365960,-755745429]},{"stable_id":"tunnel-network/strate-2/room-3/primitive-1","strate_index":2,"center_um":[1138515,-66146590,-760278313],"half_extents_um":[2460677,2267460,2091590],"floor_um":-761845643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8829183212444991,"bounds_min_um":[-3072162,-70164050,-764119903],"bounds_max_um":[5349192,-62129130,-756436723]},{"stable_id":"tunnel-network/strate-2/room-3/primitive-2","strate_index":2,"center_um":[831344,-67473205,-760119038],"half_extents_um":[2166879,2081474,1841860],"floor_um":-761845643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8354744626195101,"bounds_min_um":[-3085535,-71304679,-763710898],"bounds_max_um":[4748223,-63641731,-756527178]},{"stable_id":"tunnel-network/strate-2/room-3/side-0/primitive-0","strate_index":2,"center_um":[-832538,-74659203,-759844915],"half_extents_um":[2803968,2803968,2383389],"floor_um":-761845643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2662084728162065,"bounds_min_um":[-5386506,-79213171,-763978304],"bounds_max_um":[3721430,-70105235,-755711526]},{"stable_id":"tunnel-network/strate-2/room-3/side-0/primitive-1","strate_index":2,"center_um":[-664904,-73935223,-760260973],"half_extents_um":[1953412,2382234,2024913],"floor_um":-761845643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8513725474957650,"bounds_min_um":[-4368316,-78067457,-764035886],"bounds_max_um":[3038508,-69802989,-756486060]},{"stable_id":"tunnel-network/strate-2/room-3/side-0/primitive-2","strate_index":2,"center_um":[-1005953,-75408151,-760430148],"half_extents_um":[1889306,1687909,1605921],"floor_um":-761845643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":36217591490443,"bounds_min_um":[-4645259,-78846060,-763786069],"bounds_max_um":[2633353,-71970242,-757074227]},{"stable_id":"tunnel-network/strate-2/room-4/primitive-0","strate_index":2,"center_um":[-4263214,-79730314,-760448100],"half_extents_um":[4852450,4852450,4124612],"floor_um":-763963548,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2845885573645158,"bounds_min_um":[-10865664,-86332764,-766322712],"bounds_max_um":[2339236,-73127864,-754573488]},{"stable_id":"tunnel-network/strate-2/room-4/primitive-1","strate_index":2,"center_um":[-4061518,-78859224,-760931249],"half_extents_um":[3722391,4108989,3492665],"floor_um":-763963548,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5182219280308446,"bounds_min_um":[-9533909,-84718213,-766173914],"bounds_max_um":[1410873,-73000235,-755688584]},{"stable_id":"tunnel-network/strate-2/room-5/primitive-0","strate_index":2,"center_um":[-9498507,-92595227,-764397523],"half_extents_um":[2411668,2411668,2049932],"floor_um":-766081453,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8750423016047589,"bounds_min_um":[-13660175,-96756895,-768197455],"bounds_max_um":[-5336839,-88433559,-760597591]},{"stable_id":"tunnel-network/strate-2/room-5/primitive-1","strate_index":2,"center_um":[-9341942,-91919049,-764422404],"half_extents_um":[1724556,2065056,1755310],"floor_um":-766081453,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6729635284664735,"bounds_min_um":[-12816498,-95734105,-767927714],"bounds_max_um":[-5867386,-88103993,-760917094]},{"stable_id":"tunnel-network/strate-2/room-6/primitive-0","strate_index":2,"center_um":[-14733800,-105460141,-765360115],"half_extents_um":[4733441,4733441,4023453],"floor_um":-768199358,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2729955603759597,"bounds_min_um":[-21217241,-111943582,-771133568],"bounds_max_um":[-8250359,-98976700,-759586662]},{"stable_id":"tunnel-network/strate-2/room-6/primitive-1","strate_index":2,"center_um":[-14423768,-104121167,-765302959],"half_extents_um":[4239627,3129713,3603708],"floor_um":-768199358,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1328250596027720,"bounds_min_um":[-20413395,-109000880,-770656667],"bounds_max_um":[-8434141,-99241454,-759949251]},{"stable_id":"tunnel-network/strate-2/room-7/primitive-0","strate_index":2,"center_um":[-12555404,-90804565,-767303475],"half_extents_um":[4586293,4586293,3898377],"floor_um":-770458652,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6292182390675613,"bounds_min_um":[-18891697,-97140858,-772951852],"bounds_max_um":[-6219111,-84468272,-761655098]},{"stable_id":"tunnel-network/strate-2/room-7/primitive-1","strate_index":2,"center_um":[-12209924,-89312500,-767765873],"half_extents_um":[3710230,3763523,3199017],"floor_um":-770458652,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5824913408104067,"bounds_min_um":[-17670154,-94826023,-772714890],"bounds_max_um":[-6749694,-83798977,-762816856]},{"stable_id":"tunnel-network/strate-2/room-7/primitive-2","strate_index":2,"center_um":[-12865602,-92144253,-768162629],"half_extents_um":[2215184,3010104,2558606],"floor_um":-770458652,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8438796820341117,"bounds_min_um":[-16830786,-96904357,-772471235],"bounds_max_um":[-8900418,-87384149,-763854023]},{"stable_id":"tunnel-network/strate-2/room-8/primitive-0","strate_index":2,"center_um":[-10377008,-76148987,-771350062],"half_extents_um":[2068098,2068098,1757895],"floor_um":-772717945,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4233316435953742,"bounds_min_um":[-14195106,-79967085,-774857957],"bounds_max_um":[-6558910,-72330889,-767842167]},{"stable_id":"tunnel-network/strate-2/room-8/primitive-1","strate_index":2,"center_um":[-10279044,-75725899,-771506003],"half_extents_um":[1198135,1500898,1275772],"floor_um":-772717945,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7809339386482004,"bounds_min_um":[-13227179,-78976797,-774531775],"bounds_max_um":[-7330909,-72475001,-768480231]},{"stable_id":"tunnel-network/strate-2/room-9/primitive-0","strate_index":2,"center_um":[-8198612,-61493410,-773372824],"half_extents_um":[2600735,2600735,2210640],"floor_um":-774977238,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":928141076448714,"bounds_min_um":[-12549347,-65844145,-777333464],"bounds_max_um":[-3847877,-57142675,-769412184]},{"stable_id":"tunnel-network/strate-2/room-9/primitive-1","strate_index":2,"center_um":[-8074326,-60956639,-773282385],"half_extents_um":[2396004,1878239,2036618],"floor_um":-774977238,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5703069001346635,"bounds_min_um":[-12220330,-64584878,-777069003],"bounds_max_um":[-3928322,-57328400,-769495767]}],"cavern_edges":[{"stable_id":"tunnel-network/strate-2/departure-edge","start_anchor_stable_id":"passage/2-3/0/upper-mouth","end_room_stable_id":"tunnel-network/strate-2/room-26/primitive-0","strate_index":2,"control_points_um":[[-23061696,-4992024,-812395507],[-21316599,-703720,-811689539],[-19571501,3584584,-810983570],[-17826403,7872889,-810277601]],"polyline_um":[[-23061696,-4992024,-812395507],[-17826403,7872889,-810277601]],"bore_radius_um":4500000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3854785202241998,"bounds_min_um":[-29311696,-11242024,-814895507],"bounds_max_um":[-11576403,14122889,-804027601]},{"stable_id":"tunnel-network/strate-2/route-edge-0","start_anchor_stable_id":"passage/1-2/0/lower-mouth","end_room_stable_id":"tunnel-network/strate-2/room-0/primitive-0","strate_index":2,"control_points_um":[[21913247,-15405751,-753374023],[20168150,-19694055,-754079991],[18423053,-23982359,-754785959],[16677955,-28270663,-755491928]],"polyline_um":[[21913247,-15405751,-753374023],[16677955,-28270663,-755491928]],"bore_radius_um":4500000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3289607608418144,"bounds_min_um":[10427955,-34520663,-757991928],"bounds_max_um":[28163247,-9155751,-747124023]},{"stable_id":"tunnel-network/strate-2/route-edge-1","start_anchor_stable_id":"tunnel-network/strate-2/room-0/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-1/primitive-0","strate_index":2,"control_points_um":[[16677955,-28270663,-755491928],[14932858,-32558967,-756197896],[13187761,-36847271,-756903864],[11442663,-41135576,-757609833]],"polyline_um":[[16677955,-28270663,-755491928],[11442663,-41135576,-757609833]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8817415599178250,"bounds_min_um":[7942663,-44635576,-760109833],"bounds_max_um":[20177955,-24770663,-749241928]},{"stable_id":"tunnel-network/strate-2/route-edge-10","start_anchor_stable_id":"tunnel-network/strate-2/room-9/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-10/primitive-0","strate_index":2,"control_points_um":[[-8198612,-61493410,-774977238],[-7472480,-56608218,-775730335],[-6746348,-51723025,-776483433],[-6020215,-46837832,-777236531]],"polyline_um":[[-8198612,-61493410,-774977238],[-6020215,-46837832,-777236531]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7032802616799006,"bounds_min_um":[-11698612,-64993410,-779736531],"bounds_max_um":[-2520215,-43337832,-768727238]},{"stable_id":"tunnel-network/strate-2/route-edge-11","start_anchor_stable_id":"tunnel-network/strate-2/room-10/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-11/primitive-0","strate_index":2,"control_points_um":[[-6020215,-46837832,-777236531],[-5294083,-41952640,-777989629],[-4567951,-37067448,-778742727],[-3841819,-32182255,-779495825]],"polyline_um":[[-6020215,-46837832,-777236531],[-3841819,-32182255,-779495825]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1911978944492012,"bounds_min_um":[-9520215,-50337832,-781995825],"bounds_max_um":[-341819,-28682255,-770986531]},{"stable_id":"tunnel-network/strate-2/route-edge-12","start_anchor_stable_id":"tunnel-network/strate-2/room-11/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-12/primitive-0","strate_index":2,"control_points_um":[[-3841819,-32182255,-779495825],[-3115687,-27297063,-780248922],[-2389555,-22411871,-781002020],[-1663423,-17526678,-781755118]],"polyline_um":[[-3841819,-32182255,-779495825],[-1663423,-17526678,-781755118]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8165210430297268,"bounds_min_um":[-7341819,-35682255,-784255118],"bounds_max_um":[1836577,-14026678,-773245825]},{"stable_id":"tunnel-network/strate-2/route-edge-13","start_anchor_stable_id":"tunnel-network/strate-2/room-12/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-13/primitive-0","strate_index":2,"control_points_um":[[-1663423,-17526678,-781755118],[-1300357,-15084082,-782131666],[-937291,-12641486,-782508215],[-574225,-10198889,-782884764]],"polyline_um":[[-1663423,-17526678,-781755118],[-574225,-10198889,-782884764]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1869838225665002,"bounds_min_um":[-5163423,-21026678,-785384764],"bounds_max_um":[2925775,-6698889,-775505118]},{"stable_id":"tunnel-network/strate-2/route-edge-14","start_anchor_stable_id":"tunnel-network/strate-2/room-13/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-14/primitive-0","strate_index":2,"control_points_um":[[-574225,-10198889,-782884764],[-211159,-7756293,-783261313],[151907,-5313697,-783637862],[514974,-2871100,-784014411]],"polyline_um":[[-574225,-10198889,-782884764],[514974,-2871100,-784014411]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7495826887632453,"bounds_min_um":[-4074225,-13698889,-786514411],"bounds_max_um":[4014974,628900,-776634764]},{"stable_id":"tunnel-network/strate-2/route-edge-15","start_anchor_stable_id":"tunnel-network/strate-2/room-14/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-15/primitive-0","strate_index":2,"control_points_um":[[514974,-2871100,-784014411],[1241106,2014092,-784767508],[1967238,6899284,-785520606],[2693370,11784477,-786273704]],"polyline_um":[[514974,-2871100,-784014411],[2693370,11784477,-786273704]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1168863893760579,"bounds_min_um":[-2985026,-6371100,-788773704],"bounds_max_um":[6193370,15284477,-777764411]},{"stable_id":"tunnel-network/strate-2/route-edge-16","start_anchor_stable_id":"tunnel-network/strate-2/room-15/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-16/primitive-0","strate_index":2,"control_points_um":[[2693370,11784477,-786273704],[3419502,16669669,-787026802],[4145634,21554861,-787779900],[4871766,26440054,-788532998]],"polyline_um":[[2693370,11784477,-786273704],[4871766,26440054,-788532998]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3488700549169219,"bounds_min_um":[-806630,8284477,-791032998],"bounds_max_um":[8371766,29940054,-780023704]},{"stable_id":"tunnel-network/strate-2/route-edge-17","start_anchor_stable_id":"tunnel-network/strate-2/room-16/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-17/primitive-0","strate_index":2,"control_points_um":[[4871766,26440054,-788532998],[5597898,31325246,-789286095],[6324030,36210439,-790039193],[7050163,41095632,-790792291]],"polyline_um":[[4871766,26440054,-788532998],[7050163,41095632,-790792291]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4919553353375372,"bounds_min_um":[1371766,22940054,-793292291],"bounds_max_um":[10550163,44595632,-782282998]},{"stable_id":"tunnel-network/strate-2/route-edge-18","start_anchor_stable_id":"tunnel-network/strate-2/room-17/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-18/primitive-0","strate_index":2,"control_points_um":[[7050163,41095632,-790792291],[7776295,45980824,-791545388],[8502427,50866016,-792298486],[9228559,55751209,-793051584]],"polyline_um":[[7050163,41095632,-790792291],[9228559,55751209,-793051584]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8158943584761178,"bounds_min_um":[3550163,37595632,-795551584],"bounds_max_um":[12728559,59251209,-784542291]},{"stable_id":"tunnel-network/strate-2/route-edge-19","start_anchor_stable_id":"tunnel-network/strate-2/room-18/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-19/primitive-0","strate_index":2,"control_points_um":[[9228559,55751209,-793051584],[9954691,60636401,-793804681],[10680823,65521594,-794557779],[11406955,70406787,-795310877]],"polyline_um":[[9228559,55751209,-793051584],[11406955,70406787,-795310877]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":269617962051078,"bounds_min_um":[5728559,52251209,-797810877],"bounds_max_um":[14906955,73906787,-786801584]},{"stable_id":"tunnel-network/strate-2/route-edge-2","start_anchor_stable_id":"tunnel-network/strate-2/room-1/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-2/primitive-0","strate_index":2,"control_points_um":[[11442663,-41135576,-757609833],[9697566,-45423880,-758315801],[7952468,-49712184,-759021769],[6207370,-54000489,-759727738]],"polyline_um":[[11442663,-41135576,-757609833],[6207370,-54000489,-759727738]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8874695133302405,"bounds_min_um":[2707370,-57500489,-762227738],"bounds_max_um":[14942663,-37635576,-751359833]},{"stable_id":"tunnel-network/strate-2/route-edge-20","start_anchor_stable_id":"tunnel-network/strate-2/room-19/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-20/primitive-0","strate_index":2,"control_points_um":[[11406955,70406787,-795310877],[12133087,75291979,-796063975],[12859219,80177172,-796817073],[13585352,85062365,-797570171]],"polyline_um":[[11406955,70406787,-795310877],[13585352,85062365,-797570171]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":32404627650999,"bounds_min_um":[7906955,66906787,-800070171],"bounds_max_um":[17085352,88562365,-789060877]},{"stable_id":"tunnel-network/strate-2/route-edge-21","start_anchor_stable_id":"tunnel-network/strate-2/room-20/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-21/primitive-0","strate_index":2,"control_points_um":[[13585352,85062365,-797570171],[11840255,80774061,-798276139],[10095158,76485757,-798982107],[8350060,72197453,-799688076]],"polyline_um":[[13585352,85062365,-797570171],[8350060,72197453,-799688076]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4317428513448156,"bounds_min_um":[4850060,68697453,-802188076],"bounds_max_um":[17085352,88562365,-791320171]},{"stable_id":"tunnel-network/strate-2/route-edge-22","start_anchor_stable_id":"tunnel-network/strate-2/room-21/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-22/primitive-0","strate_index":2,"control_points_um":[[8350060,72197453,-799688076],[6604963,67909149,-800394044],[4859865,63620845,-801100012],[3114767,59332540,-801805981]],"polyline_um":[[8350060,72197453,-799688076],[3114767,59332540,-801805981]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7914775607336034,"bounds_min_um":[-385233,55832540,-804305981],"bounds_max_um":[11850060,75697453,-793438076]},{"stable_id":"tunnel-network/strate-2/route-edge-23","start_anchor_stable_id":"tunnel-network/strate-2/room-22/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-23/primitive-0","strate_index":2,"control_points_um":[[3114767,59332540,-801805981],[1369670,55044236,-802511949],[-375427,50755932,-803217917],[-2120525,46467627,-803923886]],"polyline_um":[[3114767,59332540,-801805981],[-2120525,46467627,-803923886]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4034550209005613,"bounds_min_um":[-5620525,42967627,-806423886],"bounds_max_um":[6614767,62832540,-795555981]},{"stable_id":"tunnel-network/strate-2/route-edge-24","start_anchor_stable_id":"tunnel-network/strate-2/room-23/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-24/primitive-0","strate_index":2,"control_points_um":[[-2120525,46467627,-803923886],[-3865622,42179323,-804629854],[-5610720,37891019,-805335822],[-7355818,33602715,-806041791]],"polyline_um":[[-2120525,46467627,-803923886],[-7355818,33602715,-806041791]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8223841641953534,"bounds_min_um":[-10855818,30102715,-808541791],"bounds_max_um":[1379475,49967627,-797673886]},{"stable_id":"tunnel-network/strate-2/route-edge-25","start_anchor_stable_id":"tunnel-network/strate-2/room-24/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-25/primitive-0","strate_index":2,"control_points_um":[[-7355818,33602715,-806041791],[-9100915,29314411,-806747759],[-10846012,25026107,-807453727],[-12591110,20737802,-808159696]],"polyline_um":[[-7355818,33602715,-806041791],[-12591110,20737802,-808159696]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8463461302563818,"bounds_min_um":[-16091110,17237802,-810659696],"bounds_max_um":[-3855818,37102715,-799791791]},{"stable_id":"tunnel-network/strate-2/route-edge-26","start_anchor_stable_id":"tunnel-network/strate-2/room-25/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-26/primitive-0","strate_index":2,"control_points_um":[[-12591110,20737802,-808159696],[-14336207,16449498,-808865664],[-16081305,12161194,-809571632],[-17826403,7872889,-810277601]],"polyline_um":[[-12591110,20737802,-808159696],[-17826403,7872889,-810277601]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1334322152237128,"bounds_min_um":[-21326403,4372889,-812777601],"bounds_max_um":[-9091110,24237802,-801909696]},{"stable_id":"tunnel-network/strate-2/route-edge-3","start_anchor_stable_id":"tunnel-network/strate-2/room-2/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-3/primitive-0","strate_index":2,"control_points_um":[[6207370,-54000489,-759727738],[4462273,-58288793,-760433706],[2717176,-62577097,-761139674],[972078,-66865401,-761845643]],"polyline_um":[[6207370,-54000489,-759727738],[972078,-66865401,-761845643]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6511238413130679,"bounds_min_um":[-2527922,-70365401,-764345643],"bounds_max_um":[9707370,-50500489,-753477738]},{"stable_id":"tunnel-network/strate-2/route-edge-4","start_anchor_stable_id":"tunnel-network/strate-2/room-3/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-4/primitive-0","strate_index":2,"control_points_um":[[972078,-66865401,-761845643],[-773019,-71153705,-762551611],[-2518116,-75442009,-763257579],[-4263214,-79730314,-763963548]],"polyline_um":[[972078,-66865401,-761845643],[-4263214,-79730314,-763963548]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4104421178731175,"bounds_min_um":[-7763214,-83230314,-766463548],"bounds_max_um":[4472078,-63365401,-755595643]},{"stable_id":"tunnel-network/strate-2/route-edge-5","start_anchor_stable_id":"tunnel-network/strate-2/room-4/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-5/primitive-0","strate_index":2,"control_points_um":[[-4263214,-79730314,-763963548],[-6008311,-84018618,-764669516],[-7753409,-88306922,-765375484],[-9498507,-92595227,-766081453]],"polyline_um":[[-4263214,-79730314,-763963548],[-9498507,-92595227,-766081453]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1712957457995776,"bounds_min_um":[-12998507,-96095227,-768581453],"bounds_max_um":[-763214,-76230314,-757713548]},{"stable_id":"tunnel-network/strate-2/route-edge-6","start_anchor_stable_id":"tunnel-network/strate-2/room-5/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-6/primitive-0","strate_index":2,"control_points_um":[[-9498507,-92595227,-766081453],[-11243604,-96883531,-766787421],[-12988702,-101171836,-767493389],[-14733800,-105460141,-768199358]],"polyline_um":[[-9498507,-92595227,-766081453],[-14733800,-105460141,-768199358]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":627735267583865,"bounds_min_um":[-18233800,-108960141,-770699358],"bounds_max_um":[-5998507,-89095227,-759831453]},{"stable_id":"tunnel-network/strate-2/route-edge-7","start_anchor_stable_id":"tunnel-network/strate-2/room-6/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-7/primitive-0","strate_index":2,"control_points_um":[[-14733800,-105460141,-768199358],[-14007668,-100574949,-768952456],[-13281536,-95689757,-769705554],[-12555404,-90804565,-770458652]],"polyline_um":[[-14733800,-105460141,-768199358],[-12555404,-90804565,-770458652]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2402170231604524,"bounds_min_um":[-18233800,-108960141,-772958652],"bounds_max_um":[-9055404,-87304565,-761949358]},{"stable_id":"tunnel-network/strate-2/route-edge-8","start_anchor_stable_id":"tunnel-network/strate-2/room-7/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-8/primitive-0","strate_index":2,"control_points_um":[[-12555404,-90804565,-770458652],[-11829272,-85919373,-771211749],[-11103140,-81034180,-771964847],[-10377008,-76148987,-772717945]],"polyline_um":[[-12555404,-90804565,-770458652],[-10377008,-76148987,-772717945]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":35699537972874,"bounds_min_um":[-16055404,-94304565,-775217945],"bounds_max_um":[-6877008,-72648987,-764208652]},{"stable_id":"tunnel-network/strate-2/route-edge-9","start_anchor_stable_id":"tunnel-network/strate-2/room-8/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-9/primitive-0","strate_index":2,"control_points_um":[[-10377008,-76148987,-772717945],[-9650876,-71263795,-773471042],[-8924744,-66378603,-774224140],[-8198612,-61493410,-774977238]],"polyline_um":[[-10377008,-76148987,-772717945],[-8198612,-61493410,-774977238]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8556892065984699,"bounds_min_um":[-13877008,-79648987,-777477238],"bounds_max_um":[-4698612,-57993410,-766467945]},{"stable_id":"tunnel-network/strate-2/side-edge-0","start_anchor_stable_id":"tunnel-network/strate-2/room-3/side-0/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-3/primitive-0","strate_index":2,"control_points_um":[[-832538,-74659203,-761845643],[-231000,-72061269,-761845643],[370539,-69463335,-761845643],[972078,-66865401,-761845643]],"polyline_um":[[-832538,-74659203,-761845643],[972078,-66865401,-761845643]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4352771090277651,"bounds_min_um":[-4332538,-78159203,-763595643],"bounds_max_um":[4472078,-63365401,-755595643]},{"stable_id":"tunnel-network/strate-2/side-edge-1","start_anchor_stable_id":"tunnel-network/strate-2/room-19/side-1/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-19/primitive-0","strate_index":2,"control_points_um":[[13211571,78200589,-795310877],[12610033,75602655,-795310877],[12008494,73004721,-795310877],[11406955,70406787,-795310877]],"polyline_um":[[13211571,78200589,-795310877],[11406955,70406787,-795310877]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2726979967805320,"bounds_min_um":[7906955,66906787,-797060877],"bounds_max_um":[16711571,81700589,-789060877]},{"stable_id":"tunnel-network/strate-2/side-edge-2","start_anchor_stable_id":"tunnel-network/strate-2/room-24/side-2/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-24/primitive-0","strate_index":2,"control_points_um":[[-9160434,25808913,-806041791],[-8558896,28406847,-806041791],[-7957357,31004781,-806041791],[-7355818,33602715,-806041791]],"polyline_um":[[-9160434,25808913,-806041791],[-7355818,33602715,-806041791]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7796271028373642,"bounds_min_um":[-12660434,22308913,-807791791],"bounds_max_um":[-3855818,37102715,-799791791]}],"stable_identity":"resolved-layout-v3-fnv64:ffc28f1880c1b460","resolved_strate_passage_styles":{"0":1,"1":1,"2":1,"3":1}} \ No newline at end of file diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/layout-seed0-third-design.json b/Standalone/Research/WorldAssemblerM3/Linux134/layout-seed0-third-design.json new file mode 100644 index 0000000..8bb1b14 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux134/layout-seed0-third-design.json @@ -0,0 +1 @@ +{"schema_version":3,"world_identity":"owner-world/seed/0","owner_voxel_size_um":125000,"core_voxel_size_um":250000,"strate_layout_chunk_size_um":8000000,"strate_layout_chunk_width_subvoxels":8192,"strates":[{"stable_id":"owner-world/seed/0/strate/0/generator/SurfaceWorld","order":0,"strate_index":0,"generator_kind":"SurfaceWorld","recipe_id":"SurfaceWorld.Heightfield2D","recipe_version":2,"interior_bottom_um":-504000000,"interior_top_um":8000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":7500000,"top_um":8000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-503500000,"top_um":7500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-504000000,"top_um":-503500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":1,"transition_width_um":8000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":2000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"BuildSurfaceStratumPlan uses resolved layout chunk width from ResolvedLayoutSnapshotV3","query_dispatch_path":"NativeSurfaceDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[{"kind":"cap_profile","stable_id":"biome/4097","units":"1/256 Core voxel","values":[-18022,0,64000,1536,0]},{"kind":"route_point","stable_id":"route/0","units":"1/256 Core voxel","values":[8192,24576,-381223,1024]},{"kind":"route_point","stable_id":"route/1","units":"1/256 Core voxel","values":[8192,24320,-381254,1024]},{"kind":"route_point","stable_id":"route/2","units":"1/256 Core voxel","values":[8192,24064,-381284,1024]},{"kind":"route_point","stable_id":"route/3","units":"1/256 Core voxel","values":[8192,23808,-381302,1024]},{"kind":"route_point","stable_id":"route/4","units":"1/256 Core voxel","values":[8192,23552,-381304,1024]},{"kind":"route_point","stable_id":"route/5","units":"1/256 Core voxel","values":[8192,23296,-381291,1024]},{"kind":"route_point","stable_id":"route/6","units":"1/256 Core voxel","values":[8192,23040,-381270,1024]},{"kind":"route_point","stable_id":"route/7","units":"1/256 Core voxel","values":[8192,22784,-381242,1024]},{"kind":"route_point","stable_id":"route/8","units":"1/256 Core voxel","values":[8192,22528,-381218,1024]},{"kind":"route_point","stable_id":"route/9","units":"1/256 Core voxel","values":[8192,22272,-381192,1024]},{"kind":"route_point","stable_id":"route/10","units":"1/256 Core voxel","values":[8192,22016,-381161,1024]},{"kind":"route_point","stable_id":"route/11","units":"1/256 Core voxel","values":[8192,21760,-381118,1024]},{"kind":"route_point","stable_id":"route/12","units":"1/256 Core voxel","values":[8192,21504,-381122,1024]},{"kind":"route_point","stable_id":"route/13","units":"1/256 Core voxel","values":[8192,21248,-381155,1024]},{"kind":"route_point","stable_id":"route/14","units":"1/256 Core voxel","values":[8192,20992,-381183,1024]},{"kind":"route_point","stable_id":"route/15","units":"1/256 Core voxel","values":[8192,20736,-381207,1024]},{"kind":"route_point","stable_id":"route/16","units":"1/256 Core voxel","values":[8192,20480,-381219,1024]},{"kind":"route_point","stable_id":"route/17","units":"1/256 Core voxel","values":[8192,20224,-381220,1024]},{"kind":"route_point","stable_id":"route/18","units":"1/256 Core voxel","values":[8192,19968,-381202,1024]},{"kind":"route_point","stable_id":"route/19","units":"1/256 Core voxel","values":[8192,19712,-381167,1024]},{"kind":"route_point","stable_id":"route/20","units":"1/256 Core voxel","values":[8192,19456,-381130,1024]},{"kind":"route_point","stable_id":"route/21","units":"1/256 Core voxel","values":[8192,19200,-381101,1024]},{"kind":"route_point","stable_id":"route/22","units":"1/256 Core voxel","values":[8192,18944,-381072,1024]},{"kind":"route_point","stable_id":"route/23","units":"1/256 Core voxel","values":[8192,18688,-381092,1024]},{"kind":"route_point","stable_id":"route/24","units":"1/256 Core voxel","values":[8192,18432,-381149,1024]},{"kind":"route_point","stable_id":"route/25","units":"1/256 Core voxel","values":[8192,18176,-381200,1024]},{"kind":"route_point","stable_id":"route/26","units":"1/256 Core voxel","values":[8192,17920,-381237,1024]},{"kind":"route_point","stable_id":"route/27","units":"1/256 Core voxel","values":[8192,17664,-381260,1024]},{"kind":"route_point","stable_id":"route/28","units":"1/256 Core voxel","values":[8192,17408,-381262,1024]},{"kind":"route_point","stable_id":"route/29","units":"1/256 Core voxel","values":[8192,17152,-381263,1024]},{"kind":"route_point","stable_id":"route/30","units":"1/256 Core voxel","values":[8192,16896,-381248,1024]},{"kind":"route_point","stable_id":"route/31","units":"1/256 Core voxel","values":[8192,16640,-381216,1024]},{"kind":"route_point","stable_id":"route/32","units":"1/256 Core voxel","values":[8192,16384,-381197,1024]}]},{"stable_id":"owner-world/seed/0/strate/1/generator/FloatingIslands","order":1,"strate_index":1,"generator_kind":"FloatingIslands","recipe_id":"FloatingIslands.ClusterField","recipe_version":0,"interior_bottom_um":-744000000,"interior_top_um":-504000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":-504500000,"top_um":-504000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-743500000,"top_um":-504500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-744000000,"top_um":-743500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":3,"transition_width_um":24000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":6000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"BuildFloatingIslandStratumPlan uses resolved layout chunk width from ResolvedLayoutSnapshotV3","query_dispatch_path":"NativeFloatingIslandDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[]},{"stable_id":"owner-world/seed/0/strate/2/generator/TunnelNetwork","order":2,"strate_index":2,"generator_kind":"TunnelNetwork","recipe_id":"Core.Caves.PrimitiveField","recipe_version":1,"interior_bottom_um":-872000000,"interior_top_um":-744000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":-744500000,"top_um":-744000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-871500000,"top_um":-744500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-872000000,"top_um":-871500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":2,"transition_width_um":16000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":4000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"resolved manager slot scaled by ResolvedLayoutSnapshotV3 chunk width","query_dispatch_path":"legacy Core route selects manager chunk by floor(CoreZ / resolved layout chunk width)","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[{"kind":"route_point","stable_id":"tunnel-network/route/000-arrival","units":"1/256 Core voxel","values":[19626,-14541,-770554]},{"kind":"route_point","stable_id":"tunnel-network/route/000-edge-start","units":"1/256 Core voxel","values":[22439,-15775,-770554]},{"kind":"route_point","stable_id":"tunnel-network/route/1","units":"1/256 Core voxel","values":[17078,-28949,-772723]},{"kind":"route_point","stable_id":"tunnel-network/route/2","units":"1/256 Core voxel","values":[11717,-42123,-774891]},{"kind":"route_point","stable_id":"tunnel-network/route/3","units":"1/256 Core voxel","values":[6356,-55297,-777060]},{"kind":"route_point","stable_id":"tunnel-network/route/4","units":"1/256 Core voxel","values":[995,-68470,-779229]},{"kind":"route_point","stable_id":"tunnel-network/route/5","units":"1/256 Core voxel","values":[-4366,-81644,-781398]},{"kind":"route_point","stable_id":"tunnel-network/route/6","units":"1/256 Core voxel","values":[-9726,-94818,-783566]},{"kind":"route_point","stable_id":"tunnel-network/route/7","units":"1/256 Core voxel","values":[-15087,-107991,-785735]},{"kind":"route_point","stable_id":"tunnel-network/route/8","units":"1/256 Core voxel","values":[-12857,-92984,-788049]},{"kind":"route_point","stable_id":"tunnel-network/route/9","units":"1/256 Core voxel","values":[-10626,-77977,-790362]},{"kind":"route_point","stable_id":"tunnel-network/route/10","units":"1/256 Core voxel","values":[-8395,-62969,-792676]},{"kind":"route_point","stable_id":"tunnel-network/route/11","units":"1/256 Core voxel","values":[-6165,-47962,-794989]},{"kind":"route_point","stable_id":"tunnel-network/route/12","units":"1/256 Core voxel","values":[-3934,-32955,-797303]},{"kind":"route_point","stable_id":"tunnel-network/route/13","units":"1/256 Core voxel","values":[-1703,-17947,-799616]},{"kind":"route_point","stable_id":"tunnel-network/route/14","units":"1/256 Core voxel","values":[-588,-10444,-800773]},{"kind":"route_point","stable_id":"tunnel-network/route/15","units":"1/256 Core voxel","values":[527,-2940,-801930]},{"kind":"route_point","stable_id":"tunnel-network/route/16","units":"1/256 Core voxel","values":[2758,12067,-804243]},{"kind":"route_point","stable_id":"tunnel-network/route/17","units":"1/256 Core voxel","values":[4989,27075,-806557]},{"kind":"route_point","stable_id":"tunnel-network/route/18","units":"1/256 Core voxel","values":[7219,42082,-808870]},{"kind":"route_point","stable_id":"tunnel-network/route/19","units":"1/256 Core voxel","values":[9450,57089,-811184]},{"kind":"route_point","stable_id":"tunnel-network/route/20","units":"1/256 Core voxel","values":[11681,72097,-813497]},{"kind":"route_point","stable_id":"tunnel-network/route/21","units":"1/256 Core voxel","values":[13911,87104,-815811]},{"kind":"route_point","stable_id":"tunnel-network/route/22","units":"1/256 Core voxel","values":[8550,73930,-817979]},{"kind":"route_point","stable_id":"tunnel-network/route/23","units":"1/256 Core voxel","values":[3190,60757,-820148]},{"kind":"route_point","stable_id":"tunnel-network/route/24","units":"1/256 Core voxel","values":[-2171,47583,-822317]},{"kind":"route_point","stable_id":"tunnel-network/route/25","units":"1/256 Core voxel","values":[-7532,34409,-824486]},{"kind":"route_point","stable_id":"tunnel-network/route/26","units":"1/256 Core voxel","values":[-12893,21236,-826654]},{"kind":"route_point","stable_id":"tunnel-network/route/27","units":"1/256 Core voxel","values":[-18254,8062,-828823]},{"kind":"route_point","stable_id":"tunnel-network/route/998-departure-inner","units":"1/256 Core voxel","values":[-23615,-5112,-830992]},{"kind":"route_point","stable_id":"tunnel-network/route/999-departure","units":"1/256 Core voxel","values":[-20544,-5190,-830992]}]},{"stable_id":"owner-world/seed/0/strate/3/generator/VerticalShafts","order":3,"strate_index":3,"generator_kind":"VerticalShafts","recipe_id":"legacy.VerticalShafts","recipe_version":0,"interior_bottom_um":-1000000000,"interior_top_um":-872000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":-872500000,"top_um":-872000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-999500000,"top_um":-872500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-1000000000,"top_um":-999500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":2,"transition_width_um":16000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":4000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"resolved manager slot scaled by ResolvedLayoutSnapshotV3 chunk width","query_dispatch_path":"legacy Core route selects manager chunk by floor(CoreZ / resolved layout chunk width)","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[]}],"passages":[{"stable_id":"passage/0-1/0","upper_strate_index":0,"lower_strate_index":1,"style":1,"placement_radius_q":38400,"upper_mouth_q":[8192,16384,-381197],"lower_mouth_q":[26457,3276,-754688],"mouth_radius_q":4608,"mid_radius_q":3584,"control_points":[{"position_q":[8192,16384,-380818],"radius_q":1536},{"position_q":[917,-21635,-385895],"radius_q":3584},{"position_q":[-7484,-19826,-387477],"radius_q":3584},{"position_q":[-14728,-15190,-389060],"radius_q":3584},{"position_q":[-19949,-8188,-390668],"radius_q":3584},{"position_q":[-22342,455,-392319],"radius_q":3584},{"position_q":[-21299,9658,-394024],"radius_q":3584},{"position_q":[-16638,18010,-395785],"radius_q":3584},{"position_q":[-8834,23996,-397595],"radius_q":3584},{"position_q":[917,26365,-399442],"radius_q":3584},{"position_q":[10885,24520,-401308],"radius_q":3584},{"position_q":[19213,18751,-403173],"radius_q":3584},{"position_q":[24397,10181,-405017],"radius_q":3584},{"position_q":[25658,455,-406822],"radius_q":3584},{"position_q":[23047,-8711,-408577],"radius_q":3584},{"position_q":[17303,-15931,-410275],"radius_q":3584},{"position_q":[9535,-20350,-411920],"radius_q":3584},{"position_q":[1023,-22825,-413552],"radius_q":3584},{"position_q":[-7513,-20559,-415177],"radius_q":3584},{"position_q":[-14529,-15504,-416769],"radius_q":3584},{"position_q":[-19292,-8367,-418349],"radius_q":3584},{"position_q":[-21267,48,-419940],"radius_q":3584},{"position_q":[-20089,8793,-421564],"radius_q":3584},{"position_q":[-15656,16727,-423237],"radius_q":3584},{"position_q":[-8310,22578,-424966],"radius_q":3584},{"position_q":[1023,25175,-426749],"radius_q":3584},{"position_q":[10856,23787,-428577],"radius_q":3584},{"position_q":[19412,18437,-430435],"radius_q":3584},{"position_q":[25054,10002,-432303],"radius_q":3584},{"position_q":[26733,48,-434161],"radius_q":3584},{"position_q":[24257,-9576,-435990],"radius_q":3584},{"position_q":[18285,-17214,-437775],"radius_q":3584},{"position_q":[10059,-21768,-439506],"radius_q":3584},{"position_q":[956,-24440,-441252],"radius_q":3584},{"position_q":[-7955,-21879,-442959],"radius_q":3584},{"position_q":[-15028,-16351,-444611],"radius_q":3584},{"position_q":[-19496,-8840,-446220],"radius_q":3584},{"position_q":[-20998,-368,-447804],"radius_q":3584},{"position_q":[-19445,8082,-449386],"radius_q":3584},{"position_q":[-14956,15543,-450988],"radius_q":3584},{"position_q":[-7904,21020,-452632],"radius_q":3584},{"position_q":[956,23560,-454329],"radius_q":3584},{"position_q":[10414,22467,-456081],"radius_q":3584},{"position_q":[18913,17590,-457885],"radius_q":3584},{"position_q":[24850,9530,-459728],"radius_q":3584},{"position_q":[27002,-368,-461592],"radius_q":3584},{"position_q":[24901,-10287,-463459],"radius_q":3584},{"position_q":[18986,-18398,-465307],"radius_q":3584},{"position_q":[10465,-23326,-467119],"radius_q":3584},{"position_q":[727,-25966,-468976],"radius_q":3584},{"position_q":[-8660,-23382,-470768],"radius_q":3584},{"position_q":[-16053,-17501,-472507],"radius_q":3584},{"position_q":[-20498,-9513,-474190],"radius_q":3584},{"position_q":[-21647,-721,-475822],"radius_q":3584},{"position_q":[-19618,7706,-477418],"radius_q":3584},{"position_q":[-14808,14814,-478997],"radius_q":3584},{"position_q":[-7780,19814,-480585],"radius_q":3584},{"position_q":[727,22034,-482204],"radius_q":3584},{"position_q":[9709,20964,-483869],"radius_q":3584},{"position_q":[17888,16440,-485589],"radius_q":3584},{"position_q":[23848,8856,-487365],"radius_q":3584},{"position_q":[26353,-721,-489187],"radius_q":3584},{"position_q":[24728,-10663,-491041],"radius_q":3584},{"position_q":[19133,-19128,-492909],"radius_q":3584},{"position_q":[10589,-24532,-494770],"radius_q":3584},{"position_q":[375,-26910,-496701],"radius_q":3584},{"position_q":[-9413,-24583,-498553],"radius_q":3584},{"position_q":[-17272,-18600,-500371],"radius_q":3584},{"position_q":[-21974,-10210,-502141],"radius_q":3584},{"position_q":[-23021,-953,-503856],"radius_q":3584},{"position_q":[-20591,7731,-505516],"radius_q":3584},{"position_q":[-15315,14737,-507131],"radius_q":3584},{"position_q":[-8029,19336,-508717],"radius_q":3584},{"position_q":[375,21090,-510297],"radius_q":3584},{"position_q":[8956,19763,-511896],"radius_q":3584},{"position_q":[16669,15341,-513532],"radius_q":3584},{"position_q":[22372,8159,-515220],"radius_q":3584},{"position_q":[24979,-953,-516965],"radius_q":3584},{"position_q":[23755,-10638,-518762],"radius_q":3584},{"position_q":[18626,-19204,-520600],"radius_q":3584},{"position_q":[10340,-25010,-522462],"radius_q":3584},{"position_q":[-40,-26968,-524407],"radius_q":3584},{"position_q":[-10007,-25083,-526274],"radius_q":3584},{"position_q":[-18304,-19287,-528137],"radius_q":3584},{"position_q":[-23449,-10719,-529977],"radius_q":3584},{"position_q":[-24682,-1023,-531776],"radius_q":3584},{"position_q":[-22073,8103,-533523],"radius_q":3584},{"position_q":[-16359,15296,-535214],"radius_q":3584},{"position_q":[-8631,19716,-536853],"radius_q":3584},{"position_q":[-40,21032,-538453],"radius_q":3584},{"position_q":[8363,19263,-540034],"radius_q":3584},{"position_q":[15637,14654,-541619],"radius_q":3584},{"position_q":[20897,7650,-543232],"radius_q":3584},{"position_q":[23318,-1023,-544889],"radius_q":3584},{"position_q":[22273,-10266,-546601],"radius_q":3584},{"position_q":[17582,-18646,-548369],"radius_q":3584},{"position_q":[9738,-24629,-550185],"radius_q":3584},{"position_q":[-449,-26134,-552081],"radius_q":3584},{"position_q":[-10304,-24712,-553914],"radius_q":3584},{"position_q":[-18851,-19323,-555774],"radius_q":3584},{"position_q":[-24459,-10866,-557642],"radius_q":3584},{"position_q":[-26099,-920,-559497],"radius_q":3584},{"position_q":[-23601,8670,-561322],"radius_q":3584},{"position_q":[-17638,16268,-563100],"radius_q":3584},{"position_q":[-9446,20800,-564823],"radius_q":3584},{"position_q":[-449,21866,-566491],"radius_q":3584},{"position_q":[8065,19634,-568111],"radius_q":3584},{"position_q":[15090,14619,-569700],"radius_q":3584},{"position_q":[19887,7503,-571280],"radius_q":3584},{"position_q":[21901,-920,-572874],"radius_q":3584},{"position_q":[20745,-9699,-574504],"radius_q":3584},{"position_q":[16304,-17673,-576184],"radius_q":3584},{"position_q":[8923,-23546,-577920],"radius_q":3584},{"position_q":[-781,-24695,-579719],"radius_q":3584},{"position_q":[-10277,-23587,-581479],"radius_q":3584},{"position_q":[-18792,-18673,-583289],"radius_q":3584},{"position_q":[-24714,-10575,-585135],"radius_q":3584},{"position_q":[-26829,-662,-587001],"radius_q":3584},{"position_q":[-24690,9242,-588867],"radius_q":3584},{"position_q":[-18759,17316,-590711],"radius_q":3584},{"position_q":[-10254,22207,-592517],"radius_q":3584},{"position_q":[-781,23305,-594272],"radius_q":3584},{"position_q":[8092,20759,-595972],"radius_q":3584},{"position_q":[15149,15268,-597618],"radius_q":3584},{"position_q":[19632,7794,-599222],"radius_q":3584},{"position_q":[21171,-662,-600804],"radius_q":3584},{"position_q":[19656,-9127,-602387],"radius_q":3584},{"position_q":[15182,-16625,-603994],"radius_q":3584},{"position_q":[8115,-22139,-605644],"radius_q":3584},{"position_q":[-982,-23132,-607329],"radius_q":3584},{"position_q":[-10003,-22072,-609001],"radius_q":3584},{"position_q":[-18216,-17526,-610729],"radius_q":3584},{"position_q":[-24186,-9903,-612512],"radius_q":3584},{"position_q":[-26670,-292,-614339],"radius_q":3584},{"position_q":[-25005,9660,-616196],"radius_q":3584},{"position_q":[-19376,18103,-618064],"radius_q":3584},{"position_q":[-10823,23468,-619923],"radius_q":3584},{"position_q":[-982,24868,-621753],"radius_q":3584},{"position_q":[8366,22274,-623538],"radius_q":3584},{"position_q":[15725,16415,-625270],"radius_q":3584},{"position_q":[20160,8466,-626946],"radius_q":3584},{"position_q":[21330,-292,-628572],"radius_q":3584},{"position_q":[19340,-8709,-630164],"radius_q":3584},{"position_q":[14565,-15838,-631744],"radius_q":3584},{"position_q":[7546,-20878,-633335],"radius_q":3584},{"position_q":[-1016,-21949,-634923],"radius_q":3584},{"position_q":[-9624,-20652,-636525],"radius_q":3584},{"position_q":[-17377,-16233,-638168],"radius_q":3584},{"position_q":[-23110,-9024,-639864],"radius_q":3584},{"position_q":[-25720,128,-641615],"radius_q":3584},{"position_q":[-24470,9843,-643418],"radius_q":3584},{"position_q":[-19300,18412,-645261],"radius_q":3584},{"position_q":[-10984,24192,-647125],"radius_q":3584},{"position_q":[-1016,26051,-648991],"radius_q":3584},{"position_q":[8746,23694,-650840],"radius_q":3584},{"position_q":[16564,17708,-652653],"radius_q":3584},{"position_q":[21236,9346,-654416],"radius_q":3584},{"position_q":[22280,128,-656123],"radius_q":3584},{"position_q":[19876,-8526,-657777],"radius_q":3584},{"position_q":[14641,-15529,-659386],"radius_q":3584},{"position_q":[7386,-20154,-660970],"radius_q":3584},{"position_q":[-878,-21498,-662511],"radius_q":3584},{"position_q":[-9285,-19770,-664091],"radius_q":3584},{"position_q":[-16591,-15186,-665679],"radius_q":3584},{"position_q":[-21892,-8178,-667296],"radius_q":3584},{"position_q":[-24338,526,-668961],"radius_q":3584},{"position_q":[-23288,9809,-670681],"radius_q":3584},{"position_q":[-18566,18214,-672455],"radius_q":3584},{"position_q":[-10682,24194,-674277],"radius_q":3584},{"position_q":[-878,26502,-676131],"radius_q":3584},{"position_q":[9084,24576,-677999],"radius_q":3584},{"position_q":[17350,18755,-679860],"radius_q":3584},{"position_q":[22454,10191,-681695],"radius_q":3584},{"position_q":[23662,526,-683488],"radius_q":3584},{"position_q":[21058,-8560,-685228],"radius_q":3584},{"position_q":[15375,-15727,-686911],"radius_q":3584},{"position_q":[7687,-20152,-688544],"radius_q":3584},{"position_q":[-592,-21868,-690101],"radius_q":3584},{"position_q":[-9086,-19670,-691716],"radius_q":3584},{"position_q":[-16122,-14694,-693302],"radius_q":3584},{"position_q":[-20954,-7599,-694882],"radius_q":3584},{"position_q":[-23007,835,-696480],"radius_q":3584},{"position_q":[-21871,9649,-698116],"radius_q":3584},{"position_q":[-17419,17662,-699803],"radius_q":3584},{"position_q":[-10003,23555,-701547],"radius_q":3584},{"position_q":[-592,26132,-703343],"radius_q":3584},{"position_q":[9283,24675,-705181],"radius_q":3584},{"position_q":[17819,19247,-707043],"radius_q":3584},{"position_q":[23392,10770,-708910],"radius_q":3584},{"position_q":[24993,835,-710762],"radius_q":3584},{"position_q":[22475,-8720,-712581],"radius_q":3584},{"position_q":[16523,-16280,-714353],"radius_q":3584},{"position_q":[8366,-20791,-716069],"radius_q":3584},{"position_q":[-206,-22859,-717692],"radius_q":3584},{"position_q":[-9043,-20329,-719384],"radius_q":3584},{"position_q":[-16086,-14877,-721023],"radius_q":3584},{"position_q":[-20586,-7439,-722624],"radius_q":3584},{"position_q":[-22163,1003,-724205],"radius_q":3584},{"position_q":[-20684,9485,-725789],"radius_q":3584},{"position_q":[-16224,17021,-727401],"radius_q":3584},{"position_q":[-9140,22572,-729058],"radius_q":3584},{"position_q":[-206,25141,-730769],"radius_q":3584},{"position_q":[9327,24016,-732536],"radius_q":3584},{"position_q":[17855,19064,-734352],"radius_q":3584},{"position_q":[23760,10930,-736202],"radius_q":3584},{"position_q":[25837,1003,-738069],"radius_q":3584},{"position_q":[23662,-8884,-739932],"radius_q":3584},{"position_q":[17717,-16920,-741772],"radius_q":3584},{"position_q":[9229,-21774,-743572],"radius_q":3584},{"position_q":[215,-24070,-745285],"radius_q":3584},{"position_q":[26457,3276,-751237],"radius_q":4608}]},{"stable_id":"passage/1-2/0","upper_strate_index":1,"lower_strate_index":2,"style":1,"placement_radius_q":24576,"upper_mouth_q":[19626,-14541,-749774],"lower_mouth_q":[19626,-14541,-771455],"mouth_radius_q":4608,"mid_radius_q":3072,"control_points":[{"position_q":[19626,-14541,-746323],"radius_q":4608},{"position_q":[11028,-10341,-748134],"radius_q":4608},{"position_q":[14628,-5642,-750790],"radius_q":3072},{"position_q":[16189,228,-751940],"radius_q":3072},{"position_q":[15303,6377,-753115],"radius_q":3072},{"position_q":[11982,11751,-754311],"radius_q":3072},{"position_q":[6728,15364,-755517],"radius_q":3072},{"position_q":[459,16542,-756725],"radius_q":3072},{"position_q":[-5703,15102,-757922],"radius_q":3072},{"position_q":[-10695,11381,-759100],"radius_q":3072},{"position_q":[-13754,6115,-760253],"radius_q":3072},{"position_q":[-14531,228,-761377],"radius_q":3072},{"position_q":[-13079,-5380,-762473],"radius_q":3072},{"position_q":[-9740,-9971,-763547],"radius_q":3072},{"position_q":[-5028,-13017,-764609],"radius_q":3072},{"position_q":[459,-14178,-765670],"radius_q":3072},{"position_q":[6053,-13279,-766743],"radius_q":3072},{"position_q":[11518,-10983,-767571],"radius_q":3365},{"position_q":[19626,-14541,-768004],"radius_q":4608}]},{"stable_id":"passage/2-3/0","upper_strate_index":2,"lower_strate_index":3,"style":1,"placement_radius_q":24576,"upper_mouth_q":[-20544,-5190,-831893],"lower_mouth_q":[-20544,-5190,-921962],"mouth_radius_q":4608,"mid_radius_q":3072,"control_points":[{"position_q":[-20544,-5190,-828442],"radius_q":4608},{"position_q":[-13079,-5380,-829802],"radius_q":4608},{"position_q":[-9740,-9971,-832372],"radius_q":3072},{"position_q":[-5028,-13017,-833394],"radius_q":3072},{"position_q":[459,-14178,-834416],"radius_q":3072},{"position_q":[6053,-13279,-835448],"radius_q":3072},{"position_q":[11028,-10341,-836500],"radius_q":3072},{"position_q":[14628,-5642,-837578],"radius_q":3072},{"position_q":[16189,228,-838685],"radius_q":3072},{"position_q":[15303,6377,-839816],"radius_q":3072},{"position_q":[11982,11751,-840967],"radius_q":3072},{"position_q":[6728,15364,-842128],"radius_q":3072},{"position_q":[459,16542,-843290],"radius_q":3072},{"position_q":[-5703,15102,-844443],"radius_q":3072},{"position_q":[-10695,11381,-845577],"radius_q":3072},{"position_q":[-13754,6115,-846687],"radius_q":3072},{"position_q":[-14531,228,-847768],"radius_q":3072},{"position_q":[-12750,-5469,-848855],"radius_q":3072},{"position_q":[-9640,-10128,-849876],"radius_q":3072},{"position_q":[-5042,-13384,-850902],"radius_q":3072},{"position_q":[511,-14773,-851945],"radius_q":3072},{"position_q":[6316,-13988,-853012],"radius_q":3072},{"position_q":[11518,-10983,-854106],"radius_q":3072},{"position_q":[15233,-6074,-855227],"radius_q":3072},{"position_q":[16726,24,-856371],"radius_q":3072},{"position_q":[15631,6287,-857529],"radius_q":3072},{"position_q":[12082,11594,-858692],"radius_q":3072},{"position_q":[6714,14998,-859850],"radius_q":3072},{"position_q":[511,15947,-860993],"radius_q":3072},{"position_q":[-5441,14393,-862113],"radius_q":3072},{"position_q":[-10204,10739,-863207],"radius_q":3072},{"position_q":[-13149,5682,-864273],"radius_q":3072},{"position_q":[-13994,24,-865315],"radius_q":3072},{"position_q":[-12852,-5706,-866379],"radius_q":3072},{"position_q":[-9890,-10552,-867414],"radius_q":3072},{"position_q":[-5263,-14044,-868469],"radius_q":3072},{"position_q":[478,-15580,-869552],"radius_q":3072},{"position_q":[6519,-14767,-870662],"radius_q":3072},{"position_q":[11869,-11575,-871797],"radius_q":3072},{"position_q":[15555,-6429,-872950],"radius_q":3072},{"position_q":[16861,-184,-874112],"radius_q":3072},{"position_q":[15529,6051,-875273],"radius_q":3072},{"position_q":[11833,11171,-876423],"radius_q":3072},{"position_q":[6493,14338,-877554],"radius_q":3072},{"position_q":[478,15140,-878660],"radius_q":3072},{"position_q":[-5238,13614,-879737],"radius_q":3072},{"position_q":[-9854,10147,-880789],"radius_q":3072},{"position_q":[-12827,5327,-881821],"radius_q":3072},{"position_q":[-13859,-184,-882842],"radius_q":3072},{"position_q":[-13353,-6043,-883913],"radius_q":3072},{"position_q":[-10402,-11127,-884984],"radius_q":3072},{"position_q":[-5616,-14795,-886082],"radius_q":3072},{"position_q":[363,-16343,-887207],"radius_q":3072},{"position_q":[6581,-15370,-888353],"radius_q":3072},{"position_q":[11942,-11940,-889513],"radius_q":3072},{"position_q":[15468,-6618,-890675],"radius_q":3072},{"position_q":[16536,-361,-891832],"radius_q":3072},{"position_q":[15028,5714,-892972],"radius_q":3072},{"position_q":[11320,10596,-894088],"radius_q":3072},{"position_q":[6140,13586,-895178],"radius_q":3072},{"position_q":[363,14377,-896240],"radius_q":3072},{"position_q":[-5176,13011,-897279],"radius_q":3072},{"position_q":[-9780,9783,-898303],"radius_q":3072},{"position_q":[-12913,5139,-899324],"radius_q":3072},{"position_q":[-14184,-361,-900352],"radius_q":3072},{"position_q":[-14091,-6391,-901450],"radius_q":3072},{"position_q":[-11012,-11676,-902564],"radius_q":3072},{"position_q":[-5992,-15396,-903703],"radius_q":3072},{"position_q":[187,-16815,-904857],"radius_q":3072},{"position_q":[6456,-15609,-906020],"radius_q":3072},{"position_q":[11689,-11978,-907180],"radius_q":3072},{"position_q":[14982,-6605,-908328],"radius_q":3072},{"position_q":[15849,-476,-909456],"radius_q":3072},{"position_q":[14290,5365,-910557],"radius_q":3072},{"position_q":[10710,10047,-911630],"radius_q":3072},{"position_q":[5764,12986,-912678],"radius_q":3072},{"position_q":[187,13905,-913708],"radius_q":3072},{"position_q":[-5301,12772,-914729],"radius_q":3072},{"position_q":[-10034,9744,-915752],"radius_q":3072},{"position_q":[-13400,5152,-916789],"radius_q":3072},{"position_q":[-14871,-476,-917848],"radius_q":3073},{"position_q":[-14828,-6646,-918159],"radius_q":3886},{"position_q":[-20544,-5190,-918511],"radius_q":4608}]}],"cavern_graph_vocabulary_version":1,"cavern_rooms":[{"stable_id":"tunnel-network/strate-2/room-0/primitive-0","strate_index":2,"center_um":[16677955,-28270663,-752557115],"half_extents_um":[4387063,4387063,3729030],"floor_um":-755491928,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2496338057380503,"bounds_min_um":[10540892,-34407726,-758036145],"bounds_max_um":[22815018,-22133600,-747078085]},{"stable_id":"tunnel-network/strate-2/room-0/primitive-1","strate_index":2,"center_um":[17020975,-26789220,-753339029],"half_extents_um":[2966903,2546513,2521885],"floor_um":-755491928,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1298697264498033,"bounds_min_um":[12304072,-31085733,-757610914],"bounds_max_um":[21737878,-22492707,-749067144]},{"stable_id":"tunnel-network/strate-2/room-0/primitive-2","strate_index":2,"center_um":[16450136,-29254574,-752994543],"half_extents_um":[2993025,3957768,3364126],"floor_um":-755491928,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1371456813936030,"bounds_min_um":[11707111,-34962342,-758108669],"bounds_max_um":[21193161,-23546806,-747880417]},{"stable_id":"tunnel-network/strate-2/room-1/primitive-0","strate_index":2,"center_um":[11442663,-41135576,-755226186],"half_extents_um":[3654710,3654710,3106525],"floor_um":-757609833,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":234826507654047,"bounds_min_um":[6037953,-46540286,-760082711],"bounds_max_um":[16847373,-35730866,-750369661]},{"stable_id":"tunnel-network/strate-2/room-1/primitive-1","strate_index":2,"center_um":[11594838,-40478358,-755656092],"half_extents_um":[2756758,2146566,2343261],"floor_um":-757609833,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7709850420954135,"bounds_min_um":[7088080,-44374924,-759749353],"bounds_max_um":[16101596,-36581792,-751562831]},{"stable_id":"tunnel-network/strate-2/room-1/primitive-2","strate_index":2,"center_um":[11266058,-41898301,-755492594],"half_extents_um":[2900022,2484772,2465036],"floor_um":-757609833,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8413310229855003,"bounds_min_um":[6616036,-46133073,-759707630],"bounds_max_um":[15916080,-37663529,-751277558]},{"stable_id":"tunnel-network/strate-2/room-10/primitive-0","strate_index":2,"center_um":[-6020215,-46837832,-775511224],"half_extents_um":[2387849,2387849,2029686],"floor_um":-777236531,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6716714120628840,"bounds_min_um":[-10158064,-50975681,-779290910],"bounds_max_um":[-1882366,-42699983,-771731538]},{"stable_id":"tunnel-network/strate-2/room-10/primitive-1","strate_index":2,"center_um":[-5872692,-46200705,-775741796],"half_extents_um":[1826203,1962098,1667795],"floor_um":-777236531,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":949841926828163,"bounds_min_um":[-9448895,-49912803,-779159591],"bounds_max_um":[-2296489,-42488607,-772324001]},{"stable_id":"tunnel-network/strate-2/room-11/primitive-0","strate_index":2,"center_um":[-3841819,-32182255,-777631544],"half_extents_um":[2861697,2861697,2432459],"floor_um":-779495825,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8857025401709256,"bounds_min_um":[-8453516,-36793952,-781814003],"bounds_max_um":[769878,-27570558,-773449085]},{"stable_id":"tunnel-network/strate-2/room-11/primitive-1","strate_index":2,"center_um":[-3658549,-31390746,-777838391],"half_extents_um":[2142648,1726483,1821263],"floor_um":-779495825,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2648511638984815,"bounds_min_um":[-7551197,-34867229,-781409654],"bounds_max_um":[234099,-27914263,-774267128]},{"stable_id":"tunnel-network/strate-2/room-11/primitive-2","strate_index":2,"center_um":[-4066656,-33153284,-778160634],"half_extents_um":[2084659,2032332,1771972],"floor_um":-779495825,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3923835578665842,"bounds_min_um":[-7901315,-36935616,-781682606],"bounds_max_um":[-231997,-29370952,-774638662]},{"stable_id":"tunnel-network/strate-2/room-12/primitive-0","strate_index":2,"center_um":[-1663423,-17526678,-778680904],"half_extents_um":[4903164,4903164,4167719],"floor_um":-781755118,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":974453298205719,"bounds_min_um":[-8316587,-24179842,-784598623],"bounds_max_um":[4989741,-10873514,-772763185]},{"stable_id":"tunnel-network/strate-2/room-12/primitive-1","strate_index":2,"center_um":[-1344755,-16150408,-779107843],"half_extents_um":[3265060,2532244,2775320],"floor_um":-781755118,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4287246203240359,"bounds_min_um":[-6359815,-20432652,-783633163],"bounds_max_um":[3670305,-11868164,-774582523]},{"stable_id":"tunnel-network/strate-2/room-13/primitive-0","strate_index":2,"center_um":[-574225,-10198889,-778588700],"half_extents_um":[6750000,6750000,5737541],"floor_um":-782884764,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4237475639875859,"bounds_min_um":[-9074225,-18698889,-786076241],"bounds_max_um":[7925775,-1698889,-771101159]},{"stable_id":"tunnel-network/strate-2/room-13/primitive-1","strate_index":2,"center_um":[-162640,-8421330,-778752776],"half_extents_um":[5324214,4346640,4525614],"floor_um":-782884764,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4079198432664629,"bounds_min_um":[-7236854,-14517970,-785028390],"bounds_max_um":[6911574,-2324690,-772477162]},{"stable_id":"tunnel-network/strate-2/room-14/primitive-0","strate_index":2,"center_um":[514974,-2871100,-782190765],"half_extents_um":[2513484,2513484,2136476],"floor_um":-784014411,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5513777901835640,"bounds_min_um":[-3748510,-7134584,-786077241],"bounds_max_um":[4778458,1392384,-778304289]},{"stable_id":"tunnel-network/strate-2/room-14/primitive-1","strate_index":2,"center_um":[708827,-2033881,-782363850],"half_extents_um":[1548535,2147599,1825472],"floor_um":-784014411,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4736343399867725,"bounds_min_um":[-2589708,-5931480,-785939322],"bounds_max_um":[4007362,1863718,-778788378]},{"stable_id":"tunnel-network/strate-2/room-15/primitive-0","strate_index":2,"center_um":[2693370,11784477,-784624347],"half_extents_um":[2568657,2568657,2183374],"floor_um":-786273704,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7277496228914001,"bounds_min_um":[-1625287,7465820,-788557721],"bounds_max_um":[7012027,16103134,-780690973]},{"stable_id":"tunnel-network/strate-2/room-15/primitive-1","strate_index":2,"center_um":[2825663,12355828,-784400364],"half_extents_um":[2161775,2315655,1968320],"floor_um":-786273704,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3986507348598952,"bounds_min_um":[-1086112,8290173,-788118684],"bounds_max_um":[6737438,16421483,-780682044]},{"stable_id":"tunnel-network/strate-2/room-16/primitive-0","strate_index":2,"center_um":[4871766,26440054,-787036556],"half_extents_um":[2218825,2218825,1886014],"floor_um":-788532998,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":619731278775179,"bounds_min_um":[902941,22471229,-790672570],"bounds_max_um":[8840591,30408879,-783400542]},{"stable_id":"tunnel-network/strate-2/room-16/primitive-1","strate_index":2,"center_um":[4994168,26968687,-787098831],"half_extents_um":[1879209,1675534,1597339],"floor_um":-788532998,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1702248410764664,"bounds_min_um":[1364959,23543153,-790446170],"bounds_max_um":[8623377,30394221,-783751492]},{"stable_id":"tunnel-network/strate-2/room-16/primitive-2","strate_index":2,"center_um":[4768977,25996124,-787103674],"half_extents_um":[2034374,1842627,1729230],"floor_um":-788532998,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5564952308868066,"bounds_min_um":[984603,22403497,-790582904],"bounds_max_um":[8553351,29588751,-783624444]},{"stable_id":"tunnel-network/strate-2/room-17/primitive-0","strate_index":2,"center_um":[7050163,41095632,-787158865],"half_extents_um":[4887035,4887035,4154009],"floor_um":-790792291,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8507542348025464,"bounds_min_um":[413128,34458597,-793062874],"bounds_max_um":[13687198,47732667,-781254856]},{"stable_id":"tunnel-network/strate-2/room-17/primitive-1","strate_index":2,"center_um":[7424471,42712199,-787452738],"half_extents_um":[4308519,3695270,3662267],"floor_um":-790792291,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":137070891032852,"bounds_min_um":[1365952,37266929,-792865005],"bounds_max_um":[13482990,48157469,-782040471]},{"stable_id":"tunnel-network/strate-2/room-17/primitive-2","strate_index":2,"center_um":[6826928,40131519,-788274898],"half_extents_um":[2696970,3720085,3162094],"floor_um":-790792291,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2276262411431109,"bounds_min_um":[2379958,34661434,-793186992],"bounds_max_um":[11273898,45601604,-783362804]},{"stable_id":"tunnel-network/strate-2/room-18/primitive-0","strate_index":2,"center_um":[9228559,55751209,-789503179],"half_extents_um":[4802420,4802420,4082086],"floor_um":-793051584,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4360877377612790,"bounds_min_um":[2676139,49198789,-795335265],"bounds_max_um":[15780979,62303629,-783671093]},{"stable_id":"tunnel-network/strate-2/room-18/primitive-1","strate_index":2,"center_um":[9502049,56932361,-789787541],"half_extents_um":[4092565,3247399,3478705],"floor_um":-793051584,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5597263948205663,"bounds_min_um":[3659484,51934962,-795016246],"bounds_max_um":[15344614,61929760,-784558836]},{"stable_id":"tunnel-network/strate-2/room-19/primitive-0","strate_index":2,"center_um":[11406955,70406787,-792668680],"half_extents_um":[3472456,3472456,2951608],"floor_um":-795310877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8346153217617005,"bounds_min_um":[6184499,65184331,-797370288],"bounds_max_um":[16629411,75629243,-787967072]},{"stable_id":"tunnel-network/strate-2/room-19/primitive-1","strate_index":2,"center_um":[11596697,71226250,-793261802],"half_extents_um":[2431662,2355598,2066927],"floor_um":-795310877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4360072178059799,"bounds_min_um":[7415035,67120652,-797078729],"bounds_max_um":[15778359,75331848,-789444875]},{"stable_id":"tunnel-network/strate-2/room-19/primitive-2","strate_index":2,"center_um":[11170897,69387297,-793090793],"half_extents_um":[2727321,2347079,2318239],"floor_um":-795310877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1461371555216243,"bounds_min_um":[6693576,65290218,-797159032],"bounds_max_um":[15648218,73484376,-789022554]},{"stable_id":"tunnel-network/strate-2/room-19/side-1/primitive-0","strate_index":2,"center_um":[13211571,78200589,-793479112],"half_extents_um":[2809826,2809826,2388369],"floor_um":-795310877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4071746333343996,"bounds_min_um":[8651745,73640763,-797617481],"bounds_max_um":[17771397,82760415,-789340743]},{"stable_id":"tunnel-network/strate-2/room-19/side-1/primitive-1","strate_index":2,"center_um":[13377214,78915974,-793937459],"half_extents_um":[1876747,1603207,1595246],"floor_um":-795310877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3953725205313751,"bounds_min_um":[9750467,75562767,-797282705],"bounds_max_um":[17003961,82269181,-790592213]},{"stable_id":"tunnel-network/strate-2/room-2/primitive-0","strate_index":2,"center_um":[6207370,-54000489,-756467282],"half_extents_um":[4425839,4425839,3761990],"floor_um":-759727738,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6192132064124667,"bounds_min_um":[31531,-60176328,-761979272],"bounds_max_um":[12383209,-47824650,-750955292]},{"stable_id":"tunnel-network/strate-2/room-2/primitive-1","strate_index":2,"center_um":[6392659,-53200258,-756792314],"half_extents_um":[4020641,2949099,3417569],"floor_um":-759727738,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2990344898337824,"bounds_min_um":[622018,-57899357,-761959883],"bounds_max_um":[12163300,-48501159,-751624745]},{"stable_id":"tunnel-network/strate-2/room-2/primitive-2","strate_index":2,"center_um":[5871403,-55451471,-757369489],"half_extents_um":[2488155,3301955,2806681],"floor_um":-759727738,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1276700820642230,"bounds_min_um":[1633248,-60503426,-761926170],"bounds_max_um":[10109558,-50399516,-752812808]},{"stable_id":"tunnel-network/strate-2/room-20/primitive-0","strate_index":2,"center_um":[13585352,85062365,-795021947],"half_extents_um":[4264406,4264406,3624771],"floor_um":-797570171,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3969348325447,"bounds_min_um":[7570946,79047959,-800396718],"bounds_max_um":[19599758,91076771,-789647176]},{"stable_id":"tunnel-network/strate-2/room-20/primitive-1","strate_index":2,"center_um":[13886798,86364256,-795328785],"half_extents_um":[2534435,3397287,2887714],"floor_um":-797570171,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7891806891759534,"bounds_min_um":[9602363,81216969,-799966499],"bounds_max_um":[18171233,91511543,-790691071]},{"stable_id":"tunnel-network/strate-2/room-20/primitive-2","strate_index":2,"center_um":[13403152,84275475,-794482316],"half_extents_um":[4015969,3449752,3413598],"floor_um":-797570171,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8495668019456799,"bounds_min_um":[7637183,79075723,-799645914],"bounds_max_um":[19169121,89475227,-789318718]},{"stable_id":"tunnel-network/strate-2/room-21/primitive-0","strate_index":2,"center_um":[8350060,72197453,-796679491],"half_extents_um":[4178024,4178024,3551345],"floor_um":-799688076,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8353780683996427,"bounds_min_um":[2422036,66269429,-801980836],"bounds_max_um":[14278084,78125477,-791378146]},{"stable_id":"tunnel-network/strate-2/room-21/primitive-1","strate_index":2,"center_um":[8546833,73047281,-796393638],"half_extents_um":[4034901,3040336,3429690],"floor_um":-799688076,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6275606997446609,"bounds_min_um":[2761932,68256945,-801573328],"bounds_max_um":[14331734,77837617,-791213948]},{"stable_id":"tunnel-network/strate-2/room-22/primitive-0","strate_index":2,"center_um":[3114767,59332540,-799940824],"half_extents_um":[3002189,3002189,2551878],"floor_um":-801805981,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4725229704537249,"bounds_min_um":[-1637422,54580351,-804242702],"bounds_max_um":[7866956,64084729,-795638946]},{"stable_id":"tunnel-network/strate-2/room-22/primitive-1","strate_index":2,"center_um":[3318887,60214097,-799893458],"half_extents_um":[1929253,2362089,2007790],"floor_um":-801805981,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2396133457513924,"bounds_min_um":[-360366,56102008,-803651248],"bounds_max_um":[6998140,64326186,-796135668]},{"stable_id":"tunnel-network/strate-2/room-23/primitive-0","strate_index":2,"center_um":[-2120525,46467627,-800779997],"half_extents_um":[4857437,4857437,4128851],"floor_um":-803923886,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5610586952733464,"bounds_min_um":[-8727962,39860190,-806658848],"bounds_max_um":[4486912,53075064,-794901146]},{"stable_id":"tunnel-network/strate-2/room-23/primitive-1","strate_index":2,"center_um":[-1761492,48018225,-801022107],"half_extents_um":[3810511,3727772,3238957],"floor_um":-803923886,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5164566143628108,"bounds_min_um":[-7322003,42540453,-806011064],"bounds_max_um":[3799019,53495997,-796033150]},{"stable_id":"tunnel-network/strate-2/room-24/primitive-0","strate_index":2,"center_um":[-7355818,33602715,-803832820],"half_extents_um":[3113529,3113529,2646518],"floor_um":-806041791,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":998271559690284,"bounds_min_um":[-12219347,28739186,-808229338],"bounds_max_um":[-2492289,38466244,-799436302]},{"stable_id":"tunnel-network/strate-2/room-24/primitive-1","strate_index":2,"center_um":[-7154770,34471005,-804596690],"half_extents_um":[2007431,1484070,1706328],"floor_um":-806041791,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":170759264075404,"bounds_min_um":[-10912201,31236935,-808053018],"bounds_max_um":[-3397339,37705075,-801140362]},{"stable_id":"tunnel-network/strate-2/room-24/side-2/primitive-0","strate_index":2,"center_um":[-9160434,25808913,-803619374],"half_extents_um":[3926542,3926542,3337584],"floor_um":-806041791,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7408076088762940,"bounds_min_um":[-14836976,20132371,-808706958],"bounds_max_um":[-3483892,31485455,-798531790]},{"stable_id":"tunnel-network/strate-2/room-24/side-2/primitive-1","strate_index":2,"center_um":[-8911672,26883271,-803115311],"half_extents_um":[3345687,3770645,3205071],"floor_um":-806041791,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6480493913793558,"bounds_min_um":[-14007359,21362626,-808070382],"bounds_max_um":[-3815985,32403916,-798160240]},{"stable_id":"tunnel-network/strate-2/room-25/primitive-0","strate_index":2,"center_um":[-12591110,20737802,-805663741],"half_extents_um":[3886887,3886887,3303877],"floor_um":-808159696,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6676136965576361,"bounds_min_um":[-18227997,15100915,-810717618],"bounds_max_um":[-6954223,26374689,-800609864]},{"stable_id":"tunnel-network/strate-2/room-25/primitive-1","strate_index":2,"center_um":[-12362052,21727061,-805367148],"half_extents_um":[3658012,3236315,3109332],"floor_um":-808159696,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4861500673895666,"bounds_min_um":[-17770064,16740746,-810226480],"bounds_max_um":[-6954040,26713376,-800507816]},{"stable_id":"tunnel-network/strate-2/room-25/primitive-2","strate_index":2,"center_um":[-12839139,19666610,-805409435],"half_extents_um":[3735292,3338542,3175020],"floor_um":-808159696,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6799134419770337,"bounds_min_um":[-18324431,14578068,-810334455],"bounds_max_um":[-7353847,24755152,-800484415]},{"stable_id":"tunnel-network/strate-2/room-26/primitive-0","strate_index":2,"center_um":[-17826403,7872889,-807466708],"half_extents_um":[4174468,4174468,3548323],"floor_um":-810277601,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1552637404666204,"bounds_min_um":[-23750871,1948421,-812765031],"bounds_max_um":[-11901935,13797357,-802168385]},{"stable_id":"tunnel-network/strate-2/room-26/primitive-1","strate_index":2,"center_um":[-17609466,8809803,-807391856],"half_extents_um":[3922353,3817255,3334023],"floor_um":-810277601,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1658728811274312,"bounds_min_um":[-23281819,3242548,-812475879],"bounds_max_um":[-11937113,14377058,-802307833]},{"stable_id":"tunnel-network/strate-2/room-3/primitive-0","strate_index":2,"center_um":[972078,-66865401,-759832470],"half_extents_um":[2749441,2749441,2337041],"floor_um":-761845643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2640905445479701,"bounds_min_um":[-3527363,-71364842,-763919511],"bounds_max_um":[5471519,-62365960,-755745429]},{"stable_id":"tunnel-network/strate-2/room-3/primitive-1","strate_index":2,"center_um":[1138515,-66146590,-760278313],"half_extents_um":[2460677,2267460,2091590],"floor_um":-761845643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8829183212444991,"bounds_min_um":[-3072162,-70164050,-764119903],"bounds_max_um":[5349192,-62129130,-756436723]},{"stable_id":"tunnel-network/strate-2/room-3/primitive-2","strate_index":2,"center_um":[831344,-67473205,-760119038],"half_extents_um":[2166879,2081474,1841860],"floor_um":-761845643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8354744626195101,"bounds_min_um":[-3085535,-71304679,-763710898],"bounds_max_um":[4748223,-63641731,-756527178]},{"stable_id":"tunnel-network/strate-2/room-3/side-0/primitive-0","strate_index":2,"center_um":[-832538,-74659203,-759844915],"half_extents_um":[2803968,2803968,2383389],"floor_um":-761845643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2662084728162065,"bounds_min_um":[-5386506,-79213171,-763978304],"bounds_max_um":[3721430,-70105235,-755711526]},{"stable_id":"tunnel-network/strate-2/room-3/side-0/primitive-1","strate_index":2,"center_um":[-664904,-73935223,-760260973],"half_extents_um":[1953412,2382234,2024913],"floor_um":-761845643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8513725474957650,"bounds_min_um":[-4368316,-78067457,-764035886],"bounds_max_um":[3038508,-69802989,-756486060]},{"stable_id":"tunnel-network/strate-2/room-3/side-0/primitive-2","strate_index":2,"center_um":[-1005953,-75408151,-760430148],"half_extents_um":[1889306,1687909,1605921],"floor_um":-761845643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":36217591490443,"bounds_min_um":[-4645259,-78846060,-763786069],"bounds_max_um":[2633353,-71970242,-757074227]},{"stable_id":"tunnel-network/strate-2/room-4/primitive-0","strate_index":2,"center_um":[-4263214,-79730314,-760448100],"half_extents_um":[4852450,4852450,4124612],"floor_um":-763963548,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2845885573645158,"bounds_min_um":[-10865664,-86332764,-766322712],"bounds_max_um":[2339236,-73127864,-754573488]},{"stable_id":"tunnel-network/strate-2/room-4/primitive-1","strate_index":2,"center_um":[-4061518,-78859224,-760931249],"half_extents_um":[3722391,4108989,3492665],"floor_um":-763963548,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5182219280308446,"bounds_min_um":[-9533909,-84718213,-766173914],"bounds_max_um":[1410873,-73000235,-755688584]},{"stable_id":"tunnel-network/strate-2/room-5/primitive-0","strate_index":2,"center_um":[-9498507,-92595227,-764397523],"half_extents_um":[2411668,2411668,2049932],"floor_um":-766081453,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8750423016047589,"bounds_min_um":[-13660175,-96756895,-768197455],"bounds_max_um":[-5336839,-88433559,-760597591]},{"stable_id":"tunnel-network/strate-2/room-5/primitive-1","strate_index":2,"center_um":[-9341942,-91919049,-764422404],"half_extents_um":[1724556,2065056,1755310],"floor_um":-766081453,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6729635284664735,"bounds_min_um":[-12816498,-95734105,-767927714],"bounds_max_um":[-5867386,-88103993,-760917094]},{"stable_id":"tunnel-network/strate-2/room-6/primitive-0","strate_index":2,"center_um":[-14733800,-105460141,-765360115],"half_extents_um":[4733441,4733441,4023453],"floor_um":-768199358,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2729955603759597,"bounds_min_um":[-21217241,-111943582,-771133568],"bounds_max_um":[-8250359,-98976700,-759586662]},{"stable_id":"tunnel-network/strate-2/room-6/primitive-1","strate_index":2,"center_um":[-14423768,-104121167,-765302959],"half_extents_um":[4239627,3129713,3603708],"floor_um":-768199358,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1328250596027720,"bounds_min_um":[-20413395,-109000880,-770656667],"bounds_max_um":[-8434141,-99241454,-759949251]},{"stable_id":"tunnel-network/strate-2/room-7/primitive-0","strate_index":2,"center_um":[-12555404,-90804565,-767303475],"half_extents_um":[4586293,4586293,3898377],"floor_um":-770458652,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6292182390675613,"bounds_min_um":[-18891697,-97140858,-772951852],"bounds_max_um":[-6219111,-84468272,-761655098]},{"stable_id":"tunnel-network/strate-2/room-7/primitive-1","strate_index":2,"center_um":[-12209924,-89312500,-767765873],"half_extents_um":[3710230,3763523,3199017],"floor_um":-770458652,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5824913408104067,"bounds_min_um":[-17670154,-94826023,-772714890],"bounds_max_um":[-6749694,-83798977,-762816856]},{"stable_id":"tunnel-network/strate-2/room-7/primitive-2","strate_index":2,"center_um":[-12865602,-92144253,-768162629],"half_extents_um":[2215184,3010104,2558606],"floor_um":-770458652,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8438796820341117,"bounds_min_um":[-16830786,-96904357,-772471235],"bounds_max_um":[-8900418,-87384149,-763854023]},{"stable_id":"tunnel-network/strate-2/room-8/primitive-0","strate_index":2,"center_um":[-10377008,-76148987,-771350062],"half_extents_um":[2068098,2068098,1757895],"floor_um":-772717945,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4233316435953742,"bounds_min_um":[-14195106,-79967085,-774857957],"bounds_max_um":[-6558910,-72330889,-767842167]},{"stable_id":"tunnel-network/strate-2/room-8/primitive-1","strate_index":2,"center_um":[-10279044,-75725899,-771506003],"half_extents_um":[1198135,1500898,1275772],"floor_um":-772717945,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7809339386482004,"bounds_min_um":[-13227179,-78976797,-774531775],"bounds_max_um":[-7330909,-72475001,-768480231]},{"stable_id":"tunnel-network/strate-2/room-9/primitive-0","strate_index":2,"center_um":[-8198612,-61493410,-773372824],"half_extents_um":[2600735,2600735,2210640],"floor_um":-774977238,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":928141076448714,"bounds_min_um":[-12549347,-65844145,-777333464],"bounds_max_um":[-3847877,-57142675,-769412184]},{"stable_id":"tunnel-network/strate-2/room-9/primitive-1","strate_index":2,"center_um":[-8074326,-60956639,-773282385],"half_extents_um":[2396004,1878239,2036618],"floor_um":-774977238,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5703069001346635,"bounds_min_um":[-12220330,-64584878,-777069003],"bounds_max_um":[-3928322,-57328400,-769495767]}],"cavern_edges":[{"stable_id":"tunnel-network/strate-2/departure-edge","start_anchor_stable_id":"passage/2-3/0/upper-mouth","end_room_stable_id":"tunnel-network/strate-2/room-26/primitive-0","strate_index":2,"control_points_um":[[-23061696,-4992024,-812395507],[-21316599,-703720,-811689539],[-19571501,3584584,-810983570],[-17826403,7872889,-810277601]],"polyline_um":[[-23061696,-4992024,-812395507],[-17826403,7872889,-810277601]],"bore_radius_um":4500000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3854785202241998,"bounds_min_um":[-29311696,-11242024,-814895507],"bounds_max_um":[-11576403,14122889,-804027601]},{"stable_id":"tunnel-network/strate-2/route-edge-0","start_anchor_stable_id":"passage/1-2/0/lower-mouth","end_room_stable_id":"tunnel-network/strate-2/room-0/primitive-0","strate_index":2,"control_points_um":[[21913247,-15405751,-753374023],[20168150,-19694055,-754079991],[18423053,-23982359,-754785959],[16677955,-28270663,-755491928]],"polyline_um":[[21913247,-15405751,-753374023],[16677955,-28270663,-755491928]],"bore_radius_um":4500000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3289607608418144,"bounds_min_um":[10427955,-34520663,-757991928],"bounds_max_um":[28163247,-9155751,-747124023]},{"stable_id":"tunnel-network/strate-2/route-edge-1","start_anchor_stable_id":"tunnel-network/strate-2/room-0/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-1/primitive-0","strate_index":2,"control_points_um":[[16677955,-28270663,-755491928],[14932858,-32558967,-756197896],[13187761,-36847271,-756903864],[11442663,-41135576,-757609833]],"polyline_um":[[16677955,-28270663,-755491928],[11442663,-41135576,-757609833]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8817415599178250,"bounds_min_um":[7942663,-44635576,-760109833],"bounds_max_um":[20177955,-24770663,-749241928]},{"stable_id":"tunnel-network/strate-2/route-edge-10","start_anchor_stable_id":"tunnel-network/strate-2/room-9/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-10/primitive-0","strate_index":2,"control_points_um":[[-8198612,-61493410,-774977238],[-7472480,-56608218,-775730335],[-6746348,-51723025,-776483433],[-6020215,-46837832,-777236531]],"polyline_um":[[-8198612,-61493410,-774977238],[-6020215,-46837832,-777236531]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7032802616799006,"bounds_min_um":[-11698612,-64993410,-779736531],"bounds_max_um":[-2520215,-43337832,-768727238]},{"stable_id":"tunnel-network/strate-2/route-edge-11","start_anchor_stable_id":"tunnel-network/strate-2/room-10/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-11/primitive-0","strate_index":2,"control_points_um":[[-6020215,-46837832,-777236531],[-5294083,-41952640,-777989629],[-4567951,-37067448,-778742727],[-3841819,-32182255,-779495825]],"polyline_um":[[-6020215,-46837832,-777236531],[-3841819,-32182255,-779495825]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1911978944492012,"bounds_min_um":[-9520215,-50337832,-781995825],"bounds_max_um":[-341819,-28682255,-770986531]},{"stable_id":"tunnel-network/strate-2/route-edge-12","start_anchor_stable_id":"tunnel-network/strate-2/room-11/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-12/primitive-0","strate_index":2,"control_points_um":[[-3841819,-32182255,-779495825],[-3115687,-27297063,-780248922],[-2389555,-22411871,-781002020],[-1663423,-17526678,-781755118]],"polyline_um":[[-3841819,-32182255,-779495825],[-1663423,-17526678,-781755118]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8165210430297268,"bounds_min_um":[-7341819,-35682255,-784255118],"bounds_max_um":[1836577,-14026678,-773245825]},{"stable_id":"tunnel-network/strate-2/route-edge-13","start_anchor_stable_id":"tunnel-network/strate-2/room-12/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-13/primitive-0","strate_index":2,"control_points_um":[[-1663423,-17526678,-781755118],[-1300357,-15084082,-782131666],[-937291,-12641486,-782508215],[-574225,-10198889,-782884764]],"polyline_um":[[-1663423,-17526678,-781755118],[-574225,-10198889,-782884764]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1869838225665002,"bounds_min_um":[-5163423,-21026678,-785384764],"bounds_max_um":[2925775,-6698889,-775505118]},{"stable_id":"tunnel-network/strate-2/route-edge-14","start_anchor_stable_id":"tunnel-network/strate-2/room-13/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-14/primitive-0","strate_index":2,"control_points_um":[[-574225,-10198889,-782884764],[-211159,-7756293,-783261313],[151907,-5313697,-783637862],[514974,-2871100,-784014411]],"polyline_um":[[-574225,-10198889,-782884764],[514974,-2871100,-784014411]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7495826887632453,"bounds_min_um":[-4074225,-13698889,-786514411],"bounds_max_um":[4014974,628900,-776634764]},{"stable_id":"tunnel-network/strate-2/route-edge-15","start_anchor_stable_id":"tunnel-network/strate-2/room-14/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-15/primitive-0","strate_index":2,"control_points_um":[[514974,-2871100,-784014411],[1241106,2014092,-784767508],[1967238,6899284,-785520606],[2693370,11784477,-786273704]],"polyline_um":[[514974,-2871100,-784014411],[2693370,11784477,-786273704]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1168863893760579,"bounds_min_um":[-2985026,-6371100,-788773704],"bounds_max_um":[6193370,15284477,-777764411]},{"stable_id":"tunnel-network/strate-2/route-edge-16","start_anchor_stable_id":"tunnel-network/strate-2/room-15/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-16/primitive-0","strate_index":2,"control_points_um":[[2693370,11784477,-786273704],[3419502,16669669,-787026802],[4145634,21554861,-787779900],[4871766,26440054,-788532998]],"polyline_um":[[2693370,11784477,-786273704],[4871766,26440054,-788532998]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3488700549169219,"bounds_min_um":[-806630,8284477,-791032998],"bounds_max_um":[8371766,29940054,-780023704]},{"stable_id":"tunnel-network/strate-2/route-edge-17","start_anchor_stable_id":"tunnel-network/strate-2/room-16/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-17/primitive-0","strate_index":2,"control_points_um":[[4871766,26440054,-788532998],[5597898,31325246,-789286095],[6324030,36210439,-790039193],[7050163,41095632,-790792291]],"polyline_um":[[4871766,26440054,-788532998],[7050163,41095632,-790792291]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4919553353375372,"bounds_min_um":[1371766,22940054,-793292291],"bounds_max_um":[10550163,44595632,-782282998]},{"stable_id":"tunnel-network/strate-2/route-edge-18","start_anchor_stable_id":"tunnel-network/strate-2/room-17/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-18/primitive-0","strate_index":2,"control_points_um":[[7050163,41095632,-790792291],[7776295,45980824,-791545388],[8502427,50866016,-792298486],[9228559,55751209,-793051584]],"polyline_um":[[7050163,41095632,-790792291],[9228559,55751209,-793051584]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8158943584761178,"bounds_min_um":[3550163,37595632,-795551584],"bounds_max_um":[12728559,59251209,-784542291]},{"stable_id":"tunnel-network/strate-2/route-edge-19","start_anchor_stable_id":"tunnel-network/strate-2/room-18/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-19/primitive-0","strate_index":2,"control_points_um":[[9228559,55751209,-793051584],[9954691,60636401,-793804681],[10680823,65521594,-794557779],[11406955,70406787,-795310877]],"polyline_um":[[9228559,55751209,-793051584],[11406955,70406787,-795310877]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":269617962051078,"bounds_min_um":[5728559,52251209,-797810877],"bounds_max_um":[14906955,73906787,-786801584]},{"stable_id":"tunnel-network/strate-2/route-edge-2","start_anchor_stable_id":"tunnel-network/strate-2/room-1/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-2/primitive-0","strate_index":2,"control_points_um":[[11442663,-41135576,-757609833],[9697566,-45423880,-758315801],[7952468,-49712184,-759021769],[6207370,-54000489,-759727738]],"polyline_um":[[11442663,-41135576,-757609833],[6207370,-54000489,-759727738]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8874695133302405,"bounds_min_um":[2707370,-57500489,-762227738],"bounds_max_um":[14942663,-37635576,-751359833]},{"stable_id":"tunnel-network/strate-2/route-edge-20","start_anchor_stable_id":"tunnel-network/strate-2/room-19/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-20/primitive-0","strate_index":2,"control_points_um":[[11406955,70406787,-795310877],[12133087,75291979,-796063975],[12859219,80177172,-796817073],[13585352,85062365,-797570171]],"polyline_um":[[11406955,70406787,-795310877],[13585352,85062365,-797570171]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":32404627650999,"bounds_min_um":[7906955,66906787,-800070171],"bounds_max_um":[17085352,88562365,-789060877]},{"stable_id":"tunnel-network/strate-2/route-edge-21","start_anchor_stable_id":"tunnel-network/strate-2/room-20/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-21/primitive-0","strate_index":2,"control_points_um":[[13585352,85062365,-797570171],[11840255,80774061,-798276139],[10095158,76485757,-798982107],[8350060,72197453,-799688076]],"polyline_um":[[13585352,85062365,-797570171],[8350060,72197453,-799688076]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4317428513448156,"bounds_min_um":[4850060,68697453,-802188076],"bounds_max_um":[17085352,88562365,-791320171]},{"stable_id":"tunnel-network/strate-2/route-edge-22","start_anchor_stable_id":"tunnel-network/strate-2/room-21/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-22/primitive-0","strate_index":2,"control_points_um":[[8350060,72197453,-799688076],[6604963,67909149,-800394044],[4859865,63620845,-801100012],[3114767,59332540,-801805981]],"polyline_um":[[8350060,72197453,-799688076],[3114767,59332540,-801805981]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7914775607336034,"bounds_min_um":[-385233,55832540,-804305981],"bounds_max_um":[11850060,75697453,-793438076]},{"stable_id":"tunnel-network/strate-2/route-edge-23","start_anchor_stable_id":"tunnel-network/strate-2/room-22/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-23/primitive-0","strate_index":2,"control_points_um":[[3114767,59332540,-801805981],[1369670,55044236,-802511949],[-375427,50755932,-803217917],[-2120525,46467627,-803923886]],"polyline_um":[[3114767,59332540,-801805981],[-2120525,46467627,-803923886]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4034550209005613,"bounds_min_um":[-5620525,42967627,-806423886],"bounds_max_um":[6614767,62832540,-795555981]},{"stable_id":"tunnel-network/strate-2/route-edge-24","start_anchor_stable_id":"tunnel-network/strate-2/room-23/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-24/primitive-0","strate_index":2,"control_points_um":[[-2120525,46467627,-803923886],[-3865622,42179323,-804629854],[-5610720,37891019,-805335822],[-7355818,33602715,-806041791]],"polyline_um":[[-2120525,46467627,-803923886],[-7355818,33602715,-806041791]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8223841641953534,"bounds_min_um":[-10855818,30102715,-808541791],"bounds_max_um":[1379475,49967627,-797673886]},{"stable_id":"tunnel-network/strate-2/route-edge-25","start_anchor_stable_id":"tunnel-network/strate-2/room-24/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-25/primitive-0","strate_index":2,"control_points_um":[[-7355818,33602715,-806041791],[-9100915,29314411,-806747759],[-10846012,25026107,-807453727],[-12591110,20737802,-808159696]],"polyline_um":[[-7355818,33602715,-806041791],[-12591110,20737802,-808159696]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8463461302563818,"bounds_min_um":[-16091110,17237802,-810659696],"bounds_max_um":[-3855818,37102715,-799791791]},{"stable_id":"tunnel-network/strate-2/route-edge-26","start_anchor_stable_id":"tunnel-network/strate-2/room-25/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-26/primitive-0","strate_index":2,"control_points_um":[[-12591110,20737802,-808159696],[-14336207,16449498,-808865664],[-16081305,12161194,-809571632],[-17826403,7872889,-810277601]],"polyline_um":[[-12591110,20737802,-808159696],[-17826403,7872889,-810277601]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1334322152237128,"bounds_min_um":[-21326403,4372889,-812777601],"bounds_max_um":[-9091110,24237802,-801909696]},{"stable_id":"tunnel-network/strate-2/route-edge-3","start_anchor_stable_id":"tunnel-network/strate-2/room-2/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-3/primitive-0","strate_index":2,"control_points_um":[[6207370,-54000489,-759727738],[4462273,-58288793,-760433706],[2717176,-62577097,-761139674],[972078,-66865401,-761845643]],"polyline_um":[[6207370,-54000489,-759727738],[972078,-66865401,-761845643]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6511238413130679,"bounds_min_um":[-2527922,-70365401,-764345643],"bounds_max_um":[9707370,-50500489,-753477738]},{"stable_id":"tunnel-network/strate-2/route-edge-4","start_anchor_stable_id":"tunnel-network/strate-2/room-3/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-4/primitive-0","strate_index":2,"control_points_um":[[972078,-66865401,-761845643],[-773019,-71153705,-762551611],[-2518116,-75442009,-763257579],[-4263214,-79730314,-763963548]],"polyline_um":[[972078,-66865401,-761845643],[-4263214,-79730314,-763963548]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4104421178731175,"bounds_min_um":[-7763214,-83230314,-766463548],"bounds_max_um":[4472078,-63365401,-755595643]},{"stable_id":"tunnel-network/strate-2/route-edge-5","start_anchor_stable_id":"tunnel-network/strate-2/room-4/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-5/primitive-0","strate_index":2,"control_points_um":[[-4263214,-79730314,-763963548],[-6008311,-84018618,-764669516],[-7753409,-88306922,-765375484],[-9498507,-92595227,-766081453]],"polyline_um":[[-4263214,-79730314,-763963548],[-9498507,-92595227,-766081453]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1712957457995776,"bounds_min_um":[-12998507,-96095227,-768581453],"bounds_max_um":[-763214,-76230314,-757713548]},{"stable_id":"tunnel-network/strate-2/route-edge-6","start_anchor_stable_id":"tunnel-network/strate-2/room-5/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-6/primitive-0","strate_index":2,"control_points_um":[[-9498507,-92595227,-766081453],[-11243604,-96883531,-766787421],[-12988702,-101171836,-767493389],[-14733800,-105460141,-768199358]],"polyline_um":[[-9498507,-92595227,-766081453],[-14733800,-105460141,-768199358]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":627735267583865,"bounds_min_um":[-18233800,-108960141,-770699358],"bounds_max_um":[-5998507,-89095227,-759831453]},{"stable_id":"tunnel-network/strate-2/route-edge-7","start_anchor_stable_id":"tunnel-network/strate-2/room-6/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-7/primitive-0","strate_index":2,"control_points_um":[[-14733800,-105460141,-768199358],[-14007668,-100574949,-768952456],[-13281536,-95689757,-769705554],[-12555404,-90804565,-770458652]],"polyline_um":[[-14733800,-105460141,-768199358],[-12555404,-90804565,-770458652]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2402170231604524,"bounds_min_um":[-18233800,-108960141,-772958652],"bounds_max_um":[-9055404,-87304565,-761949358]},{"stable_id":"tunnel-network/strate-2/route-edge-8","start_anchor_stable_id":"tunnel-network/strate-2/room-7/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-8/primitive-0","strate_index":2,"control_points_um":[[-12555404,-90804565,-770458652],[-11829272,-85919373,-771211749],[-11103140,-81034180,-771964847],[-10377008,-76148987,-772717945]],"polyline_um":[[-12555404,-90804565,-770458652],[-10377008,-76148987,-772717945]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":35699537972874,"bounds_min_um":[-16055404,-94304565,-775217945],"bounds_max_um":[-6877008,-72648987,-764208652]},{"stable_id":"tunnel-network/strate-2/route-edge-9","start_anchor_stable_id":"tunnel-network/strate-2/room-8/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-9/primitive-0","strate_index":2,"control_points_um":[[-10377008,-76148987,-772717945],[-9650876,-71263795,-773471042],[-8924744,-66378603,-774224140],[-8198612,-61493410,-774977238]],"polyline_um":[[-10377008,-76148987,-772717945],[-8198612,-61493410,-774977238]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8556892065984699,"bounds_min_um":[-13877008,-79648987,-777477238],"bounds_max_um":[-4698612,-57993410,-766467945]},{"stable_id":"tunnel-network/strate-2/side-edge-0","start_anchor_stable_id":"tunnel-network/strate-2/room-3/side-0/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-3/primitive-0","strate_index":2,"control_points_um":[[-832538,-74659203,-761845643],[-231000,-72061269,-761845643],[370539,-69463335,-761845643],[972078,-66865401,-761845643]],"polyline_um":[[-832538,-74659203,-761845643],[972078,-66865401,-761845643]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4352771090277651,"bounds_min_um":[-4332538,-78159203,-764345643],"bounds_max_um":[4472078,-63365401,-755595643]},{"stable_id":"tunnel-network/strate-2/side-edge-1","start_anchor_stable_id":"tunnel-network/strate-2/room-19/side-1/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-19/primitive-0","strate_index":2,"control_points_um":[[13211571,78200589,-795310877],[12610033,75602655,-795310877],[12008494,73004721,-795310877],[11406955,70406787,-795310877]],"polyline_um":[[13211571,78200589,-795310877],[11406955,70406787,-795310877]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2726979967805320,"bounds_min_um":[7906955,66906787,-797810877],"bounds_max_um":[16711571,81700589,-789060877]},{"stable_id":"tunnel-network/strate-2/side-edge-2","start_anchor_stable_id":"tunnel-network/strate-2/room-24/side-2/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-24/primitive-0","strate_index":2,"control_points_um":[[-9160434,25808913,-806041791],[-8558896,28406847,-806041791],[-7957357,31004781,-806041791],[-7355818,33602715,-806041791]],"polyline_um":[[-9160434,25808913,-806041791],[-7355818,33602715,-806041791]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":750000,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7796271028373642,"bounds_min_um":[-12660434,22308913,-808541791],"bounds_max_um":[-3855818,37102715,-799791791]}],"stable_identity":"resolved-layout-v3-fnv64:b3ef0f10b23efc77","resolved_strate_passage_styles":{"0":1,"1":1,"2":1,"3":1}} \ No newline at end of file diff --git a/Standalone/Research/WorldAssemblerM3/Linux134/layout-seed0-walk-gate.json b/Standalone/Research/WorldAssemblerM3/Linux134/layout-seed0-walk-gate.json new file mode 100644 index 0000000..d48e99c --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux134/layout-seed0-walk-gate.json @@ -0,0 +1 @@ +{"schema_version":3,"world_identity":"owner-world/seed/0","owner_voxel_size_um":125000,"core_voxel_size_um":250000,"strate_layout_chunk_size_um":8000000,"strate_layout_chunk_width_subvoxels":8192,"strates":[{"stable_id":"owner-world/seed/0/strate/0/generator/SurfaceWorld","order":0,"strate_index":0,"generator_kind":"SurfaceWorld","recipe_id":"SurfaceWorld.Heightfield2D","recipe_version":2,"interior_bottom_um":-504000000,"interior_top_um":8000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":7500000,"top_um":8000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-503500000,"top_um":7500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-504000000,"top_um":-503500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":1,"transition_width_um":8000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":2000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"BuildSurfaceStratumPlan uses resolved layout chunk width from ResolvedLayoutSnapshotV3","query_dispatch_path":"NativeSurfaceDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[{"kind":"cap_profile","stable_id":"biome/4097","units":"1/256 Core voxel","values":[-18022,0,64000,1536,0]},{"kind":"route_point","stable_id":"route/0","units":"1/256 Core voxel","values":[8192,24576,-381223,1024]},{"kind":"route_point","stable_id":"route/1","units":"1/256 Core voxel","values":[8192,24320,-381254,1024]},{"kind":"route_point","stable_id":"route/2","units":"1/256 Core voxel","values":[8192,24064,-381284,1024]},{"kind":"route_point","stable_id":"route/3","units":"1/256 Core voxel","values":[8192,23808,-381302,1024]},{"kind":"route_point","stable_id":"route/4","units":"1/256 Core voxel","values":[8192,23552,-381304,1024]},{"kind":"route_point","stable_id":"route/5","units":"1/256 Core voxel","values":[8192,23296,-381291,1024]},{"kind":"route_point","stable_id":"route/6","units":"1/256 Core voxel","values":[8192,23040,-381270,1024]},{"kind":"route_point","stable_id":"route/7","units":"1/256 Core voxel","values":[8192,22784,-381242,1024]},{"kind":"route_point","stable_id":"route/8","units":"1/256 Core voxel","values":[8192,22528,-381218,1024]},{"kind":"route_point","stable_id":"route/9","units":"1/256 Core voxel","values":[8192,22272,-381192,1024]},{"kind":"route_point","stable_id":"route/10","units":"1/256 Core voxel","values":[8192,22016,-381161,1024]},{"kind":"route_point","stable_id":"route/11","units":"1/256 Core voxel","values":[8192,21760,-381118,1024]},{"kind":"route_point","stable_id":"route/12","units":"1/256 Core voxel","values":[8192,21504,-381122,1024]},{"kind":"route_point","stable_id":"route/13","units":"1/256 Core voxel","values":[8192,21248,-381155,1024]},{"kind":"route_point","stable_id":"route/14","units":"1/256 Core voxel","values":[8192,20992,-381183,1024]},{"kind":"route_point","stable_id":"route/15","units":"1/256 Core voxel","values":[8192,20736,-381207,1024]},{"kind":"route_point","stable_id":"route/16","units":"1/256 Core voxel","values":[8192,20480,-381219,1024]},{"kind":"route_point","stable_id":"route/17","units":"1/256 Core voxel","values":[8192,20224,-381220,1024]},{"kind":"route_point","stable_id":"route/18","units":"1/256 Core voxel","values":[8192,19968,-381202,1024]},{"kind":"route_point","stable_id":"route/19","units":"1/256 Core voxel","values":[8192,19712,-381167,1024]},{"kind":"route_point","stable_id":"route/20","units":"1/256 Core voxel","values":[8192,19456,-381130,1024]},{"kind":"route_point","stable_id":"route/21","units":"1/256 Core voxel","values":[8192,19200,-381101,1024]},{"kind":"route_point","stable_id":"route/22","units":"1/256 Core voxel","values":[8192,18944,-381072,1024]},{"kind":"route_point","stable_id":"route/23","units":"1/256 Core voxel","values":[8192,18688,-381092,1024]},{"kind":"route_point","stable_id":"route/24","units":"1/256 Core voxel","values":[8192,18432,-381149,1024]},{"kind":"route_point","stable_id":"route/25","units":"1/256 Core voxel","values":[8192,18176,-381200,1024]},{"kind":"route_point","stable_id":"route/26","units":"1/256 Core voxel","values":[8192,17920,-381237,1024]},{"kind":"route_point","stable_id":"route/27","units":"1/256 Core voxel","values":[8192,17664,-381260,1024]},{"kind":"route_point","stable_id":"route/28","units":"1/256 Core voxel","values":[8192,17408,-381262,1024]},{"kind":"route_point","stable_id":"route/29","units":"1/256 Core voxel","values":[8192,17152,-381263,1024]},{"kind":"route_point","stable_id":"route/30","units":"1/256 Core voxel","values":[8192,16896,-381248,1024]},{"kind":"route_point","stable_id":"route/31","units":"1/256 Core voxel","values":[8192,16640,-381216,1024]},{"kind":"route_point","stable_id":"route/32","units":"1/256 Core voxel","values":[8192,16384,-381197,1024]}]},{"stable_id":"owner-world/seed/0/strate/1/generator/FloatingIslands","order":1,"strate_index":1,"generator_kind":"FloatingIslands","recipe_id":"FloatingIslands.ClusterField","recipe_version":0,"interior_bottom_um":-744000000,"interior_top_um":-504000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":-504500000,"top_um":-504000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-743500000,"top_um":-504500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-744000000,"top_um":-743500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":3,"transition_width_um":24000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":6000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"BuildFloatingIslandStratumPlan uses resolved layout chunk width from ResolvedLayoutSnapshotV3","query_dispatch_path":"NativeFloatingIslandDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[]},{"stable_id":"owner-world/seed/0/strate/2/generator/TunnelNetwork","order":2,"strate_index":2,"generator_kind":"TunnelNetwork","recipe_id":"Core.Caves.PrimitiveField","recipe_version":1,"interior_bottom_um":-872000000,"interior_top_um":-744000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":-744500000,"top_um":-744000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-871500000,"top_um":-744500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-872000000,"top_um":-871500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":2,"transition_width_um":16000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":4000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"resolved manager slot scaled by ResolvedLayoutSnapshotV3 chunk width","query_dispatch_path":"legacy Core route selects manager chunk by floor(CoreZ / resolved layout chunk width)","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[{"kind":"route_point","stable_id":"tunnel-network/route/000-arrival","units":"1/256 Core voxel","values":[19626,-14541,-771199]},{"kind":"route_point","stable_id":"tunnel-network/route/1","units":"1/256 Core voxel","values":[17078,-28949,-773368]},{"kind":"route_point","stable_id":"tunnel-network/route/2","units":"1/256 Core voxel","values":[11717,-42123,-775536]},{"kind":"route_point","stable_id":"tunnel-network/route/3","units":"1/256 Core voxel","values":[6356,-55297,-777705]},{"kind":"route_point","stable_id":"tunnel-network/route/4","units":"1/256 Core voxel","values":[995,-68470,-779874]},{"kind":"route_point","stable_id":"tunnel-network/route/5","units":"1/256 Core voxel","values":[-4366,-81644,-782043]},{"kind":"route_point","stable_id":"tunnel-network/route/6","units":"1/256 Core voxel","values":[-9726,-94818,-784211]},{"kind":"route_point","stable_id":"tunnel-network/route/7","units":"1/256 Core voxel","values":[-15087,-107991,-786380]},{"kind":"route_point","stable_id":"tunnel-network/route/8","units":"1/256 Core voxel","values":[-12857,-92984,-788694]},{"kind":"route_point","stable_id":"tunnel-network/route/9","units":"1/256 Core voxel","values":[-10626,-77977,-791007]},{"kind":"route_point","stable_id":"tunnel-network/route/10","units":"1/256 Core voxel","values":[-8395,-62969,-793321]},{"kind":"route_point","stable_id":"tunnel-network/route/11","units":"1/256 Core voxel","values":[-6165,-47962,-795634]},{"kind":"route_point","stable_id":"tunnel-network/route/12","units":"1/256 Core voxel","values":[-3934,-32955,-797948]},{"kind":"route_point","stable_id":"tunnel-network/route/13","units":"1/256 Core voxel","values":[-1703,-17947,-800261]},{"kind":"route_point","stable_id":"tunnel-network/route/14","units":"1/256 Core voxel","values":[-588,-10444,-801418]},{"kind":"route_point","stable_id":"tunnel-network/route/15","units":"1/256 Core voxel","values":[527,-2940,-802575]},{"kind":"route_point","stable_id":"tunnel-network/route/16","units":"1/256 Core voxel","values":[2758,12067,-804888]},{"kind":"route_point","stable_id":"tunnel-network/route/17","units":"1/256 Core voxel","values":[4989,27075,-807202]},{"kind":"route_point","stable_id":"tunnel-network/route/18","units":"1/256 Core voxel","values":[7219,42082,-809515]},{"kind":"route_point","stable_id":"tunnel-network/route/19","units":"1/256 Core voxel","values":[9450,57089,-811829]},{"kind":"route_point","stable_id":"tunnel-network/route/20","units":"1/256 Core voxel","values":[11681,72097,-814142]},{"kind":"route_point","stable_id":"tunnel-network/route/21","units":"1/256 Core voxel","values":[13911,87104,-816456]},{"kind":"route_point","stable_id":"tunnel-network/route/22","units":"1/256 Core voxel","values":[8550,73930,-818625]},{"kind":"route_point","stable_id":"tunnel-network/route/23","units":"1/256 Core voxel","values":[3190,60757,-820793]},{"kind":"route_point","stable_id":"tunnel-network/route/24","units":"1/256 Core voxel","values":[-2171,47583,-822962]},{"kind":"route_point","stable_id":"tunnel-network/route/25","units":"1/256 Core voxel","values":[-7532,34409,-825131]},{"kind":"route_point","stable_id":"tunnel-network/route/26","units":"1/256 Core voxel","values":[-12893,21236,-827300]},{"kind":"route_point","stable_id":"tunnel-network/route/27","units":"1/256 Core voxel","values":[-18254,8062,-829468]},{"kind":"route_point","stable_id":"tunnel-network/route/999-departure","units":"1/256 Core voxel","values":[-20544,-5190,-831637]}]},{"stable_id":"owner-world/seed/0/strate/3/generator/VerticalShafts","order":3,"strate_index":3,"generator_kind":"VerticalShafts","recipe_id":"legacy.VerticalShafts","recipe_version":0,"interior_bottom_um":-1000000000,"interior_top_um":-872000000,"voxel_size_um":250000,"lattice_origin_um":[0,0,0],"ceiling":{"kind":"ceiling","bottom_um":-872500000,"top_um":-872000000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid ceiling from strate BoundarySealThickness; breakable by default","density_side":"interior-facing lower surface","cap_path":"shared resolved-layout density band"},"content":{"kind":"content","bottom_um":-999500000,"top_um":-872500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":65536,"behaviour":"resolved generator interior","density_side":"none","cap_path":"none"},"bedrock":{"kind":"bedrock","bottom_um":-1000000000,"top_um":-999500000,"present":true,"diggable":true,"protected":false,"solid_density_q16":524288,"behaviour":"solid bedrock floor from strate BoundarySealThickness; breakable by default","density_side":"interior-facing upper surface","cap_path":"shared resolved-layout density band"},"transition_mode":"Gradient","transition_blend_chunks":2,"transition_width_um":16000000,"transition_warp_amplitude_um":0,"transition_interpenetration_um":4000000,"transition_honoured_in_core":true,"transition_falls_back_to_hard":false,"transition_dispatch_path":"shared composition moves both band faces together with the smooth declared-width transition field","bound_dispatch_path":"resolved manager slot scaled by ResolvedLayoutSnapshotV3 chunk width","query_dispatch_path":"legacy Core route selects manager chunk by floor(CoreZ / resolved layout chunk width)","manager_lookup":"UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate","manager_chunk_width_um":8000000,"dispatch_chunk_width_um":8000000,"anchors":[]}],"passages":[{"stable_id":"passage/0-1/0","upper_strate_index":0,"lower_strate_index":1,"style":1,"placement_radius_q":38400,"upper_mouth_q":[8192,16384,-381197],"lower_mouth_q":[26457,3276,-754688],"mouth_radius_q":4608,"mid_radius_q":3584,"control_points":[{"position_q":[8192,16384,-380818],"radius_q":1536},{"position_q":[917,-21635,-385895],"radius_q":3584},{"position_q":[-7484,-19826,-387477],"radius_q":3584},{"position_q":[-14728,-15190,-389060],"radius_q":3584},{"position_q":[-19949,-8188,-390668],"radius_q":3584},{"position_q":[-22342,455,-392319],"radius_q":3584},{"position_q":[-21299,9658,-394024],"radius_q":3584},{"position_q":[-16638,18010,-395785],"radius_q":3584},{"position_q":[-8834,23996,-397595],"radius_q":3584},{"position_q":[917,26365,-399442],"radius_q":3584},{"position_q":[10885,24520,-401308],"radius_q":3584},{"position_q":[19213,18751,-403173],"radius_q":3584},{"position_q":[24397,10181,-405017],"radius_q":3584},{"position_q":[25658,455,-406822],"radius_q":3584},{"position_q":[23047,-8711,-408577],"radius_q":3584},{"position_q":[17303,-15931,-410275],"radius_q":3584},{"position_q":[9535,-20350,-411920],"radius_q":3584},{"position_q":[1023,-22825,-413552],"radius_q":3584},{"position_q":[-7513,-20559,-415177],"radius_q":3584},{"position_q":[-14529,-15504,-416769],"radius_q":3584},{"position_q":[-19292,-8367,-418349],"radius_q":3584},{"position_q":[-21267,48,-419940],"radius_q":3584},{"position_q":[-20089,8793,-421564],"radius_q":3584},{"position_q":[-15656,16727,-423237],"radius_q":3584},{"position_q":[-8310,22578,-424966],"radius_q":3584},{"position_q":[1023,25175,-426749],"radius_q":3584},{"position_q":[10856,23787,-428577],"radius_q":3584},{"position_q":[19412,18437,-430435],"radius_q":3584},{"position_q":[25054,10002,-432303],"radius_q":3584},{"position_q":[26733,48,-434161],"radius_q":3584},{"position_q":[24257,-9576,-435990],"radius_q":3584},{"position_q":[18285,-17214,-437775],"radius_q":3584},{"position_q":[10059,-21768,-439506],"radius_q":3584},{"position_q":[956,-24440,-441252],"radius_q":3584},{"position_q":[-7955,-21879,-442959],"radius_q":3584},{"position_q":[-15028,-16351,-444611],"radius_q":3584},{"position_q":[-19496,-8840,-446220],"radius_q":3584},{"position_q":[-20998,-368,-447804],"radius_q":3584},{"position_q":[-19445,8082,-449386],"radius_q":3584},{"position_q":[-14956,15543,-450988],"radius_q":3584},{"position_q":[-7904,21020,-452632],"radius_q":3584},{"position_q":[956,23560,-454329],"radius_q":3584},{"position_q":[10414,22467,-456081],"radius_q":3584},{"position_q":[18913,17590,-457885],"radius_q":3584},{"position_q":[24850,9530,-459728],"radius_q":3584},{"position_q":[27002,-368,-461592],"radius_q":3584},{"position_q":[24901,-10287,-463459],"radius_q":3584},{"position_q":[18986,-18398,-465307],"radius_q":3584},{"position_q":[10465,-23326,-467119],"radius_q":3584},{"position_q":[727,-25966,-468976],"radius_q":3584},{"position_q":[-8660,-23382,-470768],"radius_q":3584},{"position_q":[-16053,-17501,-472507],"radius_q":3584},{"position_q":[-20498,-9513,-474190],"radius_q":3584},{"position_q":[-21647,-721,-475822],"radius_q":3584},{"position_q":[-19618,7706,-477418],"radius_q":3584},{"position_q":[-14808,14814,-478997],"radius_q":3584},{"position_q":[-7780,19814,-480585],"radius_q":3584},{"position_q":[727,22034,-482204],"radius_q":3584},{"position_q":[9709,20964,-483869],"radius_q":3584},{"position_q":[17888,16440,-485589],"radius_q":3584},{"position_q":[23848,8856,-487365],"radius_q":3584},{"position_q":[26353,-721,-489187],"radius_q":3584},{"position_q":[24728,-10663,-491041],"radius_q":3584},{"position_q":[19133,-19128,-492909],"radius_q":3584},{"position_q":[10589,-24532,-494770],"radius_q":3584},{"position_q":[375,-26910,-496701],"radius_q":3584},{"position_q":[-9413,-24583,-498553],"radius_q":3584},{"position_q":[-17272,-18600,-500371],"radius_q":3584},{"position_q":[-21974,-10210,-502141],"radius_q":3584},{"position_q":[-23021,-953,-503856],"radius_q":3584},{"position_q":[-20591,7731,-505516],"radius_q":3584},{"position_q":[-15315,14737,-507131],"radius_q":3584},{"position_q":[-8029,19336,-508717],"radius_q":3584},{"position_q":[375,21090,-510297],"radius_q":3584},{"position_q":[8956,19763,-511896],"radius_q":3584},{"position_q":[16669,15341,-513532],"radius_q":3584},{"position_q":[22372,8159,-515220],"radius_q":3584},{"position_q":[24979,-953,-516965],"radius_q":3584},{"position_q":[23755,-10638,-518762],"radius_q":3584},{"position_q":[18626,-19204,-520600],"radius_q":3584},{"position_q":[10340,-25010,-522462],"radius_q":3584},{"position_q":[-40,-26968,-524407],"radius_q":3584},{"position_q":[-10007,-25083,-526274],"radius_q":3584},{"position_q":[-18304,-19287,-528137],"radius_q":3584},{"position_q":[-23449,-10719,-529977],"radius_q":3584},{"position_q":[-24682,-1023,-531776],"radius_q":3584},{"position_q":[-22073,8103,-533523],"radius_q":3584},{"position_q":[-16359,15296,-535214],"radius_q":3584},{"position_q":[-8631,19716,-536853],"radius_q":3584},{"position_q":[-40,21032,-538453],"radius_q":3584},{"position_q":[8363,19263,-540034],"radius_q":3584},{"position_q":[15637,14654,-541619],"radius_q":3584},{"position_q":[20897,7650,-543232],"radius_q":3584},{"position_q":[23318,-1023,-544889],"radius_q":3584},{"position_q":[22273,-10266,-546601],"radius_q":3584},{"position_q":[17582,-18646,-548369],"radius_q":3584},{"position_q":[9738,-24629,-550185],"radius_q":3584},{"position_q":[-449,-26134,-552081],"radius_q":3584},{"position_q":[-10304,-24712,-553914],"radius_q":3584},{"position_q":[-18851,-19323,-555774],"radius_q":3584},{"position_q":[-24459,-10866,-557642],"radius_q":3584},{"position_q":[-26099,-920,-559497],"radius_q":3584},{"position_q":[-23601,8670,-561322],"radius_q":3584},{"position_q":[-17638,16268,-563100],"radius_q":3584},{"position_q":[-9446,20800,-564823],"radius_q":3584},{"position_q":[-449,21866,-566491],"radius_q":3584},{"position_q":[8065,19634,-568111],"radius_q":3584},{"position_q":[15090,14619,-569700],"radius_q":3584},{"position_q":[19887,7503,-571280],"radius_q":3584},{"position_q":[21901,-920,-572874],"radius_q":3584},{"position_q":[20745,-9699,-574504],"radius_q":3584},{"position_q":[16304,-17673,-576184],"radius_q":3584},{"position_q":[8923,-23546,-577920],"radius_q":3584},{"position_q":[-781,-24695,-579719],"radius_q":3584},{"position_q":[-10277,-23587,-581479],"radius_q":3584},{"position_q":[-18792,-18673,-583289],"radius_q":3584},{"position_q":[-24714,-10575,-585135],"radius_q":3584},{"position_q":[-26829,-662,-587001],"radius_q":3584},{"position_q":[-24690,9242,-588867],"radius_q":3584},{"position_q":[-18759,17316,-590711],"radius_q":3584},{"position_q":[-10254,22207,-592517],"radius_q":3584},{"position_q":[-781,23305,-594272],"radius_q":3584},{"position_q":[8092,20759,-595972],"radius_q":3584},{"position_q":[15149,15268,-597618],"radius_q":3584},{"position_q":[19632,7794,-599222],"radius_q":3584},{"position_q":[21171,-662,-600804],"radius_q":3584},{"position_q":[19656,-9127,-602387],"radius_q":3584},{"position_q":[15182,-16625,-603994],"radius_q":3584},{"position_q":[8115,-22139,-605644],"radius_q":3584},{"position_q":[-982,-23132,-607329],"radius_q":3584},{"position_q":[-10003,-22072,-609001],"radius_q":3584},{"position_q":[-18216,-17526,-610729],"radius_q":3584},{"position_q":[-24186,-9903,-612512],"radius_q":3584},{"position_q":[-26670,-292,-614339],"radius_q":3584},{"position_q":[-25005,9660,-616196],"radius_q":3584},{"position_q":[-19376,18103,-618064],"radius_q":3584},{"position_q":[-10823,23468,-619923],"radius_q":3584},{"position_q":[-982,24868,-621753],"radius_q":3584},{"position_q":[8366,22274,-623538],"radius_q":3584},{"position_q":[15725,16415,-625270],"radius_q":3584},{"position_q":[20160,8466,-626946],"radius_q":3584},{"position_q":[21330,-292,-628572],"radius_q":3584},{"position_q":[19340,-8709,-630164],"radius_q":3584},{"position_q":[14565,-15838,-631744],"radius_q":3584},{"position_q":[7546,-20878,-633335],"radius_q":3584},{"position_q":[-1016,-21949,-634923],"radius_q":3584},{"position_q":[-9624,-20652,-636525],"radius_q":3584},{"position_q":[-17377,-16233,-638168],"radius_q":3584},{"position_q":[-23110,-9024,-639864],"radius_q":3584},{"position_q":[-25720,128,-641615],"radius_q":3584},{"position_q":[-24470,9843,-643418],"radius_q":3584},{"position_q":[-19300,18412,-645261],"radius_q":3584},{"position_q":[-10984,24192,-647125],"radius_q":3584},{"position_q":[-1016,26051,-648991],"radius_q":3584},{"position_q":[8746,23694,-650840],"radius_q":3584},{"position_q":[16564,17708,-652653],"radius_q":3584},{"position_q":[21236,9346,-654416],"radius_q":3584},{"position_q":[22280,128,-656123],"radius_q":3584},{"position_q":[19876,-8526,-657777],"radius_q":3584},{"position_q":[14641,-15529,-659386],"radius_q":3584},{"position_q":[7386,-20154,-660970],"radius_q":3584},{"position_q":[-878,-21498,-662511],"radius_q":3584},{"position_q":[-9285,-19770,-664091],"radius_q":3584},{"position_q":[-16591,-15186,-665679],"radius_q":3584},{"position_q":[-21892,-8178,-667296],"radius_q":3584},{"position_q":[-24338,526,-668961],"radius_q":3584},{"position_q":[-23288,9809,-670681],"radius_q":3584},{"position_q":[-18566,18214,-672455],"radius_q":3584},{"position_q":[-10682,24194,-674277],"radius_q":3584},{"position_q":[-878,26502,-676131],"radius_q":3584},{"position_q":[9084,24576,-677999],"radius_q":3584},{"position_q":[17350,18755,-679860],"radius_q":3584},{"position_q":[22454,10191,-681695],"radius_q":3584},{"position_q":[23662,526,-683488],"radius_q":3584},{"position_q":[21058,-8560,-685228],"radius_q":3584},{"position_q":[15375,-15727,-686911],"radius_q":3584},{"position_q":[7687,-20152,-688544],"radius_q":3584},{"position_q":[-592,-21868,-690101],"radius_q":3584},{"position_q":[-9086,-19670,-691716],"radius_q":3584},{"position_q":[-16122,-14694,-693302],"radius_q":3584},{"position_q":[-20954,-7599,-694882],"radius_q":3584},{"position_q":[-23007,835,-696480],"radius_q":3584},{"position_q":[-21871,9649,-698116],"radius_q":3584},{"position_q":[-17419,17662,-699803],"radius_q":3584},{"position_q":[-10003,23555,-701547],"radius_q":3584},{"position_q":[-592,26132,-703343],"radius_q":3584},{"position_q":[9283,24675,-705181],"radius_q":3584},{"position_q":[17819,19247,-707043],"radius_q":3584},{"position_q":[23392,10770,-708910],"radius_q":3584},{"position_q":[24993,835,-710762],"radius_q":3584},{"position_q":[22475,-8720,-712581],"radius_q":3584},{"position_q":[16523,-16280,-714353],"radius_q":3584},{"position_q":[8366,-20791,-716069],"radius_q":3584},{"position_q":[-206,-22859,-717692],"radius_q":3584},{"position_q":[-9043,-20329,-719384],"radius_q":3584},{"position_q":[-16086,-14877,-721023],"radius_q":3584},{"position_q":[-20586,-7439,-722624],"radius_q":3584},{"position_q":[-22163,1003,-724205],"radius_q":3584},{"position_q":[-20684,9485,-725789],"radius_q":3584},{"position_q":[-16224,17021,-727401],"radius_q":3584},{"position_q":[-9140,22572,-729058],"radius_q":3584},{"position_q":[-206,25141,-730769],"radius_q":3584},{"position_q":[9327,24016,-732536],"radius_q":3584},{"position_q":[17855,19064,-734352],"radius_q":3584},{"position_q":[23760,10930,-736202],"radius_q":3584},{"position_q":[25837,1003,-738069],"radius_q":3584},{"position_q":[23662,-8884,-739932],"radius_q":3584},{"position_q":[17717,-16920,-741772],"radius_q":3584},{"position_q":[9229,-21774,-743572],"radius_q":3584},{"position_q":[215,-24070,-745285],"radius_q":3584},{"position_q":[26457,3276,-751237],"radius_q":4608}]},{"stable_id":"passage/1-2/0","upper_strate_index":1,"lower_strate_index":2,"style":1,"placement_radius_q":24576,"upper_mouth_q":[19626,-14541,-749774],"lower_mouth_q":[19626,-14541,-771455],"mouth_radius_q":4608,"mid_radius_q":3072,"control_points":[{"position_q":[19626,-14541,-746323],"radius_q":4608},{"position_q":[11028,-10341,-748134],"radius_q":4608},{"position_q":[14628,-5642,-750790],"radius_q":3072},{"position_q":[16189,228,-751940],"radius_q":3072},{"position_q":[15303,6377,-753115],"radius_q":3072},{"position_q":[11982,11751,-754311],"radius_q":3072},{"position_q":[6728,15364,-755517],"radius_q":3072},{"position_q":[459,16542,-756725],"radius_q":3072},{"position_q":[-5703,15102,-757922],"radius_q":3072},{"position_q":[-10695,11381,-759100],"radius_q":3072},{"position_q":[-13754,6115,-760253],"radius_q":3072},{"position_q":[-14531,228,-761377],"radius_q":3072},{"position_q":[-13079,-5380,-762473],"radius_q":3072},{"position_q":[-9740,-9971,-763547],"radius_q":3072},{"position_q":[-5028,-13017,-764609],"radius_q":3072},{"position_q":[459,-14178,-765670],"radius_q":3072},{"position_q":[6053,-13279,-766743],"radius_q":3072},{"position_q":[11518,-10983,-767571],"radius_q":3365},{"position_q":[19626,-14541,-768004],"radius_q":4608}]},{"stable_id":"passage/2-3/0","upper_strate_index":2,"lower_strate_index":3,"style":1,"placement_radius_q":24576,"upper_mouth_q":[-20544,-5190,-831893],"lower_mouth_q":[-20544,-5190,-921962],"mouth_radius_q":4608,"mid_radius_q":3072,"control_points":[{"position_q":[-20544,-5190,-828442],"radius_q":4608},{"position_q":[-13079,-5380,-829802],"radius_q":4608},{"position_q":[-9740,-9971,-832372],"radius_q":3072},{"position_q":[-5028,-13017,-833394],"radius_q":3072},{"position_q":[459,-14178,-834416],"radius_q":3072},{"position_q":[6053,-13279,-835448],"radius_q":3072},{"position_q":[11028,-10341,-836500],"radius_q":3072},{"position_q":[14628,-5642,-837578],"radius_q":3072},{"position_q":[16189,228,-838685],"radius_q":3072},{"position_q":[15303,6377,-839816],"radius_q":3072},{"position_q":[11982,11751,-840967],"radius_q":3072},{"position_q":[6728,15364,-842128],"radius_q":3072},{"position_q":[459,16542,-843290],"radius_q":3072},{"position_q":[-5703,15102,-844443],"radius_q":3072},{"position_q":[-10695,11381,-845577],"radius_q":3072},{"position_q":[-13754,6115,-846687],"radius_q":3072},{"position_q":[-14531,228,-847768],"radius_q":3072},{"position_q":[-12750,-5469,-848855],"radius_q":3072},{"position_q":[-9640,-10128,-849876],"radius_q":3072},{"position_q":[-5042,-13384,-850902],"radius_q":3072},{"position_q":[511,-14773,-851945],"radius_q":3072},{"position_q":[6316,-13988,-853012],"radius_q":3072},{"position_q":[11518,-10983,-854106],"radius_q":3072},{"position_q":[15233,-6074,-855227],"radius_q":3072},{"position_q":[16726,24,-856371],"radius_q":3072},{"position_q":[15631,6287,-857529],"radius_q":3072},{"position_q":[12082,11594,-858692],"radius_q":3072},{"position_q":[6714,14998,-859850],"radius_q":3072},{"position_q":[511,15947,-860993],"radius_q":3072},{"position_q":[-5441,14393,-862113],"radius_q":3072},{"position_q":[-10204,10739,-863207],"radius_q":3072},{"position_q":[-13149,5682,-864273],"radius_q":3072},{"position_q":[-13994,24,-865315],"radius_q":3072},{"position_q":[-12852,-5706,-866379],"radius_q":3072},{"position_q":[-9890,-10552,-867414],"radius_q":3072},{"position_q":[-5263,-14044,-868469],"radius_q":3072},{"position_q":[478,-15580,-869552],"radius_q":3072},{"position_q":[6519,-14767,-870662],"radius_q":3072},{"position_q":[11869,-11575,-871797],"radius_q":3072},{"position_q":[15555,-6429,-872950],"radius_q":3072},{"position_q":[16861,-184,-874112],"radius_q":3072},{"position_q":[15529,6051,-875273],"radius_q":3072},{"position_q":[11833,11171,-876423],"radius_q":3072},{"position_q":[6493,14338,-877554],"radius_q":3072},{"position_q":[478,15140,-878660],"radius_q":3072},{"position_q":[-5238,13614,-879737],"radius_q":3072},{"position_q":[-9854,10147,-880789],"radius_q":3072},{"position_q":[-12827,5327,-881821],"radius_q":3072},{"position_q":[-13859,-184,-882842],"radius_q":3072},{"position_q":[-13353,-6043,-883913],"radius_q":3072},{"position_q":[-10402,-11127,-884984],"radius_q":3072},{"position_q":[-5616,-14795,-886082],"radius_q":3072},{"position_q":[363,-16343,-887207],"radius_q":3072},{"position_q":[6581,-15370,-888353],"radius_q":3072},{"position_q":[11942,-11940,-889513],"radius_q":3072},{"position_q":[15468,-6618,-890675],"radius_q":3072},{"position_q":[16536,-361,-891832],"radius_q":3072},{"position_q":[15028,5714,-892972],"radius_q":3072},{"position_q":[11320,10596,-894088],"radius_q":3072},{"position_q":[6140,13586,-895178],"radius_q":3072},{"position_q":[363,14377,-896240],"radius_q":3072},{"position_q":[-5176,13011,-897279],"radius_q":3072},{"position_q":[-9780,9783,-898303],"radius_q":3072},{"position_q":[-12913,5139,-899324],"radius_q":3072},{"position_q":[-14184,-361,-900352],"radius_q":3072},{"position_q":[-14091,-6391,-901450],"radius_q":3072},{"position_q":[-11012,-11676,-902564],"radius_q":3072},{"position_q":[-5992,-15396,-903703],"radius_q":3072},{"position_q":[187,-16815,-904857],"radius_q":3072},{"position_q":[6456,-15609,-906020],"radius_q":3072},{"position_q":[11689,-11978,-907180],"radius_q":3072},{"position_q":[14982,-6605,-908328],"radius_q":3072},{"position_q":[15849,-476,-909456],"radius_q":3072},{"position_q":[14290,5365,-910557],"radius_q":3072},{"position_q":[10710,10047,-911630],"radius_q":3072},{"position_q":[5764,12986,-912678],"radius_q":3072},{"position_q":[187,13905,-913708],"radius_q":3072},{"position_q":[-5301,12772,-914729],"radius_q":3072},{"position_q":[-10034,9744,-915752],"radius_q":3072},{"position_q":[-13400,5152,-916789],"radius_q":3072},{"position_q":[-14871,-476,-917848],"radius_q":3073},{"position_q":[-14828,-6646,-918159],"radius_q":3886},{"position_q":[-20544,-5190,-918511],"radius_q":4608}]}],"cavern_graph_vocabulary_version":1,"cavern_rooms":[{"stable_id":"tunnel-network/strate-2/room-0/primitive-0","strate_index":2,"center_um":[16677955,-28270663,-753307115],"half_extents_um":[4387063,4387063,3729030],"floor_um":-756241928,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2496338057380503,"bounds_min_um":[10540892,-34407726,-758786145],"bounds_max_um":[22815018,-22133600,-747828085]},{"stable_id":"tunnel-network/strate-2/room-0/primitive-1","strate_index":2,"center_um":[17020975,-26789220,-754089029],"half_extents_um":[2966903,2546513,2521885],"floor_um":-756241928,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1298697264498033,"bounds_min_um":[12304072,-31085733,-758360914],"bounds_max_um":[21737878,-22492707,-749817144]},{"stable_id":"tunnel-network/strate-2/room-0/primitive-2","strate_index":2,"center_um":[16450136,-29254574,-753744543],"half_extents_um":[2993025,3957768,3364126],"floor_um":-756241928,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1371456813936030,"bounds_min_um":[11707111,-34962342,-758858669],"bounds_max_um":[21193161,-23546806,-748630417]},{"stable_id":"tunnel-network/strate-2/room-1/primitive-0","strate_index":2,"center_um":[11442663,-41135576,-755976186],"half_extents_um":[3654710,3654710,3106525],"floor_um":-758359833,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":234826507654047,"bounds_min_um":[6037953,-46540286,-760832711],"bounds_max_um":[16847373,-35730866,-751119661]},{"stable_id":"tunnel-network/strate-2/room-1/primitive-1","strate_index":2,"center_um":[11594838,-40478358,-756406092],"half_extents_um":[2756758,2146566,2343261],"floor_um":-758359833,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7709850420954135,"bounds_min_um":[7088080,-44374924,-760499353],"bounds_max_um":[16101596,-36581792,-752312831]},{"stable_id":"tunnel-network/strate-2/room-1/primitive-2","strate_index":2,"center_um":[11266058,-41898301,-756242594],"half_extents_um":[2900022,2484772,2465036],"floor_um":-758359833,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8413310229855003,"bounds_min_um":[6616036,-46133073,-760457630],"bounds_max_um":[15916080,-37663529,-752027558]},{"stable_id":"tunnel-network/strate-2/room-10/primitive-0","strate_index":2,"center_um":[-6020215,-46837832,-776261224],"half_extents_um":[2387849,2387849,2029686],"floor_um":-777986531,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6716714120628840,"bounds_min_um":[-10158064,-50975681,-780040910],"bounds_max_um":[-1882366,-42699983,-772481538]},{"stable_id":"tunnel-network/strate-2/room-10/primitive-1","strate_index":2,"center_um":[-5872692,-46200705,-776491796],"half_extents_um":[1826203,1962098,1667795],"floor_um":-777986531,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":949841926828163,"bounds_min_um":[-9448895,-49912803,-779909591],"bounds_max_um":[-2296489,-42488607,-773074001]},{"stable_id":"tunnel-network/strate-2/room-11/primitive-0","strate_index":2,"center_um":[-3841819,-32182255,-778381544],"half_extents_um":[2861697,2861697,2432459],"floor_um":-780245825,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8857025401709256,"bounds_min_um":[-8453516,-36793952,-782564003],"bounds_max_um":[769878,-27570558,-774199085]},{"stable_id":"tunnel-network/strate-2/room-11/primitive-1","strate_index":2,"center_um":[-3658549,-31390746,-778588391],"half_extents_um":[2142648,1726483,1821263],"floor_um":-780245825,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2648511638984815,"bounds_min_um":[-7551197,-34867229,-782159654],"bounds_max_um":[234099,-27914263,-775017128]},{"stable_id":"tunnel-network/strate-2/room-11/primitive-2","strate_index":2,"center_um":[-4066656,-33153284,-778910634],"half_extents_um":[2084659,2032332,1771972],"floor_um":-780245825,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3923835578665842,"bounds_min_um":[-7901315,-36935616,-782432606],"bounds_max_um":[-231997,-29370952,-775388662]},{"stable_id":"tunnel-network/strate-2/room-12/primitive-0","strate_index":2,"center_um":[-1663423,-17526678,-779430904],"half_extents_um":[4903164,4903164,4167719],"floor_um":-782505118,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":974453298205719,"bounds_min_um":[-8316587,-24179842,-785348623],"bounds_max_um":[4989741,-10873514,-773513185]},{"stable_id":"tunnel-network/strate-2/room-12/primitive-1","strate_index":2,"center_um":[-1344755,-16150408,-779857843],"half_extents_um":[3265060,2532244,2775320],"floor_um":-782505118,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4287246203240359,"bounds_min_um":[-6359815,-20432652,-784383163],"bounds_max_um":[3670305,-11868164,-775332523]},{"stable_id":"tunnel-network/strate-2/room-13/primitive-0","strate_index":2,"center_um":[-574225,-10198889,-779338700],"half_extents_um":[6750000,6750000,5737541],"floor_um":-783634764,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4237475639875859,"bounds_min_um":[-9074225,-18698889,-786826241],"bounds_max_um":[7925775,-1698889,-771851159]},{"stable_id":"tunnel-network/strate-2/room-13/primitive-1","strate_index":2,"center_um":[-162640,-8421330,-779502776],"half_extents_um":[5324214,4346640,4525614],"floor_um":-783634764,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4079198432664629,"bounds_min_um":[-7236854,-14517970,-785778390],"bounds_max_um":[6911574,-2324690,-773227162]},{"stable_id":"tunnel-network/strate-2/room-14/primitive-0","strate_index":2,"center_um":[514974,-2871100,-782940765],"half_extents_um":[2513484,2513484,2136476],"floor_um":-784764411,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5513777901835640,"bounds_min_um":[-3748510,-7134584,-786827241],"bounds_max_um":[4778458,1392384,-779054289]},{"stable_id":"tunnel-network/strate-2/room-14/primitive-1","strate_index":2,"center_um":[708827,-2033881,-783113850],"half_extents_um":[1548535,2147599,1825472],"floor_um":-784764411,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4736343399867725,"bounds_min_um":[-2589708,-5931480,-786689322],"bounds_max_um":[4007362,1863718,-779538378]},{"stable_id":"tunnel-network/strate-2/room-15/primitive-0","strate_index":2,"center_um":[2693370,11784477,-785374347],"half_extents_um":[2568657,2568657,2183374],"floor_um":-787023704,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7277496228914001,"bounds_min_um":[-1625287,7465820,-789307721],"bounds_max_um":[7012027,16103134,-781440973]},{"stable_id":"tunnel-network/strate-2/room-15/primitive-1","strate_index":2,"center_um":[2825663,12355828,-785150364],"half_extents_um":[2161775,2315655,1968320],"floor_um":-787023704,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3986507348598952,"bounds_min_um":[-1086112,8290173,-788868684],"bounds_max_um":[6737438,16421483,-781432044]},{"stable_id":"tunnel-network/strate-2/room-16/primitive-0","strate_index":2,"center_um":[4871766,26440054,-787786556],"half_extents_um":[2218825,2218825,1886014],"floor_um":-789282998,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":619731278775179,"bounds_min_um":[902941,22471229,-791422570],"bounds_max_um":[8840591,30408879,-784150542]},{"stable_id":"tunnel-network/strate-2/room-16/primitive-1","strate_index":2,"center_um":[4994168,26968687,-787848831],"half_extents_um":[1879209,1675534,1597339],"floor_um":-789282998,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1702248410764664,"bounds_min_um":[1364959,23543153,-791196170],"bounds_max_um":[8623377,30394221,-784501492]},{"stable_id":"tunnel-network/strate-2/room-16/primitive-2","strate_index":2,"center_um":[4768977,25996124,-787853674],"half_extents_um":[2034374,1842627,1729230],"floor_um":-789282998,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5564952308868066,"bounds_min_um":[984603,22403497,-791332904],"bounds_max_um":[8553351,29588751,-784374444]},{"stable_id":"tunnel-network/strate-2/room-17/primitive-0","strate_index":2,"center_um":[7050163,41095632,-787908865],"half_extents_um":[4887035,4887035,4154009],"floor_um":-791542291,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8507542348025464,"bounds_min_um":[413128,34458597,-793812874],"bounds_max_um":[13687198,47732667,-782004856]},{"stable_id":"tunnel-network/strate-2/room-17/primitive-1","strate_index":2,"center_um":[7424471,42712199,-788202738],"half_extents_um":[4308519,3695270,3662267],"floor_um":-791542291,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":137070891032852,"bounds_min_um":[1365952,37266929,-793615005],"bounds_max_um":[13482990,48157469,-782790471]},{"stable_id":"tunnel-network/strate-2/room-17/primitive-2","strate_index":2,"center_um":[6826928,40131519,-789024898],"half_extents_um":[2696970,3720085,3162094],"floor_um":-791542291,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2276262411431109,"bounds_min_um":[2379958,34661434,-793936992],"bounds_max_um":[11273898,45601604,-784112804]},{"stable_id":"tunnel-network/strate-2/room-18/primitive-0","strate_index":2,"center_um":[9228559,55751209,-790253179],"half_extents_um":[4802420,4802420,4082086],"floor_um":-793801584,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4360877377612790,"bounds_min_um":[2676139,49198789,-796085265],"bounds_max_um":[15780979,62303629,-784421093]},{"stable_id":"tunnel-network/strate-2/room-18/primitive-1","strate_index":2,"center_um":[9502049,56932361,-790537541],"half_extents_um":[4092565,3247399,3478705],"floor_um":-793801584,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5597263948205663,"bounds_min_um":[3659484,51934962,-795766246],"bounds_max_um":[15344614,61929760,-785308836]},{"stable_id":"tunnel-network/strate-2/room-19/primitive-0","strate_index":2,"center_um":[11406955,70406787,-793418680],"half_extents_um":[3472456,3472456,2951608],"floor_um":-796060877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8346153217617005,"bounds_min_um":[6184499,65184331,-798120288],"bounds_max_um":[16629411,75629243,-788717072]},{"stable_id":"tunnel-network/strate-2/room-19/primitive-1","strate_index":2,"center_um":[11596697,71226250,-794011802],"half_extents_um":[2431662,2355598,2066927],"floor_um":-796060877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4360072178059799,"bounds_min_um":[7415035,67120652,-797828729],"bounds_max_um":[15778359,75331848,-790194875]},{"stable_id":"tunnel-network/strate-2/room-19/primitive-2","strate_index":2,"center_um":[11170897,69387297,-793840793],"half_extents_um":[2727321,2347079,2318239],"floor_um":-796060877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1461371555216243,"bounds_min_um":[6693576,65290218,-797909032],"bounds_max_um":[15648218,73484376,-789772554]},{"stable_id":"tunnel-network/strate-2/room-19/side-1/primitive-0","strate_index":2,"center_um":[13211571,78200589,-794229112],"half_extents_um":[2809826,2809826,2388369],"floor_um":-796060877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4071746333343996,"bounds_min_um":[8651745,73640763,-798367481],"bounds_max_um":[17771397,82760415,-790090743]},{"stable_id":"tunnel-network/strate-2/room-19/side-1/primitive-1","strate_index":2,"center_um":[13377214,78915974,-794687459],"half_extents_um":[1876747,1603207,1595246],"floor_um":-796060877,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3953725205313751,"bounds_min_um":[9750467,75562767,-798032705],"bounds_max_um":[17003961,82269181,-791342213]},{"stable_id":"tunnel-network/strate-2/room-2/primitive-0","strate_index":2,"center_um":[6207370,-54000489,-757217282],"half_extents_um":[4425839,4425839,3761990],"floor_um":-760477738,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6192132064124667,"bounds_min_um":[31531,-60176328,-762729272],"bounds_max_um":[12383209,-47824650,-751705292]},{"stable_id":"tunnel-network/strate-2/room-2/primitive-1","strate_index":2,"center_um":[6392659,-53200258,-757542314],"half_extents_um":[4020641,2949099,3417569],"floor_um":-760477738,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2990344898337824,"bounds_min_um":[622018,-57899357,-762709883],"bounds_max_um":[12163300,-48501159,-752374745]},{"stable_id":"tunnel-network/strate-2/room-2/primitive-2","strate_index":2,"center_um":[5871403,-55451471,-758119489],"half_extents_um":[2488155,3301955,2806681],"floor_um":-760477738,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1276700820642230,"bounds_min_um":[1633248,-60503426,-762676170],"bounds_max_um":[10109558,-50399516,-753562808]},{"stable_id":"tunnel-network/strate-2/room-20/primitive-0","strate_index":2,"center_um":[13585352,85062365,-795771947],"half_extents_um":[4264406,4264406,3624771],"floor_um":-798320171,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3969348325447,"bounds_min_um":[7570946,79047959,-801146718],"bounds_max_um":[19599758,91076771,-790397176]},{"stable_id":"tunnel-network/strate-2/room-20/primitive-1","strate_index":2,"center_um":[13886798,86364256,-796078785],"half_extents_um":[2534435,3397287,2887714],"floor_um":-798320171,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7891806891759534,"bounds_min_um":[9602363,81216969,-800716499],"bounds_max_um":[18171233,91511543,-791441071]},{"stable_id":"tunnel-network/strate-2/room-20/primitive-2","strate_index":2,"center_um":[13403152,84275475,-795232316],"half_extents_um":[4015969,3449752,3413598],"floor_um":-798320171,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8495668019456799,"bounds_min_um":[7637183,79075723,-800395914],"bounds_max_um":[19169121,89475227,-790068718]},{"stable_id":"tunnel-network/strate-2/room-21/primitive-0","strate_index":2,"center_um":[8350060,72197453,-797429491],"half_extents_um":[4178024,4178024,3551345],"floor_um":-800438076,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8353780683996427,"bounds_min_um":[2422036,66269429,-802730836],"bounds_max_um":[14278084,78125477,-792128146]},{"stable_id":"tunnel-network/strate-2/room-21/primitive-1","strate_index":2,"center_um":[8546833,73047281,-797143638],"half_extents_um":[4034901,3040336,3429690],"floor_um":-800438076,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6275606997446609,"bounds_min_um":[2761932,68256945,-802323328],"bounds_max_um":[14331734,77837617,-791963948]},{"stable_id":"tunnel-network/strate-2/room-22/primitive-0","strate_index":2,"center_um":[3114767,59332540,-800690824],"half_extents_um":[3002189,3002189,2551878],"floor_um":-802555981,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4725229704537249,"bounds_min_um":[-1637422,54580351,-804992702],"bounds_max_um":[7866956,64084729,-796388946]},{"stable_id":"tunnel-network/strate-2/room-22/primitive-1","strate_index":2,"center_um":[3318887,60214097,-800643458],"half_extents_um":[1929253,2362089,2007790],"floor_um":-802555981,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2396133457513924,"bounds_min_um":[-360366,56102008,-804401248],"bounds_max_um":[6998140,64326186,-796885668]},{"stable_id":"tunnel-network/strate-2/room-23/primitive-0","strate_index":2,"center_um":[-2120525,46467627,-801529997],"half_extents_um":[4857437,4857437,4128851],"floor_um":-804673886,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5610586952733464,"bounds_min_um":[-8727962,39860190,-807408848],"bounds_max_um":[4486912,53075064,-795651146]},{"stable_id":"tunnel-network/strate-2/room-23/primitive-1","strate_index":2,"center_um":[-1761492,48018225,-801772107],"half_extents_um":[3810511,3727772,3238957],"floor_um":-804673886,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5164566143628108,"bounds_min_um":[-7322003,42540453,-806761064],"bounds_max_um":[3799019,53495997,-796783150]},{"stable_id":"tunnel-network/strate-2/room-24/primitive-0","strate_index":2,"center_um":[-7355818,33602715,-804582820],"half_extents_um":[3113529,3113529,2646518],"floor_um":-806791791,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":998271559690284,"bounds_min_um":[-12219347,28739186,-808979338],"bounds_max_um":[-2492289,38466244,-800186302]},{"stable_id":"tunnel-network/strate-2/room-24/primitive-1","strate_index":2,"center_um":[-7154770,34471005,-805346690],"half_extents_um":[2007431,1484070,1706328],"floor_um":-806791791,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":170759264075404,"bounds_min_um":[-10912201,31236935,-808803018],"bounds_max_um":[-3397339,37705075,-801890362]},{"stable_id":"tunnel-network/strate-2/room-24/side-2/primitive-0","strate_index":2,"center_um":[-9160434,25808913,-804369374],"half_extents_um":[3926542,3926542,3337584],"floor_um":-806791791,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7408076088762940,"bounds_min_um":[-14836976,20132371,-809456958],"bounds_max_um":[-3483892,31485455,-799281790]},{"stable_id":"tunnel-network/strate-2/room-24/side-2/primitive-1","strate_index":2,"center_um":[-8911672,26883271,-803865311],"half_extents_um":[3345687,3770645,3205071],"floor_um":-806791791,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6480493913793558,"bounds_min_um":[-14007359,21362626,-808820382],"bounds_max_um":[-3815985,32403916,-798910240]},{"stable_id":"tunnel-network/strate-2/room-25/primitive-0","strate_index":2,"center_um":[-12591110,20737802,-806413741],"half_extents_um":[3886887,3886887,3303877],"floor_um":-808909696,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6676136965576361,"bounds_min_um":[-18227997,15100915,-811467618],"bounds_max_um":[-6954223,26374689,-801359864]},{"stable_id":"tunnel-network/strate-2/room-25/primitive-1","strate_index":2,"center_um":[-12362052,21727061,-806117148],"half_extents_um":[3658012,3236315,3109332],"floor_um":-808909696,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4861500673895666,"bounds_min_um":[-17770064,16740746,-810976480],"bounds_max_um":[-6954040,26713376,-801257816]},{"stable_id":"tunnel-network/strate-2/room-25/primitive-2","strate_index":2,"center_um":[-12839139,19666610,-806159435],"half_extents_um":[3735292,3338542,3175020],"floor_um":-808909696,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6799134419770337,"bounds_min_um":[-18324431,14578068,-811084455],"bounds_max_um":[-7353847,24755152,-801234415]},{"stable_id":"tunnel-network/strate-2/room-26/primitive-0","strate_index":2,"center_um":[-17826403,7872889,-808216708],"half_extents_um":[4174468,4174468,3548323],"floor_um":-811027601,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1552637404666204,"bounds_min_um":[-23750871,1948421,-813515031],"bounds_max_um":[-11901935,13797357,-802918385]},{"stable_id":"tunnel-network/strate-2/room-26/primitive-1","strate_index":2,"center_um":[-17609466,8809803,-808141856],"half_extents_um":[3922353,3817255,3334023],"floor_um":-811027601,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1658728811274312,"bounds_min_um":[-23281819,3242548,-813225879],"bounds_max_um":[-11937113,14377058,-803057833]},{"stable_id":"tunnel-network/strate-2/room-3/primitive-0","strate_index":2,"center_um":[972078,-66865401,-760582470],"half_extents_um":[2749441,2749441,2337041],"floor_um":-762595643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2640905445479701,"bounds_min_um":[-3527363,-71364842,-764669511],"bounds_max_um":[5471519,-62365960,-756495429]},{"stable_id":"tunnel-network/strate-2/room-3/primitive-1","strate_index":2,"center_um":[1138515,-66146590,-761028313],"half_extents_um":[2460677,2267460,2091590],"floor_um":-762595643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8829183212444991,"bounds_min_um":[-3072162,-70164050,-764869903],"bounds_max_um":[5349192,-62129130,-757186723]},{"stable_id":"tunnel-network/strate-2/room-3/primitive-2","strate_index":2,"center_um":[831344,-67473205,-760869038],"half_extents_um":[2166879,2081474,1841860],"floor_um":-762595643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8354744626195101,"bounds_min_um":[-3085535,-71304679,-764460898],"bounds_max_um":[4748223,-63641731,-757277178]},{"stable_id":"tunnel-network/strate-2/room-3/side-0/primitive-0","strate_index":2,"center_um":[-832538,-74659203,-760594915],"half_extents_um":[2803968,2803968,2383389],"floor_um":-762595643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2662084728162065,"bounds_min_um":[-5386506,-79213171,-764728304],"bounds_max_um":[3721430,-70105235,-756461526]},{"stable_id":"tunnel-network/strate-2/room-3/side-0/primitive-1","strate_index":2,"center_um":[-664904,-73935223,-761010973],"half_extents_um":[1953412,2382234,2024913],"floor_um":-762595643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8513725474957650,"bounds_min_um":[-4368316,-78067457,-764785886],"bounds_max_um":[3038508,-69802989,-757236060]},{"stable_id":"tunnel-network/strate-2/room-3/side-0/primitive-2","strate_index":2,"center_um":[-1005953,-75408151,-761180148],"half_extents_um":[1889306,1687909,1605921],"floor_um":-762595643,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":36217591490443,"bounds_min_um":[-4645259,-78846060,-764536069],"bounds_max_um":[2633353,-71970242,-757824227]},{"stable_id":"tunnel-network/strate-2/room-4/primitive-0","strate_index":2,"center_um":[-4263214,-79730314,-761198100],"half_extents_um":[4852450,4852450,4124612],"floor_um":-764713548,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2845885573645158,"bounds_min_um":[-10865664,-86332764,-767072712],"bounds_max_um":[2339236,-73127864,-755323488]},{"stable_id":"tunnel-network/strate-2/room-4/primitive-1","strate_index":2,"center_um":[-4061518,-78859224,-761681249],"half_extents_um":[3722391,4108989,3492665],"floor_um":-764713548,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5182219280308446,"bounds_min_um":[-9533909,-84718213,-766923914],"bounds_max_um":[1410873,-73000235,-756438584]},{"stable_id":"tunnel-network/strate-2/room-5/primitive-0","strate_index":2,"center_um":[-9498507,-92595227,-765147523],"half_extents_um":[2411668,2411668,2049932],"floor_um":-766831453,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8750423016047589,"bounds_min_um":[-13660175,-96756895,-768947455],"bounds_max_um":[-5336839,-88433559,-761347591]},{"stable_id":"tunnel-network/strate-2/room-5/primitive-1","strate_index":2,"center_um":[-9341942,-91919049,-765172404],"half_extents_um":[1724556,2065056,1755310],"floor_um":-766831453,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6729635284664735,"bounds_min_um":[-12816498,-95734105,-768677714],"bounds_max_um":[-5867386,-88103993,-761667094]},{"stable_id":"tunnel-network/strate-2/room-6/primitive-0","strate_index":2,"center_um":[-14733800,-105460141,-766110115],"half_extents_um":[4733441,4733441,4023453],"floor_um":-768949358,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2729955603759597,"bounds_min_um":[-21217241,-111943582,-771883568],"bounds_max_um":[-8250359,-98976700,-760336662]},{"stable_id":"tunnel-network/strate-2/room-6/primitive-1","strate_index":2,"center_um":[-14423768,-104121167,-766052959],"half_extents_um":[4239627,3129713,3603708],"floor_um":-768949358,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1328250596027720,"bounds_min_um":[-20413395,-109000880,-771406667],"bounds_max_um":[-8434141,-99241454,-760699251]},{"stable_id":"tunnel-network/strate-2/room-7/primitive-0","strate_index":2,"center_um":[-12555404,-90804565,-768053475],"half_extents_um":[4586293,4586293,3898377],"floor_um":-771208652,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6292182390675613,"bounds_min_um":[-18891697,-97140858,-773701852],"bounds_max_um":[-6219111,-84468272,-762405098]},{"stable_id":"tunnel-network/strate-2/room-7/primitive-1","strate_index":2,"center_um":[-12209924,-89312500,-768515873],"half_extents_um":[3710230,3763523,3199017],"floor_um":-771208652,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5824913408104067,"bounds_min_um":[-17670154,-94826023,-773464890],"bounds_max_um":[-6749694,-83798977,-763566856]},{"stable_id":"tunnel-network/strate-2/room-7/primitive-2","strate_index":2,"center_um":[-12865602,-92144253,-768912629],"half_extents_um":[2215184,3010104,2558606],"floor_um":-771208652,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8438796820341117,"bounds_min_um":[-16830786,-96904357,-773221235],"bounds_max_um":[-8900418,-87384149,-764604023]},{"stable_id":"tunnel-network/strate-2/room-8/primitive-0","strate_index":2,"center_um":[-10377008,-76148987,-772100062],"half_extents_um":[2068098,2068098,1757895],"floor_um":-773467945,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4233316435953742,"bounds_min_um":[-14195106,-79967085,-775607957],"bounds_max_um":[-6558910,-72330889,-768592167]},{"stable_id":"tunnel-network/strate-2/room-8/primitive-1","strate_index":2,"center_um":[-10279044,-75725899,-772256003],"half_extents_um":[1198135,1500898,1275772],"floor_um":-773467945,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7809339386482004,"bounds_min_um":[-13227179,-78976797,-775281775],"bounds_max_um":[-7330909,-72475001,-769230231]},{"stable_id":"tunnel-network/strate-2/room-9/primitive-0","strate_index":2,"center_um":[-8198612,-61493410,-774122824],"half_extents_um":[2600735,2600735,2210640],"floor_um":-775727238,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":928141076448714,"bounds_min_um":[-12549347,-65844145,-778083464],"bounds_max_um":[-3847877,-57142675,-770162184]},{"stable_id":"tunnel-network/strate-2/room-9/primitive-1","strate_index":2,"center_um":[-8074326,-60956639,-774032385],"half_extents_um":[2396004,1878239,2036618],"floor_um":-775727238,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":5703069001346635,"bounds_min_um":[-12220330,-64584878,-777819003],"bounds_max_um":[-3928322,-57328400,-770245767]}],"cavern_edges":[{"stable_id":"tunnel-network/strate-2/departure-edge","start_anchor_stable_id":"passage/2-3/0/upper-mouth","end_room_stable_id":"tunnel-network/strate-2/room-26/primitive-0","strate_index":2,"control_points_um":[[-23061696,-4992024,-813145507],[-21316599,-703720,-812439539],[-19571501,3584584,-811733570],[-17826403,7872889,-811027601]],"polyline_um":[[-23061696,-4992024,-813145507],[-17826403,7872889,-811027601]],"bore_radius_um":4500000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3854785202241998,"bounds_min_um":[-29311696,-11242024,-814895507],"bounds_max_um":[-11576403,14122889,-804777601]},{"stable_id":"tunnel-network/strate-2/route-edge-0","start_anchor_stable_id":"passage/1-2/0/lower-mouth","end_room_stable_id":"tunnel-network/strate-2/room-0/primitive-0","strate_index":2,"control_points_um":[[21913247,-15405751,-754124023],[20168150,-19694055,-754829991],[18423053,-23982359,-755535959],[16677955,-28270663,-756241928]],"polyline_um":[[21913247,-15405751,-754124023],[16677955,-28270663,-756241928]],"bore_radius_um":4500000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3289607608418144,"bounds_min_um":[10427955,-34520663,-757991928],"bounds_max_um":[28163247,-9155751,-747874023]},{"stable_id":"tunnel-network/strate-2/route-edge-1","start_anchor_stable_id":"tunnel-network/strate-2/room-0/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-1/primitive-0","strate_index":2,"control_points_um":[[16677955,-28270663,-756241928],[14932858,-32558967,-756947896],[13187761,-36847271,-757653864],[11442663,-41135576,-758359833]],"polyline_um":[[16677955,-28270663,-756241928],[11442663,-41135576,-758359833]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8817415599178250,"bounds_min_um":[7942663,-44635576,-760109833],"bounds_max_um":[20177955,-24770663,-749991928]},{"stable_id":"tunnel-network/strate-2/route-edge-10","start_anchor_stable_id":"tunnel-network/strate-2/room-9/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-10/primitive-0","strate_index":2,"control_points_um":[[-8198612,-61493410,-775727238],[-7472480,-56608218,-776480335],[-6746348,-51723025,-777233433],[-6020215,-46837832,-777986531]],"polyline_um":[[-8198612,-61493410,-775727238],[-6020215,-46837832,-777986531]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7032802616799006,"bounds_min_um":[-11698612,-64993410,-779736531],"bounds_max_um":[-2520215,-43337832,-769477238]},{"stable_id":"tunnel-network/strate-2/route-edge-11","start_anchor_stable_id":"tunnel-network/strate-2/room-10/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-11/primitive-0","strate_index":2,"control_points_um":[[-6020215,-46837832,-777986531],[-5294083,-41952640,-778739629],[-4567951,-37067448,-779492727],[-3841819,-32182255,-780245825]],"polyline_um":[[-6020215,-46837832,-777986531],[-3841819,-32182255,-780245825]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1911978944492012,"bounds_min_um":[-9520215,-50337832,-781995825],"bounds_max_um":[-341819,-28682255,-771736531]},{"stable_id":"tunnel-network/strate-2/route-edge-12","start_anchor_stable_id":"tunnel-network/strate-2/room-11/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-12/primitive-0","strate_index":2,"control_points_um":[[-3841819,-32182255,-780245825],[-3115687,-27297063,-780998922],[-2389555,-22411871,-781752020],[-1663423,-17526678,-782505118]],"polyline_um":[[-3841819,-32182255,-780245825],[-1663423,-17526678,-782505118]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8165210430297268,"bounds_min_um":[-7341819,-35682255,-784255118],"bounds_max_um":[1836577,-14026678,-773995825]},{"stable_id":"tunnel-network/strate-2/route-edge-13","start_anchor_stable_id":"tunnel-network/strate-2/room-12/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-13/primitive-0","strate_index":2,"control_points_um":[[-1663423,-17526678,-782505118],[-1300357,-15084082,-782881666],[-937291,-12641486,-783258215],[-574225,-10198889,-783634764]],"polyline_um":[[-1663423,-17526678,-782505118],[-574225,-10198889,-783634764]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1869838225665002,"bounds_min_um":[-5163423,-21026678,-785384764],"bounds_max_um":[2925775,-6698889,-776255118]},{"stable_id":"tunnel-network/strate-2/route-edge-14","start_anchor_stable_id":"tunnel-network/strate-2/room-13/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-14/primitive-0","strate_index":2,"control_points_um":[[-574225,-10198889,-783634764],[-211159,-7756293,-784011313],[151907,-5313697,-784387862],[514974,-2871100,-784764411]],"polyline_um":[[-574225,-10198889,-783634764],[514974,-2871100,-784764411]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7495826887632453,"bounds_min_um":[-4074225,-13698889,-786514411],"bounds_max_um":[4014974,628900,-777384764]},{"stable_id":"tunnel-network/strate-2/route-edge-15","start_anchor_stable_id":"tunnel-network/strate-2/room-14/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-15/primitive-0","strate_index":2,"control_points_um":[[514974,-2871100,-784764411],[1241106,2014092,-785517508],[1967238,6899284,-786270606],[2693370,11784477,-787023704]],"polyline_um":[[514974,-2871100,-784764411],[2693370,11784477,-787023704]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1168863893760579,"bounds_min_um":[-2985026,-6371100,-788773704],"bounds_max_um":[6193370,15284477,-778514411]},{"stable_id":"tunnel-network/strate-2/route-edge-16","start_anchor_stable_id":"tunnel-network/strate-2/room-15/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-16/primitive-0","strate_index":2,"control_points_um":[[2693370,11784477,-787023704],[3419502,16669669,-787776802],[4145634,21554861,-788529900],[4871766,26440054,-789282998]],"polyline_um":[[2693370,11784477,-787023704],[4871766,26440054,-789282998]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":3488700549169219,"bounds_min_um":[-806630,8284477,-791032998],"bounds_max_um":[8371766,29940054,-780773704]},{"stable_id":"tunnel-network/strate-2/route-edge-17","start_anchor_stable_id":"tunnel-network/strate-2/room-16/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-17/primitive-0","strate_index":2,"control_points_um":[[4871766,26440054,-789282998],[5597898,31325246,-790036095],[6324030,36210439,-790789193],[7050163,41095632,-791542291]],"polyline_um":[[4871766,26440054,-789282998],[7050163,41095632,-791542291]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4919553353375372,"bounds_min_um":[1371766,22940054,-793292291],"bounds_max_um":[10550163,44595632,-783032998]},{"stable_id":"tunnel-network/strate-2/route-edge-18","start_anchor_stable_id":"tunnel-network/strate-2/room-17/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-18/primitive-0","strate_index":2,"control_points_um":[[7050163,41095632,-791542291],[7776295,45980824,-792295388],[8502427,50866016,-793048486],[9228559,55751209,-793801584]],"polyline_um":[[7050163,41095632,-791542291],[9228559,55751209,-793801584]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8158943584761178,"bounds_min_um":[3550163,37595632,-795551584],"bounds_max_um":[12728559,59251209,-785292291]},{"stable_id":"tunnel-network/strate-2/route-edge-19","start_anchor_stable_id":"tunnel-network/strate-2/room-18/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-19/primitive-0","strate_index":2,"control_points_um":[[9228559,55751209,-793801584],[9954691,60636401,-794554681],[10680823,65521594,-795307779],[11406955,70406787,-796060877]],"polyline_um":[[9228559,55751209,-793801584],[11406955,70406787,-796060877]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":269617962051078,"bounds_min_um":[5728559,52251209,-797810877],"bounds_max_um":[14906955,73906787,-787551584]},{"stable_id":"tunnel-network/strate-2/route-edge-2","start_anchor_stable_id":"tunnel-network/strate-2/room-1/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-2/primitive-0","strate_index":2,"control_points_um":[[11442663,-41135576,-758359833],[9697566,-45423880,-759065801],[7952468,-49712184,-759771769],[6207370,-54000489,-760477738]],"polyline_um":[[11442663,-41135576,-758359833],[6207370,-54000489,-760477738]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8874695133302405,"bounds_min_um":[2707370,-57500489,-762227738],"bounds_max_um":[14942663,-37635576,-752109833]},{"stable_id":"tunnel-network/strate-2/route-edge-20","start_anchor_stable_id":"tunnel-network/strate-2/room-19/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-20/primitive-0","strate_index":2,"control_points_um":[[11406955,70406787,-796060877],[12133087,75291979,-796813975],[12859219,80177172,-797567073],[13585352,85062365,-798320171]],"polyline_um":[[11406955,70406787,-796060877],[13585352,85062365,-798320171]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":32404627650999,"bounds_min_um":[7906955,66906787,-800070171],"bounds_max_um":[17085352,88562365,-789810877]},{"stable_id":"tunnel-network/strate-2/route-edge-21","start_anchor_stable_id":"tunnel-network/strate-2/room-20/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-21/primitive-0","strate_index":2,"control_points_um":[[13585352,85062365,-798320171],[11840255,80774061,-799026139],[10095158,76485757,-799732107],[8350060,72197453,-800438076]],"polyline_um":[[13585352,85062365,-798320171],[8350060,72197453,-800438076]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4317428513448156,"bounds_min_um":[4850060,68697453,-802188076],"bounds_max_um":[17085352,88562365,-792070171]},{"stable_id":"tunnel-network/strate-2/route-edge-22","start_anchor_stable_id":"tunnel-network/strate-2/room-21/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-22/primitive-0","strate_index":2,"control_points_um":[[8350060,72197453,-800438076],[6604963,67909149,-801144044],[4859865,63620845,-801850012],[3114767,59332540,-802555981]],"polyline_um":[[8350060,72197453,-800438076],[3114767,59332540,-802555981]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7914775607336034,"bounds_min_um":[-385233,55832540,-804305981],"bounds_max_um":[11850060,75697453,-794188076]},{"stable_id":"tunnel-network/strate-2/route-edge-23","start_anchor_stable_id":"tunnel-network/strate-2/room-22/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-23/primitive-0","strate_index":2,"control_points_um":[[3114767,59332540,-802555981],[1369670,55044236,-803261949],[-375427,50755932,-803967917],[-2120525,46467627,-804673886]],"polyline_um":[[3114767,59332540,-802555981],[-2120525,46467627,-804673886]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4034550209005613,"bounds_min_um":[-5620525,42967627,-806423886],"bounds_max_um":[6614767,62832540,-796305981]},{"stable_id":"tunnel-network/strate-2/route-edge-24","start_anchor_stable_id":"tunnel-network/strate-2/room-23/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-24/primitive-0","strate_index":2,"control_points_um":[[-2120525,46467627,-804673886],[-3865622,42179323,-805379854],[-5610720,37891019,-806085822],[-7355818,33602715,-806791791]],"polyline_um":[[-2120525,46467627,-804673886],[-7355818,33602715,-806791791]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8223841641953534,"bounds_min_um":[-10855818,30102715,-808541791],"bounds_max_um":[1379475,49967627,-798423886]},{"stable_id":"tunnel-network/strate-2/route-edge-25","start_anchor_stable_id":"tunnel-network/strate-2/room-24/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-25/primitive-0","strate_index":2,"control_points_um":[[-7355818,33602715,-806791791],[-9100915,29314411,-807497759],[-10846012,25026107,-808203727],[-12591110,20737802,-808909696]],"polyline_um":[[-7355818,33602715,-806791791],[-12591110,20737802,-808909696]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8463461302563818,"bounds_min_um":[-16091110,17237802,-810659696],"bounds_max_um":[-3855818,37102715,-800541791]},{"stable_id":"tunnel-network/strate-2/route-edge-26","start_anchor_stable_id":"tunnel-network/strate-2/room-25/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-26/primitive-0","strate_index":2,"control_points_um":[[-12591110,20737802,-808909696],[-14336207,16449498,-809615664],[-16081305,12161194,-810321632],[-17826403,7872889,-811027601]],"polyline_um":[[-12591110,20737802,-808909696],[-17826403,7872889,-811027601]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1334322152237128,"bounds_min_um":[-21326403,4372889,-812777601],"bounds_max_um":[-9091110,24237802,-802659696]},{"stable_id":"tunnel-network/strate-2/route-edge-3","start_anchor_stable_id":"tunnel-network/strate-2/room-2/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-3/primitive-0","strate_index":2,"control_points_um":[[6207370,-54000489,-760477738],[4462273,-58288793,-761183706],[2717176,-62577097,-761889674],[972078,-66865401,-762595643]],"polyline_um":[[6207370,-54000489,-760477738],[972078,-66865401,-762595643]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":6511238413130679,"bounds_min_um":[-2527922,-70365401,-764345643],"bounds_max_um":[9707370,-50500489,-754227738]},{"stable_id":"tunnel-network/strate-2/route-edge-4","start_anchor_stable_id":"tunnel-network/strate-2/room-3/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-4/primitive-0","strate_index":2,"control_points_um":[[972078,-66865401,-762595643],[-773019,-71153705,-763301611],[-2518116,-75442009,-764007579],[-4263214,-79730314,-764713548]],"polyline_um":[[972078,-66865401,-762595643],[-4263214,-79730314,-764713548]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4104421178731175,"bounds_min_um":[-7763214,-83230314,-766463548],"bounds_max_um":[4472078,-63365401,-756345643]},{"stable_id":"tunnel-network/strate-2/route-edge-5","start_anchor_stable_id":"tunnel-network/strate-2/room-4/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-5/primitive-0","strate_index":2,"control_points_um":[[-4263214,-79730314,-764713548],[-6008311,-84018618,-765419516],[-7753409,-88306922,-766125484],[-9498507,-92595227,-766831453]],"polyline_um":[[-4263214,-79730314,-764713548],[-9498507,-92595227,-766831453]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":1712957457995776,"bounds_min_um":[-12998507,-96095227,-768581453],"bounds_max_um":[-763214,-76230314,-758463548]},{"stable_id":"tunnel-network/strate-2/route-edge-6","start_anchor_stable_id":"tunnel-network/strate-2/room-5/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-6/primitive-0","strate_index":2,"control_points_um":[[-9498507,-92595227,-766831453],[-11243604,-96883531,-767537421],[-12988702,-101171836,-768243389],[-14733800,-105460141,-768949358]],"polyline_um":[[-9498507,-92595227,-766831453],[-14733800,-105460141,-768949358]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":627735267583865,"bounds_min_um":[-18233800,-108960141,-770699358],"bounds_max_um":[-5998507,-89095227,-760581453]},{"stable_id":"tunnel-network/strate-2/route-edge-7","start_anchor_stable_id":"tunnel-network/strate-2/room-6/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-7/primitive-0","strate_index":2,"control_points_um":[[-14733800,-105460141,-768949358],[-14007668,-100574949,-769702456],[-13281536,-95689757,-770455554],[-12555404,-90804565,-771208652]],"polyline_um":[[-14733800,-105460141,-768949358],[-12555404,-90804565,-771208652]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2402170231604524,"bounds_min_um":[-18233800,-108960141,-772958652],"bounds_max_um":[-9055404,-87304565,-762699358]},{"stable_id":"tunnel-network/strate-2/route-edge-8","start_anchor_stable_id":"tunnel-network/strate-2/room-7/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-8/primitive-0","strate_index":2,"control_points_um":[[-12555404,-90804565,-771208652],[-11829272,-85919373,-771961749],[-11103140,-81034180,-772714847],[-10377008,-76148987,-773467945]],"polyline_um":[[-12555404,-90804565,-771208652],[-10377008,-76148987,-773467945]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":35699537972874,"bounds_min_um":[-16055404,-94304565,-775217945],"bounds_max_um":[-6877008,-72648987,-764958652]},{"stable_id":"tunnel-network/strate-2/route-edge-9","start_anchor_stable_id":"tunnel-network/strate-2/room-8/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-9/primitive-0","strate_index":2,"control_points_um":[[-10377008,-76148987,-773467945],[-9650876,-71263795,-774221042],[-8924744,-66378603,-774974140],[-8198612,-61493410,-775727238]],"polyline_um":[[-10377008,-76148987,-773467945],[-8198612,-61493410,-775727238]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":8556892065984699,"bounds_min_um":[-13877008,-79648987,-777477238],"bounds_max_um":[-4698612,-57993410,-767217945]},{"stable_id":"tunnel-network/strate-2/side-edge-0","start_anchor_stable_id":"tunnel-network/strate-2/room-3/side-0/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-3/primitive-0","strate_index":2,"control_points_um":[[-832538,-74659203,-762595643],[-231000,-72061269,-762595643],[370539,-69463335,-762595643],[972078,-66865401,-762595643]],"polyline_um":[[-832538,-74659203,-762595643],[972078,-66865401,-762595643]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":4352771090277651,"bounds_min_um":[-4332538,-78159203,-764345643],"bounds_max_um":[4472078,-63365401,-756345643]},{"stable_id":"tunnel-network/strate-2/side-edge-1","start_anchor_stable_id":"tunnel-network/strate-2/room-19/side-1/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-19/primitive-0","strate_index":2,"control_points_um":[[13211571,78200589,-796060877],[12610033,75602655,-796060877],[12008494,73004721,-796060877],[11406955,70406787,-796060877]],"polyline_um":[[13211571,78200589,-796060877],[11406955,70406787,-796060877]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":2726979967805320,"bounds_min_um":[7906955,66906787,-797810877],"bounds_max_um":[16711571,81700589,-789810877]},{"stable_id":"tunnel-network/strate-2/side-edge-2","start_anchor_stable_id":"tunnel-network/strate-2/room-24/side-2/primitive-0","end_room_stable_id":"tunnel-network/strate-2/room-24/primitive-0","strate_index":2,"control_points_um":[[-9160434,25808913,-806791791],[-8558896,28406847,-806791791],[-7957357,31004781,-806791791],[-7355818,33602715,-806791791]],"polyline_um":[[-9160434,25808913,-806791791],[-7355818,33602715,-806791791]],"bore_radius_um":1750000,"bore_height_um":4500000,"floor_support_depth_um":0,"smooth_width_um":1500000,"irregularity_amplitude_um":250000,"irregularity_wavelength_um":1250000,"irregularity_seed":7796271028373642,"bounds_min_um":[-12660434,22308913,-808541791],"bounds_max_um":[-3855818,37102715,-800541791]}],"stable_identity":"resolved-layout-v3-fnv64:28ef34e1ef6f5214","resolved_strate_passage_styles":{"0":1,"1":1,"2":1,"3":1}} \ No newline at end of file diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux135/REPORT.md new file mode 100644 index 0000000..3a09abd --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux135/REPORT.md @@ -0,0 +1,32 @@ +# Linux135 — Tunnel Network planner + +Source v236 (`8dc4294ce0250a591ee4515471c3c547e796ffa4346ab44f0ae1740bbc3797f5`) on `wip/l134-cave-planner`. + +## Route audits + +Seeds 0–15 pass the composed passage, seam, shell, naturalness, mouth-profile, ripple, and blocked-route controls (16/16). The negative control still detects all three blocked routes. No audit threshold changed. + +The mouth joint now takes its floor/profile from the resolved mouth geometry and carries that shared profile into the planner's first and last edges. This fixes seed 1's blocked arrival and seed 2's unsupported arrival. For seeds 0 and 3, the first 648 regular route candidates exceeded the grade bound; the bounded deterministic fallback finds a route. “No sweepable route candidates” was a planner search miss, not an empty cave. + +## Cost + +L80 medians are near-ready / whole-view percent deltas. The serialized 144-run matrix started at load 0.81 / 98.4% CPU idle. Each cell gives `assembler/world` first, then exact L134 (`a3c8e36`). + +| Layer | W8 | W12 | +|---|---:|---:| +| 0 | +3.88 / +0.27%; +1.79 / -0.21% | +8.22 / +4.09%; +3.89 / +2.49% | +| 1 | +8.82 / +4.96%; +4.14 / +1.71% | +11.83 / +5.35%; +4.10 / +2.09% | +| 2 | +13.33 / +3.77%; +2.34 / -0.09% | +12.56 / +5.20%; +1.40 / +0.61% | +| 3 | +13.65 / +3.99%; -0.25 / +0.44% | +12.71 / +5.14%; +2.97 / +2.12% | + +The two speed changes were bounded cave AABBs with a 1 mm float guard, and normalized edge-BVH bounds with one normalization per sample. This improves on L134, but misses the `assembler/world` target: near-ready remains 3.88–13.65% slower. Layers 1–3 overlap the native cave volume and spend time on roughly 4.95–5.76 million near-LOD cave samples per view; layer 0 has none at LOD0–3 and is still up 3.88% at W8, showing some generic composer/classification cost as well. + +Planning is about 3.75–4.19 ms per view (cavern planning 0.46–0.49 ms). At LOD0–3, room/edge candidates average 0.39/0.23 per composer tile on layer 1, 0.47/0.27 on layer 2, and 0.06/0.04 on layer 3. Tile proofs skip work at LOD0–3; measured cave views have no tile skips at LOD4–8. The reported 180–320 ns density-call estimate includes all shape fields, not cave-only time. Profile: `Saved/Linux/l135/profile-summary-speed2.json`. + +## Gates and pictures + +Fast tier passes in 301.7 s. Clang and GCC each match 13 hash lines at W1/W8/W12. Route seams and the negative controls pass; canonical baselines are v236. The post-timing VFHost Linux Development build succeeds: 27/27 actions, 77.70 s, using `-NoUBA -MaxParallelActions=4`. + +The seven-view gallery has four planner views at the planned floor plus 1.6 m and three legacy views at legacy coordinates. Open `captures/COMPARE.html`; camera coordinates and renderer settings are in `captures/capture-index.json`. + +Sweep: `Saved/Linux/l135/optimized-seed-sweep.json`. Timing: `Saved/Linux/l135/timing/summary.json`. Determinism: `Saved/Linux/l135/determinism-summary.json`. Host build log: `Saved/Linux/l135/ubt-l135-console.log`. diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/RunL135Captures.py b/Standalone/Research/WorldAssemblerM3/Linux135/RunL135Captures.py new file mode 100644 index 0000000..2d67a31 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux135/RunL135Captures.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Capture eye-height planner interiors and legacy interiors at legacy coordinates.""" +from __future__ import annotations + +import json +import math +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux135/captures" +SAVED = ROOT / "Saved/Linux/l135" +DATA = ROOT / "Standalone/data/owner_world.json" +LAYOUT = SAVED / "capture-default-layout.json" +HEADLESS = SAVED / "bin/VoxelForgeCoreHeadless-clang-l135" +RENDERER = SAVED / "bin/VoxelForgeOffscreenCapture-clang-l135" +STAMP = "8dc4294ce0250a591ee4515471c3c547e796ffa4346ab44f0ae1740bbc3797f5" +EYE_METRES = 1.6 + + +def run(command: list[str], log_path: Path) -> None: + wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", *command] + with log_path.open("w", encoding="utf-8") as log: + result = subprocess.run(wrapped, cwd=ROOT, stdout=log, + stderr=subprocess.STDOUT, timeout=1740, + check=False) + if result.returncode: + raise SystemExit(f"command failed ({result.returncode}): {log_path}") + + +def xyz(value: list[int] | list[float]) -> tuple[float, float, float]: + return tuple(component / 1_000_000.0 for component in value) # type: ignore[return-value] + + +def yaw_to(dx: float, dy: float) -> float: + # The renderer's yaw zero points along +Y (Forward = sin(yaw), cos(yaw), 0). + return math.degrees(math.atan2(dx, dy)) + + +def camera_arg(camera: tuple[float, float, float], yaw: float) -> str: + return ",".join(f"{part:.9f}" for part in (*camera, yaw, 0.0)) + + +def make_view(name: str, recipe: str, camera: tuple[float, float, float], + yaw: float) -> dict[str, object]: + OUT.mkdir(parents=True, exist_ok=True) + mesh = SAVED / f"capture-{name}.vfmesh" + image = OUT / f"{name}.png" + mesh_log = SAVED / f"capture-{name}-mesh.log" + render_log = SAVED / f"capture-{name}-render.log" + command = [str(HEADLESS), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", + "--clip-radius=1", "--max-clip-level=0", "--max-level=0", + f"--camera={camera_arg(camera, yaw)}", f"--mesh-export={mesh}", + "--capture-skip-walkability", "--force-generate", + "--render-distance-chunks=0"] + # The checked-in fixture is already seed 0. Keeping its Unreal-resolved owner + # plan avoids rebuilding the broad SurfaceWorld spawn search for each camera. + if recipe == "legacy-tunnel-network": + command.append("--legacy-tunnel-network") + else: + command.append("--recipe=surface-world") + run(command, mesh_log) + render_command = [str(RENDERER), str(mesh), str(image), "--size=1600x900", + "--shade=readable", "--height-tint=on", "--headlamp=off"] + run(render_command, render_log) + mesh_output = mesh_log.read_text(encoding="utf-8", errors="replace") + render_output = render_log.read_text(encoding="utf-8", errors="replace") + if STAMP not in mesh_output or STAMP not in render_output: + raise SystemExit(f"source stamp mismatch for {name}") + if "status=PASS" not in render_output: + raise SystemExit(f"render failed for {name}") + return {"name": name, "recipe": recipe, "camera_m": list(camera), + "yaw_deg": yaw, + "camera_height_basis": "floor + player eye offset" if recipe == "surface-world" + else ("legacy room floor + player eye offset" if name.endswith("room") + else "near the legacy tunnel centerline"), + "eye_offset_m": EYE_METRES + if recipe == "surface-world" or name.endswith("room") else None, + "mesh": str(mesh.relative_to(ROOT)), "image": str(image.relative_to(ROOT)), + "mesh_log": str(mesh_log.relative_to(ROOT)), + "render_log": str(render_log.relative_to(ROOT))} + + +def main() -> int: + layout = json.loads(LAYOUT.read_text(encoding="utf-8")) + rooms = {room["stable_id"]: room for room in layout["cavern_rooms"]} + edges = {edge["stable_id"]: edge for edge in layout["cavern_edges"]} + views: list[dict[str, object]] = [] + + arrival = rooms["tunnel-network/strate-2/room-0/primitive-0"] + room1 = rooms["tunnel-network/strate-2/room-1/primitive-0"] + arrival_center = xyz(arrival["center_um"]) + arrival_floor = arrival["floor_um"] / 1_000_000.0 + views.append(make_view("planner-arrival-room", "surface-world", + (arrival_center[0], arrival_center[1], arrival_floor + EYE_METRES), + yaw_to(room1["center_um"][0] - arrival["center_um"][0], + room1["center_um"][1] - arrival["center_um"][1]))) + + tunnel = edges["tunnel-network/strate-2/route-edge-1"]["polyline_um"] + tunnel_start, tunnel_end = xyz(tunnel[0]), xyz(tunnel[-1]) + tunnel_floor = tunnel[1][2] / 1_000_000.0 + views.append(make_view("planner-tunnel", "surface-world", + ((tunnel_start[0] + tunnel_end[0]) * 0.5, + (tunnel_start[1] + tunnel_end[1]) * 0.5, + tunnel_floor + EYE_METRES), + yaw_to(tunnel_end[0] - tunnel_start[0], tunnel_end[1] - tunnel_start[1]))) + + compound = rooms["tunnel-network/strate-2/room-19/primitive-0"] + compound_center = xyz(compound["center_um"]) + compound_next = rooms["tunnel-network/strate-2/room-20/primitive-0"] + views.append(make_view("planner-compound-room", "surface-world", + (compound_center[0], compound_center[1], + compound["floor_um"] / 1_000_000.0 + EYE_METRES), + yaw_to(compound_next["center_um"][0] - compound["center_um"][0], + compound_next["center_um"][1] - compound["center_um"][1]))) + + destination = rooms["tunnel-network/strate-2/room-33/primitive-0"] + destination_center = xyz(destination["center_um"]) + upstream = rooms["tunnel-network/strate-2/room-32/primitive-0"] + views.append(make_view("planner-destination-chamber", "surface-world", + (destination_center[0], destination_center[1], + destination["floor_um"] / 1_000_000.0 + EYE_METRES), + yaw_to(upstream["center_um"][0] - destination["center_um"][0], + upstream["center_um"][1] - destination["center_um"][1]))) + + # The compiled legacy network has a deterministic origin chamber at (0, 0). + # Its room floor is hash-rolled from the owner fixture; these are legacy world + # coordinates, independent of the native planner's passage route. + legacy_origin_floor = -819.9169921875 + legacy_origin_eye = legacy_origin_floor + EYE_METRES + legacy_neighbor = (39.327854, 5.115777, -797.724731) + views.append(make_view("legacy-origin-room", "legacy-tunnel-network", + (0.0, 0.0, legacy_origin_eye), yaw_to(legacy_neighbor[0], legacy_neighbor[1]))) + + # Hash-selected (cell 1,0) is the origin graph's nearest connected room. Its + # cut-plane floor estimate is -801.0 m before the authored local relief. + legacy_neighbor_floor = -800.9986572265625 + legacy_neighbor_eye = legacy_neighbor_floor + EYE_METRES + views.append(make_view("legacy-connected-room", "legacy-tunnel-network", + (legacy_neighbor[0], legacy_neighbor[1], legacy_neighbor_eye), + yaw_to(-legacy_neighbor[0], -legacy_neighbor[1]))) + + legacy_tunnel_mid = (legacy_neighbor[0] * 0.5, legacy_neighbor[1] * 0.5, + (-812.0 + legacy_neighbor[2]) * 0.5) + views.append(make_view("legacy-origin-tunnel", "legacy-tunnel-network", + legacy_tunnel_mid, yaw_to(legacy_neighbor[0], legacy_neighbor[1]))) + + manifest = {"source_stamp": STAMP, "seed": 0, + "renderer": "readable, height tint on, headlamp off; 1600x900", + "views": views} + (OUT / "capture-index.json").write_text( + json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + cards = "\n".join( + f'
' + f'{view[' + f'
{view["name"]}
' for view in views) + (OUT / "COMPARE.html").write_text( + "L135 interior views\n" + "\n" + "

Planner and legacy interiors, seed 0

\n" + + cards + "\n
\n", encoding="utf-8") + print(f"[L135CaptureSummary] views={len(views)} status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/RunL135Determinism.py b/Standalone/Research/WorldAssemblerM3/Linux135/RunL135Determinism.py new file mode 100644 index 0000000..5277c68 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux135/RunL135Determinism.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Check Clang/GCC cave-path pipeline hashes at 1, 8, and 12 workers.""" +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/l135" +DATA = ROOT / "Standalone/data/regression/owner_world_2strate_l61.json" +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-clang-l135", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-gcc-l135", +} +CAMERA = "-4.688214,38.662384,-505.781342,-28.446,0" +HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|MaterialHash|LODHash|ExactQueryHash)\].*$", + re.M) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def main() -> int: + if any(not path.is_file() for path in (DATA, *BINARIES.values())): + raise SystemExit("missing L135 determinism binary or owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + rows = [] + baseline: tuple[str, ...] | None = None + stamp: str | None = None + for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + label = f"determinism-{compiler}-w{workers}" + log = OUT / f"{label}.log" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + f"--workers={workers}", "--clip-radius=3", "--max-clip-level=0", + "--max-level=0", f"--camera={CAMERA}", "--stream-hash-tiles", + "--force-generate", "--recipe=surface-world", "--render-distance-chunks=0", + "--landmark-chamber-demo", "--ore-veins-demo"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + found_stamp = STAMP_RE.search(content) + hashes = tuple(HASH_RE.findall(content)) + passed = result.returncode == 0 and found_stamp is not None and len(hashes) >= 4 + if found_stamp is not None: + if stamp is None: + stamp = found_stamp.group(1) + passed &= stamp == found_stamp.group(1) + if baseline is None and passed: + baseline = hashes + elif baseline is not None: + passed &= hashes == baseline + row = {"compiler": compiler, "workers": workers, + "exit": result.returncode, "stamp": found_stamp.group(1) + if found_stamp else None, + "hash_lines": len(hashes), "hashes_match_first": hashes == baseline, + "passed": bool(passed), "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT))} + rows.append(row) + print("[L135DeterminismRun] " + json.dumps(row, sort_keys=True), flush=True) + if not passed: + (OUT / "determinism-summary.json").write_text(json.dumps( + {"status": "FAIL", "runs": rows}, indent=2) + "\n") + raise SystemExit(f"determinism mismatch; see {log}") + summary = {"status": "PASS", "stamp": stamp, "runs": rows} + (OUT / "determinism-summary.json").write_text( + json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L135DeterminismSummary] runs={len(rows)} stamp={stamp} status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/RunL135Profiles.py b/Standalone/Research/WorldAssemblerM3/Linux135/RunL135Profiles.py new file mode 100644 index 0000000..53401b9 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux135/RunL135Profiles.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Collect final-stamp cave, candidate, proof, and startup counters by stratum.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from collections import defaultdict +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l135" +BIN = OUT / "bin/VoxelForgeCoreHeadless-clang-l135" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "8dc4294ce0250a591ee4515471c3c547e796ffa4346ab44f0ae1740bbc3797f5" +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), +} +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +PLAN_RE = re.compile(r"^\[WorldAssemblerPlanTiming\] total_ms=([0-9.]+) status=([^ ]+)$", re.M) +CAVE_RE = re.compile(r"^\[CavernPlannerV1\].* plan_ms=([0-9.]+) status=([^ ]+)$", re.M) +TILE_RE = re.compile(r"^\[WorldAssemblerTilePipeline\] (.*)$", re.M) +CLASS_RE = re.compile(r"^\[WorldAssemblerLODClassification\] (.*)$", re.M) +COMPOSER_RE = re.compile(r"^\[PassageComposerTile\] (.*)$", re.M) +FIELDS_RE = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") + + +def fields(line: str) -> dict[str, str]: + return dict(FIELDS_RE.findall(line)) + + +def main() -> int: + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L135 profile binary or owner fixture") + rows = [] + for strate, xyz in CAMERAS.items(): + camera = ",".join(f"{value:.12f}" for value in xyz) + ",0,0" + log = OUT / f"profile-final-speed2-s{strate}.log" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), 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", "--profile-passage-composer", + "--stream-scheduler-policy=3"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + found_stamp = STAMP_RE.search(content) + if result.returncode or found_stamp is None or found_stamp.group(1) != STAMP: + raise SystemExit(f"final-stamp profile failed: {log}, exit={result.returncode}") + plan, cave = PLAN_RE.search(content), CAVE_RE.search(content) + tile = TILE_RE.search(content) + if plan is None or cave is None or tile is None: + raise SystemExit(f"profile counters missing: {log}") + aggregate = defaultdict(lambda: defaultdict(int)) + for match in CLASS_RE.finditer(content): + v = fields(match.group(1)); lod = int(v["lod"]) + for key in ("tiles", "tile_skips", "density_point_calls", "field_eval_sample_points", + "proof_air_cells", "proof_solid_cells"): + aggregate[lod][key] = int(v.get(key, "0")) + aggregate[lod]["field_eval_ms_est"] = float(v.get("field_eval_ms_est", "0")) + for match in COMPOSER_RE.finditer(content): + v = fields(match.group(1)); lod = int(v["key"].split("lod", 1)[1].rstrip(")")) + aggregate[lod]["composer_tiles"] += 1 + for key in ("samples", "native_cave_samples", "room_candidates", "edge_candidates", + "features", "route_candidates", "passage_iterations"): + aggregate[lod][key] += int(v.get(key, "0")) + aggregate[lod]["composer_worker_ms"] += float(v.get("worker_ms", "0")) + for lod, values in aggregate.items(): + calls = values.get("density_point_calls", 0) + values["field_eval_ns_per_density_call_est"] = round( + values.get("field_eval_ms_est", 0.0) * 1.0e6 / calls, 1) if calls else None + composer_tiles = values.get("composer_tiles", 0) + values["room_candidates_per_composer_tile"] = round( + values.get("room_candidates", 0) / composer_tiles, 3) if composer_tiles else None + values["edge_candidates_per_composer_tile"] = round( + values.get("edge_candidates", 0) / composer_tiles, 3) if composer_tiles else None + tile_fields = fields(tile.group(1)) + tile_summary = {} + for key, value in tile_fields.items(): + try: + tile_summary[key] = int(value) + except ValueError: + try: + tile_summary[key] = float(value) + except ValueError: + continue + row = {"strate": strate, "source_stamp": found_stamp.group(1), + "world_assembler_plan_ms": float(plan.group(1)), + "cavern_planner_ms": float(cave.group(1)), + "tile_pipeline": tile_summary, + "lod": {str(k): dict(v) for k, v in sorted(aggregate.items())}, + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT))} + rows.append(row) + print("[L135ProfileRun] " + json.dumps(row, sort_keys=True), flush=True) + summary = {"status": "PASS", "source_stamp": STAMP, "strata": rows, + "field_eval_ns_per_density_call_note": + "Estimate includes all shape-field density calls at this view, not native-cave-exclusive time.", + "tile_skip_note": + "tile_pipeline.whole_tile_skips and lod.tile_skips are aggregate classifiers; they are not cave-only attribution."} + (OUT / "profile-summary-speed2.json").write_text( + json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print("[L135ProfileSummary] strata=4 status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/RunL135SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux135/RunL135SeedSweep.py new file mode 100644 index 0000000..3e1bb35 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux135/RunL135SeedSweep.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Run the L135 composed cave-passage and tile-seam audits for fixed seeds.""" +from __future__ import annotations + +import json +import re +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l135" +BIN = OUT / "bin/VoxelForgeCoreHeadless-clang-l135" +DATA = ROOT / "Standalone/data/owner_world.json" +REQUIRED = { + "passage": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "route_negative_control": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} + + +def main() -> int: + seeds = [int(value) for value in sys.argv[1:]] or list(range(16)) + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L135 headless binary or owner fixture") + results = [] + for seed in seeds: + log = OUT / f"optimized-seed{seed}.log" + layout = OUT / f"optimized-seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=0", + "--force-generate", f"--seed={seed}", "--recipe=surface-world", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + blocked_controls = re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M) + checks["three_blocked_controls"] = len(blocked_controls) == 3 + passed = result.returncode == 0 and all(checks.values()) + row = {"seed": seed, "exit": result.returncode, "passed": passed, + "checks": checks, "blocked_controls": len(blocked_controls), + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L135SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + if not passed: + raise SystemExit(f"seed {seed} failed a composed audit; see {log}") + summary = {"seeds": seeds, "passed": len(results), "failed": 0, + "results": results} + (OUT / "optimized-seed-sweep.json").write_text( + json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L135SeedSweepSummary] passed={len(results)}/{len(seeds)} status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/RunL135Timing.py b/Standalone/Research/WorldAssemblerM3/Linux135/RunL135Timing.py new file mode 100644 index 0000000..e9cffa6 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux135/RunL135Timing.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Five interleaved L80 timings against assembler/world and the L134 revision.""" +from __future__ import annotations + +import json +import os +import re +import statistics +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l135/timing" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": ROOT / "Saved/Linux/l130/bin/VoxelForgeCoreHeadless-l130-clang", + "a3c8e36-v230": ROOT / "Saved/Linux/l134/bin/VoxelForgeCoreHeadless-l134-clang", + "l135-v236": ROOT / "Saved/Linux/l135/bin/VoxelForgeCoreHeadless-clang-l135", +} +CAMERAS = { + 0: (8.0, 16.0, -372.2626953125), + 1: (25.8369140625, 3.19921875, -737.0), + 2: (19.166015625, -14.2001953125, -753.3740234375), + 3: (-20.0625, -5.068359375, -900.353515625), +} +PIPELINE_RE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M) +FIELDS_RE = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +NEAR_RE = re.compile( + r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$", + re.M) +PLAN_RE = re.compile(r"^\[WorldAssemblerPlanTiming\] total_ms=([0-9.]+) status=([^ ]+)$", re.M) +CAVE_RE = re.compile(r"^\[CavernPlannerV1\].* plan_ms=([0-9.]+) status=([^ ]+)$", re.M) + + +def run(strate: int, workers: int, version: str, phase: str, + replicate: int) -> dict: + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + label = f"s{strate}-w{workers}-{version}-{phase}{replicate}-l135" + log = OUT / f"{label}.log" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARIES[version]), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8", + "--max-level=8", f"--camera={camera}", "--profile-lod-work", + "--stream-scheduler-policy=3"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline, near = PIPELINE_RE.search(content), NEAR_RE.search(content) + if result.returncode or pipeline is None or near is None: + raise RuntimeError(f"timing run failed: {log}; exit={result.returncode}") + fields = dict(FIELDS_RE.findall(pipeline.group(1))) + plan, cave = PLAN_RE.search(content), CAVE_RE.search(content) + row = {"strate": strate, "workers": workers, "version": version, + "phase": phase, "replicate": replicate, + "lod02_ready_s": float(near.group(3)), + "whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")), + "plan_ms": float(plan.group(1)) if plan else None, + "cave_plan_ms": float(cave.group(1)) if cave else None, + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT))} + print("[L135TimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def quiet_machine() -> tuple[float, float]: + load_1m = os.getloadavg()[0] + def totals() -> tuple[int, int]: + fields = Path("/proc/stat").read_text().splitlines()[0].split() + values = [int(value) for value in fields[1:]] + return sum(values), values[3] + values[4] + before = totals() + time.sleep(2.0) + after = totals() + idle = 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0]) + return load_1m, idle + + +def main() -> int: + if any(not path.is_file() for path in (DATA, *BINARIES.values())): + raise SystemExit("missing L135 timing input") + OUT.mkdir(parents=True, exist_ok=True) + load_1m, idle = quiet_machine() + (OUT / "machine-load-before.txt").write_text( + f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n") + print(f"[L135TimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True) + if load_1m >= 0.9 or idle < 97.0: + raise SystemExit("timing refused: machine is above the L80 quiet-load gate") + + rows = [] + for strate in range(4): + for workers in (8, 12): + for version in BINARIES: + rows.append(run(strate, workers, version, "warmup", 0)) + for replicate in range(1, 6): + order = tuple(BINARIES) if replicate % 2 else tuple(reversed(BINARIES)) + for version in order: + rows.append(run(strate, workers, version, "measured", replicate)) + (OUT / "runs-partial.json").write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8") + + measured = [row for row in rows if row["phase"] == "measured"] + summary = [] + for strate in range(4): + for workers in (8, 12): + item = {"strate": strate, "workers": workers, + "replicates_per_version": 5} + medians = {} + for version in BINARIES: + group = [row for row in measured if row["strate"] == strate + and row["workers"] == workers and row["version"] == version] + medians[version] = {} + for key in ("lod02_ready_s", "whole_view_ready_s"): + values = [row[key] for row in group] + medians[version][key] = statistics.median(values) + item[f"{version}_{key}_values"] = values + item[f"{version}_{key}_median"] = medians[version][key] + for baseline in ("assembler-world", "a3c8e36-v230"): + for key in ("lod02_ready_s", "whole_view_ready_s"): + item[f"vs_{baseline}_{key}_delta_pct"] = round( + 100.0 * (medians["l135-v236"][key] + / medians[baseline][key] - 1.0), 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()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/captures/COMPARE.html b/Standalone/Research/WorldAssemblerM3/Linux135/captures/COMPARE.html new file mode 100644 index 0000000..da1f60a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux135/captures/COMPARE.html @@ -0,0 +1,11 @@ +L135 interior views + +

Planner and legacy interiors, seed 0

+
planner-arrival-room
planner-arrival-room
+
planner-tunnel
planner-tunnel
+
planner-compound-room
planner-compound-room
+
planner-destination-chamber
planner-destination-chamber
+
legacy-origin-room
legacy-origin-room
+
legacy-connected-room
legacy-connected-room
+
legacy-origin-tunnel
legacy-origin-tunnel
+
diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/captures/capture-index.json b/Standalone/Research/WorldAssemblerM3/Linux135/captures/capture-index.json new file mode 100644 index 0000000..e43e0d1 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux135/captures/capture-index.json @@ -0,0 +1,119 @@ +{ + "source_stamp": "8dc4294ce0250a591ee4515471c3c547e796ffa4346ab44f0ae1740bbc3797f5", + "seed": 0, + "renderer": "readable, height tint on, headlamp off; 1600x900", + "views": [ + { + "name": "planner-arrival-room", + "recipe": "surface-world", + "camera_m": [ + 35.593114, + -21.55801, + -755.864011 + ], + "yaw_deg": 114.47628370565772, + "camera_height_basis": "floor + player eye offset", + "eye_offset_m": 1.6, + "mesh": "Saved/Linux/l135/capture-planner-arrival-room.vfmesh", + "image": "Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-arrival-room.png", + "mesh_log": "Saved/Linux/l135/capture-planner-arrival-room-mesh.log", + "render_log": "Saved/Linux/l135/capture-planner-arrival-room-render.log" + }, + { + "name": "planner-tunnel", + "recipe": "surface-world", + "camera_m": [ + 42.874177, + -24.8725425, + -757.112565 + ], + "yaw_deg": 114.47628370565774, + "camera_height_basis": "floor + player eye offset", + "eye_offset_m": 1.6, + "mesh": "Saved/Linux/l135/capture-planner-tunnel.vfmesh", + "image": "Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-tunnel.png", + "mesh_log": "Saved/Linux/l135/capture-planner-tunnel-mesh.log", + "render_log": "Saved/Linux/l135/capture-planner-tunnel-render.log" + }, + { + "name": "planner-compound-room", + "recipe": "surface-world", + "camera_m": [ + -39.519627, + -52.685596, + -785.85419 + ], + "yaw_deg": -74.782578137959, + "camera_height_basis": "floor + player eye offset", + "eye_offset_m": 1.6, + "mesh": "Saved/Linux/l135/capture-planner-compound-room.vfmesh", + "image": "Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-compound-room.png", + "mesh_log": "Saved/Linux/l135/capture-planner-compound-room-mesh.log", + "render_log": "Saved/Linux/l135/capture-planner-compound-room-render.log" + }, + { + "name": "planner-destination-chamber", + "recipe": "surface-world", + "camera_m": [ + -28.634259, + -4.85816, + -810.5142579999999 + ], + "yaw_deg": -89.33032962272898, + "camera_height_basis": "floor + player eye offset", + "eye_offset_m": 1.6, + "mesh": "Saved/Linux/l135/capture-planner-destination-chamber.vfmesh", + "image": "Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-destination-chamber.png", + "mesh_log": "Saved/Linux/l135/capture-planner-destination-chamber-mesh.log", + "render_log": "Saved/Linux/l135/capture-planner-destination-chamber-render.log" + }, + { + "name": "legacy-origin-room", + "recipe": "legacy-tunnel-network", + "camera_m": [ + 0.0, + 0.0, + -818.3169921875 + ], + "yaw_deg": 82.58856646117444, + "camera_height_basis": "legacy room floor + player eye offset", + "eye_offset_m": 1.6, + "mesh": "Saved/Linux/l135/capture-legacy-origin-room.vfmesh", + "image": "Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-origin-room.png", + "mesh_log": "Saved/Linux/l135/capture-legacy-origin-room-mesh.log", + "render_log": "Saved/Linux/l135/capture-legacy-origin-room-render.log" + }, + { + "name": "legacy-connected-room", + "recipe": "legacy-tunnel-network", + "camera_m": [ + 39.327854, + 5.115777, + -799.3986572265625 + ], + "yaw_deg": -97.41143353882556, + "camera_height_basis": "legacy room floor + player eye offset", + "eye_offset_m": 1.6, + "mesh": "Saved/Linux/l135/capture-legacy-connected-room.vfmesh", + "image": "Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-connected-room.png", + "mesh_log": "Saved/Linux/l135/capture-legacy-connected-room-mesh.log", + "render_log": "Saved/Linux/l135/capture-legacy-connected-room-render.log" + }, + { + "name": "legacy-origin-tunnel", + "recipe": "legacy-tunnel-network", + "camera_m": [ + 19.663927, + 2.5578885, + -804.8623655 + ], + "yaw_deg": 82.58856646117444, + "camera_height_basis": "near the legacy tunnel centerline", + "eye_offset_m": null, + "mesh": "Saved/Linux/l135/capture-legacy-origin-tunnel.vfmesh", + "image": "Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-origin-tunnel.png", + "mesh_log": "Saved/Linux/l135/capture-legacy-origin-tunnel-mesh.log", + "render_log": "Saved/Linux/l135/capture-legacy-origin-tunnel-render.log" + } + ] +} diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-connected-room.png b/Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-connected-room.png new file mode 100644 index 0000000..a144bf3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-connected-room.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-neighbor-room.png b/Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-neighbor-room.png new file mode 100644 index 0000000..b51cf1f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-neighbor-room.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-origin-room.png b/Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-origin-room.png new file mode 100644 index 0000000..bf3a89b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-origin-room.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-origin-tunnel.png b/Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-origin-tunnel.png new file mode 100644 index 0000000..a3331d0 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux135/captures/legacy-origin-tunnel.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-arrival-room.png b/Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-arrival-room.png new file mode 100644 index 0000000..1c17da4 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-arrival-room.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-compound-room.png b/Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-compound-room.png new file mode 100644 index 0000000..9f1cc4d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-compound-room.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-destination-chamber.png b/Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-destination-chamber.png new file mode 100644 index 0000000..bc872c8 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-destination-chamber.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-tunnel.png b/Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-tunnel.png new file mode 100644 index 0000000..a5cee29 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux135/captures/planner-tunnel.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux136/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux136/REPORT.md new file mode 100644 index 0000000..a7a61bd --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux136/REPORT.md @@ -0,0 +1,33 @@ +# Linux136 — cave planner speed + +Source stamp `ebe6b32a7cd96c5aa69c6dc64ae6b9c6ff4bc18bc484efc5148611c48723c28f` (v239; v236 is commit `2d89023`). + +## Findings + +The tile query now keeps sorted passage IDs with route candidates, so the composer skips unrelated routes. A route-free tile returns before per-sample coordinate and scale math. Native cave evaluation also returns the exact host-rock value when its conservative tile feature list is empty. These paths preserve v236 output; no version bump was needed. + +The LOD0–2 census shows the shared composer does substantial work outside Tunnel Network. Surface World has zero native cave samples at LOD0–3, yet near-ready passage visits are 19.61M on both `assembler/world` and v236. The route-ID path reduces that count to 3.20M, but cumulative near-ready composer worker time stays around 3.5 s (3.42 s baseline, 3.51 s v236, 3.52 s v239). Whole-view classification worker time is 584 ms / 641 ms / 623 ms. Planning is about 4 ms. This rules out cave evaluation and planning as the Surface World regression, but the census does not isolate mouth-profile checks from the other shared composer/classifier work. + +Floating Islands and Vertical Shafts have unrelated-route churn: v236 adds 7.79M and 8.43M passage visits over baseline. v239 brings those totals to 7.89M and 2.54M, and reduces composer worker time by about 9% and 8% versus v236. Tunnel Network falls from 34.63M visits in v236 to 7.47M; its composer worker time returns to baseline. The views still process about 4.7–5.4M native cave samples. The attempted LOD4–8 cave tile proofs produced no skips: every cave-view tile at those LODs remained unclassified. + +## L80 timing + +Five interleaved measured runs per version and cell, one warmup per version. The pre-run gate was load 0.74 and 99.3% CPU idle. Cells show near-ready and whole-view median deltas versus `assembler/world`, in W8/W12 order; the last column shows near-ready deltas versus v236. + +| Stratum | Near-ready Δ | Whole-view Δ | Near-ready Δ vs v236 | +|---|---:|---:|---:| +| Surface World (0) | +4.29% / +5.78% | +2.45% / −0.17% | +0.52% / −1.99% | +| Floating Islands (1) | +8.88% / +5.48% | +1.31% / −0.48% | −2.40% / −4.29% | +| Tunnel Network (2) | +8.23% / +8.26% | +0.35% / +0.88% | −3.51% / −3.97% | +| Vertical Shafts (3) | +13.29% / +6.36% | +1.26% / −1.97% | −4.22% / −3.81% | + +v239 improves on v236 in seven of eight near-ready cells, but misses the requested ≤+3% target in all eight cells versus `assembler/world`. Whole-view deltas stay within +2.45%/−1.97%. + +## Gates + +- Fast tier: PASS, 289.9 s. +- Composed passage, seam, shell, mouth, ripple, naturalness, and blocked-route audits: 16/16 seeds PASS. +- Clang and GCC at W1/W8/W12: all six runs match 13 hash lines. +- Linux VFHostEditor Development: PASS, 5/5 actions in 41.33 s with `-NoUBA -MaxParallelActions=4`; no `prlimit`. + +Timing, census, seed, determinism, and build logs are under `Saved/Linux/l136/`. The completed timing matrix is `Saved/Linux/l136/timing/summary.json`; the interrupted partial matrix is excluded. No commit or push was made. diff --git a/Standalone/Research/WorldAssemblerM3/Linux136/RunL136Census.py b/Standalone/Research/WorldAssemblerM3/Linux136/RunL136Census.py new file mode 100644 index 0000000..dd0ab23 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux136/RunL136Census.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Compare assembler/world and v236 composer/classifier phase counters.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from collections import defaultdict +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l136/census" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": ROOT / "Saved/Linux/l130/bin/VoxelForgeCoreHeadless-l130-clang", + "v236": ROOT / "Saved/Linux/l135/bin/VoxelForgeCoreHeadless-clang-l135", + "v239": ROOT / "Saved/Linux/l136/bin/VoxelForgeCoreHeadless-l136-clang", +} +STAMPS = { + "assembler-world": "8b3e7bfb4cde2b408ab940925eab973d8375d6b719774d6f72f64ee72b526c35", + "v236": "8dc4294ce0250a591ee4515471c3c547e796ffa4346ab44f0ae1740bbc3797f5", + "v239": "ebe6b32a7cd96c5aa69c6dc64ae6b9c6ff4bc18bc484efc5148611c48723c28f", +} +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), +} +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") + + +def field(line: str) -> dict[str, str]: + return dict(FIELDS.findall(line)) + + +def run(strate: int, version: str) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"{version}-s{strate}.log" + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARIES[version]), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--profile-passage-composer", + "--stream-scheduler-policy=3"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp_match = re.search(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", content, re.M) + if result.returncode or stamp_match is None or stamp_match.group(1) != STAMPS[version]: + raise SystemExit(f"census failed/stamp mismatch: {log}, exit={result.returncode}, stamp={stamp_match}") + lod: dict[int, dict] = defaultdict(lambda: defaultdict(float)) + for line in content.splitlines(): + if line.startswith("[WorldAssemblerLODClassification] "): + values = field(line) + target = lod[int(values["lod"])] + for name in ("tiles", "all_air", "all_solid", "mixed", "unclassified", + "tile_skips", "density_point_calls", "field_eval_sample_points", + "proof_air_cells", "proof_solid_cells"): + target[name] = int(values.get(name, "0")) + target["field_eval_ms_est"] = float(values.get("field_eval_ms_est", "0")) + elif line.startswith("[PassageComposerTile] "): + values = field(line) + level = int(re.search(r"lod(\d+)\)", values["key"]).group(1)) + target = lod[level] + target["composer_tiles"] += 1 + for name in ("samples", "field_calls", "native_cave_samples", "native_field_calls", + "room_candidates", "edge_candidates", "features", "route_candidates", + "passage_iterations", "carve_segment_visits"): + target[name] += int(values.get(name, "0")) + target["composer_worker_ms"] += float(values.get("worker_ms", "0")) + plan = re.search(r"^\[WorldAssemblerPlanTiming\] total_ms=([0-9.]+)", content, re.M) + cave = re.search(r"^\[CavernPlannerV1\].* plan_ms=([0-9.]+)", content, re.M) + pipeline = re.search(r"^\[WorldAssemblerTilePipeline\] (.*)$", content, re.M) + pipeline_fields = field(pipeline.group(1)) if pipeline else {} + result_row = { + "strate": strate, "version": version, "source_stamp": stamp_match.group(1), + "plan_ms": float(plan.group(1)) if plan else None, + "cave_plan_ms": float(cave.group(1)) if cave else None, + "tile_pipeline": {key: (float(value) if "." in value else int(value)) + for key, value in pipeline_fields.items() + if re.fullmatch(r"-?\d+(?:\.\d+)?", value)}, + "lod": {str(key): dict(values) for key, values in sorted(lod.items())}, + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), + } + print("[L136CensusRun] " + json.dumps(result_row, sort_keys=True), flush=True) + return result_row + + +def main() -> int: + if any(not path.is_file() for path in (DATA, *BINARIES.values())): + raise SystemExit("missing census fixture or binary") + rows = [] + for strate in range(4): + for version in BINARIES: + rows.append(run(strate, version)) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n") + summary = {"status": "PASS", "rows": rows, + "notes": [ + "classifier milliseconds and composer worker milliseconds are cumulative counters, not exclusive wall time", + "assembler/world predates native cave field counters; field sample fields are only comparable where emitted", + ]} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux136/RunL136Determinism.py b/Standalone/Research/WorldAssemblerM3/Linux136/RunL136Determinism.py new file mode 100644 index 0000000..ec87f62 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux136/RunL136Determinism.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Check exact-stamp Clang/GCC cave-path hashes at 1, 8, and 12 workers.""" +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/l136/determinism" +DATA = ROOT / "Standalone/data/regression/owner_world_2strate_l61.json" +BINARIES = { + "clang": ROOT / "Saved/Linux/l136/fast/bin/VoxelForgeCoreHeadless-fast-clang", + "gcc": ROOT / "Saved/Linux/l136/fast/bin/VoxelForgeCoreHeadless-fast-gcc", +} +STAMP = "ebe6b32a7cd96c5aa69c6dc64ae6b9c6ff4bc18bc484efc5148611c48723c28f" +CAMERA = "-4.688214,38.662384,-505.781342,-28.446,0" +HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|MaterialHash|LODHash|ExactQueryHash)\].*$", + re.M) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def main() -> int: + if any(not path.is_file() for path in (DATA, *BINARIES.values())): + raise SystemExit("missing L136 determinism binary or owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + rows = [] + baseline: tuple[str, ...] | None = None + for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + label = f"determinism-{compiler}-w{workers}" + log = OUT / f"{label}.log" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + f"--workers={workers}", "--clip-radius=3", "--max-clip-level=0", + "--max-level=0", f"--camera={CAMERA}", "--stream-hash-tiles", + "--force-generate", "--recipe=surface-world", "--render-distance-chunks=0", + "--landmark-chamber-demo", "--ore-veins-demo"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + found_stamp = STAMP_RE.search(content) + hashes = tuple(HASH_RE.findall(content)) + passed = (result.returncode == 0 and found_stamp is not None + and found_stamp.group(1) == STAMP and len(hashes) >= 4) + if baseline is None and passed: + baseline = hashes + elif baseline is not None: + passed &= hashes == baseline + row = {"compiler": compiler, "workers": workers, + "exit": result.returncode, "stamp": found_stamp.group(1) + if found_stamp else None, + "hash_lines": len(hashes), "hashes_match_first": hashes == baseline, + "passed": bool(passed), "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT))} + rows.append(row) + print("[L136DeterminismRun] " + json.dumps(row, sort_keys=True), flush=True) + if not passed: + (OUT / "summary.json").write_text(json.dumps( + {"status": "FAIL", "stamp": STAMP, "runs": rows}, indent=2) + "\n") + raise SystemExit(f"determinism mismatch; see {log}") + (OUT / "summary.json").write_text(json.dumps( + {"status": "PASS", "stamp": STAMP, "runs": rows}, indent=2) + "\n") + print(f"[L136DeterminismSummary] runs={len(rows)} stamp={STAMP} status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux136/RunL136SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux136/RunL136SeedSweep.py new file mode 100644 index 0000000..aa8ca80 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux136/RunL136SeedSweep.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Run the v239 composed cave-passage and tile-seam audits for fixed seeds.""" +from __future__ import annotations + +import json +import re +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l136/seeds" +BIN = ROOT / "Saved/Linux/l136/bin/VoxelForgeCoreHeadless-l136-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "ebe6b32a7cd96c5aa69c6dc64ae6b9c6ff4bc18bc484efc5148611c48723c28f" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +REQUIRED = { + "passage": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "route_negative_control": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} + + +def main() -> int: + seeds = [int(value) for value in sys.argv[1:]] or list(range(16)) + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L136 headless binary or owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + results = [] + for seed in seeds: + log = OUT / f"seed{seed}.log" + layout = OUT / f"seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=0", + "--force-generate", f"--seed={seed}", "--recipe=surface-world", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + found_stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["source_stamp"] = found_stamp is not None and found_stamp.group(1) == STAMP + blocked_controls = re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M) + checks["three_blocked_controls"] = len(blocked_controls) == 3 + passed = result.returncode == 0 and all(checks.values()) + row = {"seed": seed, "exit": result.returncode, "passed": passed, + "checks": checks, "blocked_controls": len(blocked_controls), + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L136SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + (OUT / "runs-partial.json").write_text(json.dumps(results, indent=2) + "\n") + if not passed: + raise SystemExit(f"seed {seed} failed a composed audit; see {log}") + summary = {"source_stamp": STAMP, "seeds": seeds, "passed": len(results), + "failed": 0, "results": results} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + print(f"[L136SeedSweepSummary] passed={len(results)}/{len(seeds)} status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux136/RunL136Timing.py b/Standalone/Research/WorldAssemblerM3/Linux136/RunL136Timing.py new file mode 100644 index 0000000..817a612 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux136/RunL136Timing.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Five interleaved L80 timings for assembler/world, v236, and v239.""" +from __future__ import annotations + +import json +import os +import re +import statistics +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l136/timing" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": (ROOT / "Saved/Linux/l130/bin/VoxelForgeCoreHeadless-l130-clang", + "8b3e7bfb4cde2b408ab940925eab973d8375d6b719774d6f72f64ee72b526c35"), + "v236": (ROOT / "Saved/Linux/l135/bin/VoxelForgeCoreHeadless-clang-l135", + "8dc4294ce0250a591ee4515471c3c547e796ffa4346ab44f0ae1740bbc3797f5"), + "v239": (ROOT / "Saved/Linux/l136/bin/VoxelForgeCoreHeadless-l136-clang", + "ebe6b32a7cd96c5aa69c6dc64ae6b9c6ff4bc18bc484efc5148611c48723c28f"), +} +CAMERAS = { + 0: (8.0, 16.0, -372.2626953125), + 1: (25.8369140625, 3.19921875, -737.0), + 2: (19.166015625, -14.2001953125, -753.3740234375), + 3: (-20.0625, -5.068359375, -900.353515625), +} +PIPELINE_RE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M) +FIELDS_RE = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +NEAR_RE = re.compile( + r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$", + re.M) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +PLAN_RE = re.compile(r"^\[WorldAssemblerPlanTiming\] total_ms=([0-9.]+) status=([^ ]+)$", re.M) +CAVE_RE = re.compile(r"^\[CavernPlannerV1\].* plan_ms=([0-9.]+) status=([^ ]+)$", re.M) + + +def run(strate: int, workers: int, version: str, phase: str, + replicate: int) -> dict: + binary, expected_stamp = BINARIES[version] + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + label = f"s{strate}-w{workers}-{version}-{phase}{replicate}" + log = OUT / f"{label}.log" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline, near, stamp = (PIPELINE_RE.search(content), NEAR_RE.search(content), + STAMP_RE.search(content)) + if (result.returncode or pipeline is None or near is None or stamp is None + or stamp.group(1) != expected_stamp): + raise SystemExit(f"L80 timing failed/stamp mismatch: {log}; exit={result.returncode}") + fields = dict(FIELDS_RE.findall(pipeline.group(1))) + plan, cave = PLAN_RE.search(content), CAVE_RE.search(content) + row = {"strate": strate, "workers": workers, "version": version, + "phase": phase, "replicate": replicate, "source_stamp": stamp.group(1), + "lod02_ready_s": float(near.group(3)), + "whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")), + "plan_ms": float(plan.group(1)) if plan else None, + "cave_plan_ms": float(cave.group(1)) if cave else None, + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT))} + print("[L136TimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def quiet_machine() -> tuple[float, float]: + load_1m = os.getloadavg()[0] + def totals() -> tuple[int, int]: + fields = Path("/proc/stat").read_text().splitlines()[0].split() + values = [int(value) for value in fields[1:]] + return sum(values), values[3] + values[4] + before = totals() + time.sleep(2.0) + after = totals() + idle = 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0]) + return load_1m, idle + + +def main() -> int: + if any(not path.is_file() for path, _ in BINARIES.values()) or not DATA.is_file(): + raise SystemExit("missing L136 timing input") + OUT.mkdir(parents=True, exist_ok=True) + load_1m, idle = quiet_machine() + (OUT / "machine-load-before.txt").write_text( + f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n") + print(f"[L136TimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True) + if load_1m >= 0.9 or idle < 97.0: + raise SystemExit("timing refused: machine is above the L80 quiet-load gate") + + rows = [] + for strate in range(4): + for workers in (8, 12): + for version in BINARIES: + run(strate, workers, version, "warmup", 0) + for replicate in range(1, 6): + order = tuple(BINARIES) if replicate % 2 else tuple(reversed(BINARIES)) + for version in order: + rows.append(run(strate, workers, version, "measured", replicate)) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n") + + measured = [row for row in rows if row["phase"] == "measured"] + summary = [] + for strate in range(4): + for workers in (8, 12): + item = {"strate": strate, "workers": workers, "replicates_per_version": 5} + medians = {} + for version in BINARIES: + group = [row for row in measured if row["strate"] == strate + and row["workers"] == workers and row["version"] == version] + medians[version] = {} + for key in ("lod02_ready_s", "whole_view_ready_s"): + values = [row[key] for row in group] + medians[version][key] = statistics.median(values) + item[f"{version}_{key}_values"] = values + item[f"{version}_{key}_median"] = medians[version][key] + for baseline in ("assembler-world", "v236"): + for key in ("lod02_ready_s", "whole_view_ready_s"): + item[f"v239_vs_{baseline}_{key}_delta_pct"] = round( + 100.0 * (medians["v239"][key] / medians[baseline][key] - 1.0), 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()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux137/AnalyzeL137Profile.py b/Standalone/Research/WorldAssemblerM3/Linux137/AnalyzeL137Profile.py new file mode 100644 index 0000000..22ad47a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux137/AnalyzeL137Profile.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Symbolize L137 L77 samples and compare inclusive worker functions.""" +from __future__ import annotations + +import csv +import json +import re +import subprocess +from collections import Counter +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l137/profile" +BINARIES = { + "assembler-world": ROOT / "Saved/Linux/l137/profile-bin/VoxelForgeCoreHeadless-l137-profile-base", + "head": ROOT / "Saved/Linux/l137/profile-bin/VoxelForgeCoreHeadless-l137-profile-head", +} +SAMPLE_RE = re.compile( + r"^\[L77Sample\] pc=(0x\w+) lod=(\d+) stage=(\S+) route=(\S+) " + r"phase=(\S+) halo=(\d+) weight=(\d+)$", re.M) + + +def samples(version: str) -> tuple[list[dict], set[str]]: + rows, pcs = [], set() + for replicate in (1, 2, 3): + path = OUT / f"sw-{version}-r{replicate}.log" + for pc, lod, stage, route, phase, halo, weight in SAMPLE_RE.findall( + path.read_text(encoding="utf-8", errors="replace")): + lod_value = int(lod) + if not 0 <= lod_value <= 8: + continue + pcs.add(pc) + rows.append({"version": version, "replicate": replicate, + "pc": pc, "lod": lod_value, "stage": stage, + "route": route, "phase": phase, "halo": int(halo), + "weight": int(weight)}) + return rows, pcs + + +def symbolize(version: str, pcs: set[str]) -> dict[str, str]: + command = ["llvm-symbolizer-19", "--demangle", "--inlines", "--output-style=JSON", + f"--obj={BINARIES[version]}"] + result = subprocess.run(command, input="\n".join(sorted(pcs)) + "\n", + capture_output=True, text=True, check=True) + decoder = json.JSONDecoder() + offset, output = 0, {} + while offset < len(result.stdout): + while offset < len(result.stdout) and result.stdout[offset].isspace(): + offset += 1 + if offset == len(result.stdout): + break + item, offset = decoder.raw_decode(result.stdout, offset) + frames = item.get("Symbol", []) + output[item["Address"]] = ( + frames[0].get("FunctionName", "?") if frames else "?") + if output.keys() != pcs: + raise RuntimeError(f"{version}: symbolizer resolved {len(output)}/{len(pcs)} PCs") + return output + + +def main() -> int: + totals: dict[str, Counter] = {} + lod_totals: dict[str, Counter] = {} + detail_rows = [] + for version in BINARIES: + rows, pcs = samples(version) + symbols = symbolize(version, pcs) + totals[version] = Counter() + lod_totals[version] = Counter() + for row in rows: + function = symbols[row["pc"]] + weight = row["weight"] + totals[version][function] += weight + lod_totals[version][row["lod"]] += weight + detail_rows.append({**row, "function": function}) + total_by_version = {key: sum(counter.values()) for key, counter in totals.items()} + functions = set(totals["assembler-world"]) | set(totals["head"]) + result = [] + for function in functions: + base_ticks = totals["assembler-world"][function] + head_ticks = totals["head"][function] + base_pct = 100.0 * base_ticks / total_by_version["assembler-world"] + head_pct = 100.0 * head_ticks / total_by_version["head"] + result.append({ + "function": function, + "assembler_world_ticks": base_ticks, + "assembler_world_share_pct": round(base_pct, 3), + "head_ticks": head_ticks, + "head_share_pct": round(head_pct, 3), + "share_delta_pp": round(head_pct - base_pct, 3), + }) + result.sort(key=lambda row: abs(row["share_delta_pp"]), reverse=True) + with (OUT / "function-diff.csv").open("w", encoding="utf-8", newline="") as stream: + writer = csv.DictWriter(stream, fieldnames=list(result[0])) + writer.writeheader() + writer.writerows(result) + with (OUT / "symbolized-samples.csv").open( + "w", encoding="utf-8", newline="") as stream: + writer = csv.DictWriter(stream, fieldnames=list(detail_rows[0])) + writer.writeheader() + writer.writerows(detail_rows) + summary = { + "scope": "worker samples LOD0-8, all LODs included; lod=255 main-thread samples excluded", + "runs_per_version": 3, + "weighted_ticks": total_by_version, + "lod_weighted_ticks": {version: dict(sorted(values.items())) + for version, values in lod_totals.items()}, + "function_diff": result, + } + (OUT / "function-diff.json").write_text( + json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print("[L137ProfileDiff] " + json.dumps({ + "scope": summary["scope"], "weighted_ticks": total_by_version, + "lod_weighted_ticks": summary["lod_weighted_ticks"], + "largest_share_deltas": result[:20]}, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux137/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux137/REPORT.md new file mode 100644 index 0000000..6abb917 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux137/REPORT.md @@ -0,0 +1,26 @@ +# Linux137 report + +## Measurement + +Two `assembler/world` binaries had identical SHA-256. On Surface World, their L80 near-ready medians differed by +0.52% at W8 and −0.59% at W12. The no-op v228 control differed by −0.37% and −0.05%; its stream hashes matched v227. Use ±1% as the median noise floor. Individual paired controls varied by up to about 2.5% (3.6% for one no-op pair). + +The three-run L77 profile sampled LOD 0–8 with no lost samples. Function share changes, `HEAD` versus `assembler/world`: `CachedColumn` +0.93 pp, shared column `TryRead` +0.58 pp, `prepareSweptTubeBase` +0.38 pp, and `sweptSample` +0.17 pp. `ApplyResolvedStratePassageCarves` fell by 0.97 pp and `FindBoundStratumForManagerZ` by 0.39 pp. The profile points more toward column-cache work and the sweep lookup than mouth-profile work; it does not isolate one cause for every world. + +Changes retained: removed the unused `floorShellSignedDistance` sample field; removed per-lookup shared-pointer refcounting and batched diagnostic counters per query scope. + +## L80 near-ready medians + +Each delta is candidate versus `assembler/world`; the A/A median noise floor is ±1%. + +| World | W8 delta | W12 delta | +|---|---:|---:| +| Surface World | +3.58% (±1%) | +2.90% (±1%) | +| Floating Islands | +0.19% (±1%) | +2.20% (±1%) | +| Tunnel Network | −3.60% (±1%) | +6.84% (±1%) | +| Vertical Shafts | +4.31% (±1%) | +3.96% (±1%) | + +The requested targets are not met except Floating Islands W8 and Tunnel Network W8. Whole-view medians were within −8.39% to +0.27% of baseline. + +## Verification + +Fast tier passed (273 checks); the composed sweep passed 16/16 seeds with seams and blocked controls; Clang/GCC W1/W8/W12 matched all 13 hashes. Linux UE host build passed with `-NoUBA -MaxParallelActions=4` and no `prlimit`. Output audits stayed green, so version and baselines were left unchanged. No commit, push, or stash. diff --git a/Standalone/Research/WorldAssemblerM3/Linux137/RunL137AA.py b/Standalone/Research/WorldAssemblerM3/Linux137/RunL137AA.py new file mode 100644 index 0000000..b144076 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux137/RunL137AA.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""L80 Surface World A/A and no-op evaluator-version control timings.""" +from __future__ import annotations + +import json +import os +import re +import statistics +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l137/timing/aa" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "aa_a": ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-aa-a", + "aa_b": ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-aa-b", + "v228": ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-v228", +} +CAMERA = "8.000000000000,16.000000000000,-372.262695312500,0,0" +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) +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def run(workers: int, version: str, phase: str, replicate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + label = f"sw-w{workers}-{version}-{phase}{replicate}" + log = OUT / f"{label}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARIES[version]), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8", + "--max-level=8", f"--camera={CAMERA}", "--profile-lod-work", + "--stream-scheduler-policy=3", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline, near, stamp = PIPELINE.search(content), NEAR.search(content), STAMP.search(content) + if result.returncode or pipeline is None or near is None or stamp is None: + raise SystemExit(f"L80 run failed or trace missing: {log}; exit={result.returncode}") + fields = dict(FIELDS.findall(pipeline.group(1))) + row = { + "strate": 0, "workers": workers, "version": version, + "phase": phase, "replicate": replicate, "source_stamp": stamp.group(1), + "lod02_ready_s": float(near.group(3)), + "whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")), + "whole_tiles": int(fields["whole_tiles"].split("/")[0]), + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), + } + print("[L137AARun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def quiet_machine() -> tuple[float, float]: + load_1m = os.getloadavg()[0] + + def totals() -> tuple[int, int]: + fields = Path("/proc/stat").read_text().splitlines()[0].split() + values = [int(value) for value in fields[1:]] + return sum(values), values[3] + values[4] + + before = totals() + time.sleep(2.0) + after = totals() + idle = 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0]) + return load_1m, idle + + +def main() -> int: + missing = [str(path) for path in [DATA, *BINARIES.values()] if not path.is_file()] + if missing: + raise SystemExit("missing L137 A/A input: " + ", ".join(missing)) + OUT.mkdir(parents=True, exist_ok=True) + load_1m, idle = quiet_machine() + (OUT / "machine-load-before.txt").write_text( + f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n") + print(f"[L137AAGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True) + if load_1m >= 0.9 or idle < 97.0: + raise SystemExit("timing refused: machine is above the L80 quiet-load gate") + + rows = [] + versions = tuple(BINARIES) + for workers in (8, 12): + for version in versions: + run(workers, version, "warmup", 0) + orders = (versions, ("v228", "aa_b", "aa_a"), + ("aa_b", "aa_a", "v228"), ("aa_a", "v228", "aa_b"), + ("v228", "aa_a", "aa_b")) + for replicate, order in enumerate(orders, 1): + for version in order: + rows.append(run(workers, version, "measured", replicate)) + (OUT / "runs-partial.json").write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8") + + summary = [] + for workers in (8, 12): + group = [row for row in rows if row["workers"] == workers] + medians = { + version: { + key: statistics.median(row[key] for row in group + if row["version"] == version) + for key in ("lod02_ready_s", "whole_view_ready_s") + } + for version in versions + } + item = {"strate": 0, "workers": workers, "replicates_per_version": 5, + "medians": medians} + for metric in ("lod02_ready_s", "whole_view_ready_s"): + for lhs, rhs in (("aa_b", "aa_a"), ("v228", "aa_a")): + item[f"{lhs}_vs_{rhs}_{metric}_delta_pct"] = round( + 100.0 * (medians[lhs][metric] / medians[rhs][metric] - 1.0), 3) + paired = [] + for replicate in range(1, 6): + values = {row["version"]: row[metric] for row in group + if row["phase"] == "measured" and row["replicate"] == replicate} + paired.append({"replicate": replicate, + "aa_b_vs_aa_a_pct": round( + 100.0 * (values["aa_b"] / values["aa_a"] - 1.0), 3), + "v228_vs_aa_a_pct": round( + 100.0 * (values["v228"] / values["aa_a"] - 1.0), 3)}) + item[f"{metric}_paired_deltas_pct"] = paired + 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()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux137/RunL137Determinism.py b/Standalone/Research/WorldAssemblerM3/Linux137/RunL137Determinism.py new file mode 100644 index 0000000..8156980 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux137/RunL137Determinism.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Compare L137 Clang/GCC composed hashes at W1, W8, and W12.""" +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/l137/determinism" +DATA = ROOT / "Standalone/data/regression/owner_world_2strate_l61.json" +BINARIES = { + "clang": ROOT / "Saved/Linux/l137/fast/bin/VoxelForgeCoreHeadless-fast-clang", + "gcc": ROOT / "Saved/Linux/l137/fast/bin/VoxelForgeCoreHeadless-fast-gcc", +} +STAMP = "dcf2ffa55fd6d62b3c7faf76229c3b99c635868dc1b694fc6e8f39e738775a4e" +CAMERA = "-4.688214,38.662384,-505.781342,-28.446,0" +HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|MaterialHash|LODHash|ExactQueryHash)\].*$", + re.M) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def main() -> int: + if any(not path.is_file() for path in (DATA, *BINARIES.values())): + raise SystemExit("missing L137 determinism binary or owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + rows = [] + baseline: tuple[str, ...] | None = None + for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + label = f"determinism-{compiler}-w{workers}" + log = OUT / f"{label}.log" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + f"--workers={workers}", "--clip-radius=3", "--max-clip-level=0", + "--max-level=0", f"--camera={CAMERA}", "--stream-hash-tiles", + "--force-generate", "--recipe=surface-world", "--render-distance-chunks=0", + "--landmark-chamber-demo", "--ore-veins-demo"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + hashes = tuple(HASH_RE.findall(content)) + passed = (result.returncode == 0 and stamp is not None + and stamp.group(1) == STAMP and len(hashes) >= 4) + if baseline is None and passed: + baseline = hashes + elif baseline is not None: + passed &= hashes == baseline + row = {"compiler": compiler, "workers": workers, "exit": result.returncode, + "stamp": stamp.group(1) if stamp else None, "hash_lines": len(hashes), + "hashes_match_first": hashes == baseline, "passed": bool(passed), + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT))} + rows.append(row) + print("[L137DeterminismRun] " + json.dumps(row, sort_keys=True), flush=True) + if not passed: + (OUT / "summary.json").write_text(json.dumps( + {"status": "FAIL", "stamp": STAMP, "runs": rows}, indent=2) + "\n") + raise SystemExit(f"determinism mismatch; see {log}") + (OUT / "summary.json").write_text(json.dumps( + {"status": "PASS", "stamp": STAMP, "runs": rows}, indent=2) + "\n") + print(f"[L137DeterminismSummary] runs={len(rows)} stamp={STAMP} status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux137/RunL137FinalTiming.py b/Standalone/Research/WorldAssemblerM3/Linux137/RunL137FinalTiming.py new file mode 100644 index 0000000..5666c94 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux137/RunL137FinalTiming.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""Five interleaved L80 timings for assembler/world and the L137 candidate.""" +from __future__ import annotations + +import json +import os +import re +import statistics +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l137/timing/final" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": ( + ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-aa-a", + "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621"), + "l137": ( + ROOT / "Saved/Linux/l137/fast/bin/VoxelForgeCoreHeadless-fast-clang", + "dcf2ffa55fd6d62b3c7faf76229c3b99c635868dc1b694fc6e8f39e738775a4e"), +} +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) +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + binary, expected_stamp = BINARIES[version] + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + label = f"s{strate}-w{workers}-{version}-{phase}{replicate}" + log = OUT / f"{label}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline, near, stamp = PIPELINE.search(content), NEAR.search(content), STAMP.search(content) + if (result.returncode or pipeline is None or near is None or stamp is None + or stamp.group(1) != expected_stamp): + raise SystemExit(f"L80 timing failed or stamp mismatch: {log}; exit={result.returncode}") + fields = dict(FIELDS.findall(pipeline.group(1))) + row = { + "strate": strate, "workers": workers, "version": version, + "phase": phase, "replicate": replicate, "source_stamp": stamp.group(1), + "lod02_ready_s": float(near.group(3)), + "whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")), + "whole_tiles": int(fields["whole_tiles"].split("/")[0]), + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), + } + print("[L137FinalTimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def quiet_machine() -> tuple[float, float]: + load_1m = os.getloadavg()[0] + + def totals() -> tuple[int, int]: + fields = Path("/proc/stat").read_text().splitlines()[0].split() + values = [int(value) for value in fields[1:]] + return sum(values), values[3] + values[4] + + before = totals() + time.sleep(2.0) + after = totals() + idle = 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0]) + return load_1m, idle + + +def main() -> int: + if any(not path.is_file() for path in [DATA, *(entry[0] for entry in BINARIES.values())]): + raise SystemExit("missing L137 timing input") + OUT.mkdir(parents=True, exist_ok=True) + load_1m, idle = quiet_machine() + (OUT / "machine-load-before.txt").write_text( + f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n") + print(f"[L137FinalTimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True) + if load_1m >= 0.9 or idle < 97.0: + raise SystemExit("timing refused: machine is above the L80 quiet-load gate") + + rows = [] + for strate in range(4): + for workers in (8, 12): + for version in BINARIES: + run(strate, workers, version, "warmup", 0) + for replicate in range(1, 6): + order = tuple(BINARIES) if replicate % 2 else tuple(reversed(BINARIES)) + for version in order: + rows.append(run(strate, workers, version, "measured", replicate)) + (OUT / "runs-partial.json").write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8") + + measured = [row for row in rows if row["phase"] == "measured"] + summary = [] + for strate in range(4): + for workers in (8, 12): + group = [row for row in measured + if row["strate"] == strate and row["workers"] == workers] + entry = {"strate": strate, "workers": workers, "replicates_per_version": 5} + medians = {} + for version in BINARIES: + version_rows = [row for row in group if row["version"] == version] + medians[version] = { + key: statistics.median(row[key] for row in version_rows) + for key in ("lod02_ready_s", "whole_view_ready_s")} + for key in medians[version]: + entry[f"{version}_{key}_values"] = [row[key] for row in version_rows] + entry[f"{version}_{key}_median"] = medians[version][key] + for key in ("lod02_ready_s", "whole_view_ready_s"): + entry[f"l137_vs_assembler-world_{key}_delta_pct"] = round( + 100.0 * (medians["l137"][key] + / medians["assembler-world"][key] - 1.0), 2) + entry[f"{key}_paired_deltas_pct"] = [ + round(100.0 * (next(row[key] for row in group + if row["version"] == "l137" and row["replicate"] == replicate) + / next(row[key] for row in group + if row["version"] == "assembler-world" + and row["replicate"] == replicate) - 1.0), 3) + for replicate in range(1, 6)] + 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()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux137/RunL137Profile.py b/Standalone/Research/WorldAssemblerM3/Linux137/RunL137Profile.py new file mode 100644 index 0000000..d77f296 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux137/RunL137Profile.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""All-LOD inclusive L77 samples for assembler/world and L137 HEAD.""" +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/l137/profile" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": ROOT / "Saved/Linux/l137/profile-bin/VoxelForgeCoreHeadless-l137-profile-base", + "head": ROOT / "Saved/Linux/l137/profile-bin/VoxelForgeCoreHeadless-l137-profile-head", +} +STAMPS = { + "assembler-world": "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621", + "head": "dcf2ffa55fd6d62b3c7faf76229c3b99c635868dc1b694fc6e8f39e738775a4e", +} +CAMERA = "8.000000000000,16.000000000000,-372.262695312500,0,0" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +SAMPLER_RE = re.compile(r"^\[L77Sampler\] action=stop (.*)$", re.M) +SAMPLE_RE = re.compile( + r"^\[L77Sample\] pc=(0x\w+) lod=(\d+) stage=(\S+) route=(\S+) " + r"phase=(\S+) halo=(\d+) weight=(\d+)$", re.M) + + +def run(version: str, replicate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"sw-{version}-r{replicate}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARIES[version]), 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 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") + stamp, sampler = STAMP_RE.search(content), SAMPLER_RE.search(content) + samples = SAMPLE_RE.findall(content) + if (result.returncode or stamp is None or sampler is None + or stamp.group(1) != STAMPS[version] or "status=PASS" not in sampler.group(1)): + raise SystemExit(f"L77 profile failed or stamp mismatch: {log}; exit={result.returncode}") + sam = dict(re.findall(r"([a-zA-Z0-9_]+)=([^ ]+)", sampler.group(1))) + lods = sorted({int(row[1]) for row in samples}) + row = { + "world": "Surface World", "version": version, "replicate": replicate, + "source_stamp": stamp.group(1), "workers": 8, "lods_sampled": lods, + "stored_samples": len(samples), "expanded_ticks": int(sam["expanded_ticks"]), + "lost": int(sam["lost"]), "timer_threads": int(sam["timer_threads"]), + "profiled_thread_cpu_s": float(sam["profiled_thread_cpu_s"]), + "handler_cpu_ms": float(sam["handler_cpu_ms"]), + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), + } + print("[L137ProfileRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + missing = [str(path) for path in [DATA, *BINARIES.values()] if not path.is_file()] + if missing: + raise SystemExit("missing L137 profile input: " + ", ".join(missing)) + rows = [] + for replicate, order in enumerate(( + ("assembler-world", "head"), ("head", "assembler-world"), + ("assembler-world", "head")), 1): + for version in order: + rows.append(run(version, replicate)) + (OUT / "runs-partial.json").write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8") + (OUT / "runs.json").write_text(json.dumps(rows, indent=2) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux137/RunL137SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux137/RunL137SeedSweep.py new file mode 100644 index 0000000..bd5cb54 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux137/RunL137SeedSweep.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Run the L137 composed passage and tile-seam audits for seeds 0 through 15.""" +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/l137/seeds" +BIN = ROOT / "Saved/Linux/l137/fast/bin/VoxelForgeCoreHeadless-fast-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "dcf2ffa55fd6d62b3c7faf76229c3b99c635868dc1b694fc6e8f39e738775a4e" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +REQUIRED = { + "passage": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "route_negative_control": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} + + +def main() -> int: + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L137 fast binary or owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + results = [] + for seed in range(16): + log = OUT / f"seed{seed}.log" + layout = OUT / f"seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=0", + "--force-generate", f"--seed={seed}", "--recipe=surface-world", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + found_stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["source_stamp"] = found_stamp is not None and found_stamp.group(1) == STAMP + blocked = re.findall(r"^\[ResolvedPassageBlockedControl\].*detected=true$", + content, re.M) + checks["three_blocked_controls"] = len(blocked) == 3 + passed = result.returncode == 0 and all(checks.values()) + row = {"seed": seed, "exit": result.returncode, "passed": passed, + "checks": checks, "blocked_controls": len(blocked), + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L137SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + (OUT / "runs-partial.json").write_text(json.dumps(results, indent=2) + "\n") + if not passed: + raise SystemExit(f"seed {seed} failed composed audits; see {log}") + summary = {"source_stamp": STAMP, "seeds": list(range(16)), + "passed": len(results), "failed": 0, "results": results} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + print("[L137SeedSweepSummary] passed=16/16 status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux137/VerifyL137VersionOutput.py b/Standalone/Research/WorldAssemblerM3/Linux137/VerifyL137VersionOutput.py new file mode 100644 index 0000000..4dc0e14 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux137/VerifyL137VersionOutput.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Compare rendered Surface World hashes for v227 and the no-op v228 bump.""" +from __future__ import annotations + +import json +import re +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l137/output-check" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "v227": ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-aa-a", + "v228": ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-v228", +} +CAMERA = "8.000000000000,16.000000000000,-372.262695312500,0,0" +HASHES = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|MaterialHash|LODHash|ExactQueryHash)\].*$", + re.M) + + +def run(version: str) -> tuple[list[str], str]: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"surface-world-{version}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARIES[version]), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={CAMERA}", "--recipe=surface-world", "--stream-hash", + "--stream-scheduler-policy=3", + ] + 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") + hashes = HASHES.findall(content) + if result.returncode or len(hashes) < 4: + raise SystemExit(f"hash run failed: {version}; exit={result.returncode}; log={log}") + return hashes, str(log.relative_to(ROOT)) + + +def main() -> int: + rows = [] + for version in BINARIES: + hashes, log = run(version) + rows.append({"version": version, "hashes": hashes, "log": log}) + match = rows[0]["hashes"] == rows[1]["hashes"] + summary = {"output_hashes_match": match, "rows": rows} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + print("[L137VersionOutput] " + json.dumps({ + "output_hashes_match": match, + "hash_lines": len(rows[0]["hashes"]), + "v227": rows[0]["hashes"], "v228": rows[1]["hashes"]}, sort_keys=True)) + return 0 if match else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux138/AnalyzeL138PrechangeCacheCounts.py b/Standalone/Research/WorldAssemblerM3/Linux138/AnalyzeL138PrechangeCacheCounts.py new file mode 100644 index 0000000..9c07ed2 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux138/AnalyzeL138PrechangeCacheCounts.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Summarize per-tile cache counters from the five L137 paired timing runs.""" +from __future__ import annotations + +import json +import re +import statistics +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +SOURCE = ROOT / "Saved/Linux/l137/timing/final-design1" +OUT = ROOT / "Saved/Linux/l138/prechange/column-cache-counts.json" +STAMPS = { + "assembler-world": "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621", + "l137": "7913d019d968de2dbaea2282b90fa8ff41b53f996c022e9e6fed92712cb40ecf", +} +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +METRICS = ( + "column_point_calls", "column_local_hits", "column_local_misses", + "column_conflict_misses", "column_shared_hits", "column_formula_evaluations", +) + + +def read_run(strate: int, workers: int, version: str, replicate: int) -> dict: + path = SOURCE / f"s{strate}-w{workers}-{version}-measured{replicate}.log" + if not path.is_file(): + raise SystemExit(f"missing pre-change L137 run: {path}") + text = path.read_text(encoding="utf-8", errors="replace") + stamp = re.search(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", text, re.M) + if stamp is None or stamp.group(1) != STAMPS[version]: + raise SystemExit(f"unexpected source stamp in {path}") + rows = {} + for line in text.splitlines(): + if not line.startswith("[WorldAssemblerLODCostDetail] "): + continue + fields = dict(FIELDS.findall(line)) + rows[int(fields["lod"])] = fields + if set(rows) != set(range(9)): + raise SystemExit(f"incomplete LOD counters in {path}: {sorted(rows)}") + return {"path": str(path.relative_to(ROOT)), "rows": rows} + + +def summarize(runs: list[dict], lods: range) -> dict: + per_run = [] + for run in runs: + selected = [run["rows"][lod] for lod in lods] + tile_count = sum(int(row["tiles"]) for row in selected) + per_run.append({ + "tiles": tile_count, + **{metric: sum(int(row[metric]) for row in selected) / tile_count + for metric in METRICS}, + }) + return { + "tiles_per_run": statistics.mean(row["tiles"] for row in per_run), + "mean_per_tile": { + metric: statistics.mean(row[metric] for row in per_run) + for metric in METRICS + }, + "run_range_per_tile": { + metric: [min(row[metric] for row in per_run), + max(row[metric] for row in per_run)] + for metric in METRICS + }, + } + + +def main() -> int: + result = { + "source": "five measured L137 L80 runs per version, worker count, and stratum", + "pre_change_stamps": STAMPS, + "cells": [], + } + for strate in range(4): + for workers in (8, 12): + versions = { + version: [read_run(strate, workers, version, replicate) + for replicate in range(1, 6)] + for version in STAMPS + } + item = {"strate": strate, "workers": workers} + for window, lods in (("near_lod0_2", range(3)), ("whole_lod0_8", range(9))): + item[window] = { + version: summarize(runs, lods) + for version, runs in versions.items() + } + item[window]["candidate_minus_baseline_per_tile"] = { + metric: (item[window]["l137"]["mean_per_tile"][metric] + - item[window]["assembler-world"]["mean_per_tile"][metric]) + for metric in METRICS + } + item["logs"] = { + version: [run["path"] for run in runs] + for version, runs in versions.items() + } + result["cells"].append(item) + OUT.parent.mkdir(parents=True, exist_ok=True) + OUT.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + print(f"[L138PrechangeCacheCounts] cells={len(result['cells'])} output={OUT.relative_to(ROOT)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux138/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux138/REPORT.md new file mode 100644 index 0000000..4167d0e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux138/REPORT.md @@ -0,0 +1,49 @@ +# Linux138 — final cave planner speed pass + +Source stamp: `9aade4e6cb33e86c36b89440644d9ef5d440288fb6ff0af03af128291c95f93f`. + +## Result + +The suspected column-cache regression is not present in the measured counters. Calls and formula evaluations per tile match `assembler/world`; the small miss deltas change sign between W8 and W12. The L77 `CachedColumn` and shared `TryRead` increases were normalized sample-share changes, not evidence of more cache calls. + +I removed repeated route identity checks and added a fast path for prepared sweep samples that are stamped outside all passage influence. The path preserves the sample's flat distance in the union and skips wall/floor shell work whose bounds are positive. The speed bar is still missed on several near-ready cells, especially Vertical Shafts. No version bump or baseline update was needed: output audits remained green. + +## Before-change cache census + +Means per near-ready LOD0–2 tile, averaged across the five measured L137 L80 runs per worker count. Each cell is `assembler/world / L137 candidate`. + +| Stratum / workers | Point calls | Local hits | Local misses | Shared hits | Formula evaluations | +|---|---:|---:|---:|---:|---:| +| Surface World / W8 | 6529.395 / 6529.396 | 5712.251 / 5720.303 | 817.144 / 809.092 | 570.207 / 562.158 | 246.937 / 246.934 | +| Surface World / W12 | 6529.395 / 6529.396 | 5642.386 / 5641.558 | 887.009 / 887.838 | 639.976 / 640.851 | 247.033 / 246.987 | + +Floating Islands, Tunnel Network, and Vertical Shafts had zero generic heightfield column calls and cache hits/misses per near-ready tile in both binaries. Floating Islands' separate local/shared cache counters were also zero in its profiled LOD0–2 tiles. The shared `TryRead` path is reached on local misses: W8 had about 8 fewer misses per tile on the candidate, while W12 had 0.83 more. Cache code, key, and window are unchanged versus `assembler/world` in `WorldAssemblerInternal.h`, `SharedColumnCache.h`, and `FloatingIslandsInternal.h`. + +L77 reports each function's share of sampled worker ticks. A higher share can result when other work takes a smaller share; it is not a cache-call counter. The before-change census is in `Saved/Linux/l138/prechange/column-cache-counts.json`; the separate Floating Islands counters are in `Saved/Linux/l138/prechange/island-cache/summary.json`. + +## Change and timing + +`ShapeWorldInternal.h` now reuses the already-computed Tunnel Network stratum predicate in the passage loop. For a swept sample with no exact contribution, it adds the same flat outside distance to the union, then skips relief, mouth, wall, and shell calculations. The influence bound is 1.053 m (0.75 m shell + 0.21 m relief + 0.083 m corner support + 0.01 m guard); the stamped sentinel is one subvoxel beyond it. + +Final L80 used one warm-up and five interleaved runs per binary, at W8/W12 across all four strata. The quiet-host gate passed at load 1.34 and 98.9% CPU idle. Deltas are candidate medians versus `assembler/world`; the L137 A/A noise floor is ±1%. + +| Stratum | Near-ready W8 / W12 | Whole-view W8 / W12 | +|---|---:|---:| +| Surface World | +0.34% / +1.72% | −0.48% / −0.93% | +| Floating Islands | −4.34% / +2.87% | −3.11% / −1.50% | +| Tunnel Network | −2.54% / +2.86% | −3.96% / −1.73% | +| Vertical Shafts | +4.66% / +3.25% | −1.73% / −2.06% | + +Only Surface World W8 is within ±1% near-ready. The per-LOD worker totals show LOD2 down 24–39% on Floating Islands, Tunnel Network, and Vertical Shafts, but Vertical Shafts LOD0/1 are up 5–22%; that remaining work keeps its near-ready medians outside the bar. Tunnel Network W8 is the closest of its two near-ready cells. + +## Checks + +- Fast tier: PASS, 273 checks, 286.0 s. +- Composed sweep: PASS, 16/16 seeds, with passage, seam, shell, mouth-profile, naturalness, ripple, and blocked-route controls. +- Clang/GCC W1/W8/W12: PASS, all 13 determinism hash lines match. +- Linux `VFHostEditor` Development: PASS, 5 actions, 38.19 s with `-NoUBA -MaxParallelActions=4`; no `prlimit`. +- The full verification tier and Windows UE build were not run. No Unreal `Source/` files changed. + +The first fast-tier invocation used the verifier's default `l42` output directory and refused its stale binary. I reran it with `--out Saved/Linux/l138`; the final-source run passed. + +Build, fast-tier, seed, determinism, cache-census, and timing logs are under `Saved/Linux/l138/`; the final timing matrix is `Saved/Linux/l138/timing/summary.json`. Changes are uncommitted. diff --git a/Standalone/Research/WorldAssemblerM3/Linux138/RunL138Determinism.py b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138Determinism.py new file mode 100644 index 0000000..cf53a60 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138Determinism.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Compare L138 Clang/GCC composed hashes at W1, W8, and W12.""" +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/l138/determinism" +DATA = ROOT / "Standalone/data/regression/owner_world_2strate_l61.json" +BINARIES = { + "clang": ROOT / "Saved/Linux/l138/bin/VoxelForgeCoreHeadless-l138-clang", + "gcc": ROOT / "Saved/Linux/l138/bin/VoxelForgeCoreHeadless-l138-gcc", +} +STAMP = "9aade4e6cb33e86c36b89440644d9ef5d440288fb6ff0af03af128291c95f93f" +CAMERA = "-4.688214,38.662384,-505.781342,-28.446,0" +HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|MaterialHash|LODHash|ExactQueryHash)\].*$", + re.M) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def main() -> int: + if any(not path.is_file() for path in (DATA, *BINARIES.values())): + raise SystemExit("missing L138 determinism binary or owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + rows = [] + reference: tuple[str, ...] | None = None + for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + name = f"determinism-{compiler}-w{workers}" + log = OUT / f"{name}.log" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + f"--workers={workers}", "--clip-radius=3", "--max-clip-level=0", + "--max-level=0", f"--camera={CAMERA}", "--stream-hash-tiles", + "--force-generate", "--recipe=surface-world", "--render-distance-chunks=0", + "--landmark-chamber-demo", "--ore-veins-demo"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + hashes = tuple(HASH_RE.findall(content)) + passed = (proc.returncode == 0 and stamp is not None + and stamp.group(1) == STAMP and len(hashes) >= 4) + if reference is None and passed: + reference = hashes + elif reference is not None: + passed = passed and hashes == reference + row = {"compiler": compiler, "workers": workers, "exit": proc.returncode, + "stamp": stamp.group(1) if stamp else None, "hash_lines": len(hashes), + "hashes_match_first": hashes == reference, "passed": bool(passed), + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT))} + rows.append(row) + print("[L138DeterminismRun] " + json.dumps(row, sort_keys=True), flush=True) + if not passed: + (OUT / "summary.json").write_text(json.dumps( + {"status": "FAIL", "stamp": STAMP, "runs": rows}, indent=2) + "\n") + raise SystemExit(f"determinism mismatch; see {log}") + (OUT / "summary.json").write_text(json.dumps( + {"status": "PASS", "stamp": STAMP, "runs": rows}, indent=2) + "\n") + print(f"[L138DeterminismSummary] runs={len(rows)} stamp={STAMP} status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux138/RunL138FinalTiming.py b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138FinalTiming.py new file mode 100644 index 0000000..96946c6 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138FinalTiming.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Run five interleaved L80 timings for assembler/world and the L138 candidate.""" +from __future__ import annotations + +import json +import os +import re +import statistics +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l138/timing" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": ( + ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-aa-a", + "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621"), + "l138": ( + ROOT / "Saved/Linux/l138/bin/VoxelForgeCoreHeadless-l138-clang", + "9aade4e6cb33e86c36b89440644d9ef5d440288fb6ff0af03af128291c95f93f"), +} +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) +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + binary, expected_stamp = BINARIES[version] + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + label = f"s{strate}-w{workers}-{version}-{phase}{replicate}" + log = OUT / f"{label}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline, near, stamp = PIPELINE.search(content), NEAR.search(content), STAMP.search(content) + if (proc.returncode or pipeline is None or near is None or stamp is None + or stamp.group(1) != expected_stamp): + raise SystemExit(f"L80 timing failed or stamp mismatch: {log}; exit={proc.returncode}") + fields = dict(FIELDS.findall(pipeline.group(1))) + row = { + "strate": strate, "workers": workers, "version": version, + "phase": phase, "replicate": replicate, "source_stamp": stamp.group(1), + "lod02_ready_s": float(near.group(3)), + "whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")), + "whole_tiles": int(fields["whole_tiles"].split("/")[0]), + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), + } + print("[L138FinalTimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def quiet_machine() -> tuple[float, float]: + load_1m = os.getloadavg()[0] + + def totals() -> tuple[int, int]: + fields = Path("/proc/stat").read_text().splitlines()[0].split() + values = [int(value) for value in fields[1:]] + return sum(values), values[3] + values[4] + + before = totals() + time.sleep(2.0) + after = totals() + idle = 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0]) + return load_1m, idle + + +def main() -> int: + if any(not path.is_file() for path in [DATA, *(entry[0] for entry in BINARIES.values())]): + raise SystemExit("missing L138 timing input") + OUT.mkdir(parents=True, exist_ok=True) + load_1m, idle = quiet_machine() + (OUT / "machine-load-before.txt").write_text( + f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n") + print(f"[L138FinalTimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True) + if load_1m >= 1.5 or idle < 97.0: + raise SystemExit("timing refused: machine is above the L80 quiet-load gate") + + rows = [] + for strate in range(4): + for workers in (8, 12): + for version in BINARIES: + run(strate, workers, version, "warmup", 0) + for replicate in range(1, 6): + order = tuple(BINARIES) if replicate % 2 else tuple(reversed(BINARIES)) + for version in order: + rows.append(run(strate, workers, version, "measured", replicate)) + (OUT / "runs-partial.json").write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8") + + measured = [row for row in rows if row["phase"] == "measured"] + summary = [] + for strate in range(4): + for workers in (8, 12): + group = [row for row in measured + if row["strate"] == strate and row["workers"] == workers] + entry = {"strate": strate, "workers": workers, "replicates_per_version": 5} + medians = { + version: { + key: statistics.median(row[key] for row in group + if row["version"] == version) + for key in ("lod02_ready_s", "whole_view_ready_s") + } + for version in BINARIES + } + entry["medians"] = medians + for key in ("lod02_ready_s", "whole_view_ready_s"): + entry[f"l138_vs_assembler-world_{key}_delta_pct"] = round( + 100.0 * (medians["l138"][key] / medians["assembler-world"][key] - 1.0), 2) + entry[f"{key}_paired_deltas_pct"] = [ + round(100.0 * ( + next(row[key] for row in group + if row["version"] == "l138" and row["replicate"] == replicate) + / next(row[key] for row in group + if row["version"] == "assembler-world" + and row["replicate"] == replicate) - 1.0), 3) + for replicate in range(1, 6) + ] + summary.append(entry) + (OUT / "summary.json").write_text(json.dumps({ + "a_a_noise_floor_pct": 1.0, + "timing_protocol": "one warmup per binary, five interleaved runs, W8/W12, all four strata", + "runs": rows, + "summary": summary, + }, indent=2) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux138/RunL138PrechangeIslandCacheCensus.py b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138PrechangeIslandCacheCensus.py new file mode 100644 index 0000000..040c361 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138PrechangeIslandCacheCensus.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Count pre-change Floating Islands cache calls from L137 binaries.""" +from __future__ import annotations + +import json +import re +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l138/prechange/island-cache" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": ( + ROOT / "Saved/Linux/l137/profile-bin/VoxelForgeCoreHeadless-l137-profile-base", + "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621"), + "l137": ( + ROOT / "Saved/Linux/l137/profile-bin/VoxelForgeCoreHeadless-l137-profile-head", + "dcf2ffa55fd6d62b3c7faf76229c3b99c635868dc1b694fc6e8f39e738775a4e"), +} +CAMERAS = { + 1: (25.8369140625, 3.19921875, -737.0), +} +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +TILE = re.compile(r"^\[CpuTileStages\] key=\([^)]*,lod(\d+)\).*", re.M) +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +CACHE_FIELDS = ( + "fi_local_hit", "fi_local_miss", "fi_local_conflicts", + "fi_shared_hit", "fi_shared_miss", "fi_shared_computes", + "fi_shared_conflicts", +) + + +def main() -> int: + OUT.mkdir(parents=True, exist_ok=True) + rows = [] + for workers in (8, 12): + for version, (binary, expected_stamp) in BINARIES.items(): + camera = ",".join(f"{value:.12f}" for value in CAMERAS[1]) + ",0,0" + log = OUT / f"s1-w{workers}-{version}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8", + "--max-level=8", f"--camera={camera}", "--profile-lod-work", + "--profile-passage-composer", "--stream-scheduler-policy=3", + ] + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + text = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP.search(text) + if proc.returncode or not stamp or stamp.group(1) != expected_stamp: + raise SystemExit(f"pre-change cache census failed: {log}") + tiles = [] + for line in text.splitlines(): + match = TILE.match(line) + if not match or int(match.group(1)) > 2: + continue + fields = dict(FIELDS.findall(line)) + tiles.append({key: int(fields.get(key, "0")) for key in CACHE_FIELDS}) + if not tiles: + raise SystemExit(f"no LOD0-2 tile-cache counters in {log}") + total = {key: sum(tile[key] for tile in tiles) for key in CACHE_FIELDS} + row = { + "strate": 1, "workers": workers, "version": version, + "source_stamp": stamp.group(1), "tiles": len(tiles), + "totals": total, + "per_tile": {key: value / len(tiles) for key, value in total.items()}, + "logs": str(log.relative_to(ROOT)), + } + rows.append(row) + print("[L138PrechangeIslandCache] " + json.dumps(row, sort_keys=True), flush=True) + (OUT / "summary.json").write_text(json.dumps(rows, indent=2) + "\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux138/RunL138SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138SeedSweep.py new file mode 100644 index 0000000..a6ce234 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux138/RunL138SeedSweep.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Run the 16-seed composed passage and seam audits on the L138 candidate.""" +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/l138/seeds" +BIN = ROOT / "Saved/Linux/l138/bin/VoxelForgeCoreHeadless-l138-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "9aade4e6cb33e86c36b89440644d9ef5d440288fb6ff0af03af128291c95f93f" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +REQUIRED = { + "passage": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "route_negative_control": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} + + +def main() -> int: + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L138 Clang binary or owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + results = [] + for seed in range(16): + log = OUT / f"seed{seed}.log" + layout = OUT / f"seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=0", + "--force-generate", f"--seed={seed}", "--recipe=surface-world", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP + blocked = re.findall(r"^\[ResolvedPassageBlockedControl\].*detected=true$", + content, re.M) + checks["three_blocked_controls"] = len(blocked) == 3 + passed = proc.returncode == 0 and all(checks.values()) + row = {"seed": seed, "exit": proc.returncode, "passed": passed, + "checks": checks, "blocked_controls": len(blocked), + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L138SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + (OUT / "runs-partial.json").write_text(json.dumps(results, indent=2) + "\n") + if not passed: + raise SystemExit(f"seed {seed} failed composed audits; see {log}") + summary = {"source_stamp": STAMP, "seeds": list(range(16)), + "passed": len(results), "failed": 0, "results": results} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + print("[L138SeedSweepSummary] passed=16/16 status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux139/REPORT.md new file mode 100644 index 0000000..c25e7f5 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux139/REPORT.md @@ -0,0 +1,92 @@ +# Linux139 — Vertical Shafts on the Core planner + +## Result + +S3 now reports `Core.Caves.PrimitiveField` / `Caves-native`, using the shared Tunnel Network +planner. The composed acceptance gate is not green: **15/16 seeds pass; seed 2 fails the passage +shell and tile seam checks at the arrival from passage 2–3.** This worktree is not promoted as an +accepted Vertical Shafts generator. + +The basal primordial route law means arrival passage 2–3 to a walkable terminal landing above the +bedrock seal. There is no departure mouth at the bottom of the owner world. The shared planner +builds that walk-only spine, its access tunnels, and landings within the existing grade contract. +The full-height open shafts and their ledges are an exploration tier: a drop or climb is not +walk-only traversal, and climb or mine access stays in the reporting tiers. `Core.Lattice` was not +used because L58's VerticalShafts lattice route looked like a corkscrew, not an open shaft. + +## Plan and parameter mapping + +`PlanResolvedCavernNetworkV1` is reused without a planner fork. `Core.Caves.PrimitiveField` is the +shared evaluator, extended with `shapeKind=1`: the existing room/tunnel shapes do not represent a +capped, full-height vertical bore, and the L58 lattice route was a corkscrew. Shaft records use +that cylinder primitive with ledges, short access bores, and cross-shaft links. Generic cave +branches are disabled in shaft mode. + +| `FVerticalShaftParams` field(s) | Core setting | +|---|---| +| `ShaftSpacing` | Shared planner room spacing; owner voxels convert to integer micrometres. | +| `ShaftDensity` | `roomDensityQ16`, controlling shaft placement; a nonzero setting guarantees the route's initial shaft. | +| `ShaftMinRadius`, `ShaftMaxRadius` | Minimum and maximum shaft radii. | +| `CrossConnectChance` | `tunnelDensityQ16` for links between shafts. | +| `ConnectorRadius` | Equal minimum/maximum radius for access and connector bores. | +| `LedgeSpacing`, `LedgeDepth` | Vertical shelf interval and intrusion; zero spacing disables shelves and zero depth makes them nonintrusive. | +| `SurfaceRoughness` | Roughness amplitude; frequency derives as `max(0.5 m, 4 owner voxels)` because no frequency field exists. | +| `BoundarySealThickness`, `BaseDensity` | Existing resolved top/basal boundary bands via `sealAndDensity`, outside planner settings. | + +## Measurements + +The final source stamp was `b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1`. +The composed sweep covered seeds 0–15 with one run per seed (reported process time 320.485 s in +total; individual runs 10.296–34.015 s). Seeds 0, 1, and 3–15 passed passages, route, seams, shell, +and blocked-route controls. Seed 2's route grades/support, naturalness, mouth profile, ripple, +dispatch, and negative controls passed; its composed passage, seam, and shell summaries failed. +The shell audit reported eight `wall_missing` samples on `passage/2-3/0`, at route stations +489.627–490.321 m. The final log records positive native density at those samples and negative +wall-shell margins. See `Saved/Linux/l139/seeds/seed2-final.log` and +`Saved/Linux/l139/seeds/summary.json`. + +Three geometry variants were tried against this same shell gate: starting passage clearance at +12 m, adding a guarded cubic lateral bow, and starting clearance at 8 m. The first and third still +failed wall checks; the bow failed floor checks. Per the three-failure stop rule, these were +discarded. The final binary retains the successful Q16 endpoint-normalization correction needed +for seed 1 and no seed-2 geometry patch. + +L80 ran after the seed sweep. The host gate was **load 1.31, CPU idle 99.4%**. It used one warm-up +per build and stratum/worker cell, then five interleaved measured runs for each of three builds at +W8 and W12 across all four strata: 120 measured runs total. Comparisons use `fa4417a`, +`assembler/world`, and the L139 binary; the established L137 A/A noise floor is ±1% ([A/A report](../Linux137/REPORT.md)). +Times are medians in seconds; deltas are L139 relative to each comparison build. Paired values in +each cell are W8 / W12. + +| Stratum | LOD0–2 L139 | Δ vs `fa4417a` | Δ vs `assembler/world` | Whole-view L139 | Δ vs `fa4417a` | Δ vs `assembler/world` | +|---|---:|---:|---:|---:|---:|---:| +| S0 Surface World | 0.838 / 0.886 | +1.71% / +3.97% | +2.92% / +5.06% | 1.862 / 1.804 | +2.42% / +1.63% | −0.11% / −0.33% | +| S1 Floating Islands | 1.036 / 1.038 | +4.72% / +4.61% | +2.09% / +5.35% | 1.842 / 1.830 | +0.11% / +1.16% | −6.73% / −1.24% | +| S2 Tunnel Network | 1.105 / 1.078 | +2.44% / +3.07% | +3.57% / +7.57% | 1.876 / 1.808 | −1.11% / +0.00% | −4.33% / −1.15% | +| S3 Vertical Shafts | 0.899 / 0.884 | −3.81% / −0.94% | −0.95% / +2.00% | 1.706 / 1.657 | −6.57% / −2.53% | −6.21% / −4.77% | + +Whole-view medians are numerically lower than `assembler/world` in all eight cells; the S0 W8 and +W12 differences are within the ±1% noise floor. Seven of eight LOD0–2 cells are slower than +`assembler/world`; the remaining cell is within the noise floor. This is characterization of an +unaccepted build, not a promotion claim. Raw runs and medians are in +`Saved/Linux/l139/timing/summary.json`. + +## Checks and captures + +- Fast tier: **PASS**, 284.0 s; the S3 dispatch line is `VerticalShafts` → `Core.Caves.PrimitiveField` v1 / `Caves-native` / `FCavesFieldAdapter`. +- Clang and GCC determinism: **PASS**, W1/W8/W12 (six cases), 13 hash lines and resolved-layout identity matched. Both compilers built the final source stamp above. +- Canonical v237 baselines added for Surface World and Floating Islands; both match their v236 values. +- Five eye-height views were captured before and after: arrival mouth, shaft access landing, looking down, looking up from a ledge, and basal terminal landing. All five L139 views pass the existing quality threshold. Two L138 baseline views fail it: the old layer has no shaft at the selected site, so the down-shaft view is flat, and the upward view is low contrast. The threshold was not relaxed. Images and camera details are in `captures/`. +- The outside views are `captures/after-whole-layer-density-cutaway.jpg` (coarse LOD density-mesh slice at y=−5 m) and `captures/after-whole-layer-side-elevation.svg` (resolved-plan projection that labels the walk route and all 53 shaft primitives). The cutaway passes the unchanged image-quality threshold; its plan companion makes the full shaft layout clearer. +- Linux `VFHostEditor` Development build: **PASS**, 27 actions, 72.58 s, run after timing with `-NoUBA -MaxParallelActions=4` and no `prlimit`. It emitted three pre-existing deprecated RealtimeMesh API warnings from `VoxelWorld.cpp`; that file was not changed. +- `git diff --check` and Python bytecode compilation of the L139 runners passed. Windows build and Windows determinism were not run. + +Logs and source-stamped binaries are under `Saved/Linux/l139/`. The only changed Unreal `Source/` +file is `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`. + +## Remaining risk + +The passage shell and seam break at the incoming mouth on seed 2 remain unresolved. The local +clearance/heading premise failed three geometry attempts; a new route-to-mouth design premise is +needed before another geometry change. Walk-only connectivity stays guaranteed by construction; +vertical drops, climbs, and mine access remain reporting tiers. diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/RunL139Captures.py b/Standalone/Research/WorldAssemblerM3/Linux139/RunL139Captures.py new file mode 100644 index 0000000..3697632 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux139/RunL139Captures.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +"""Capture comparable player-height views of the L138 and L139 owner shaft layer.""" +from __future__ import annotations + +import json +import math +import os +from pathlib import Path +import re +import subprocess +import sys +import time + + +ROOT = Path(__file__).resolve().parents[4] +DATA = ROOT / "Standalone/data/owner_world.json" +LAYOUT = ROOT / "Saved/Linux/l139/seeds/seed0.json" +OUT = ROOT / "Saved/Linux/l139/captures" +ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux139/captures" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +VERSIONS = { + "before-l138": { + "headless": ROOT / "Saved/Linux/l138/bin/VoxelForgeCoreHeadless-l138-clang", + "renderer": ROOT / "Saved/Linux/l138/bin/VoxelForgeOffscreenCapture-l138-clang", + "stamp": "9aade4e6cb33e86c36b89440644d9ef5d440288fb6ff0af03af128291c95f93f", + }, + "after-l139": { + "headless": ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-clang", + "renderer": ROOT / "Saved/Linux/l139/bin/VoxelForgeOffscreenCapture-l139-clang", + "stamp": "b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1", + }, +} +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) + + +def fmt(values: tuple[float, ...]) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def angles(camera: tuple[float, float, float], + target: tuple[float, float, float]) -> tuple[float, float]: + dx, dy, dz = (target[i] - camera[i] for i in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees( + math.atan2(dz, math.hypot(dx, dy))) + + +def stamped_run(name: str, command: list[str], log: Path, expected: str, + env: dict[str, str]) -> str: + binary = Path(command[0]) + probe = subprocess.run([*LIMIT, "timeout", "15s", str(binary), "--print-source-stamp"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=20) + match = STAMP_RE.search(probe.stdout or "") + if probe.returncode != 0 or not match or match.group(1) != expected: + raise RuntimeError(f"{name}: executable stamp mismatch for {binary}") + log.parent.mkdir(parents=True, exist_ok=True) + started = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, + timeout=1745, check=False) + output = log.read_text(encoding="utf-8", errors="replace") + emitted = STAMP_RE.search(output) + if (result.returncode != 0 or not emitted or emitted.group(1) != expected): + raise RuntimeError(f"{name}: process failed or source changed; see {log}") + print(f"[L139CaptureRun] step={name} elapsed_s={time.monotonic()-started:.1f} " + f"log={log.relative_to(ROOT)}", flush=True) + return output + + +def point_um(values: list[int]) -> tuple[float, float, float]: + return tuple(value / 1_000_000.0 for value in values) + + +def make_views(layout: dict) -> list[dict]: + edges = {row["stable_id"]: row for row in layout["cavern_edges"]} + rooms = {row["stable_id"]: row for row in layout["cavern_rooms"]} + shaft = rooms["vertical-shafts/strate-3/shaft-0/primitive-0"] + access = edges["vertical-shafts/strate-3/shaft-access-0"]["polyline_um"] + arrival = edges["vertical-shafts/strate-3/route-edge-0"]["polyline_um"] + basal = rooms["vertical-shafts/strate-3/basal-landing/primitive-0"] + center = point_um(shaft["center_um"]) + half = point_um(shaft["half_extents_um"]) + shaft_top = center[2] + half[2] + shaft_bottom = center[2] - half[2] + route_a, route_b = point_um(arrival[0]), point_um(arrival[1]) + tx, ty = route_b[0] - route_a[0], route_b[1] - route_a[1] + tlen = max(1.0e-9, math.hypot(tx, ty)) + tangent = (tx / tlen, ty / tlen) + mouth_camera = (route_a[0] - tangent[0] * 2.0, + route_a[1] - tangent[1] * 2.0, route_a[2] + 1.45) + mouth_target = (route_a[0] + tangent[0] * 8.0, + route_a[1] + tangent[1] * 8.0, route_a[2] + 1.45) + access_a, access_b = point_um(access[0]), point_um(access[-1]) + access_camera = (access_a[0], access_a[1], access_a[2] + 1.45) + access_target = (access_b[0], access_b[1], access_b[2] + 1.45) + shaft_lip_camera = (access_b[0], access_b[1], access_b[2] + 1.45) + basal_center = point_um(basal["center_um"]) + basal_camera = (basal_center[0], basal_center[1], basal["floor_um"] / 1.0e6 + 1.45) + basal_target = (basal_center[0] + tangent[0] * 8.0, + basal_center[1] + tangent[1] * 8.0, basal_camera[2]) + ledge_z = shaft_top - 12 * (shaft["ledge_spacing_um"] / 1.0e6) + return [ + {"name": "arrival-mouth", "camera": mouth_camera, "target": mouth_target, + "focus": (route_a[0], route_a[1], route_a[2] + 1.4, 12.0), + "note": "player eye at the passage arrival, looking into the connected layer"}, + {"name": "landing-at-shaft-access", "camera": access_camera, + "target": access_target, + "focus": (center[0], center[1], access_a[2], 14.0), + "note": "walkable route landing and short shaft access tunnel"}, + {"name": "looking-down-shaft", "camera": shaft_lip_camera, + "target": (center[0], center[1], shaft_top - 30.0), + "focus": (center[0], center[1], shaft_top - 25.0, 14.0), + "fov": 35.0, + "note": "player eye at the upper access landing, looking down the open shaft"}, + {"name": "looking-up-from-ledge", "camera": (center[0], center[1], ledge_z + 1.45), + "target": (center[0], center[1], shaft_top - 2.0), + "focus": (center[0], center[1], shaft_top - 22.0, 20.0), + "note": "player eye at an internal ledge, looking up; reaching this ledge is a climb tier"}, + {"name": "basal-terminal-landing", "camera": basal_camera, + "target": basal_target, + "focus": (basal_center[0], basal_center[1], basal_center[2], 16.0), + "note": "walkable terminal landing above the basal seal"}, + ] + + +def write_overview(layout: dict, destination: Path) -> None: + shafts = [room for room in layout["cavern_rooms"] if room.get("shape_kind") == 1] + route_edges = sorted((edge for edge in layout["cavern_edges"] + if edge["stable_id"].startswith("vertical-shafts/strate-3/route-edge-")), + key=lambda edge: int(edge["stable_id"].rsplit("-", 1)[1])) + if not shafts or not route_edges: + raise ValueError("overview needs the resolved shaft primitives and walk route") + points = [point_um(row) for edge in route_edges for row in edge["polyline_um"]] + shaft_x_bounds = [(point_um(room["center_um"])[0] + -room["half_extents_um"][0]/1e6, + point_um(room["center_um"])[0]+room["half_extents_um"][0]/1e6) + for room in shafts] + x_min = min(min(bounds[0] for bounds in shaft_x_bounds), + min(row[0] for row in points)) + x_max = max(x + room["half_extents_um"][0] / 1e6 + for room in shafts for x in [point_um(room["center_um"])[0]]) + x_max = max(x_max, max(row[0] for row in points)) + z_min = min(point_um(room["center_um"])[2] - room["half_extents_um"][2] / 1e6 + for room in shafts) + z_max = max(point_um(room["center_um"])[2] + room["half_extents_um"][2] / 1e6 + for room in shafts) + z_min = min(z_min, min(row[2] for row in points)) + z_max = max(z_max, max(row[2] for row in points)) + width, height, margin = 1800.0, 760.0, 72.0 + sx = (width - 2 * margin) / max(1.0, x_max - x_min) + sz = (height - 2 * margin) / max(1.0, z_max - z_min) + scale = min(sx, sz) + x_offset = margin + ((width - 2 * margin) - (x_max - x_min) * scale) * 0.5 + z_offset = margin + ((height - 2 * margin) - (z_max - z_min) * scale) * 0.5 + def xy(x: float, z: float) -> tuple[float, float]: + return x_offset + (x - x_min) * scale, height - (z_offset + (z - z_min) * scale) + route_xy = [xy(p[0], p[2]) for p in points] + route = " ".join(("M" if i == 0 else "L") + f"{x:.2f},{y:.2f}" + for i, (x, y) in enumerate(route_xy)) + cylinders = [] + for room in shafts: + x, _y, z = point_um(room["center_um"]) + radius = room["half_extents_um"][0] / 1e6 + half_z = room["half_extents_um"][2] / 1e6 + left, top = xy(x - radius, z + half_z) + right, bottom = xy(x + radius, z - half_z) + cylinders.append(f'') + rx0, ry0 = xy(x_min, 0.0) + rx1, ry1 = xy(x_max, 0.0) + svg = f''' + +Vertical Shafts — whole-layer side elevation +Resolved plan • seed 0 • strate 3 • vertical axis and route shown in metres • Y projected out + +{''.join(cylinders)} + +walk-only graded spine and landings + +open vertical shaft primitives (53) +Plan projection, not a density mesh +''' + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(svg, encoding="utf-8") + + +def main() -> int: + if not DATA.is_file() or not LAYOUT.is_file(): + raise SystemExit("missing owner fixture or passing seed-0 resolved layout") + layout = json.loads(LAYOUT.read_text(encoding="utf-8")) + views = make_views(layout) + OUT.mkdir(parents=True, exist_ok=True) + ARTIFACTS.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache") + quality = ROOT / "Standalone/Tools/verify/capture_quality.py" + manifest = [] + quality_failures = [] + for version, config in VERSIONS.items(): + phase = "before" if version.startswith("before") else "after" + for view in views: + name = view["name"] + camera, target = view["camera"], view["target"] + yaw, pitch = angles(camera, target) + mesh_dir = OUT / "meshes" / phase + mesh_dir.mkdir(parents=True, exist_ok=True) + mesh = mesh_dir / f"{name}.vfmesh" + capture_layout = mesh_dir / f"{name}-layout.json" + export_log = OUT / f"{phase}-{name}-export.log" + export = [config["headless"], DATA, "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=12", + "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", "--seed=0", "--recipe=surface-world", + "--render-distance-chunks=0", f"--camera={fmt((*camera,yaw,pitch))}", + f"--resolved-layout-output={capture_layout}", f"--mesh-export={mesh}"] + stamped_run(f"{phase}-{name}-export", list(map(str, export)), export_log, + config["stamp"], env) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{phase}-{name}: empty mesh export") + png = OUT / f"{phase}-{name}.png" + jpg = ARTIFACTS / f"{phase}-{name}.jpg" + fov = float(view.get("fov", 78.0)) + render = [config["renderer"], mesh, png, "--size=1280x720", + f"--fov-deg={fov:.1f}", f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", + f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable", + "--headlamp=on", "--height-tint=on", "--height-center-m=-950", + "--height-range-m=60", "--tile-edges=off", + f"--focus-region={fmt(view['focus'])}"] + stamped_run(f"{phase}-{name}-render", list(map(str, render)), + OUT / f"{phase}-{name}-render.log", config["stamp"], env) + jpeg = ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y", + "-i", png, "-frames:v", "1", "-q:v", "4", jpg] + subprocess.run([*LIMIT, "timeout", "120s", *map(str, jpeg)], cwd=ROOT, + env=env, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, + timeout=125) + check = subprocess.run([*LIMIT, "timeout", "120s", sys.executable, + quality, jpg], cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=125) + quality_ok = check.returncode == 0 and "status=PASS" in check.stdout + quality_result = (check.stdout.strip().splitlines()[-1] + if check.stdout.strip() else f"quality command exit={check.returncode}") + if not quality_ok: + quality_failures.append(f"{phase}-{name}") + manifest.append({"phase": phase, "view": name, "file": str(jpg.relative_to(ROOT)), + "bytes": jpg.stat().st_size, "camera_m": camera, "target_m": target, + "yaw_deg": yaw, "pitch_deg": pitch, "fov_deg": fov, + "note": view["note"], + "quality_status": "PASS" if quality_ok else "FAIL", + "quality": quality_result}) + print(f"[L139Capture] phase={phase} view={name} file={jpg.relative_to(ROOT)} " + f"bytes={jpg.stat().st_size} quality={'PASS' if quality_ok else 'FAIL'}", + flush=True) + overview = ARTIFACTS / "after-whole-layer-side-elevation.svg" + write_overview(layout, overview) + manifest.append({"phase": "after", "view": "whole-layer-side-elevation", + "file": str(overview.relative_to(ROOT)), "shafts": sum( + room.get("shape_kind") == 1 for room in layout["cavern_rooms"]), + "walk_route_edges": len([edge for edge in layout["cavern_edges"] + if edge["stable_id"].startswith("vertical-shafts/strate-3/route-edge-")]), + "note": "resolved-plan side projection; not a density-mesh capture"}) + (ARTIFACTS / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", + encoding="utf-8") + status = "PASS" if not quality_failures else "FAIL" + print(f"[L139CaptureSummary] spot_views={len(views)} comparisons={len(views)*2} " + f"whole_layer_projection=1 quality_failures={len(quality_failures)} " + f"status={status}", flush=True) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L139CaptureFailure] {error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/RunL139Determinism.py b/Standalone/Research/WorldAssemblerM3/Linux139/RunL139Determinism.py new file mode 100644 index 0000000..b7b88e9 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux139/RunL139Determinism.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Compare composed owner hashes and resolved graph identity at W1/W8/W12.""" +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/l139/determinism" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1" +BINARIES = { + "clang": ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-clang", + "gcc": ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-gcc", +} +CAMERA = "-20.062500,-5.068359,-900.353516,0,0" +HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|MaterialHash|LODHash|ExactQueryHash)\].*$", + re.M) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def main() -> int: + if any(not path.is_file() for path in (DATA, *BINARIES.values())): + raise SystemExit("missing L139 determinism binary or composed owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + rows = [] + reference_hashes: tuple[str, ...] | None = None + reference_layout_identity: str | None = None + for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + name = f"determinism-{compiler}-w{workers}" + log, layout = OUT / f"{name}.log", OUT / f"{name}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=0", + f"--camera={CAMERA}", "--stream-hash", "--force-generate", "--seed=0", + "--recipe=surface-world", "--render-distance-chunks=0", + "--density-dispatch-report", f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + hashes = tuple(HASH_RE.findall(content)) + current_layout = json.loads(layout.read_text(encoding="utf-8")) + stable_identity = current_layout.get("stable_identity") + dispatch_ok = bool(re.search( + r"^\[DensityDispatch\] strate=3 generator=VerticalShafts " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", content, re.M)) + passed = (proc.returncode == 0 and stamp is not None and stamp.group(1) == STAMP + and len(hashes) >= 4 and dispatch_ok and bool(stable_identity)) + if reference_hashes is None and passed: + reference_hashes = hashes + reference_layout_identity = stable_identity + else: + passed = (passed and hashes == reference_hashes + and stable_identity == reference_layout_identity) + row = {"compiler": compiler, "workers": workers, "exit": proc.returncode, + "stamp": stamp.group(1) if stamp else None, "hash_lines": len(hashes), + "hashes_match_first": hashes == reference_hashes, + "layout_identity": stable_identity, + "layout_matches_first": stable_identity == reference_layout_identity, + "shaft_dispatch": dispatch_ok, "passed": bool(passed), + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + rows.append(row) + print("[L139DeterminismRun] " + json.dumps(row, sort_keys=True), flush=True) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n") + if not passed: + raise SystemExit(f"composed owner determinism mismatch; see {log}") + (OUT / "summary.json").write_text(json.dumps({"status": "PASS", "stamp": STAMP, + "layout_identity": reference_layout_identity, "runs": rows}, indent=2) + "\n") + print(f"[L139DeterminismSummary] runs={len(rows)} source_stamp={STAMP} status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/RunL139FinalTiming.py b/Standalone/Research/WorldAssemblerM3/Linux139/RunL139FinalTiming.py new file mode 100644 index 0000000..e727f37 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux139/RunL139FinalTiming.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +"""Run interleaved L80 timings for L139, fa4417a, and assembler/world.""" +from __future__ import annotations + +import json +import os +import re +import statistics +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l139/timing" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": ( + ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-aa-a", + "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621"), + "fa4417a": ( + ROOT / "Saved/Linux/l138/bin/VoxelForgeCoreHeadless-l138-clang", + "9aade4e6cb33e86c36b89440644d9ef5d440288fb6ff0af03af128291c95f93f"), + "l139": ( + ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-clang", + "b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1"), +} +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) +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + binary, expected_stamp = BINARIES[version] + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + label = f"s{strate}-w{workers}-{version}-{phase}{replicate}" + log = OUT / f"{label}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline, near, stamp = PIPELINE.search(content), NEAR.search(content), STAMP.search(content) + if (proc.returncode or pipeline is None or near is None or stamp is None + or stamp.group(1) != expected_stamp): + raise SystemExit(f"L80 timing failed or stamp mismatch: {log}; exit={proc.returncode}") + fields = dict(FIELDS.findall(pipeline.group(1))) + row = { + "strate": strate, "workers": workers, "version": version, + "phase": phase, "replicate": replicate, "source_stamp": stamp.group(1), + "lod02_ready_s": float(near.group(3)), + "whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")), + "whole_tiles": int(fields["whole_tiles"].split("/")[0]), + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), + } + print("[L139FinalTimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def quiet_machine() -> tuple[float, float]: + load_1m = os.getloadavg()[0] + + def totals() -> tuple[int, int]: + fields = Path("/proc/stat").read_text().splitlines()[0].split() + values = [int(value) for value in fields[1:]] + return sum(values), values[3] + values[4] + + before = totals() + time.sleep(2.0) + after = totals() + idle = 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0]) + return load_1m, idle + + +def main() -> int: + if any(not path.is_file() for path in [DATA, *(entry[0] for entry in BINARIES.values())]): + raise SystemExit("missing L139 timing input") + OUT.mkdir(parents=True, exist_ok=True) + load_1m, idle = quiet_machine() + (OUT / "machine-load-before.txt").write_text( + f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n") + print(f"[L139FinalTimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True) + if load_1m >= 1.5 or idle < 97.0: + raise SystemExit("timing refused: machine is above the L80 quiet-load gate") + + rows = [] + for strate in range(4): + for workers in (8, 12): + for version in BINARIES: + run(strate, workers, version, "warmup", 0) + for replicate in range(1, 6): + order = tuple(BINARIES) + order = order[replicate % 3:] + order[:replicate % 3] + if replicate % 2 == 0: + order = tuple(reversed(order)) + for version in order: + rows.append(run(strate, workers, version, "measured", replicate)) + (OUT / "runs-partial.json").write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8") + + measured = [row for row in rows if row["phase"] == "measured"] + summary = [] + for strate in range(4): + for workers in (8, 12): + group = [row for row in measured + if row["strate"] == strate and row["workers"] == workers] + medians = { + version: { + key: statistics.median(row[key] for row in group + if row["version"] == version) + for key in ("lod02_ready_s", "whole_view_ready_s") + } + for version in BINARIES + } + entry = {"strate": strate, "workers": workers, + "replicates_per_version": 5, "medians": medians} + for key in ("lod02_ready_s", "whole_view_ready_s"): + for baseline in ("fa4417a", "assembler-world"): + entry[f"l139_vs_{baseline}_{key}_delta_pct"] = round( + 100.0 * (medians["l139"][key] / medians[baseline][key] - 1.0), 2) + entry[f"l139_vs_fa4417a_{key}_paired_deltas_pct"] = [ + round(100.0 * ( + next(row[key] for row in group + if row["version"] == "l139" and row["replicate"] == replicate) + / next(row[key] for row in group + if row["version"] == "fa4417a" and row["replicate"] == replicate) + - 1.0), 3) for replicate in range(1, 6)] + entry[f"l139_vs_assembler-world_{key}_paired_deltas_pct"] = [ + round(100.0 * ( + next(row[key] for row in group + if row["version"] == "l139" and row["replicate"] == replicate) + / next(row[key] for row in group + if row["version"] == "assembler-world" + and row["replicate"] == replicate) - 1.0), 3) + for replicate in range(1, 6)] + summary.append(entry) + (OUT / "summary.json").write_text(json.dumps({ + "a_a_noise_floor_pct": 1.0, + "fa4417a_commit": "fa4417a", + "timing_protocol": ( + "one warmup per binary, five interleaved runs, W8/W12, all four owner layers"), + "runs": rows, + "summary": summary, + }, indent=2) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/RunL139SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux139/RunL139SeedSweep.py new file mode 100644 index 0000000..25a7bbf --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux139/RunL139SeedSweep.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Audit the composed owner world, including the basal Vertical Shafts route.""" +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/l139/seeds" +BIN = ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +REQUIRED = { + "passages": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "route": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=3 .*unsupported=0 grade_failures=0 .*status=PASS$", + re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "shaft_dispatch": re.compile( + r"^\[DensityDispatch\] strate=3 generator=VerticalShafts " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), + "shaft_blocked_route": re.compile( + r"^\[VerticalShaftsBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} + + +def main() -> int: + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L139 Clang binary or composed owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + results = [] + partial = OUT / "runs-partial.json" + if partial.is_file(): + for prior in json.loads(partial.read_text(encoding="utf-8")): + seed = len(results) + if prior.get("seed") != seed: + break + log = ROOT / prior.get("log", "") + layout = ROOT / prior.get("layout", "") + if not log.is_file() or not layout.is_file(): + break + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) + for name, pattern in REQUIRED.items()} + checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP + checks["blocked_passage_controls"] = len(re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3 + checks["passage_count"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*walkability=PASS$", content, re.M)) == 3 + passed = prior.get("exit") == 0 and all(checks.values()) + if checks != prior.get("checks") or passed != prior.get("passed"): + break + results.append(prior) + print(f"[L139SeedSweepResume] reused={len(results)} source_stamp={STAMP}", + flush=True) + for seed in range(len(results), 16): + log = OUT / f"seed{seed}.log" + layout = OUT / f"seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=0", "--force-generate", f"--seed={seed}", + "--recipe=surface-world", "--density-dispatch-report", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP + checks["blocked_passage_controls"] = len(re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3 + checks["passage_count"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*walkability=PASS$", content, re.M)) == 3 + passed = proc.returncode == 0 and all(checks.values()) + row = {"seed": seed, "exit": proc.returncode, "passed": passed, + "checks": checks, "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L139SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + partial.write_text(json.dumps(results, indent=2) + "\n") + passed_count = sum(row["passed"] for row in results) + failed_count = len(results) - passed_count + summary = {"source_stamp": STAMP, "seeds": list(range(len(results))), + "passed": passed_count, "failed": failed_count, "results": results} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + print(f"[L139SeedSweepSummary] passed={passed_count}/16 failed={failed_count} " + f"status={'PASS' if len(results)==16 and failed_count==0 else 'FAIL'}") + return 0 if len(results) == 16 and failed_count == 0 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/RunL139WholeLayerCapture.py b/Standalone/Research/WorldAssemblerM3/Linux139/RunL139WholeLayerCapture.py new file mode 100644 index 0000000..a4a8eda --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux139/RunL139WholeLayerCapture.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Render a coarse whole-strate cutaway from the L139 density mesh.""" +from __future__ import annotations + +import json +import os +from pathlib import Path +import re +import subprocess +import sys +import time + + +ROOT = Path(__file__).resolve().parents[4] +DATA = ROOT / "Standalone/data/owner_world.json" +BIN = ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-clang" +RENDERER = ROOT / "Saved/Linux/l139/bin/VoxelForgeOffscreenCapture-l139-clang" +OUT = ROOT / "Saved/Linux/l139/captures" +ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux139/captures" +STAMP = "b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +SLICE_Y = -5.0 +CAMERA = (-450.0, SLICE_Y - 100.0, -950.0) +YAW, PITCH, FOV = 0.0, 0.0, 70.0 + + +def stamped_run(label: str, command: list[str], log: Path, env: dict[str, str]) -> str: + probe = subprocess.run([*LIMIT, "timeout", "15s", str(command[0]), "--print-source-stamp"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=20) + match = STAMP_RE.search(probe.stdout or "") + if probe.returncode != 0 or not match or match.group(1) != STAMP: + raise RuntimeError(f"{label}: source stamp mismatch") + log.parent.mkdir(parents=True, exist_ok=True) + started = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, + timeout=1745, check=False) + content = log.read_text(encoding="utf-8", errors="replace") + emitted = STAMP_RE.search(content) + if result.returncode != 0 or not emitted or emitted.group(1) != STAMP: + raise RuntimeError(f"{label}: process failed or source changed; see {log}") + print(f"[L139WholeLayerCapture] step={label} elapsed_s={time.monotonic()-started:.1f} " + f"log={log.relative_to(ROOT)}", flush=True) + return content + + +def main() -> int: + if not DATA.is_file() or not BIN.is_file() or not RENDERER.is_file(): + raise SystemExit("missing owner fixture or final L139 capture binaries") + OUT.mkdir(parents=True, exist_ok=True) + ARTIFACTS.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache") + stem = "whole-layer-density-cutaway" + mesh = OUT / f"{stem}.vfmesh" + layout = OUT / f"{stem}-layout.json" + camera_text = ",".join(f"{value:.6f}" for value in (*CAMERA, YAW, PITCH)) + export = [BIN, DATA, "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--workers=12", "--clip-radius=3", "--max-clip-level=8", + "--max-level=8", "--force-generate", "--seed=0", "--recipe=surface-world", + "--render-distance-chunks=0", "--capture-skip-walkability", "--density-dispatch-report", + f"--camera={camera_text}", f"--resolved-layout-output={layout}", f"--mesh-export={mesh}"] + content = stamped_run("export", export, OUT / f"{stem}-export.log", env) + dispatch = re.search( + r"^\[DensityDispatch\] strate=3 generator=VerticalShafts " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", content, re.M) + if not dispatch or not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError("coarse density export is empty or lacks native shaft dispatch") + png = OUT / f"{stem}.png" + jpg = ARTIFACTS / "after-whole-layer-density-cutaway.jpg" + render = [RENDERER, mesh, png, "--size=2200x320", f"--fov-deg={FOV:.1f}", + "--camera-m=" + ",".join(f"{value:.6f}" for value in CAMERA), + f"--yaw-deg={YAW:.6f}", f"--pitch-deg={PITCH:.6f}", + f"--clip-plane=0,-1,0,{SLICE_Y:.6f}", "--tile-skirts=off", "--shade=readable", + "--headlamp=on", "--height-tint=off", "--height-center-m=-950", + "--height-range-m=80", "--tile-edges=off"] + stamped_run("render", render, OUT / f"{stem}-render.log", env) + subprocess.run([*LIMIT, "timeout", "120s", "ffmpeg", "-nostdin", "-hide_banner", + "-loglevel", "error", "-y", "-i", png, "-frames:v", "1", "-q:v", "3", jpg], + cwd=ROOT, env=env, check=True, stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT, timeout=125) + quality = subprocess.run([*LIMIT, "timeout", "120s", sys.executable, + ROOT / "Standalone/Tools/verify/capture_quality.py", jpg], cwd=ROOT, env=env, + text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + check=False, timeout=125) + quality_line = quality.stdout.strip().splitlines()[-1] if quality.stdout.strip() else "" + ok = quality.returncode == 0 and "status=PASS" in quality_line + manifest_path = ARTIFACTS / "manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if manifest_path.is_file() else [] + manifest = [row for row in manifest if row.get("view") != "whole-layer-density-cutaway"] + manifest.append({"phase": "after", "view": "whole-layer-density-cutaway", + "file": str(jpg.relative_to(ROOT)), "bytes": jpg.stat().st_size, + "camera_m": CAMERA, "target_m": (-450.0, SLICE_Y, -950.0), + "yaw_deg": YAW, "pitch_deg": PITCH, "fov_deg": FOV, + "clip_plane": [0.0, -1.0, 0.0, SLICE_Y], + "note": "outside side cutaway through the coarse LOD density mesh across the whole Vertical Shafts strate at y=-5 m", + "quality_status": "PASS" if ok else "FAIL", "quality": quality_line}) + manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + print(f"[L139WholeLayerCaptureSummary] file={jpg.relative_to(ROOT)} " + f"quality={'PASS' if ok else 'FAIL'} {quality_line}", flush=True) + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-arrival-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-arrival-mouth.jpg new file mode 100644 index 0000000..54fdd4e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-arrival-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-basal-terminal-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-basal-terminal-landing.jpg new file mode 100644 index 0000000..8ffbbab Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-basal-terminal-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-landing-at-shaft-access.jpg b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-landing-at-shaft-access.jpg new file mode 100644 index 0000000..3fbc671 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-landing-at-shaft-access.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-down-shaft.jpg b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-down-shaft.jpg new file mode 100644 index 0000000..2726ce4 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-down-shaft.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-up-from-ledge.jpg b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-up-from-ledge.jpg new file mode 100644 index 0000000..c54a4de Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-up-from-ledge.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-whole-layer-density-cutaway.jpg b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-whole-layer-density-cutaway.jpg new file mode 100644 index 0000000..3b6bbc0 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-whole-layer-density-cutaway.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-whole-layer-side-elevation.svg b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-whole-layer-side-elevation.svg new file mode 100644 index 0000000..59c276e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-whole-layer-side-elevation.svg @@ -0,0 +1,12 @@ + + +Vertical Shafts — whole-layer side elevation +Resolved plan • seed 0 • strate 3 • vertical axis and route shown in metres • Y projected out + + + +walk-only graded spine and landings + +open vertical shaft primitives (53) +Plan projection, not a density mesh + \ No newline at end of file diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-arrival-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-arrival-mouth.jpg new file mode 100644 index 0000000..37481e7 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-arrival-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-basal-terminal-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-basal-terminal-landing.jpg new file mode 100644 index 0000000..d8cf062 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-basal-terminal-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-landing-at-shaft-access.jpg b/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-landing-at-shaft-access.jpg new file mode 100644 index 0000000..35ce025 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-landing-at-shaft-access.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-down-shaft.jpg b/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-down-shaft.jpg new file mode 100644 index 0000000..148e76e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-down-shaft.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-up-from-ledge.jpg b/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-up-from-ledge.jpg new file mode 100644 index 0000000..8f7ad60 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-up-from-ledge.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux139/captures/manifest.json b/Standalone/Research/WorldAssemblerM3/Linux139/captures/manifest.json new file mode 100644 index 0000000..2a9396b --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux139/captures/manifest.json @@ -0,0 +1,258 @@ +[ + { + "phase": "before", + "view": "arrival-mouth", + "file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/before-arrival-mouth.jpg", + "bytes": 21788, + "camera_m": [ + -20.0625, + -5.068359, + -900.237499 + ], + "target_m": [ + -30.0625, + -5.068359, + -900.237499 + ], + "yaw_deg": -90.0, + "pitch_deg": 0.0, + "fov_deg": 78.0, + "note": "player eye at the passage arrival, looking into the connected layer", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-arrival-mouth.jpg pixels=921600 mean_luma=110.102 stddev_luma=3.380 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "before", + "view": "landing-at-shaft-access", + "file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/before-landing-at-shaft-access.jpg", + "bytes": 15114, + "camera_m": [ + -31.354577, + -5.068359, + -901.1982009999999 + ], + "target_m": [ + -31.354577, + -9.889916, + -901.1982009999999 + ], + "yaw_deg": 180.0, + "pitch_deg": 0.0, + "fov_deg": 78.0, + "note": "walkable route landing and short shaft access tunnel", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-landing-at-shaft-access.jpg pixels=921600 mean_luma=110.563 stddev_luma=3.056 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "before", + "view": "looking-down-shaft", + "file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-down-shaft.jpg", + "bytes": 11005, + "camera_m": [ + -31.354577, + -9.889916, + -901.1982009999999 + ], + "target_m": [ + -31.354577, + -10.852462, + -928.6482010000001 + ], + "yaw_deg": 180.0, + "pitch_deg": -87.99172181188044, + "fov_deg": 35.0, + "note": "player eye at the upper access landing, looking down the open shaft", + "quality_status": "FAIL", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-down-shaft.jpg pixels=921600 mean_luma=112.000 stddev_luma=0.000 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=FAIL" + }, + { + "phase": "before", + "view": "looking-up-from-ledge", + "file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-up-from-ledge.jpg", + "bytes": 15507, + "camera_m": [ + -31.354577, + -10.852462, + -945.198201 + ], + "target_m": [ + -31.354577, + -10.852462, + -900.6482010000001 + ], + "yaw_deg": 0.0, + "pitch_deg": 90.0, + "fov_deg": 78.0, + "note": "player eye at an internal ledge, looking up; reaching this ledge is a climb tier", + "quality_status": "FAIL", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-looking-up-from-ledge.jpg pixels=921600 mean_luma=111.693 stddev_luma=1.515 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=FAIL" + }, + { + "phase": "before", + "view": "basal-terminal-landing", + "file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/before-basal-terminal-landing.jpg", + "bytes": 13933, + "camera_m": [ + -884.858259, + -5.068359, + -995.05 + ], + "target_m": [ + -892.858259, + -5.068359, + -995.05 + ], + "yaw_deg": -90.0, + "pitch_deg": 0.0, + "fov_deg": 78.0, + "note": "walkable terminal landing above the basal seal", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/before-basal-terminal-landing.jpg pixels=921600 mean_luma=108.996 stddev_luma=6.932 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "arrival-mouth", + "file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-arrival-mouth.jpg", + "bytes": 19044, + "camera_m": [ + -20.0625, + -5.068359, + -900.237499 + ], + "target_m": [ + -30.0625, + -5.068359, + -900.237499 + ], + "yaw_deg": -90.0, + "pitch_deg": 0.0, + "fov_deg": 78.0, + "note": "player eye at the passage arrival, looking into the connected layer", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-arrival-mouth.jpg pixels=921600 mean_luma=125.004 stddev_luma=16.734 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "landing-at-shaft-access", + "file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-landing-at-shaft-access.jpg", + "bytes": 15595, + "camera_m": [ + -31.354577, + -5.068359, + -901.1982009999999 + ], + "target_m": [ + -31.354577, + -9.889916, + -901.1982009999999 + ], + "yaw_deg": 180.0, + "pitch_deg": 0.0, + "fov_deg": 78.0, + "note": "walkable route landing and short shaft access tunnel", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-landing-at-shaft-access.jpg pixels=921600 mean_luma=117.622 stddev_luma=14.149 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "looking-down-shaft", + "file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-down-shaft.jpg", + "bytes": 24396, + "camera_m": [ + -31.354577, + -9.889916, + -901.1982009999999 + ], + "target_m": [ + -31.354577, + -10.852462, + -928.6482010000001 + ], + "yaw_deg": 180.0, + "pitch_deg": -87.99172181188044, + "fov_deg": 35.0, + "note": "player eye at the upper access landing, looking down the open shaft", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-down-shaft.jpg pixels=921600 mean_luma=112.365 stddev_luma=3.113 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "looking-up-from-ledge", + "file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-up-from-ledge.jpg", + "bytes": 23172, + "camera_m": [ + -31.354577, + -10.852462, + -945.198201 + ], + "target_m": [ + -31.354577, + -10.852462, + -900.6482010000001 + ], + "yaw_deg": 0.0, + "pitch_deg": 90.0, + "fov_deg": 78.0, + "note": "player eye at an internal ledge, looking up; reaching this ledge is a climb tier", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-looking-up-from-ledge.jpg pixels=921600 mean_luma=111.549 stddev_luma=2.538 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "basal-terminal-landing", + "file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-basal-terminal-landing.jpg", + "bytes": 12527, + "camera_m": [ + -884.858259, + -5.068359, + -995.05 + ], + "target_m": [ + -892.858259, + -5.068359, + -995.05 + ], + "yaw_deg": -90.0, + "pitch_deg": 0.0, + "fov_deg": 78.0, + "note": "walkable terminal landing above the basal seal", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-basal-terminal-landing.jpg pixels=921600 mean_luma=125.238 stddev_luma=3.555 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "whole-layer-side-elevation", + "file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-whole-layer-side-elevation.svg", + "shafts": 53, + "walk_route_edges": 91, + "note": "resolved-plan side projection; not a density-mesh capture" + }, + { + "phase": "after", + "view": "whole-layer-density-cutaway", + "file": "Standalone/Research/WorldAssemblerM3/Linux139/captures/after-whole-layer-density-cutaway.jpg", + "bytes": 20280, + "camera_m": [ + -450.0, + -105.0, + -950.0 + ], + "target_m": [ + -450.0, + -5.0, + -950.0 + ], + "yaw_deg": 0.0, + "pitch_deg": 0.0, + "fov_deg": 70.0, + "clip_plane": [ + 0.0, + -1.0, + 0.0, + -5.0 + ], + "note": "outside side cutaway through the coarse LOD density mesh across the whole Vertical Shafts strate at the denser shaft row", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux139/captures/after-whole-layer-density-cutaway.jpg pixels=704000 mean_luma=113.286 stddev_luma=3.364 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + } +] diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux140/REPORT.md new file mode 100644 index 0000000..449e31e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux140/REPORT.md @@ -0,0 +1,34 @@ +# L140 — Floating Islands planned around passages + +**Not accepted: 0/16 composed seeds passed.** The new Core recipe reaches native S1 dispatch and meets its dedicated route, passage-clearance, and mouth-support checks on seeds 0 and 10. Seed 10 still has one blocked capsule in the composed owner route; both seeds fail the unchanged passage-ripple gate. Fourteen seeds cannot produce a corridor-aware plan. + +## Change + +Core evaluator version is 239 and Floating Islands geometry version is 10. The owner adapter resolves the fixed passage layout first, then plans nine native islands and a walk route around the 0–1 arrival and 1–2 departure tubes. The plan records route and island anchors in the resolved layout. The clearance check reserves the 1.053 m passage influence reach plus the 0.880 m capsule half-height and checks five support probes at each mouth. Shape composition preserves native landing support and passage shell distance for this recipe. Direct legacy recipe fixtures retain their earlier attach-before-layout order. + +S1 binds to `FloatingIslands-native` in the explicit Core owner mode. `bUseVoxelForgeCore` remains opt-in; the fast-tier default owner pose still reports the legacy analytic S1 path. + +## Composed sweep + +The final-source Clang run covered seeds 0–15. Results and per-seed logs are in [`summary.json`](../../../../Saved/Linux/l140/seeds/summary.json) and `Saved/Linux/l140/seeds/` (source stamp `6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da`). + +- **Seed 0:** the planner tried 11 detours; its 94.5 m route has 1.47 m conservative tube clearance. Native route audit: 128 points, 0 blocked capsule samples, 0 unsupported points, 0 grade failures. Passage check: 5,325 tube samples, 0 island conflicts, 676,275 route checks, 0 route conflicts, arrival and departure support 5/5, 2.824 m minimum excess clearance. The composed S1 route and seams pass. Ripple is 1/3: wall ripple is 6.85 cm on 0–1 and 7.10 cm on 1–2 against the unchanged 3.50 cm limit. +- **Seed 10:** the native route and passage checks pass, with 5/5 mouth support and 0 isolated route conflicts. The composed S1 capsule audit finds one blocked sample at `(-6.246, -28.615, -737.982) m`, inside the resolved 0–1 passage; composed density is `-0.002062` while native island density is `+1.730469`. Ripple is again 1/3 (6.22 cm on 0–1 and 7.19 cm on 1–2). Seams and negative controls pass. +- **Seeds 1, 2, 7, 8, 9, 12, 13, 15:** no candidate clears the tubes; best conservative clearances range from −5.994 m to −0.406 m. **Seeds 3, 5, 6, 14:** candidates clear the tubes but the supported island profile cannot reach both fixed mouth heights. **Seeds 4 and 11:** candidate islands escape the sealed vertical layout. + +The dedicated gate is therefore 0/16. No geometry variant was attempted after these results. The repeated 0–1/1–2 ripple failure is also consistent with L67's closed authored-radius experiment: it measured 7.27 cm on 1–2 against the same 3.50 cm limit ([L67 report](../../Linux67/REPORT.md)). + +## Other checks + +- **Fast tier:** PASS, 287.8 s; negative controls and existing worker-hash checks passed. It continues to verify the default legacy S1 dispatch. +- **Determinism:** PASS, six runs (Clang/GCC × W1/W8/W12). All produced the same 512-tile density, mesh, payload, topology, material, and exact-query hashes, with layout identity `resolved-layout-v3-fnv64:08da20d55ff6820f`. Native recipe hash: `0b6a23bc73a3b867`. See the [baseline](baselines/native-owner-fi-seed0.json) and [`summary.json`](../../../../Saved/Linux/l140/determinism/summary.json). +- **Captures:** ten before/after JPEGs passed the existing image-quality gate. The [manifest](captures/manifest.json) records eye-height arrival, island edge, route between islands, departure, and wide silhouette views for both versions. See [before](captures/before-arrival-mouth.jpg), [before edge](captures/before-island-edge.jpg), [before route](captures/before-walk-between-islands.jpg), [before departure](captures/before-departure-mouth.jpg), [before wide](captures/before-wide-silhouette.jpg); [after](captures/after-arrival-mouth.jpg), [after edge](captures/after-island-edge.jpg), [after route](captures/after-walk-between-islands.jpg), [after departure](captures/after-departure-mouth.jpg), [after wide](captures/after-wide-silhouette.jpg). +- **Linux VFHostEditor:** PASS in 73.58 s. Build log: [`ubt.log`](../../../../Saved/Linux/l140/ubt.log). +- **L80 timing:** not completed. The machine gate passed (load 0.97, 2-second CPU idle 98.5%), but the candidate warm-up could not construct native S1 around the checked-in Unreal-exported passage layout: 0 detours, best clearance −3.509 m. No measured L80 pairs were recorded; the ±1% A/A floor remains the L137 reference. A separate 30-second setup probe with a regenerated seed-0 owner plan timed out during passage-route planning, so I did not run a mismatched or incomplete timing matrix. +- `git diff --check`: PASS. + +## Remaining issues + +The fixed exported owner layout and many generated seeds leave no route-clear island arrangement. On seed 10, the native-only clearance check also misses a collision that the final composed owner route catches. Ripple remains above the existing limit on both upper passages. Native S1 is still opt-in and is not promoted by this round. + +Changed Unreal source: [`VoxelForgeCoreAdapter.cpp`](../../../../Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp). Windows compilation remains for the owner. diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/RunL140Captures.py b/Standalone/Research/WorldAssemblerM3/Linux140/RunL140Captures.py new file mode 100644 index 0000000..1e300ba --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux140/RunL140Captures.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +"""Capture comparable player-height and wide views of owner S1 before and after L140.""" +from __future__ import annotations + +import json +import argparse +import math +import os +import re +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +DATA = ROOT / "Standalone/data/owner_world.json" +LAYOUT = ROOT / "Saved/Linux/l140/seeds/seed0-composed-layout.json" +OUT = ROOT / "Saved/Linux/l140/captures" +ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux140/captures" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) +VERSIONS = { + "before-head": { + "headless": ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-clang", + "renderer": ROOT / "Saved/Linux/l139/bin/VoxelForgeOffscreenCapture-l139-clang", + "stamp": "b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1", + "native": False, + }, + "after-l140": { + "headless": ROOT / "Saved/Linux/l140/bin/VoxelForgeCoreHeadless-l140-clang", + "renderer": ROOT / "Saved/Linux/l140/bin/VoxelForgeOffscreenCapture-l140-clang", + "stamp": "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da", + "native": True, + }, +} + + +def fmt(values: tuple[float, ...]) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def angles(camera: tuple[float, float, float], target: tuple[float, float, float]) -> tuple[float, float]: + dx, dy, dz = (target[i] - camera[i] for i in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees(math.atan2(dz, math.hypot(dx, dy))) + + +def stamped_run(name: str, command: list[str], log: Path, expected: str, + env: dict[str, str]) -> str: + binary = Path(command[0]) + probe = subprocess.run([*LIMIT, "timeout", "15s", str(binary), "--print-source-stamp"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=20) + match = STAMP_RE.search(probe.stdout or "") + if probe.returncode != 0 or not match or match.group(1) != expected: + raise RuntimeError(f"{name}: executable stamp mismatch for {binary}") + log.parent.mkdir(parents=True, exist_ok=True) + started = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, + timeout=1745, check=False) + output = log.read_text(encoding="utf-8", errors="replace") + emitted = STAMP_RE.search(output) + if result.returncode != 0 or not emitted or emitted.group(1) != expected: + raise RuntimeError(f"{name}: process failed or source changed; see {log}") + print(f"[L140CaptureRun] step={name} elapsed_s={time.monotonic()-started:.1f} " + f"log={log.relative_to(ROOT)}", flush=True) + return output + + +def qpoint(values: list[int], scale_m: float) -> tuple[float, float, float]: + return tuple(value * scale_m for value in values[:3]) + + +def make_views(layout: dict) -> list[dict[str, object]]: + scale_m = layout["core_voxel_size_um"] / 1_000_000.0 / 256.0 + strate = next(row for row in layout["strates"] if row["strate_index"] == 1) + anchors = strate["anchors"] + route = [qpoint(row["values"], scale_m) for row in anchors if row["kind"] == "route_point"] + islands = [row for row in anchors if row["kind"] == "island_anchor"] + if len(route) < 8 or len(islands) < 5: + raise ValueError("seed-0 resolved layout lacks a complete S1 route or island chain") + + def tangent(index: int) -> tuple[float, float]: + a, b = route[max(0, index - 1)], route[min(len(route) - 1, index + 1)] + dx, dy = b[0] - a[0], b[1] - a[1] + length = max(1e-9, math.hypot(dx, dy)) + return dx / length, dy / length + + def eye(point: tuple[float, float, float]) -> tuple[float, float, float]: + return point[0], point[1], point[2] + 1.45 + + i = len(route) // 3 + tx, ty = tangent(i) + arrival_t = tangent(0) + departure_t = tangent(len(route) - 1) + arrival, departure = route[0], route[-1] + arrival_camera = eye((arrival[0] - 2.5 * arrival_t[0], arrival[1] - 2.5 * arrival_t[1], arrival[2])) + arrival_target = eye((arrival[0] + 8.0 * arrival_t[0], arrival[1] + 8.0 * arrival_t[1], arrival[2])) + departure_camera = eye((departure[0] - 2.5 * departure_t[0], departure[1] - 2.5 * departure_t[1], departure[2])) + departure_target = eye((departure[0] + 8.0 * departure_t[0], departure[1] + 8.0 * departure_t[1], departure[2])) + edge_point = route[i] + edge_camera = eye((edge_point[0] + 6.0 * ty, edge_point[1] - 6.0 * tx, edge_point[2])) + edge_target = (edge_point[0] + 4.0 * tx, edge_point[1] + 4.0 * ty, edge_point[2] + 0.35) + centers = [qpoint(row["values"], scale_m) for row in islands] + middle_index = len(route) // 2 + midpoint = route[middle_index] + mid_t = tangent(middle_index) + walk_camera = eye((midpoint[0] - 2.0 * mid_t[0], midpoint[1] - 2.0 * mid_t[1], midpoint[2])) + next_island_index = min(len(centers) - 1, 5) + next_island_top = centers[next_island_index][2] + islands[next_island_index]["values"][4] * scale_m + walk_target = (centers[next_island_index][0], centers[next_island_index][1], next_island_top + 0.5) + walk_focus = tuple((centers[4][axis] + centers[5][axis]) * 0.5 for axis in range(3)) + (18.0,) + cx = sum(p[0] for p in centers) / len(centers) + cy = sum(p[1] for p in centers) / len(centers) + cz = sum(p[2] + row["values"][4] * scale_m for p, row in zip(centers, islands)) / len(centers) + wide_camera = (cx, cy + 23.0, cz + 10.0) + wide_target = (cx, cy, cz) + return [ + {"name": "arrival-mouth", "camera": arrival_camera, "target": arrival_target, + "focus": (*arrival, 14.0), "note": "player eye at the fixed 0-1 arrival mouth"}, + {"name": "island-edge", "camera": edge_camera, "target": edge_target, + "focus": (*edge_point, 18.0), "note": "player eye beside the route on an island edge"}, + {"name": "walk-between-islands", "camera": walk_camera, "target": walk_target, + "focus": walk_focus, "note": "player eye looking from one route landing toward the next island"}, + {"name": "departure-mouth", "camera": departure_camera, "target": departure_target, + "focus": (*departure, 14.0), "note": "player eye approaching the fixed 1-2 departure mouth"}, + {"name": "wide-silhouette", "camera": wide_camera, "target": wide_target, + "focus": (cx, cy, cz, 48.0), "fov": 72.0, + "note": "wide side view showing the island chain silhouette"}, + ] + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--views", nargs="*", default=None, + help="optional view names to rerender in both phases") + args = parser.parse_args() + if not DATA.is_file() or not LAYOUT.is_file(): + raise SystemExit("missing owner fixture or final seed-0 resolved layout") + layout = json.loads(LAYOUT.read_text(encoding="utf-8")) + views = make_views(layout) + if args.views: + unknown = set(args.views) - {str(view["name"]) for view in views} + if unknown: + raise SystemExit(f"unknown capture views: {sorted(unknown)}") + views = [view for view in views if view["name"] in set(args.views)] + OUT.mkdir(parents=True, exist_ok=True) + ARTIFACTS.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache") + quality = ROOT / "Standalone/Tools/verify/capture_quality.py" + manifest: list[dict[str, object]] = [] + old_manifest_path = ARTIFACTS / "manifest.json" + existing = json.loads(old_manifest_path.read_text(encoding="utf-8")) \ + if old_manifest_path.is_file() else {"captures": []} + rerendered = {str(view["name"]) for view in views} + for row in existing.get("captures", []): + if row.get("view") not in rerendered: + manifest.append(row) + for version, config in VERSIONS.items(): + phase = "before" if version.startswith("before") else "after" + for view in views: + name = str(view["name"]) + camera, target = view["camera"], view["target"] + assert isinstance(camera, tuple) and isinstance(target, tuple) + yaw, pitch = angles(camera, target) + mesh_dir = OUT / "meshes" / phase + mesh_dir.mkdir(parents=True, exist_ok=True) + mesh = mesh_dir / f"{name}.vfmesh" + capture_layout = mesh_dir / f"{name}-layout.json" + flags = ["--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=12", "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", "--seed=0", "--recipe=surface-world", + "--render-distance-chunks=0", f"--camera={fmt((*camera,yaw,pitch))}", + f"--resolved-layout-output={capture_layout}", f"--mesh-export={mesh}"] + if config["native"]: + flags.extend(["--native-owner-fi", "--density-dispatch-report"]) + export = [config["headless"], DATA, *flags] + stamped_run(f"{phase}-{name}-export", list(map(str, export)), + OUT / f"{phase}-{name}-export.log", config["stamp"], env) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{phase}-{name}: empty mesh export") + png = OUT / f"{phase}-{name}.png" + jpg = ARTIFACTS / f"{phase}-{name}.jpg" + focus = view["focus"] + assert isinstance(focus, tuple) + fov = float(view.get("fov", 78.0)) + render = [config["renderer"], mesh, png, "--size=1280x720", + f"--fov-deg={fov:.1f}", f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", + f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable", + "--headlamp=on", "--height-tint=on", "--height-center-m=-740", + "--height-range-m=70", "--tile-edges=off", f"--focus-region={fmt(focus)}"] + stamped_run(f"{phase}-{name}-render", list(map(str, render)), + OUT / f"{phase}-{name}-render.log", config["stamp"], env) + subprocess.run([*LIMIT, "timeout", "120s", "ffmpeg", "-nostdin", "-hide_banner", + "-loglevel", "error", "-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg], + cwd=ROOT, env=env, check=True, stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT, timeout=125) + check = subprocess.run([*LIMIT, "timeout", "120s", sys.executable, + quality, jpg], cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=125) + result = check.stdout.strip().splitlines()[-1] if check.stdout.strip() else f"exit={check.returncode}" + ok = check.returncode == 0 and "status=PASS" in check.stdout + manifest.append({"phase": phase, "view": name, "file": str(jpg.relative_to(ROOT)), + "bytes": jpg.stat().st_size, "camera_m": camera, "target_m": target, + "yaw_deg": yaw, "pitch_deg": pitch, "fov_deg": fov, + "note": view["note"], "quality_status": "PASS" if ok else "FAIL", "quality": result}) + print(f"[L140Capture] phase={phase} view={name} file={jpg.relative_to(ROOT)} " + f"quality={'PASS' if ok else 'FAIL'} {result}", flush=True) + manifest.sort(key=lambda row: (str(row["phase"]), str(row["view"]))) + summary = {"seed": 0, "source_stamp": VERSIONS["after-l140"]["stamp"], + "before_stamp": VERSIONS["before-head"]["stamp"], "views_per_phase": 5, + "all_quality_pass": all(row["quality_status"] == "PASS" for row in manifest), + "captures": manifest} + (ARTIFACTS / "manifest.json").write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L140CaptureSummary] views_per_phase={len(views)} captures={len(manifest)} " + f"all_quality_pass={summary['all_quality_pass']}", flush=True) + return 0 if summary["all_quality_pass"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/RunL140Determinism.py b/Standalone/Research/WorldAssemblerM3/Linux140/RunL140Determinism.py new file mode 100644 index 0000000..0e9a438 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux140/RunL140Determinism.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Check native Floating Islands owner identity and hashes at W1/W8/W12.""" +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/l140/determinism" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da" +BINARIES = { + "clang": ROOT / "Saved/Linux/l140/bin/VoxelForgeCoreHeadless-l140-clang", + "gcc": ROOT / "Saved/Linux/l140/bin/VoxelForgeCoreHeadless-l140-gcc", +} +CAMERA = "25.836914,3.199219,-737.000000,0,0" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|MaterialHash|LODHash|ExactQueryHash)\].*$", + re.M) +NATIVE_FI = re.compile( + r"^\[DensityDispatch\] strate=1 generator=FloatingIslands " + r"recipe=FloatingIslands\.ClusterField version=10 " + r"bound_path=FloatingIslands-native adapter=FFloatingIslandFieldAdapter$", re.M) +FI_RECIPE_HASH = re.compile(r"^\[FloatingIslandWorldAssembly\].*recipe_hash=([0-9a-f]+) status=PASS$", re.M) +FI_CLEARANCE = re.compile(r"^\[FloatingIslandPassageClearance\].*status=PASS$", re.M) +FI_ROUTE_AUDIT = re.compile(r"^\[FloatingIslandRouteAudit\].*status=PASS$", re.M) + + +def main() -> int: + if not DATA.is_file() or any(not path.is_file() for path in BINARIES.values()): + raise SystemExit("missing L140 Clang/GCC determinism binary or owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + rows: list[dict[str, object]] = [] + reference_hashes: list[str] | None = None + reference_identity: str | None = None + for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + name = f"native-owner-fi-{compiler}-w{workers}" + log, layout = OUT / f"{name}.log", OUT / f"{name}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=0", + f"--camera={CAMERA}", "--stream-hash", "--force-generate", "--seed=0", + "--recipe=surface-world", "--native-owner-fi", "--render-distance-chunks=0", + "--density-dispatch-report", f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + hashes = HASH_RE.findall(content) + current_layout = json.loads(layout.read_text(encoding="utf-8")) if layout.is_file() else {} + identity = current_layout.get("stable_identity") + native_ok = bool(NATIVE_FI.search(content)) + plan_ok = ("[FloatingIslandRouteAudit]" in content + and "[FloatingIslandPassageClearance]" in content) + recipe_hash = FI_RECIPE_HASH.search(content) + clearance = FI_CLEARANCE.search(content) + route_audit = FI_ROUTE_AUDIT.search(content) + passed = (proc.returncode == 0 and stamp is not None and stamp.group(1) == STAMP + and len(hashes) >= 4 and native_ok and plan_ok and recipe_hash is not None + and clearance is not None and route_audit is not None and bool(identity)) + if reference_hashes is None and passed: + reference_hashes, reference_identity = hashes, identity + else: + passed = passed and hashes == reference_hashes and identity == reference_identity + row = {"compiler": compiler, "workers": workers, "exit": proc.returncode, + "source_stamp": stamp.group(1) if stamp else None, "hash_lines": hashes, + "layout_identity": identity, "native_fi_dispatch": native_ok, + "native_plan_audits": plan_ok, "floating_island_recipe_hash": + recipe_hash.group(1) if recipe_hash else None, + "passage_clearance_line": clearance.group(0) if clearance else None, + "route_audit_line": route_audit.group(0) if route_audit else None, + "passed": bool(passed), + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + rows.append(row) + print("[L140DeterminismRun] " + json.dumps(row, sort_keys=True), flush=True) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n") + if not passed: + (OUT / "summary.json").write_text(json.dumps({"status": "FAIL", "runs": rows}, indent=2) + "\n") + raise SystemExit(f"native owner determinism mismatch; see {log}") + summary = {"status": "PASS", "source_stamp": STAMP, + "layout_identity": reference_identity, "runs": rows} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + (ROOT / "Standalone/Research/WorldAssemblerM3/Linux140/baselines").mkdir(parents=True, exist_ok=True) + (ROOT / "Standalone/Research/WorldAssemblerM3/Linux140/baselines/native-owner-fi-seed0.json").write_text( + json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L140DeterminismSummary] runs={len(rows)} source_stamp={STAMP} status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/RunL140SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux140/RunL140SeedSweep.py new file mode 100644 index 0000000..d94d91e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux140/RunL140SeedSweep.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Run the composed L140 owner gates for all requested seeds.""" +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/l140/seeds" +BIN = ROOT / "Saved/Linux/l140/bin/VoxelForgeCoreHeadless-l140-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +CHECKS = { + "native_dispatch": re.compile( + r"^\[DensityDispatch\] strate=1 generator=FloatingIslands " + r"recipe=FloatingIslands\.ClusterField version=10 " + r"bound_path=FloatingIslands-native adapter=FFloatingIslandFieldAdapter$", re.M), + "island_route": re.compile(r"^\[FloatingIslandRouteAudit\].*status=PASS$", re.M), + "passage_clearance": re.compile(r"^\[FloatingIslandPassageClearance\].*status=PASS$", re.M), + "strate1_route": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=1 .*blocked=0 .*unsupported=0 " + r"grade_failures=0 mouth_binding=PASS status=PASS$", re.M), + "route_summary": re.compile( + r"^\[ResolvedPassageAuditSummary\].*route_walkable=4 .*route_bindings=4 " + r"route_binding_failures=0 .*blocked_controls=3/3", re.M), + "passages": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), +} + + +def run(seed: int, label: str, seams_only: bool) -> tuple[int, str, float]: + log = OUT / f"seed{seed}-{label}.log" + layout = OUT / f"seed{seed}-{label}-layout.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=0", "--force-generate", f"--seed={seed}", + "--recipe=surface-world", "--native-owner-fi", "--density-dispatch-report"] + command.append("--audit-passage-tile-seams" if seams_only else "--audit-resolved-layout-passages") + if not seams_only: + command.append("--audit-passage-tile-seams") + command.append(f"--resolved-layout-output={layout}") + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + return proc.returncode, str(log.relative_to(ROOT)), round(time.monotonic() - started, 3) + + +def main() -> int: + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L140 Clang binary or composed owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + results = [] + for seed in range(16): + exit_code, log_name, elapsed = run(seed, "composed", False) + content = (ROOT / log_name).read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in CHECKS.items()} + checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP + checks["blocked_controls"] = len(re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3 + checks["negative_controls"] = ( + "[ResolvedLayoutValidatorControls] rejected=10/10 status=PASS" in content + and len(re.findall(r"^\[PassageNaturalnessNegativeControl\].*detected=true$", + content, re.M)) == 1 + and len(re.findall(r"^\[PassageMouthProfileNegativeControl\].*detected=true$", + content, re.M)) == 1 + and len(re.findall(r"^\[PassageFloorStepNegativeControl\].*detected=true$", + content, re.M)) == 3 + and "synthetic_spheres_negative_control=DETECTED" in content + and "floor_beaded_negative_control=DETECTED" in content) + seam_match = re.search(r"^\[PassageTileSeamAudit\].*status=(PASS|FAIL)$", content, re.M) + seam_exit = exit_code + seam_log = log_name + seam_elapsed = 0.0 + if seam_match is None: + seam_exit, seam_log, seam_elapsed = run(seed, "seams", True) + seam_content = (ROOT / seam_log).read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(seam_content) + checks["source_stamp"] &= stamp is not None and stamp.group(1) == STAMP + seam_match = re.search( + r"^\[PassageTileSeamAudit\].*status=(PASS|FAIL)$", seam_content, re.M) + content += "\n" + seam_content + checks["seams"] = seam_match is not None and seam_match.group(1) == "PASS" + all_gates = all(checks.values()) and exit_code == 0 and seam_exit == 0 + row = {"seed": seed, "exit": exit_code, "seam_exit": seam_exit, + "passed": all_gates, "checks": checks, + "elapsed_s": elapsed + seam_elapsed, "log": log_name, "seam_log": seam_log} + results.append(row) + print("[L140SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + (OUT / "runs-partial.json").write_text(json.dumps(results, indent=2) + "\n") + passed = sum(row["passed"] for row in results) + summary = {"source_stamp": STAMP, "seeds": list(range(16)), + "passed": passed, "failed": 16 - passed, "results": results} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + print(f"[L140SeedSweepSummary] passed={passed}/16 failed={16-passed} status={'PASS' if passed==16 else 'FAIL'}", + flush=True) + return 0 if passed == 16 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/RunL140Timing.py b/Standalone/Research/WorldAssemblerM3/Linux140/RunL140Timing.py new file mode 100644 index 0000000..a9da171 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux140/RunL140Timing.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""L80 timing: native L140 vs L139 HEAD and assembler/world, all owner strata.""" +from __future__ import annotations + +import json +import os +import re +import statistics +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l140/timing" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": ( + ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-aa-a", + "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621"), + "head": ( + ROOT / "Saved/Linux/l139/bin/VoxelForgeCoreHeadless-l139-clang", + "b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1"), + "l140-native-fi": ( + ROOT / "Saved/Linux/l140/bin/VoxelForgeCoreHeadless-l140-clang", + "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da"), +} +CAMERAS = { + # The camera override also selects the owner SurfaceWorld departure mouth. + # Hold XY to the seed-0 native-FI valid mouth so every layer/version shares one layout. + 0: (25.8369140625, 3.19921875, -372.2626953125), + 1: (25.8369140625, 3.19921875, -737.0), + 2: (25.8369140625, 3.19921875, -753.3740234375), + 3: (25.8369140625, 3.19921875, -900.353515625), +} +PIPELINE_RE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M) +FIELDS_RE = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +NEAR_RE = re.compile( + r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$", + re.M) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) +L140_AA_FLOOR_PCT = 1.0 # L137's same-binary A/A median control. + + +def quiet_machine() -> tuple[float, float]: + load_1m = os.getloadavg()[0] + + def totals() -> tuple[int, int]: + fields = Path("/proc/stat").read_text().splitlines()[0].split() + values = [int(value) for value in fields[1:]] + return sum(values), values[3] + values[4] + + before = totals() + time.sleep(2.0) + after = totals() + idle = 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0]) + return load_1m, idle + + +def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict[str, object]: + binary, expected_stamp = BINARIES[version] + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + label = f"s{strate}-w{workers}-{version}-{phase}{replicate}" + log = OUT / f"{label}.log" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3", + "--recipe=surface-world"] + if version == "l140-native-fi": + command.append("--native-owner-fi") + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline, near, stamp = PIPELINE_RE.search(content), NEAR_RE.search(content), STAMP_RE.search(content) + if (proc.returncode or pipeline is None or near is None or stamp is None + or stamp.group(1) != expected_stamp): + raise SystemExit(f"L80 run failed or stamp mismatch: {log}; exit={proc.returncode}") + if version == "l140-native-fi" and not re.search( + r"^\[FloatingIslandWorldAssembly\].*dispatch=FloatingIslands-native .*status=PASS$", + content, re.M): + raise SystemExit(f"candidate timing did not bind native FloatingIslands: {log}") + fields = dict(FIELDS_RE.findall(pipeline.group(1))) + row = {"strate": strate, "workers": workers, "version": version, + "phase": phase, "replicate": replicate, "source_stamp": stamp.group(1), + "lod02_ready_s": float(near.group(3)), + "whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")), + "whole_tiles": int(fields["whole_tiles"].split("/")[0]), + "wall_s": round(time.monotonic() - started, 3), "log": str(log.relative_to(ROOT))} + print("[L140TimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + if any(not path.is_file() for path, _ in BINARIES.values()) or not DATA.is_file(): + raise SystemExit("missing L80 timing binaries or owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + load_1m, idle = quiet_machine() + (OUT / "machine-load-before.txt").write_text( + f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n") + print(f"[L140TimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True) + if load_1m >= 1.5 or idle < 97.0: + raise SystemExit("timing refused: machine is above the L80 quiet-load gate") + rows: list[dict[str, object]] = [] + for strate in range(4): + for workers in (8, 12): + for version in BINARIES: + run(strate, workers, version, "warmup", 0) + for replicate in range(1, 6): + order = tuple(BINARIES) + order = order[replicate % len(order):] + order[:replicate % len(order)] + if replicate % 2 == 0: + order = tuple(reversed(order)) + for version in order: + rows.append(run(strate, workers, version, "measured", replicate)) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n") + measured = [row for row in rows if row["phase"] == "measured"] + summary = [] + for strate in range(4): + for workers in (8, 12): + group = [row for row in measured if row["strate"] == strate and row["workers"] == workers] + medians = {version: {key: statistics.median( + row[key] for row in group if row["version"] == version) + for key in ("lod02_ready_s", "whole_view_ready_s")} for version in BINARIES} + entry: dict[str, object] = {"strate": strate, "workers": workers, + "replicates_per_version": 5, "medians": medians} + for key in ("lod02_ready_s", "whole_view_ready_s"): + for baseline in ("head", "assembler-world"): + entry[f"l140_vs_{baseline}_{key}_delta_pct"] = round( + 100.0 * (medians["l140-native-fi"][key] / medians[baseline][key] - 1.0), 2) + entry[f"l140_vs_{baseline}_{key}_paired_deltas_pct"] = [round(100.0 * ( + next(row[key] for row in group if row["version"] == "l140-native-fi" + and row["replicate"] == replicate) + / next(row[key] for row in group if row["version"] == baseline + and row["replicate"] == replicate) - 1.0), 3) + for replicate in range(1, 6)] + summary.append(entry) + result = {"a_a_noise_floor_pct": L140_AA_FLOOR_PCT, + "a_a_reference": "L137 same-binary median control; assembler/world source stamp", + "timing_protocol": "quiet machine; one warm-up per binary per cell; five interleaved measured runs; W8/W12; all four owner layers", + "machine": {"load_1m": load_1m, "cpu_idle_2s_pct": idle}, + "source_stamps": {name: stamp for name, (_path, stamp) in BINARIES.items()}, + "runs": rows, "summary": summary} + (OUT / "summary.json").write_text(json.dumps(result, indent=2) + "\n") + print(f"[L140TimingSummary] measured_runs={len(measured)} warmups=24 aa_floor_pct=1.0 status=PASS", + flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/__pycache__/RunL140SeedSweep.cpython-313.pyc b/Standalone/Research/WorldAssemblerM3/Linux140/__pycache__/RunL140SeedSweep.cpython-313.pyc new file mode 100644 index 0000000..2e2fe0d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux140/__pycache__/RunL140SeedSweep.cpython-313.pyc differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/baselines/native-owner-fi-seed0.json b/Standalone/Research/WorldAssemblerM3/Linux140/baselines/native-owner-fi-seed0.json new file mode 100644 index 0000000..1d6d810 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux140/baselines/native-owner-fi-seed0.json @@ -0,0 +1,201 @@ +{ + "status": "PASS", + "source_stamp": "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da", + "layout_identity": "resolved-layout-v3-fnv64:08da20d55ff6820f", + "runs": [ + { + "compiler": "clang", + "workers": 1, + "exit": 0, + "source_stamp": "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da", + "hash_lines": [ + "[PipelineFullDetailHash] tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineWholeViewHash] tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineMaterialHash] tiles=512 material=0xc3cb5fcccf78f4ee", + "[PipelineLODHash] lod=0 tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineLODHash] lod=1 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=2 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=3 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=4 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=5 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=6 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=7 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=8 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineExactQueryHash] probes=9 density=0x74eac0457ddc2026 unclamped=0x74eac0457ddc2026 detail=0xefcba20c0c70c415 collision=0xdd5b083a1fa82cc7 path=direct-Core-queries" + ], + "layout_identity": "resolved-layout-v3-fnv64:08da20d55ff6820f", + "native_fi_dispatch": true, + "native_plan_audits": true, + "passed": true, + "elapsed_s": 101.92, + "log": "Saved/Linux/l140/determinism/native-owner-fi-clang-w1.log", + "layout": "Saved/Linux/l140/determinism/native-owner-fi-clang-w1.json", + "floating_island_recipe_hash": "0b6a23bc73a3b867", + "passage_clearance_line": "[FloatingIslandPassageClearance] strate=1 tube_samples=5325 island_blocked=0 route_checks=676275 route_blocked=0 arrival_support=5/5 departure_support=5/5 clearance_excess_m=2.824 portal_exclusion_m=13.0 influence_reach_m=1.053 capsule_bound_m=0.880 status=PASS", + "route_audit_line": "[FloatingIslandRouteAudit] points=128 centers=911 capsule_samples=247792 blocked=0 support_samples=6961 unsupported=0 grade_failures=0 status=PASS" + }, + { + "compiler": "clang", + "workers": 8, + "exit": 0, + "source_stamp": "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da", + "hash_lines": [ + "[PipelineFullDetailHash] tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineWholeViewHash] tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineMaterialHash] tiles=512 material=0xc3cb5fcccf78f4ee", + "[PipelineLODHash] lod=0 tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineLODHash] lod=1 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=2 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=3 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=4 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=5 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=6 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=7 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=8 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineExactQueryHash] probes=9 density=0x74eac0457ddc2026 unclamped=0x74eac0457ddc2026 detail=0xefcba20c0c70c415 collision=0xdd5b083a1fa82cc7 path=direct-Core-queries" + ], + "layout_identity": "resolved-layout-v3-fnv64:08da20d55ff6820f", + "native_fi_dispatch": true, + "native_plan_audits": true, + "passed": true, + "elapsed_s": 68.892, + "log": "Saved/Linux/l140/determinism/native-owner-fi-clang-w8.log", + "layout": "Saved/Linux/l140/determinism/native-owner-fi-clang-w8.json", + "floating_island_recipe_hash": "0b6a23bc73a3b867", + "passage_clearance_line": "[FloatingIslandPassageClearance] strate=1 tube_samples=5325 island_blocked=0 route_checks=676275 route_blocked=0 arrival_support=5/5 departure_support=5/5 clearance_excess_m=2.824 portal_exclusion_m=13.0 influence_reach_m=1.053 capsule_bound_m=0.880 status=PASS", + "route_audit_line": "[FloatingIslandRouteAudit] points=128 centers=911 capsule_samples=247792 blocked=0 support_samples=6961 unsupported=0 grade_failures=0 status=PASS" + }, + { + "compiler": "clang", + "workers": 12, + "exit": 0, + "source_stamp": "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da", + "hash_lines": [ + "[PipelineFullDetailHash] tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineWholeViewHash] tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineMaterialHash] tiles=512 material=0xc3cb5fcccf78f4ee", + "[PipelineLODHash] lod=0 tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineLODHash] lod=1 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=2 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=3 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=4 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=5 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=6 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=7 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=8 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineExactQueryHash] probes=9 density=0x74eac0457ddc2026 unclamped=0x74eac0457ddc2026 detail=0xefcba20c0c70c415 collision=0xdd5b083a1fa82cc7 path=direct-Core-queries" + ], + "layout_identity": "resolved-layout-v3-fnv64:08da20d55ff6820f", + "native_fi_dispatch": true, + "native_plan_audits": true, + "passed": true, + "elapsed_s": 68.96, + "log": "Saved/Linux/l140/determinism/native-owner-fi-clang-w12.log", + "layout": "Saved/Linux/l140/determinism/native-owner-fi-clang-w12.json", + "floating_island_recipe_hash": "0b6a23bc73a3b867", + "passage_clearance_line": "[FloatingIslandPassageClearance] strate=1 tube_samples=5325 island_blocked=0 route_checks=676275 route_blocked=0 arrival_support=5/5 departure_support=5/5 clearance_excess_m=2.824 portal_exclusion_m=13.0 influence_reach_m=1.053 capsule_bound_m=0.880 status=PASS", + "route_audit_line": "[FloatingIslandRouteAudit] points=128 centers=911 capsule_samples=247792 blocked=0 support_samples=6961 unsupported=0 grade_failures=0 status=PASS" + }, + { + "compiler": "gcc", + "workers": 1, + "exit": 0, + "source_stamp": "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da", + "hash_lines": [ + "[PipelineFullDetailHash] tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineWholeViewHash] tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineMaterialHash] tiles=512 material=0xc3cb5fcccf78f4ee", + "[PipelineLODHash] lod=0 tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineLODHash] lod=1 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=2 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=3 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=4 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=5 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=6 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=7 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=8 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineExactQueryHash] probes=9 density=0x74eac0457ddc2026 unclamped=0x74eac0457ddc2026 detail=0xefcba20c0c70c415 collision=0xdd5b083a1fa82cc7 path=direct-Core-queries" + ], + "layout_identity": "resolved-layout-v3-fnv64:08da20d55ff6820f", + "native_fi_dispatch": true, + "native_plan_audits": true, + "passed": true, + "elapsed_s": 100.676, + "log": "Saved/Linux/l140/determinism/native-owner-fi-gcc-w1.log", + "layout": "Saved/Linux/l140/determinism/native-owner-fi-gcc-w1.json", + "floating_island_recipe_hash": "0b6a23bc73a3b867", + "passage_clearance_line": "[FloatingIslandPassageClearance] strate=1 tube_samples=5325 island_blocked=0 route_checks=676275 route_blocked=0 arrival_support=5/5 departure_support=5/5 clearance_excess_m=2.824 portal_exclusion_m=13.0 influence_reach_m=1.053 capsule_bound_m=0.880 status=PASS", + "route_audit_line": "[FloatingIslandRouteAudit] points=128 centers=911 capsule_samples=247792 blocked=0 support_samples=6961 unsupported=0 grade_failures=0 status=PASS" + }, + { + "compiler": "gcc", + "workers": 8, + "exit": 0, + "source_stamp": "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da", + "hash_lines": [ + "[PipelineFullDetailHash] tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineWholeViewHash] tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineMaterialHash] tiles=512 material=0xc3cb5fcccf78f4ee", + "[PipelineLODHash] lod=0 tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineLODHash] lod=1 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=2 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=3 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=4 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=5 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=6 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=7 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=8 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineExactQueryHash] probes=9 density=0x74eac0457ddc2026 unclamped=0x74eac0457ddc2026 detail=0xefcba20c0c70c415 collision=0xdd5b083a1fa82cc7 path=direct-Core-queries" + ], + "layout_identity": "resolved-layout-v3-fnv64:08da20d55ff6820f", + "native_fi_dispatch": true, + "native_plan_audits": true, + "passed": true, + "elapsed_s": 70.398, + "log": "Saved/Linux/l140/determinism/native-owner-fi-gcc-w8.log", + "layout": "Saved/Linux/l140/determinism/native-owner-fi-gcc-w8.json", + "floating_island_recipe_hash": "0b6a23bc73a3b867", + "passage_clearance_line": "[FloatingIslandPassageClearance] strate=1 tube_samples=5325 island_blocked=0 route_checks=676275 route_blocked=0 arrival_support=5/5 departure_support=5/5 clearance_excess_m=2.824 portal_exclusion_m=13.0 influence_reach_m=1.053 capsule_bound_m=0.880 status=PASS", + "route_audit_line": "[FloatingIslandRouteAudit] points=128 centers=911 capsule_samples=247792 blocked=0 support_samples=6961 unsupported=0 grade_failures=0 status=PASS" + }, + { + "compiler": "gcc", + "workers": 12, + "exit": 0, + "source_stamp": "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da", + "hash_lines": [ + "[PipelineFullDetailHash] tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineWholeViewHash] tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineMaterialHash] tiles=512 material=0xc3cb5fcccf78f4ee", + "[PipelineLODHash] lod=0 tiles=512 density=0xe10ac12305f1dbc0 mesh=0x822e56e8f5bfa6f0 payload=0x0be1f08906997251 topology=0x650bbeb9e554d390", + "[PipelineLODHash] lod=1 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=2 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=3 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=4 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=5 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=6 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=7 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=8 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineExactQueryHash] probes=9 density=0x74eac0457ddc2026 unclamped=0x74eac0457ddc2026 detail=0xefcba20c0c70c415 collision=0xdd5b083a1fa82cc7 path=direct-Core-queries" + ], + "layout_identity": "resolved-layout-v3-fnv64:08da20d55ff6820f", + "native_fi_dispatch": true, + "native_plan_audits": true, + "passed": true, + "elapsed_s": 69.061, + "log": "Saved/Linux/l140/determinism/native-owner-fi-gcc-w12.log", + "layout": "Saved/Linux/l140/determinism/native-owner-fi-gcc-w12.json", + "floating_island_recipe_hash": "0b6a23bc73a3b867", + "passage_clearance_line": "[FloatingIslandPassageClearance] strate=1 tube_samples=5325 island_blocked=0 route_checks=676275 route_blocked=0 arrival_support=5/5 departure_support=5/5 clearance_excess_m=2.824 portal_exclusion_m=13.0 influence_reach_m=1.053 capsule_bound_m=0.880 status=PASS", + "route_audit_line": "[FloatingIslandRouteAudit] points=128 centers=911 capsule_samples=247792 blocked=0 support_samples=6961 unsupported=0 grade_failures=0 status=PASS" + } + ], + "world_assembly_evaluator_version": 239, + "floating_island_geometry_version": 10, + "native_dispatch": "FloatingIslands-native", + "route_and_clearance_baseline": { + "route_audit": "[FloatingIslandRouteAudit] points=128 centers=... status=PASS", + "passage_clearance": "[FloatingIslandPassageClearance] strate=1 tube_samples=5325 island_blocked=0 route_checks=676275 route_blocked=0 arrival_support=5/5 departure_support=5/5 clearance_excess_m=2.824 portal_exclusion_m=13.0 influence_reach_m=1.053 capsule_bound_m=0.880 status=PASS", + "note": "The six identical hash/layout rows are the concrete baselines; route point counts differ by seed only in other sweep cases." + } +} diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-arrival-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-arrival-mouth.jpg new file mode 100644 index 0000000..58f9af4 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-arrival-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-departure-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-departure-mouth.jpg new file mode 100644 index 0000000..3aa7176 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-departure-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-island-edge.jpg b/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-island-edge.jpg new file mode 100644 index 0000000..0e8d0f5 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-island-edge.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-walk-between-islands.jpg b/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-walk-between-islands.jpg new file mode 100644 index 0000000..3f2d9c8 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-walk-between-islands.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-wide-silhouette.jpg b/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-wide-silhouette.jpg new file mode 100644 index 0000000..b37c0a5 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-wide-silhouette.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-arrival-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-arrival-mouth.jpg new file mode 100644 index 0000000..ffa177c Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-arrival-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-departure-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-departure-mouth.jpg new file mode 100644 index 0000000..f92800a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-departure-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-island-edge.jpg b/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-island-edge.jpg new file mode 100644 index 0000000..03ebc0b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-island-edge.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-walk-between-islands.jpg b/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-walk-between-islands.jpg new file mode 100644 index 0000000..a20445b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-walk-between-islands.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-wide-silhouette.jpg b/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-wide-silhouette.jpg new file mode 100644 index 0000000..a8414e1 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-wide-silhouette.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/captures/manifest.json b/Standalone/Research/WorldAssemblerM3/Linux140/captures/manifest.json new file mode 100644 index 0000000..9cbe0e7 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux140/captures/manifest.json @@ -0,0 +1,229 @@ +{ + "seed": 0, + "source_stamp": "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da", + "before_stamp": "b8603b6cb3975ca0f9669759e4e2464561c557fdb7aa6836acef123ecf0b99d1", + "views_per_phase": 5, + "all_quality_pass": true, + "captures": [ + { + "phase": "after", + "view": "arrival-mouth", + "file": "Standalone/Research/WorldAssemblerM3/Linux140/captures/after-arrival-mouth.jpg", + "bytes": 20205, + "camera_m": [ + 23.77218612173332, + 4.6087926326387905, + -735.55 + ], + "target_m": [ + 32.44404347295337, + -1.3114176744441277, + -735.55 + ], + "yaw_deg": 124.32105280346487, + "pitch_deg": 0.0, + "fov_deg": 78.0, + "note": "player eye at the fixed 0-1 arrival mouth", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-arrival-mouth.jpg pixels=921600 mean_luma=111.598 stddev_luma=5.081 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "departure-mouth", + "file": "Standalone/Research/WorldAssemblerM3/Linux140/captures/after-departure-mouth.jpg", + "bytes": 23901, + "camera_m": [ + 21.556817528943192, + -14.930993681478485, + -730.751171875 + ], + "target_m": [ + 11.51544953238179, + -11.861640531768845, + -730.751171875 + ], + "yaw_deg": -73.00313688448165, + "pitch_deg": 0.0, + "fov_deg": 78.0, + "note": "player eye approaching the fixed 1-2 departure mouth", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-departure-mouth.jpg pixels=921600 mean_luma=112.186 stddev_luma=5.529 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "island-edge", + "file": "Standalone/Research/WorldAssemblerM3/Linux140/captures/after-island-edge.jpg", + "bytes": 19984, + "camera_m": [ + 47.84242754056169, + -19.331304817383565, + -734.4728515625 + ], + "target_m": [ + 54.57926831575571, + -16.75934518129221, + -735.5728515625 + ], + "yaw_deg": 69.10437717375885, + "pitch_deg": -8.673184187101441, + "fov_deg": 78.0, + "note": "player eye beside the route on an island edge", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-island-edge.jpg pixels=921600 mean_luma=114.169 stddev_luma=8.688 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "walk-between-islands", + "file": "Standalone/Research/WorldAssemblerM3/Linux140/captures/after-walk-between-islands.jpg", + "bytes": 20752, + "camera_m": [ + 64.3971173756484, + -22.884388870806152, + -733.137890625 + ], + "target_m": [ + 53.12109375, + -24.2802734375, + -734.537109375 + ], + "yaw_deg": -97.05687302971492, + "pitch_deg": -7.020510748634912, + "fov_deg": 78.0, + "note": "player eye looking from one route landing toward the next island", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-walk-between-islands.jpg pixels=921600 mean_luma=111.849 stddev_luma=7.186 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "wide-silhouette", + "file": "Standalone/Research/WorldAssemblerM3/Linux140/captures/after-wide-silhouette.jpg", + "bytes": 22894, + "camera_m": [ + 41.099609375, + 8.620008680555555, + -725.6368272569445 + ], + "target_m": [ + 41.099609375, + -14.379991319444445, + -735.6368272569445 + ], + "yaw_deg": 180.0, + "pitch_deg": -23.498565675952097, + "fov_deg": 72.0, + "note": "wide side view showing the island chain silhouette", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux140/captures/after-wide-silhouette.jpg pixels=921600 mean_luma=116.109 stddev_luma=7.003 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "before", + "view": "arrival-mouth", + "file": "Standalone/Research/WorldAssemblerM3/Linux140/captures/before-arrival-mouth.jpg", + "bytes": 13808, + "camera_m": [ + 23.77218612173332, + 4.6087926326387905, + -735.55 + ], + "target_m": [ + 32.44404347295337, + -1.3114176744441277, + -735.55 + ], + "yaw_deg": 124.32105280346487, + "pitch_deg": 0.0, + "fov_deg": 78.0, + "note": "player eye at the fixed 0-1 arrival mouth", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-arrival-mouth.jpg pixels=921600 mean_luma=132.078 stddev_luma=8.080 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "before", + "view": "departure-mouth", + "file": "Standalone/Research/WorldAssemblerM3/Linux140/captures/before-departure-mouth.jpg", + "bytes": 17309, + "camera_m": [ + 21.556817528943192, + -14.930993681478485, + -730.751171875 + ], + "target_m": [ + 11.51544953238179, + -11.861640531768845, + -730.751171875 + ], + "yaw_deg": -73.00313688448165, + "pitch_deg": 0.0, + "fov_deg": 78.0, + "note": "player eye approaching the fixed 1-2 departure mouth", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-departure-mouth.jpg pixels=921600 mean_luma=118.383 stddev_luma=9.908 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "before", + "view": "island-edge", + "file": "Standalone/Research/WorldAssemblerM3/Linux140/captures/before-island-edge.jpg", + "bytes": 15208, + "camera_m": [ + 47.84242754056169, + -19.331304817383565, + -734.4728515625 + ], + "target_m": [ + 54.57926831575571, + -16.75934518129221, + -735.5728515625 + ], + "yaw_deg": 69.10437717375885, + "pitch_deg": -8.673184187101441, + "fov_deg": 78.0, + "note": "player eye beside the route on an island edge", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-island-edge.jpg pixels=921600 mean_luma=109.945 stddev_luma=4.383 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "before", + "view": "walk-between-islands", + "file": "Standalone/Research/WorldAssemblerM3/Linux140/captures/before-walk-between-islands.jpg", + "bytes": 15126, + "camera_m": [ + 64.3971173756484, + -22.884388870806152, + -733.137890625 + ], + "target_m": [ + 53.12109375, + -24.2802734375, + -734.537109375 + ], + "yaw_deg": -97.05687302971492, + "pitch_deg": -7.020510748634912, + "fov_deg": 78.0, + "note": "player eye looking from one route landing toward the next island", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-walk-between-islands.jpg pixels=921600 mean_luma=110.305 stddev_luma=4.132 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "before", + "view": "wide-silhouette", + "file": "Standalone/Research/WorldAssemblerM3/Linux140/captures/before-wide-silhouette.jpg", + "bytes": 15268, + "camera_m": [ + 41.099609375, + 8.620008680555555, + -725.6368272569445 + ], + "target_m": [ + 41.099609375, + -14.379991319444445, + -735.6368272569445 + ], + "yaw_deg": 180.0, + "pitch_deg": -23.498565675952097, + "fov_deg": 72.0, + "note": "wide side view showing the island chain silhouette", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux140/captures/before-wide-silhouette.jpg pixels=921600 mean_luma=114.186 stddev_luma=3.531 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + } + ] +} diff --git a/Standalone/Research/WorldAssemblerM3/Linux140/l140-fi-around-tunnels.patch b/Standalone/Research/WorldAssemblerM3/Linux140/l140-fi-around-tunnels.patch new file mode 100644 index 0000000..c4bcb95 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux140/l140-fi-around-tunnels.patch @@ -0,0 +1,1279 @@ +diff --git a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp +index 5e66412..8d21685 100644 +--- a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp ++++ b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp +@@ -892,6 +892,7 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager, + + bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager, + const FStrateSlot& Slot, VoxelForgeCore::WorldAssemblyManifest& Manifest, ++ const VoxelForgeCore::ResolvedLayoutSnapshotV3& Layout, + double CoreVoxelSizeCm, std::int64_t LayoutChunkWidthSubvoxels, FString& OutError) + { + using namespace VoxelForgeCore; +@@ -903,7 +904,8 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager, + IslandPlan.topChunkZ = Slot.TopChunkZ; + IslandPlan.bottomChunkZ = Slot.BottomChunkZ; + auto& Recipe = IslandPlan.manifest.recipe; +- Recipe.geometryVersion = 8; ++ Recipe.geometryVersion = 10; ++ Recipe.primaryIslandCount = 9; + Recipe.terrainFollowingRoute = static_cast(Passage.Style) == 0; + Recipe.layoutChunkWidthSubvoxels = LayoutChunkWidthSubvoxels; + Recipe.seed = static_cast(StrateManager->GetWorldSeed()); +@@ -929,13 +931,24 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager, + / Recipe.ownerToCoreScaleDenominator; + Recipe.spacingSubvoxels = WorldAssemblyFixed(Params.IslandSpacing * OwnerToCoreScale); + Recipe.minimumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMinRadius * OwnerToCoreScale); +- Recipe.maximumRadiusSubvoxels = WorldAssemblyFixed(Params.IslandMaxRadius * OwnerToCoreScale); ++ Recipe.maximumRadiusSubvoxels = WorldAssemblyFixed(28.0 * OwnerToCoreScale); + Recipe.thicknessRatioQ16 = WorldAssemblyQ16(Params.ThicknessRatio); + Recipe.verticalJitterQ16 = WorldAssemblyQ16(Params.VerticalJitter); + Recipe.topFlattenQ16 = WorldAssemblyQ16(Params.TopFlatten); +- Recipe.roughnessSubvoxels = WorldAssemblyFixed(Params.SurfaceRoughness * OwnerToCoreScale); +- Recipe.blendRadiusSubvoxels = WorldAssemblyFixed(Params.SDFBlendRadius * OwnerToCoreScale); ++ Recipe.roughnessSubvoxels = std::min(WorldAssemblyFixed( ++ Params.SurfaceRoughness * OwnerToCoreScale), WorldAssemblySubvoxelsPerVoxel / 2); ++ Recipe.blendRadiusSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel; ++ Recipe.contourWarpSubvoxels = WorldAssemblySubvoxelsPerVoxel; + Recipe.boundarySealSubvoxels = WorldAssemblyFixed(Params.BoundarySealThickness * OwnerToCoreScale); ++ Recipe.routeConstraints.version = 1; ++ Recipe.routeConstraints.capsuleRadiusSubvoxels = 348; ++ Recipe.routeConstraints.capsuleHeightSubvoxels = 1802; ++ Recipe.routeConstraints.maximumGradeQ16 = 17560; ++ Recipe.routeConstraints.sweepSpacingSubvoxels = 128; ++ Recipe.routeConstraints.supportProbeRadiusSubvoxels = 230; ++ Recipe.routeConstraints.supportSearchDepthSubvoxels = 512; ++ Recipe.routeConstraints.minimumClearanceSubvoxels = 128; ++ Recipe.routeConstraints.minimumSupportProbes = 3; + const auto OwnerVoxelsToCoreQ=[&](double Value) + { + return static_cast(std::llround(Value*Recipe.ownerVoxelSizeMicrons +@@ -944,40 +957,21 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager, + Recipe.anchorIslandOrdinal=Recipe.primaryIslandCount-1; + Recipe.anchorPlacementRadiusSubvoxels=WorldAssemblyMicrometresToSubvoxels( + Slot.PassageDistanceMaximumMicrometres, Recipe.coreVoxelSizeMicrons); +- const std::int64_t spiralRadius=WorldAssemblyMicrometresToSubvoxels( +- Slot.PassageSpiralRadiusMicrometres, Recipe.coreVoxelSizeMicrons); +- // Reserve at least a 1.5 m mid-bore through the anchor root. +- constexpr double AnchorPassageMinimumBoreOwnerVoxels=12.0; +- const std::int64_t OwnerVoxelMicrometres = Recipe.ownerVoxelSizeMicrons; +- const std::int64_t mouthBore=std::max( +- WorldAssemblyMicrometresToSubvoxels(Slot.PassageMouthRadiusMicrometres, +- Recipe.coreVoxelSizeMicrons), +- WorldAssemblyMicrometresToSubvoxels(12 * OwnerVoxelMicrometres, +- Recipe.coreVoxelSizeMicrons)); +- const std::int64_t bore=std::max( +- WorldAssemblyMicrometresToSubvoxels(Slot.PassageMidRadiusMicrometres, +- Recipe.coreVoxelSizeMicrons), +- WorldAssemblyMicrometresToSubvoxels( +- static_cast(AnchorPassageMinimumBoreOwnerVoxels * OwnerVoxelMicrometres), +- Recipe.coreVoxelSizeMicrons)); +- const std::int64_t wallReserve=WorldAssemblyMicrometresToSubvoxels( +- 12 * OwnerVoxelMicrometres, Recipe.coreVoxelSizeMicrons); +- const std::int64_t FourOwnerVoxelsSubvoxels=WorldAssemblyMicrometresToSubvoxels( +- 4 * OwnerVoxelMicrometres, Recipe.coreVoxelSizeMicrons); +- Recipe.anchorMouthOffsetSubvoxels=spiralRadius+mouthBore+FourOwnerVoxelsSubvoxels; +- const std::int64_t excursion=std::max( +- WorldAssemblyMicrometresToSubvoxels(Slot.PassageWanderMicrometres, +- Recipe.coreVoxelSizeMicrons), spiralRadius); +- Recipe.anchorRootRadiusSubvoxels=excursion+bore+wallReserve; +- Recipe.anchorIslandMinimumRadiusSubvoxels=std::max(Recipe.minimumRadiusSubvoxels, +- Recipe.anchorMouthOffsetSubvoxels+mouthBore+FourOwnerVoxelsSubvoxels); ++ Recipe.anchorMouthOffsetSubvoxels = OwnerVoxelsToCoreQ(4.0); ++ Recipe.anchorRootRadiusSubvoxels = std::max(OwnerVoxelsToCoreQ(16.0), ++ Recipe.anchorMouthOffsetSubvoxels + Recipe.routeConstraints.capsuleRadiusSubvoxels ++ + WorldAssemblySubvoxelsPerVoxel); ++ Recipe.anchorIslandMinimumRadiusSubvoxels = std::max(Recipe.maximumRadiusSubvoxels, ++ Recipe.anchorMouthOffsetSubvoxels + Recipe.routeConstraints.capsuleRadiusSubvoxels ++ + WorldAssemblySubvoxelsPerVoxel); + Recipe.anchorRootBottomZSubvoxels=Recipe.layoutMinimumZSubvoxels + +Recipe.boundarySealSubvoxels; + std::string PlanError; +- const std::optional Planned = PlanFloatingIslandCluster(Recipe, PlanError); ++ const std::optional Planned = ++ PlanFloatingIslandClusterAroundPassages(Recipe, Layout, Slot.StrateIndex, PlanError); + if (!Planned) + { +- OutError = FString::Printf(TEXT("FloatingIslands automatic plan for strate %d failed: %s"), ++ OutError = FString::Printf(TEXT("FloatingIslands passage-aware plan for strate %d failed: %s"), + Slot.StrateIndex, UTF8_TO_TCHAR(PlanError.c_str())); + return false; + } +@@ -1671,18 +1665,6 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, + Generated.plan.strates.push_back(std::move(Stratum)); + ++BuiltSurfaceCount; + } +- // Native floating-island evaluation remains an explicit imported-plan opt-in. +- // The default owner path uses the legacy generator for this slot; emitting an +- // automatic FI plan here silently switches density dispatch and changes the +- // shared passage mouth before the owner has opted into that field. +- else if (Type == ECaveGeneratorType::FloatingIslands && bImportedOverride) +- { +- if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, Generated, +- CoreVoxelSizeCm, LayoutChunkWidthSubvoxels, OutError)) +- { +- return false; +- } +- } + } + if (BuiltSurfaceCount > 1) + { +@@ -2031,6 +2013,98 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, + { + return false; + } ++ bool bAddedFloatingIslandPlan = false; ++ for (const FStrateSlot& Slot : StrateManager->GetLayout()) ++ { ++ if (Slot.Definition == nullptr ++ || StrateManager->GetGeneratorTypeForChunk(FIntVector(0, 0, Slot.TopChunkZ)) ++ != ECaveGeneratorType::FloatingIslands) ++ { ++ continue; ++ } ++ const auto Existing = std::find_if(Manifest->floatingIslands.begin(), ++ Manifest->floatingIslands.end(), [&Slot](const auto& Plan) ++ { return Plan.strateIndex == Slot.StrateIndex; }); ++ if (Existing != Manifest->floatingIslands.end()) continue; ++ if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, *Manifest, Layout, ++ CoreVoxelSizeCm, LayoutChunkWidthSubvoxels, OutError)) ++ { ++ return false; ++ } ++ bAddedFloatingIslandPlan = true; ++ } ++ if (bAddedFloatingIslandPlan) ++ { ++ if (!Manifest->finalize(CoreError)) ++ { ++ OutError = FString::Printf(TEXT("Core passage-aware FloatingIslands planning failed: %s"), ++ UTF8_TO_TCHAR(CoreError.c_str())); ++ return false; ++ } ++ ManifestHash = FString::Printf(TEXT("%016llx"), ++ static_cast(Manifest->contentHash)); ++ for (const auto& Plan : Manifest->floatingIslands) ++ { ++ auto Stratum = std::find_if(Layout.strates.begin(), Layout.strates.end(), ++ [&Plan](const auto& Item) { return Item.strateIndex == Plan.strateIndex; }); ++ if (Stratum == Layout.strates.end()) ++ { ++ OutError = FString::Printf(TEXT("FloatingIslands strate %d disappeared before anchor export."), ++ Plan.strateIndex); ++ return false; ++ } ++ Stratum->recipeVersion = Plan.manifest.recipe.geometryVersion; ++ std::erase_if(Stratum->anchors, [](const auto& Anchor) ++ { ++ return Anchor.kind == "island_anchor" || Anchor.kind == "passage_anchor" ++ || Anchor.kind == "route_point"; ++ }); ++ const auto& NativePlan = Plan.manifest.plan; ++ const auto& NativeRecipe = Plan.manifest.recipe; ++ for (std::size_t IslandIndex = 0; IslandIndex < NativePlan.islands.size(); ++IslandIndex) ++ { ++ const auto& Island = NativePlan.islands[IslandIndex]; ++ VoxelForgeCore::ResolvedLayoutAnchorV2 Anchor; ++ Anchor.kind = "island_anchor"; ++ Anchor.stableId = "island/" + std::to_string(IslandIndex); ++ Anchor.units = "1/256 Core voxel"; ++ Anchor.values = { Island.centerSubvoxels[0], Island.centerSubvoxels[1], ++ Island.centerSubvoxels[2], Island.radiusSubvoxels, Island.topHalfSubvoxels }; ++ Stratum->anchors.push_back(std::move(Anchor)); ++ if (IslandIndex == NativeRecipe.anchorIslandOrdinal) ++ { ++ VoxelForgeCore::ResolvedLayoutAnchorV2 PassageAnchor; ++ PassageAnchor.kind = "passage_anchor"; ++ PassageAnchor.stableId = "passage-anchor/island/" ++ + std::to_string(IslandIndex); ++ PassageAnchor.units = "1/256 Core voxel"; ++ PassageAnchor.values = { Island.centerSubvoxels[0], Island.centerSubvoxels[1], ++ NativeRecipe.anchorRootRadiusSubvoxels, ++ NativeRecipe.anchorMouthOffsetSubvoxels, ++ NativeRecipe.anchorPlacementRadiusSubvoxels, ++ NativeRecipe.anchorRootBottomZSubvoxels, Island.radiusSubvoxels }; ++ if (!NativePlan.route.empty()) ++ { ++ const auto& Mouth = NativePlan.route.back().positionSubvoxels; ++ PassageAnchor.values.insert(PassageAnchor.values.end(), ++ { Mouth[0], Mouth[1], Mouth[2] }); ++ } ++ Stratum->anchors.push_back(std::move(PassageAnchor)); ++ } ++ } ++ for (std::size_t RouteIndex = 0; RouteIndex < NativePlan.route.size(); ++RouteIndex) ++ { ++ const auto& Point = NativePlan.route[RouteIndex]; ++ VoxelForgeCore::ResolvedLayoutAnchorV2 Anchor; ++ Anchor.kind = "route_point"; ++ Anchor.stableId = "route/" + std::to_string(RouteIndex); ++ Anchor.units = "1/256 Core voxel"; ++ Anchor.values = { Point.positionSubvoxels[0], Point.positionSubvoxels[1], ++ Point.positionSubvoxels[2], Point.radiusSubvoxels }; ++ Stratum->anchors.push_back(std::move(Anchor)); ++ } ++ } ++ } + std::string LayoutError; + if (!Layout.finalize(LayoutError)) + { +diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +index 8c44461..ec7cfe3 100644 +--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h ++++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +@@ -511,7 +511,7 @@ namespace VoxelForgeCore + // the planned graph route, preserves its walk corridor through shell overlaps, and keeps + // boundary-blended host rock from widening the authored passage wall. + // v237 adds the shared Core cave-graph shaft primitive and basal terminal route. +- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 237; ++ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 239; + + enum class FieldChannel : std::uint32_t + { +@@ -1417,6 +1417,19 @@ namespace VoxelForgeCore + bool passed = false; + }; + ++ struct FloatingIslandPassageClearanceAuditResult ++ { ++ std::uint64_t passageSamples = 0; ++ std::uint64_t blockedIslandSamples = 0; ++ std::uint64_t routeSamples = 0; ++ std::uint64_t blockedRouteSamples = 0; ++ std::uint32_t arrivalMouthSupportProbes = 0; ++ std::uint32_t departureMouthSupportProbes = 0; ++ std::int64_t minimumExcessClearanceSubvoxels = 0; ++ double portalExclusionMetres = 0.0; ++ bool passed = false; ++ }; ++ + struct FloatingIslandInfluenceBounds + { + bool finite = false; +@@ -1461,11 +1474,17 @@ namespace VoxelForgeCore + // Planner output is ordinary reviewed data; clients can load it without re-running the planner. + VFCORE_API std::optional PlanFloatingIslandCluster( + const FloatingIslandRecipeV1& recipe, std::string& error); ++ VFCORE_API std::optional PlanFloatingIslandClusterAroundPassages( ++ const FloatingIslandRecipeV1& recipe, const ResolvedLayoutSnapshotV3& layout, ++ std::int32_t strateIndex, std::string& error); + VFCORE_API FloatingIslandBoundsAuditResult AuditFloatingIslandBounds( + const FloatingIslandFieldV1& field, std::uint32_t boxCount = 256, + std::uint64_t seed = 0x49534c414e444231ull); + VFCORE_API FloatingIslandRouteAuditResult AuditFloatingIslandRoute( + const FloatingIslandFieldV1& field, std::int64_t routeOffsetSubvoxels = 0); ++ VFCORE_API FloatingIslandPassageClearanceAuditResult AuditFloatingIslandPassageClearance( ++ const FloatingIslandFieldV1& field, const ResolvedLayoutSnapshotV3& layout, ++ std::int32_t strateIndex, double portalExclusionMetres = 10.0); + VFCORE_API FloatingIslandThicknessAuditResult AuditFloatingIslandThickness( + const FloatingIslandPlanV1& plan, double minimumDepthToDiameterRatio = 0.70); + +diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h +index 33d97d6..58f0959 100644 +--- a/Standalone/Core/src/FloatingIslandsInternal.h ++++ b/Standalone/Core/src/FloatingIslandsInternal.h +@@ -4,6 +4,7 @@ + #include "VoxelForgeCore/CpuCycleCounter.h" + #include "VoxelForgeCore/FloatingIslandInstrumentation.h" + #include "VoxelForgeCore/SharedColumnCache.h" ++#include "VoxelForgeCore/PassageSpline.h" + #include "Json.h" + #include "VoxelForgeCore/VoxelForgeCore.h" + +@@ -285,7 +286,7 @@ namespace floating_island_detail + ? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0; + const std::uint32_t ExpectedDenominator = ScaleGcd > 0 + ? recipe.coreVoxelSizeMicrons / ScaleGcd : 0; +- if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 8 ++ if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 10 + || (recipe.geometryVersion == 1 + && (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0)) + || recipe.unitsPerVoxel != Q +@@ -1988,6 +1989,229 @@ std::string FloatingIslandManifestV1::ToJson() const + return out.str(); + } + ++namespace floating_island_detail ++{ ++ struct FIslandPassageSpan ++ { ++ PassageSpline::FSegment segment; ++ double alongMetres = 0.0; ++ double totalMetres = 0.0; ++ double radiusSubvoxels = 0.0; ++ bool portalAtStart = false; ++ }; ++ ++ inline double SegmentDistanceSquared3D(const PassageSpline::FPoint& p1, ++ const PassageSpline::FPoint& q1, const PassageSpline::FPoint& p2, ++ const PassageSpline::FPoint& q2) ++ { ++ const std::array d1={q1[0]-p1[0],q1[1]-p1[1],q1[2]-p1[2]}; ++ const std::array d2={q2[0]-p2[0],q2[1]-p2[1],q2[2]-p2[2]}; ++ const std::array r={p1[0]-p2[0],p1[1]-p2[1],p1[2]-p2[2]}; ++ const auto dot=[](const auto& a,const auto& b) ++ {return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];}; ++ const double a=dot(d1,d1),e=dot(d2,d2),f=dot(d2,r); ++ double s=0.0,t=0.0; ++ if(a<=1.0e-18&&e<=1.0e-18)return dot(r,r); ++ if(a<=1.0e-18)t=std::clamp(f/e,0.0,1.0); ++ else ++ { ++ const double c=dot(d1,r); ++ if(e<=1.0e-18)s=std::clamp(-c/a,0.0,1.0); ++ else ++ { ++ const double b=dot(d1,d2); ++ const double denominator=a*e-b*b; ++ if(std::abs(denominator)>1.0e-18) ++ s=std::clamp((b*f-c*e)/denominator,0.0,1.0); ++ t=(b*s+f)/e; ++ if(t<0.0){t=0.0;s=std::clamp(-c/a,0.0,1.0);} ++ else if(t>1.0){t=1.0;s=std::clamp((b-c)/a,0.0,1.0);} ++ } ++ } ++ std::array delta{}; ++ for(std::size_t axis=0;axis<3;++axis) ++ delta[axis]=r[axis]+s*d1[axis]-t*d2[axis]; ++ return dot(delta,delta); ++ } ++ ++ inline std::vector BuildIslandPassageSpans( ++ const ResolvedLayoutSnapshotV3& layout,std::int32_t strateIndex, ++ const FloatingIslandRecipeV1& recipe,double portalExclusionMetres) ++ { ++ std::vector spans; ++ const double qPerMetre=static_cast(WorldAssemblySubvoxelsPerVoxel) ++ *1000000.0/std::max(1,recipe.coreVoxelSizeMicrons); ++ for(const auto& passage:layout.passages) ++ { ++ const bool upper=passage.upperStrateIndex==strateIndex; ++ const bool lower=passage.lowerStrateIndex==strateIndex; ++ if((!upper&&!lower)||passage.controlPoints.size()<2)continue; ++ std::vector controls; ++ controls.reserve(passage.controlPoints.size()); ++ for(const auto& point:passage.controlPoints) ++ controls.push_back({static_cast(point.positionSubvoxels[0]), ++ static_cast(point.positionSubvoxels[1]), ++ static_cast(point.positionSubvoxels[2]), ++ static_cast(point.radiusSubvoxels)}); ++ const auto built=PassageSpline::BuildSegments(controls,controls.size()-1, ++ 0.25,qPerMetre,{},0.04); ++ if(built.empty())continue; ++ const double total=built.back().alongMetres+built.back().lengthMetres; ++ const auto& first=controls.front(); ++ const auto& last=controls.back(); ++ const auto distanceSquaredTo=[&](const PassageSpline::FPoint& point, ++ const std::array& mouth) ++ { ++ double result=0.0; ++ for(std::size_t axis=0;axis<3;++axis) ++ {const double delta=point[axis]-mouth[axis];result+=delta*delta;} ++ return result; ++ }; ++ const bool upperAtStart=distanceSquaredTo(first,passage.upperMouthSubvoxels) ++ <=distanceSquaredTo(last,passage.upperMouthSubvoxels); ++ const bool portalAtStart=upper?upperAtStart:!upperAtStart; ++ for(const auto& segment:built) ++ { ++ const double along=segment.alongMetres+0.5*segment.lengthMetres; ++ const double portalDistance=portalAtStart?along:total-along; ++ if(portalDistance<=portalExclusionMetres)continue; ++ const PassageSpline::FPoint midpoint={ ++ 0.5*(segment.a[0]+segment.b[0]), ++ 0.5*(segment.a[1]+segment.b[1]), ++ 0.5*(segment.a[2]+segment.b[2]), ++ 0.5*(segment.a[3]+segment.b[3])}; ++ const double minZ=std::min(segment.a[2],segment.b[2]); ++ const double maxZ=std::max(segment.a[2],segment.b[2]); ++ if(maxZ(recipe.layoutMinimumZSubvoxels) ++ ||minZ>static_cast(recipe.layoutMaximumZSubvoxels))continue; ++ spans.push_back({segment,along,total,midpoint[3],portalAtStart}); ++ } ++ } ++ return spans; ++ } ++ ++ inline std::array EvaluateIslandRouteBezier( ++ const std::array& a,const std::array& b, ++ const std::array& waypoint,double t) ++ { ++ const double u=1.0-t; ++ const std::array result={ ++ u*u*u*a[0]+3.0*u*u*t*waypoint[0]+3.0*u*t*t*waypoint[0]+t*t*t*b[0], ++ u*u*u*a[1]+3.0*u*u*t*waypoint[1]+3.0*u*t*t*waypoint[1]+t*t*t*b[1], ++ std::lerp(a[2],b[2],t),t}; ++ return result; ++ } ++ ++ struct FIslandDetourCandidate ++ { ++ std::array,9> centers{}; ++ double horizontalLengthMetres=0.0; ++ double conservativeClearanceMetres=-std::numeric_limits::infinity(); ++ double waypointAngle=0.0; ++ double waypointRadiusMetres=0.0; ++ }; ++ ++ inline std::vector BuildIslandDetourCandidates( ++ const FloatingIslandRecipeV1& recipe,const ResolvedLayoutPassageV1& arrival, ++ const ResolvedLayoutPassageV1& departure, ++ const std::vector& spans) ++ { ++ std::vector candidates; ++ const double qPerMetre=static_cast(WorldAssemblySubvoxelsPerVoxel) ++ *1000000.0/std::max(1,recipe.coreVoxelSizeMicrons); ++ const std::array a={static_cast(arrival.lowerMouthSubvoxels[0]), ++ static_cast(arrival.lowerMouthSubvoxels[1]), ++ static_cast(arrival.lowerMouthSubvoxels[2])}; ++ const std::array b={static_cast(departure.upperMouthSubvoxels[0]), ++ static_cast(departure.upperMouthSubvoxels[1]), ++ static_cast(departure.upperMouthSubvoxels[2])}; ++ for(int angleIndex=0;angleIndex<16;++angleIndex) ++ for(const double radiusMetres:{60.0,72.0,84.0}) ++ { ++ FIslandDetourCandidate candidate; ++ candidate.waypointAngle=2.0*Pi*angleIndex/16.0; ++ candidate.waypointRadiusMetres=radiusMetres; ++ const std::array waypoint={ ++ static_cast(recipe.layoutOriginXSubvoxels) ++ +std::cos(candidate.waypointAngle)*radiusMetres*qPerMetre, ++ static_cast(recipe.layoutOriginYSubvoxels) ++ +std::sin(candidate.waypointAngle)*radiusMetres*qPerMetre, ++ 0.5*(a[2]+b[2])}; ++ constexpr std::size_t CurveSteps=512; ++ std::array arc{}; ++ std::array,CurveSteps+1> curve{}; ++ auto prior=EvaluateIslandRouteBezier(a,b,waypoint,0.0); ++ curve[0]={prior[0],prior[1],prior[2]}; ++ for(std::size_t n=1;n<=CurveSteps;++n) ++ { ++ const auto value=EvaluateIslandRouteBezier(a,b,waypoint, ++ static_cast(n)/CurveSteps); ++ curve[n]={value[0],value[1],value[2]}; ++ const double dx=(value[0]-prior[0])/qPerMetre; ++ const double dy=(value[1]-prior[1])/qPerMetre; ++ arc[n]=arc[n-1]+std::hypot(dx,dy); ++ prior=value; ++ } ++ candidate.horizontalLengthMetres=arc.back(); ++ if(!(candidate.horizontalLengthMetres>1.0))continue; ++ for(std::size_t island=0;island( ++ CurveSteps,static_cast(upper-arc.begin())); ++ const std::size_t lowerIndex=upperIndex?upperIndex-1:0; ++ const double span=arc[upperIndex]-arc[lowerIndex]; ++ const double t=span>1.0e-9?(target-arc[lowerIndex])/span:0.0; ++ auto point=std::array{ ++ std::lerp(curve[lowerIndex][0],curve[upperIndex][0],t), ++ std::lerp(curve[lowerIndex][1],curve[upperIndex][1],t), ++ std::lerp(a[2],b[2],target/arc.back())}; ++ if(island==0)point=a; ++ if(island+1==candidate.centers.size())point=b; ++ for(std::size_t axis=0;axis<3;++axis) ++ candidate.centers[island][axis]=RoundQ(point[axis]); ++ } ++ double minimumClearance=std::numeric_limits::infinity(); ++ for(std::size_t routeIndex=1;routeIndex(candidate.centers[routeIndex-1][axis]); ++ routeB[axis]=static_cast(candidate.centers[routeIndex][axis]); ++ } ++ for(const auto& span:spans) ++ { ++ const double distance=std::sqrt(SegmentDistanceSquared3D( ++ routeA,routeB,span.segment.a,span.segment.b))/qPerMetre; ++ const double passageRadius=span.radiusSubvoxels/qPerMetre; ++ const double capsuleRadius=0.5*recipe.routeConstraints.capsuleHeightSubvoxels/qPerMetre; ++ const double featureReach=recipe.maximumRadiusSubvoxels/qPerMetre ++ +(recipe.contourWarpSubvoxels+IslandBlendMargin(recipe) ++ +recipe.roughnessSubvoxels)/qPerMetre; ++ minimumClearance=std::min(minimumClearance,distance-passageRadius ++ -1.0533-capsuleRadius-featureReach-0.15); ++ } ++ } ++ candidate.conservativeClearanceMetres=std::isfinite(minimumClearance) ++ ?minimumClearance:1.0e6; ++ candidates.push_back(candidate); ++ } ++ std::stable_sort(candidates.begin(),candidates.end(), ++ [](const auto& left,const auto& right) ++ { ++ const bool leftSafe=left.conservativeClearanceMetres>=0.0; ++ const bool rightSafe=right.conservativeClearanceMetres>=0.0; ++ if(leftSafe!=rightSafe)return leftSafe; ++ if(left.horizontalLengthMetres!=right.horizontalLengthMetres) ++ return left.horizontalLengthMetresright.conservativeClearanceMetres; ++ }); ++ return candidates; ++ } ++} ++ + std::optional PlanFloatingIslandCluster( + const FloatingIslandRecipeV1& inputRecipe, std::string& error) + { +@@ -2264,6 +2488,210 @@ std::optional PlanFloatingIslandCluster( + return manifest; + } + ++std::optional PlanFloatingIslandClusterAroundPassages( ++ const FloatingIslandRecipeV1& inputRecipe,const ResolvedLayoutSnapshotV3& layout, ++ std::int32_t strateIndex,std::string& error) ++{ ++ using namespace floating_island_detail; ++ FloatingIslandRecipeV1 recipe=inputRecipe; ++ if(recipe.routeConstraints.version==0) ++ { ++ recipe.routeConstraints.version=1; ++ recipe.routeConstraints.capsuleRadiusSubvoxels=348; ++ recipe.routeConstraints.capsuleHeightSubvoxels=1802; ++ recipe.routeConstraints.maximumGradeQ16=17560; ++ recipe.routeConstraints.sweepSpacingSubvoxels=128; ++ recipe.routeConstraints.supportProbeRadiusSubvoxels=230; ++ recipe.routeConstraints.supportSearchDepthSubvoxels=512; ++ recipe.routeConstraints.minimumClearanceSubvoxels=128; ++ recipe.routeConstraints.minimumSupportProbes=3; ++ } ++ if(recipe.geometryVersion<10||recipe.primaryIslandCount!=9) ++ {error="corridor-aware FloatingIslands requires geometry version 10 and nine route islands";return std::nullopt;} ++ if(!ValidateRecipe(recipe,error))return std::nullopt; ++ ++ const ResolvedLayoutPassageV1* arrival=nullptr; ++ const ResolvedLayoutPassageV1* departure=nullptr; ++ for(const auto& passage:layout.passages) ++ { ++ if(passage.lowerStrateIndex==strateIndex ++ &&passage.upperStrateIndexstrateIndex)departure=&passage; ++ } ++ if(!arrival||!departure) ++ {error="corridor-aware FloatingIslands needs resolved arrival and departure mouths";return std::nullopt;} ++ ++ constexpr double PortalExclusionMetres=13.0; ++ const auto passageSpans=BuildIslandPassageSpans(layout,strateIndex,recipe, ++ PortalExclusionMetres); ++ if(passageSpans.empty()) ++ {error="corridor-aware FloatingIslands found no resolved passage spans inside its strate";return std::nullopt;} ++ auto candidates=BuildIslandDetourCandidates(recipe,*arrival,*departure,passageSpans); ++ const double qPerMetre=static_cast(WorldAssemblySubvoxelsPerVoxel) ++ *1000000.0/std::max(1,recipe.coreVoxelSizeMicrons); ++ const double capsuleHalf=0.5*recipe.routeConstraints.capsuleHeightSubvoxels; ++ const double routeLift=capsuleHalf+recipe.routeConstraints.minimumClearanceSubvoxels+Q/8.0; ++ std::uint32_t tried=0; ++ std::string lastFailure="no outer detour has conservative tube clearance"; ++ for(const auto& candidate:candidates) ++ { ++ if(candidate.conservativeClearanceMetres<0.0)break; ++ ++tried; ++ FloatingIslandManifestV1 manifest; ++ manifest.recipe=recipe; ++ auto& plan=manifest.plan; ++ plan.islands.reserve(recipe.primaryIslandCount); ++ bool candidateValid=true; ++ for(std::uint32_t ordinal=0;ordinal(ordinal)/(recipe.primaryIslandCount-1); ++ const std::int64_t topZ=RoundQ(std::lerp( ++ static_cast(arrival->lowerMouthSubvoxels[2])-routeLift, ++ static_cast(departure->upperMouthSubvoxels[2])-routeLift,t)); ++ auto island=MakeIsland(recipe,ordinal,center[0],center[1],topZ, ++ 0x100000000ull+ordinal); ++ if(ordinal==recipe.anchorIslandOrdinal) ++ island.radiusSubvoxels=std::max(island.radiusSubvoxels, ++ recipe.anchorIslandMinimumRadiusSubvoxels); ++ island.topHalfSubvoxels=std::max(1,island.radiusSubvoxels/5); ++ island.centerSubvoxels[2]=island.topZSubvoxels-island.topHalfSubvoxels; ++ island.bottomZSubvoxels=IslandBottom(recipe,island.topZSubvoxels, ++ island.radiusSubvoxels); ++ SetIslandBounds(recipe,island); ++ if(island.bottomZSubvoxelsrecipe.layoutMaximumZSubvoxels ++ -recipe.boundarySealSubvoxels) ++ {candidateValid=false;lastFailure="detour island escaped the sealed vertical layout";break;} ++ plan.islands.push_back(std::move(island)); ++ } ++ if(!candidateValid)continue; ++ for(std::uint32_t ordinal=0;ordinal+1(b[0]-a[0]); ++ const double dy=static_cast(b[1]-a[1]); ++ const std::uint32_t pieces=static_cast(std::max(1.0, ++ std::ceil(std::hypot(dx,dy)/routeStep))); ++ for(std::uint32_t piece=segment==0?0:1;piece<=pieces;++piece) ++ { ++ const double t=static_cast(piece)/pieces; ++ const std::int64_t x=RoundQ(static_cast(a[0])+dx*t); ++ const std::int64_t y=RoundQ(static_cast(a[1])+dy*t); ++ const auto surface=FindCapsuleFootingTopZ(recipe,plan,x,y); ++ if(!surface) ++ {candidateValid=false;lastFailure="detour route has no full capsule-width island support";break;} ++ WorldAssemblyRoutePointV1 point; ++ point.stableFeatureId=StableId(recipe.seed,0x500000000ull+routeOrdinal, ++ 0x524f555445ull); ++ point.positionSubvoxels={x,y,*surface+static_cast(std::llround(routeLift))}; ++ point.radiusSubvoxels=4*Q; ++ if(segment==0&&piece==0) ++ point.positionSubvoxels=arrival->lowerMouthSubvoxels; ++ if(segment+2==plan.islands.size()&&piece==pieces) ++ point.positionSubvoxels=departure->upperMouthSubvoxels; ++ if(!plan.route.empty()&&plan.route.back().positionSubvoxels==point.positionSubvoxels) ++ continue; ++ plan.route.push_back(point); ++ ++routeOrdinal; ++ } ++ if(!candidateValid)break; ++ } ++ if(!candidateValid||plan.route.size()<2)continue; ++ ++ const double maximumGrade=Q16ToDouble(recipe.routeConstraints.maximumGradeQ16)*0.95; ++ std::vector baseHeights; ++ baseHeights.reserve(plan.route.size()); ++ for(const auto& point:plan.route)baseHeights.push_back(point.positionSubvoxels[2]); ++ std::vector profile=baseHeights; ++ for(std::size_t i=0;i(baseHeights[i]); ++ for(std::size_t j=0;j(plan.route[i].positionSubvoxels[0] ++ -plan.route[j].positionSubvoxels[0]); ++ const double dy=static_cast(plan.route[i].positionSubvoxels[1] ++ -plan.route[j].positionSubvoxels[1]); ++ adjusted=std::max(adjusted,static_cast(baseHeights[j]) ++ -maximumGrade*std::hypot(dx,dy)); ++ } ++ profile[i]=RoundQ(adjusted); ++ } ++ const auto arrivalZ=arrival->lowerMouthSubvoxels[2]; ++ const auto departureZ=departure->upperMouthSubvoxels[2]; ++ if(profile.front()>arrivalZ+Q/2||profile.back()>departureZ+Q/2) ++ {lastFailure="island support profile cannot meet both fixed mouth elevations";continue;} ++ for(std::size_t i=1;i+1lowerMouthSubvoxels; ++ plan.route.back().positionSubvoxels=departure->upperMouthSubvoxels; ++ ++ if(!manifest.finalize(error)) ++ {lastFailure=error;continue;} ++ const FloatingIslandFieldV1 field(manifest.recipe,manifest.plan); ++ const auto routeAudit=AuditFloatingIslandRoute(field); ++ if(!routeAudit.passed) ++ { ++ lastFailure="detour route sweep failed: blocked=" ++ +std::to_string(routeAudit.blockedCapsuleSamples)+" unsupported=" ++ +std::to_string(routeAudit.unsupportedCenterSamples)+" grade=" ++ +std::to_string(routeAudit.gradeFailures)+" detour_angle_deg=" ++ +std::to_string(candidate.waypointAngle*180.0/Pi)+" detour_radius_m=" ++ +std::to_string(candidate.waypointRadiusMetres)+" first_unsupported_q=(" ++ +std::to_string(routeAudit.firstUnsupportedCenterSubvoxels[0])+","+ ++ std::to_string(routeAudit.firstUnsupportedCenterSubvoxels[1])+","+ ++ std::to_string(routeAudit.firstUnsupportedCenterSubvoxels[2])+") segment=" ++ +std::to_string(routeAudit.firstUnsupportedSegment); ++ continue; ++ } ++ const auto clearance=AuditFloatingIslandPassageClearance(field,layout, ++ strateIndex,PortalExclusionMetres); ++ if(!clearance.passed) ++ { ++ lastFailure="passage clearance failed: island=" ++ +std::to_string(clearance.blockedIslandSamples)+" route=" ++ +std::to_string(clearance.blockedRouteSamples)+" mouth_support=" ++ +std::to_string(clearance.arrivalMouthSupportProbes)+"/" ++ +std::to_string(clearance.departureMouthSupportProbes); ++ continue; ++ } ++ const std::string encoded=manifest.ToJson(); ++ const auto roundTrip=FloatingIslandManifestV1::FromJson(encoded,error); ++ if(!roundTrip||roundTrip->ToJson()!=encoded ++ ||roundTrip->contentHash!=manifest.contentHash) ++ {if(error.empty())error="corridor-aware floating-island plan did not canonical-round-trip";return std::nullopt;} ++ std::printf("[FloatingIslandCorridorPlan] strate=%d detours_tried=%u waypoint_angle_deg=%.1f waypoint_radius_m=%.1f route_m=%.1f conservative_clearance_m=%.2f portal_exclusion_m=%.1f islands=%zu arrival_binding=PASS departure_binding=PASS status=PASS\n", ++ strateIndex,tried,candidate.waypointAngle*180.0/Pi, ++ candidate.waypointRadiusMetres,candidate.horizontalLengthMetres, ++ candidate.conservativeClearanceMetres,PortalExclusionMetres, ++ manifest.plan.islands.size()); ++ error.clear(); ++ return manifest; ++ } ++ const auto bestCandidate=std::max_element(candidates.begin(),candidates.end(), ++ [](const auto& left,const auto& right) ++ {return left.conservativeClearanceMetresconservativeClearanceMetres)) ++ +" last="+lastFailure; ++ return std::nullopt; ++} ++ + FloatingIslandFieldV1::FloatingIslandFieldV1(const FloatingIslandRecipeV1& recipe, + const FloatingIslandPlanV1& plan) noexcept : recipe_(&recipe), plan_(&plan) + {} +@@ -2848,6 +3276,107 @@ FloatingIslandRouteAuditResult AuditFloatingIslandRoute(const FloatingIslandFiel + return result; + } + ++FloatingIslandPassageClearanceAuditResult AuditFloatingIslandPassageClearance( ++ const FloatingIslandFieldV1& field,const ResolvedLayoutSnapshotV3& layout, ++ std::int32_t strateIndex,double portalExclusionMetres) ++{ ++ using namespace floating_island_detail; ++ FloatingIslandPassageClearanceAuditResult result; ++ result.portalExclusionMetres=portalExclusionMetres; ++ if(!field.isValid()||!field.recipe()||!field.plan() ++ ||field.plan()->route.size()<2||!std::isfinite(portalExclusionMetres) ++ ||portalExclusionMetres<0.0)return result; ++ const auto& recipe=*field.recipe(); ++ const double qPerMetre=static_cast(WorldAssemblySubvoxelsPerVoxel) ++ *1000000.0/std::max(1,recipe.coreVoxelSizeMicrons); ++ const std::int64_t influenceReach=RoundQ(1.0533*qPerMetre); ++ const std::int64_t capsuleReach=recipe.routeConstraints.capsuleHeightSubvoxels/2; ++ const std::int64_t sampleGuard=RoundQ(0.15*qPerMetre); ++ const auto spans=BuildIslandPassageSpans(layout,strateIndex,recipe, ++ portalExclusionMetres); ++ bool haveClearance=false; ++ std::int64_t minimumExcess=std::numeric_limits::max(); ++ for(const auto& span:spans) ++ { ++ const PassageSpline::FPoint center={ ++ 0.5*(span.segment.a[0]+span.segment.b[0]), ++ 0.5*(span.segment.a[1]+span.segment.b[1]), ++ 0.5*(span.segment.a[2]+span.segment.b[2]), ++ 0.5*(span.segment.a[3]+span.segment.b[3])}; ++ const std::int64_t density=field.densitySubvoxels(RoundQ(center[0]), ++ RoundQ(center[1]),RoundQ(center[2])); ++ const std::int64_t required=RoundQ(center[3])+influenceReach ++ +capsuleReach+sampleGuard; ++ const std::int64_t excess=density-required; ++ minimumExcess=std::min(minimumExcess,excess); ++ haveClearance=true; ++ ++result.passageSamples; ++ if(excess<0)++result.blockedIslandSamples; ++ ++ for(std::size_t routeSegment=0;routeSegment+1route.size();++routeSegment) ++ { ++ const auto& routeA=field.plan()->route[routeSegment].positionSubvoxels; ++ const auto& routeB=field.plan()->route[routeSegment+1].positionSubvoxels; ++ PassageSpline::FPoint a{},b{}; ++ for(std::size_t axis=0;axis<3;++axis) ++ {a[axis]=static_cast(routeA[axis]);b[axis]=static_cast(routeB[axis]);} ++ const double distance=std::sqrt(SegmentDistanceSquared3D(a,b, ++ span.segment.a,span.segment.b)); ++ const double requiredDistance=center[3]+influenceReach ++ +capsuleReach+sampleGuard; ++ ++result.routeSamples; ++ if(distancestrateIndex) ++ departure=&passage; ++ } ++ const auto mouthSupport=[&](const std::array& mouth) ++ { ++ constexpr std::array,5> probes={{{0.0,0.0}, ++ {1.0,0.0},{-1.0,0.0},{0.0,1.0},{0.0,-1.0}}}; ++ const double radius=static_cast(recipe.routeConstraints.supportProbeRadiusSubvoxels); ++ const std::int64_t foot=mouth[2]-recipe.routeConstraints.capsuleHeightSubvoxels/2; ++ const std::int64_t sampleZ=foot-recipe.routeConstraints.minimumClearanceSubvoxels-Q/2; ++ std::uint32_t supported=0; ++ for(const auto& probe:probes) ++ { ++ const auto x=RoundQ(static_cast(mouth[0])+probe[0]*radius); ++ const auto y=RoundQ(static_cast(mouth[1])+probe[1]*radius); ++ if(field.densitySubvoxels(x,y,sampleZ)<0)++supported; ++ } ++ return supported; ++ }; ++ bool bound=false; ++ if(arrival&&departure) ++ { ++ const auto& route=field.plan()->route; ++ const auto close=[](const auto& a,const auto& b) ++ { ++ for(std::size_t axis=0;axis<3;++axis) ++ if(std::abs(a[axis]-b[axis])>2)return false; ++ return true; ++ }; ++ bound=close(route.front().positionSubvoxels,arrival->lowerMouthSubvoxels) ++ &&close(route.back().positionSubvoxels,departure->upperMouthSubvoxels); ++ result.arrivalMouthSupportProbes=mouthSupport(arrival->lowerMouthSubvoxels); ++ result.departureMouthSupportProbes=mouthSupport(departure->upperMouthSubvoxels); ++ } ++ result.passed=bound&&haveClearance&&result.blockedIslandSamples==0 ++ &&result.blockedRouteSamples==0 ++ &&result.arrivalMouthSupportProbes>=recipe.routeConstraints.minimumSupportProbes ++ &&result.departureMouthSupportProbes>=recipe.routeConstraints.minimumSupportProbes; ++ return result; ++} ++ + FloatingIslandThicknessAuditResult AuditFloatingIslandThickness( + const FloatingIslandPlanV1& plan, double minimumDepthToDiameterRatio) + { +diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h +index 1a57f85..98dabf5 100644 +--- a/Standalone/Core/src/ShapeWorldInternal.h ++++ b/Standalone/Core/src/ShapeWorldInternal.h +@@ -1754,9 +1754,81 @@ public: + ?passageAirDistance:density); + if(nativeTunnelFieldDensity!=std::numeric_limits::lowest()) + return std::max(density,passageAirDistance); ++ const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1* landingPlan=nullptr; ++ if(WorldAssembly&&queryStratum&&queryStratum->Resolved ++ &&queryStratum->Resolved->generatorKind=="FloatingIslands") ++ { ++ const auto island=std::find_if(WorldAssembly->floatingIslands.begin(), ++ WorldAssembly->floatingIslands.end(),[&](const auto& candidate) ++ {return candidate.strateIndex==queryStratum->StrateIndex;}); ++ if(island!=WorldAssembly->floatingIslands.end())landingPlan=&*island; ++ } ++ const auto preserveFloatingIslandLandingSupport=[&](float composedDensity) ++ { ++ if(!landingPlan||landingPlan->manifest.recipe.geometryVersion<10) ++ return composedDensity; ++ const auto& constraints=landingPlan->manifest.recipe.routeConstraints; ++ if(constraints.version!=1)return composedDensity; ++ const auto* routeField=std::get_if( ++ &queryStratum->Evaluator); ++ const auto& route=landingPlan->manifest.plan.route; ++ if(!routeField||route.size()<2)return composedDensity; ++ ++ constexpr std::size_t LandingRouteSegments=18; ++ const double supportRadiusMetres=std::min(0.45, ++ constraints.supportProbeRadiusSubvoxels/qPerMetre+0.10); ++ const double queryX=qx,queryY=qy,queryZ=qz; ++ for(std::size_t segment=0;segment+1=LandingRouteSegments ++ &&segment+LandingRouteSegments(b[0]-a[0]); ++ const double dy=static_cast(b[1]-a[1]); ++ const double dz=static_cast(b[2]-a[2]); ++ const double horizontalSquared=dx*dx+dy*dy; ++ if(horizontalSquared<=1.0e-12)continue; ++ const double t=std::clamp(((queryX-a[0])*dx+(queryY-a[1])*dy) ++ /horizontalSquared,0.0,1.0); ++ const double routeX=a[0]+t*dx,routeY=a[1]+t*dy; ++ if(std::hypot(queryX-routeX,queryY-routeY) ++ >supportRadiusMetres*qPerMetre) ++ continue; ++ const double routeCenterZ=a[2]+t*dz; ++ const double footZ=routeCenterZ ++ -0.5*constraints.capsuleHeightSubvoxels; ++ const double shallowDepth=constraints.minimumClearanceSubvoxels+Q/2.0; ++ const double deepDepth=constraints.minimumClearanceSubvoxels ++ +constraints.supportSearchDepthSubvoxels; ++ if(queryZ<=footZ-shallowDepth&&queryZ>=footZ-deepDepth) ++ { ++ const std::int64_t nativeDensity=routeField->densitySubvoxelsAt( ++ static_cast(std::llround(queryX)), ++ static_cast(std::llround(queryY)), ++ static_cast(std::llround(queryZ))); ++ if(nativeDensity<0) ++ return std::min(composedDensity, ++ static_cast(static_cast(nativeDensity)/Q)); ++ } ++ } ++ return composedDensity; ++ }; + if(unionDistance>0.0&&unionShellDistance<0.0&&density>=0.0f) + return static_cast(unionShellDistance/Q); +- return std::max(density,passageAirDistance); ++ float composedDensity=std::max(density,passageAirDistance); ++ if(landingPlan&&landingPlan->manifest.recipe.geometryVersion>=10 ++ &&density>=0.0f&&unionDistance<0.0&&unionShellDistance>0.0) ++ { ++ // In open-air parts of the island stratum, the boolean air union saturates ++ // the passage's positive interior distance to the farther island-air ++ // distance. Keep the nearest positive bore/shell distance so the explicit ++ // passage shell remains measurable without changing any field signs. ++ composedDensity=std::min(composedDensity, ++ std::min(passageAirDistance, ++ static_cast(unionShellDistance/Q))); ++ } ++ return preserveFloatingIslandLandingSupport(composedDensity); + } + + float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const +@@ -7217,9 +7289,11 @@ private: + Density=filteredDensity; + if(routeNativeBlend>0.0&&NativeDensity<0) + { ++ const double routePreservation=Plan->manifest.recipe.geometryVersion>=10 ++ ?routeNativeBlend:routeNativeBlend*0.60; + const double routePreserved=std::lerp(static_cast(Density), + static_cast(std::min(Density,NativeDensity)), +- routeNativeBlend*0.60); ++ routePreservation); + Density=static_cast(std::llround(routePreserved)); + } + // Marching Cubes can retain an isolated solid with no two-metre axis +diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp +index 1c0ed4c..80ad688 100644 +--- a/Standalone/Source/StandaloneViewer.cpp ++++ b/Standalone/Source/StandaloneViewer.cpp +@@ -9208,11 +9208,15 @@ int main(int Argc,char** Argv) + { + const auto PlanStart=std::chrono::steady_clock::now(); + VoxelForgeCore::WorldAssemblyManifest WorldAssembly; ++ VoxelForgeCore::ResolvedLayoutSnapshotV3 ResolvedLayout; + const bool bPlanReady=StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(*App.World,App.OwnerJsonRoot, + Error,WorldAssemblyManifestOutputPath,&WorldAssembly) + &&(!bFloatingIslandsRecipe + ||StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly( +- *App.World,App.OwnerJsonRoot,WorldAssembly,Error)); ++ *App.World,App.OwnerJsonRoot,WorldAssembly,Error)) ++ &&(!bFloatingIslandsRecipe ++ ||StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot( ++ *App.World,WorldAssembly,"floating-islands",ResolvedLayout,Error)); + const double PlanMilliseconds=std::chrono::duration( + std::chrono::steady_clock::now()-PlanStart).count(); + std::printf("[WorldAssemblerPlanTiming] total_ms=%.3f status=%s\n", +diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp +index 1afcff5..f22afe9 100644 +--- a/Standalone/Source/StreamBenchmarkHeadless.cpp ++++ b/Standalone/Source/StreamBenchmarkHeadless.cpp +@@ -1830,31 +1830,11 @@ int main(int argc,char** argv) + else if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype( + *ownerWorld,ownerJsonRoot,error,{},&worldAssembly)) + {std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;} +- if(((!useExportedCorePlan&&options.FloatingIslandsRecipe) +- ||(options.NativeOwnerFloatingIslandsRecipe +- &&worldAssembly.floatingIslands.empty())) ++ if(!useExportedCorePlan&&options.FloatingIslandsRecipe ++ &&!options.NativeOwnerFloatingIslandsRecipe + &&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly( + *ownerWorld,ownerJsonRoot,worldAssembly,error)) + {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;} +- if(!options.WorldAssemblyManifestOutputPath.empty()) +- { +- const std::string manifestText=worldAssembly.ToJson(); +- const auto& outputPath=options.WorldAssemblyManifestOutputPath; +- std::error_code directoryError; +- if(!outputPath.parent_path().empty()) +- std::filesystem::create_directories(outputPath.parent_path(),directoryError); +- if(directoryError) +- {std::fprintf(stderr,"Could not create the complete world assembly manifest output directory.\n");return 2;} +- std::ofstream output(outputPath,std::ios::binary); +- if(!output) +- {std::fprintf(stderr,"Could not open the complete world assembly manifest output path.\n");return 2;} +- output.write(manifestText.data(),static_cast(manifestText.size())); +- if(!output) +- {std::fprintf(stderr,"Could not write the complete world assembly manifest payload.\n");return 2;} +- std::printf("[WorldAssemblerFullManifest] bytes=%zu surface_strates=%zu floating_islands=%zu path=%s status=PASS\n", +- manifestText.size(),worldAssembly.plan.strates.size(),worldAssembly.floatingIslands.size(), +- outputPath.string().c_str()); +- } + if (options.AuditHeightfieldProfile && !RunHeightfieldProfileAudit(worldAssembly)) + { + std::fprintf(stderr,"Profiled SurfaceWorld interval, classification or route audit failed.\n"); +@@ -1886,6 +1866,31 @@ int main(int argc,char** argv) + &&!StandaloneWorldAssemblyPrototype::ValidateExportedCorePlanMatchesOwner( + *ownerWorld,worldAssembly,resolvedLayout,error)) + {std::fprintf(stderr,"Unreal-resolved Core plan/owner slot validation failed: %s\n",error.c_str());return 2;} ++ if(options.NativeOwnerFloatingIslandsRecipe ++ &&worldAssembly.floatingIslands.empty() ++ &&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly( ++ *ownerWorld,ownerJsonRoot,worldAssembly,resolvedLayout,error, ++ true)) ++ {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;} ++ if(!options.WorldAssemblyManifestOutputPath.empty()) ++ { ++ const std::string manifestText=worldAssembly.ToJson(); ++ const auto& outputPath=options.WorldAssemblyManifestOutputPath; ++ std::error_code directoryError; ++ if(!outputPath.parent_path().empty()) ++ std::filesystem::create_directories(outputPath.parent_path(),directoryError); ++ if(directoryError) ++ {std::fprintf(stderr,"Could not create the complete world assembly manifest output directory.\n");return 2;} ++ std::ofstream output(outputPath,std::ios::binary); ++ if(!output) ++ {std::fprintf(stderr,"Could not open the complete world assembly manifest output path.\n");return 2;} ++ output.write(manifestText.data(),static_cast(manifestText.size())); ++ if(!output) ++ {std::fprintf(stderr,"Could not write the complete world assembly manifest payload.\n");return 2;} ++ std::printf("[WorldAssemblerFullManifest] bytes=%zu surface_strates=%zu floating_islands=%zu path=%s status=PASS\n", ++ manifestText.size(),worldAssembly.plan.strates.size(),worldAssembly.floatingIslands.size(), ++ outputPath.string().c_str()); ++ } + if(!std::holds_alternative(ownerJsonRoot.Get("motif_family_override").Data)) + { + std::vector requests; +diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h +index 4e20068..70c4c61 100644 +--- a/Standalone/Source/WorldAssemblyPrototype.h ++++ b/Standalone/Source/WorldAssemblyPrototype.h +@@ -1366,7 +1366,9 @@ namespace StandaloneWorldAssemblyPrototype + } + + inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner, +- VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,std::string& error) ++ VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly, ++ VoxelForgeCore::ResolvedLayoutSnapshotV3& resolvedLayout, ++ std::string& error,bool planAroundPassages=false) + { + if(!owner.Manager) + {error="floating-island assembly needs the resolved standalone strate manager";return false;} +@@ -1386,7 +1388,8 @@ namespace StandaloneWorldAssemblyPrototype + FIntVector(0,0,islandSlot->TopChunkZ)); + const FStratePassageConfig& passage=islandSlot->Definition->PassageConfig; + recipe.seed=static_cast(owner.Seed); +- recipe.geometryVersion=8; ++ recipe.geometryVersion=planAroundPassages?10:8; ++ if(planAroundPassages)recipe.primaryIslandCount=9; + recipe.terrainFollowingRoute=static_cast(passage.Style)==0; + recipe.ownerVoxelSizeMicrons=owner.OwnerVoxelSizeMicrons; + recipe.coreVoxelSizeMicrons=owner.CoreVoxelSizeMicrons; +@@ -1406,43 +1409,79 @@ namespace StandaloneWorldAssemblyPrototype + recipe.islandDensityQ16=WorldAssemblyQ16(std::clamp(params.IslandDensity,0.0,1.0)); + recipe.spacingSubvoxels=WorldAssemblyFixed(params.IslandSpacing*ownerToCoreScale); + recipe.minimumRadiusSubvoxels=WorldAssemblyFixed(params.IslandMinRadius*ownerToCoreScale); +- recipe.maximumRadiusSubvoxels=WorldAssemblyFixed(params.IslandMaxRadius*ownerToCoreScale); ++ recipe.maximumRadiusSubvoxels=WorldAssemblyFixed((planAroundPassages ++ ?28.0:params.IslandMaxRadius)*ownerToCoreScale); + recipe.thicknessRatioQ16=WorldAssemblyQ16(params.ThicknessRatio); + recipe.verticalJitterQ16=WorldAssemblyQ16(params.VerticalJitter); + recipe.topFlattenQ16=WorldAssemblyQ16(params.TopFlatten); +- recipe.blendRadiusSubvoxels=WorldAssemblyFixed(params.SDFBlendRadius*ownerToCoreScale); ++ recipe.blendRadiusSubvoxels=planAroundPassages ++ ?2*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel ++ :WorldAssemblyFixed(params.SDFBlendRadius*ownerToCoreScale); ++ if(planAroundPassages) ++ recipe.contourWarpSubvoxels=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + recipe.roughnessSubvoxels=WorldAssemblyFixed(params.SurfaceRoughness*ownerToCoreScale); ++ if(planAroundPassages) ++ recipe.roughnessSubvoxels=std::min(recipe.roughnessSubvoxels, ++ VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/2); + recipe.boundarySealSubvoxels=WorldAssemblyFixed(params.BoundarySealThickness*ownerToCoreScale); ++ if(planAroundPassages) ++ { ++ recipe.routeConstraints.version=1; ++ recipe.routeConstraints.capsuleRadiusSubvoxels=348; ++ recipe.routeConstraints.capsuleHeightSubvoxels=1802; ++ recipe.routeConstraints.maximumGradeQ16=17560; ++ recipe.routeConstraints.sweepSpacingSubvoxels=128; ++ recipe.routeConstraints.supportProbeRadiusSubvoxels=230; ++ recipe.routeConstraints.supportSearchDepthSubvoxels=512; ++ recipe.routeConstraints.minimumClearanceSubvoxels=128; ++ recipe.routeConstraints.minimumSupportProbes=3; ++ } + const auto ownerVoxelsToCoreQ=[&](double value) + {return static_cast(std::llround(value*owner.OwnerVoxelSizeMicrons + *VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/owner.CoreVoxelSizeMicrons));}; + recipe.anchorIslandOrdinal=recipe.primaryIslandCount-1; + recipe.anchorPlacementRadiusSubvoxels=ownerVoxelsToCoreQ(passage.DistanceMax); +- const std::int64_t spiralRadius=ownerVoxelsToCoreQ(passage.SpiralRadius); +- const std::int64_t mouthBore=std::max(ownerVoxelsToCoreQ(passage.MouthRadius), +- ownerVoxelsToCoreQ(36.0)); +- const std::int64_t bore=std::max(ownerVoxelsToCoreQ(passage.MidRadius), +- ownerVoxelsToCoreQ(24.0)); +- const std::int64_t wallReserve=ownerVoxelsToCoreQ(20.0); +- // Retain a 4.5-metre mouth for capsule clearance while keeping the long +- // ramp at three metres to leave more rock around the first coil. +- recipe.anchorMouthOffsetSubvoxels=ownerVoxelsToCoreQ(40.0); +- const std::int64_t supportOverhang=ownerVoxelsToCoreQ(16.0); +- const std::int64_t rootTaperReserve=ownerVoxelsToCoreQ(10.0); +- const std::int64_t mouthEnvelope=recipe.anchorMouthOffsetSubvoxels+mouthBore; +- const std::int64_t treadEnvelope=spiralRadius+bore+supportOverhang; +- recipe.anchorRootRadiusSubvoxels=std::max(mouthEnvelope,treadEnvelope) +- +wallReserve+rootTaperReserve; +- recipe.anchorIslandMinimumRadiusSubvoxels=std::max(recipe.minimumRadiusSubvoxels, +- std::max(recipe.anchorMouthOffsetSubvoxels+mouthBore+ownerVoxelsToCoreQ(4.0), +- recipe.anchorRootRadiusSubvoxels+wallReserve)); ++ if(planAroundPassages) ++ { ++ recipe.anchorMouthOffsetSubvoxels=ownerVoxelsToCoreQ(4.0); ++ recipe.anchorRootRadiusSubvoxels=std::max(ownerVoxelsToCoreQ(16.0), ++ recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels ++ +VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); ++ recipe.anchorIslandMinimumRadiusSubvoxels=std::max(recipe.maximumRadiusSubvoxels, ++ recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels ++ +VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); ++ } ++ else ++ { ++ const std::int64_t spiralRadius=ownerVoxelsToCoreQ(passage.SpiralRadius); ++ const std::int64_t mouthBore=std::max(ownerVoxelsToCoreQ(passage.MouthRadius), ++ ownerVoxelsToCoreQ(36.0)); ++ const std::int64_t bore=std::max(ownerVoxelsToCoreQ(passage.MidRadius), ++ ownerVoxelsToCoreQ(24.0)); ++ const std::int64_t wallReserve=ownerVoxelsToCoreQ(20.0); ++ recipe.anchorMouthOffsetSubvoxels=ownerVoxelsToCoreQ(40.0); ++ const std::int64_t supportOverhang=ownerVoxelsToCoreQ(16.0); ++ const std::int64_t rootTaperReserve=ownerVoxelsToCoreQ(10.0); ++ const std::int64_t mouthEnvelope=recipe.anchorMouthOffsetSubvoxels+mouthBore; ++ const std::int64_t treadEnvelope=spiralRadius+bore+supportOverhang; ++ recipe.anchorRootRadiusSubvoxels=std::max(mouthEnvelope,treadEnvelope) ++ +wallReserve+rootTaperReserve; ++ recipe.anchorIslandMinimumRadiusSubvoxels=std::max(recipe.minimumRadiusSubvoxels, ++ std::max(recipe.anchorMouthOffsetSubvoxels+mouthBore+ownerVoxelsToCoreQ(4.0), ++ recipe.anchorRootRadiusSubvoxels+wallReserve)); ++ } + recipe.anchorRootBottomZSubvoxels=recipe.layoutMinimumZSubvoxels + +recipe.boundarySealSubvoxels; + +- auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,error); ++ auto planned=planAroundPassages ++ ?VoxelForgeCore::PlanFloatingIslandClusterAroundPassages( ++ recipe,resolvedLayout,islandSlot->StrateIndex,error) ++ :VoxelForgeCore::PlanFloatingIslandCluster(recipe,error); + if(!planned)return false; + const VoxelForgeCore::FloatingIslandFieldV1 routeField( + planned->recipe,planned->plan); ++ const double metresPerSubvoxel=static_cast(owner.CoreVoxelSizeMicrons) ++ /(1000000.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + const auto routeAudit=VoxelForgeCore::AuditFloatingIslandRoute(routeField); + std::printf("[FloatingIslandRouteAudit] points=%zu centers=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu status=%s\n", + planned->plan.route.size(), +@@ -1453,8 +1492,27 @@ namespace StandaloneWorldAssemblyPrototype + static_cast(routeAudit.unsupportedCenterSamples), + static_cast(routeAudit.gradeFailures), + routeAudit.passed?"PASS":"FAIL"); +- const double metresPerSubvoxel=static_cast(owner.CoreVoxelSizeMicrons) +- /(1000000.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); ++ VoxelForgeCore::FloatingIslandPassageClearanceAuditResult passageAudit; ++ if(planAroundPassages) ++ { ++ constexpr double PortalExclusionMetres=13.0; ++ passageAudit=VoxelForgeCore::AuditFloatingIslandPassageClearance( ++ routeField,resolvedLayout,islandSlot->StrateIndex,PortalExclusionMetres); ++ std::printf("[FloatingIslandPassageClearance] strate=%d tube_samples=%llu island_blocked=%llu route_checks=%llu route_blocked=%llu arrival_support=%u/5 departure_support=%u/5 clearance_excess_m=%.3f portal_exclusion_m=%.1f influence_reach_m=1.053 capsule_bound_m=%.3f status=%s\n", ++ islandSlot->StrateIndex, ++ static_cast(passageAudit.passageSamples), ++ static_cast(passageAudit.blockedIslandSamples), ++ static_cast(passageAudit.routeSamples), ++ static_cast(passageAudit.blockedRouteSamples), ++ passageAudit.arrivalMouthSupportProbes, ++ passageAudit.departureMouthSupportProbes, ++ passageAudit.minimumExcessClearanceSubvoxels*metresPerSubvoxel, ++ passageAudit.portalExclusionMetres, ++ planned->recipe.routeConstraints.capsuleHeightSubvoxels*metresPerSubvoxel*0.5, ++ passageAudit.passed?"PASS":"FAIL"); ++ if(!routeAudit.passed||!passageAudit.passed) ++ {error="corridor-aware floating-island route or passage audit failed";return false;} ++ } + const double bedrockTopMetres=(minimumZ+recipe.boundarySealSubvoxels)*metresPerSubvoxel; + const double ceilingBottomMetres=(maximumZ-recipe.boundarySealSubvoxels)*metresPerSubvoxel; + for(const auto& island:planned->plan.islands) +@@ -1473,18 +1531,82 @@ namespace StandaloneWorldAssemblyPrototype + assembly.floatingIslands.push_back(std::move(islands)); + if(!assembly.finalize(error))return false; + ++ if(planAroundPassages) ++ { ++ auto resolvedStratum=std::find_if(resolvedLayout.strates.begin(), ++ resolvedLayout.strates.end(),[&](const auto& item) ++ {return item.strateIndex==islandSlot->StrateIndex;}); ++ if(resolvedStratum==resolvedLayout.strates.end()) ++ {error="resolved FloatingIslands stratum disappeared before native anchor attachment";return false;} ++ resolvedStratum->recipeVersion=assembly.floatingIslands.back().manifest.recipe.geometryVersion; ++ resolvedStratum->anchors.erase(std::remove_if(resolvedStratum->anchors.begin(), ++ resolvedStratum->anchors.end(),[](const auto& anchor) ++ {return anchor.kind=="island_anchor"||anchor.kind=="passage_anchor" ++ ||anchor.kind=="route_point";}),resolvedStratum->anchors.end()); ++ const auto& nativePlan=assembly.floatingIslands.back().manifest.plan; ++ const auto& nativeRecipe=assembly.floatingIslands.back().manifest.recipe; ++ for(std::size_t n=0;nanchors.push_back(std::move(anchor)); ++ if(n==nativeRecipe.anchorIslandOrdinal) ++ { ++ VoxelForgeCore::ResolvedLayoutAnchorV2 passageAnchor; ++ passageAnchor.kind="passage_anchor"; ++ passageAnchor.stableId="passage-anchor/island/"+std::to_string(n); ++ passageAnchor.units="1/256 Core voxel"; ++ passageAnchor.values={island.centerSubvoxels[0],island.centerSubvoxels[1], ++ nativeRecipe.anchorRootRadiusSubvoxels, ++ nativeRecipe.anchorMouthOffsetSubvoxels, ++ nativeRecipe.anchorPlacementRadiusSubvoxels, ++ nativeRecipe.anchorRootBottomZSubvoxels,island.radiusSubvoxels}; ++ if(!nativePlan.route.empty()) ++ { ++ const auto& mouth=nativePlan.route.back().positionSubvoxels; ++ passageAnchor.values.insert(passageAnchor.values.end(), ++ {mouth[0],mouth[1],mouth[2]}); ++ } ++ resolvedStratum->anchors.push_back(std::move(passageAnchor)); ++ } ++ } ++ for(std::size_t n=0;nanchors.push_back(std::move(anchor)); ++ } ++ if(!resolvedLayout.finalize(error))return false; ++ } ++ + VFJson::Value assemblyJson; + if(!VFJson::Parser(assembly.ToJson()).Parse(assemblyJson,error))return false; + auto* rootObject=std::get_if(&root.Data); + if(!rootObject){error="owner snapshot root is not a JSON object";return false;} + (*rootObject)["world_assembly"]=std::move(assemblyJson); +- std::printf("[FloatingIslandWorldAssembly] seed=%d strate_index=%d chunks=%d..%d island_count=%zu recipe_hash=%016llx status=PASS\n", ++ std::printf("[FloatingIslandWorldAssembly] seed=%d strate_index=%d chunks=%d..%d island_count=%zu dispatch=FloatingIslands-native recipe_hash=%016llx status=PASS\n", + owner.Seed,islandSlot->StrateIndex,islandSlot->TopChunkZ,islandSlot->BottomChunkZ, + assembly.floatingIslands.back().manifest.plan.islands.size(), + static_cast(assembly.floatingIslands.back().manifest.recipe.contentHash)); + return true; + } + ++ inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner, ++ VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly, ++ std::string& error) ++ { ++ VoxelForgeCore::ResolvedLayoutSnapshotV3 unusedLayout; ++ return AttachFloatingIslandWorldAssembly(owner,root,assembly,unusedLayout,error,false); ++ } ++ + inline const char* GeneratorKind(ECaveGeneratorType type) + { + switch(type) +@@ -1964,6 +2086,7 @@ namespace StandaloneWorldAssemblyPrototype + :it->manifest.plan.route.size()-1; + std::size_t resolvedRoutePointCount=it->manifest.plan.route.size(); + if(it->manifest.recipe.geometryVersion>=5 ++ &&it->manifest.recipe.geometryVersion<10 + &&!it->manifest.plan.route.empty() + &&it->manifest.recipe.anchorIslandOrdinalmanifest.plan.islands.size()) + { diff --git a/Standalone/Research/WorldAssemblerM3/Linux147/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux147/REPORT.md new file mode 100644 index 0000000..5902769 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux147/REPORT.md @@ -0,0 +1,52 @@ +# Linux147 — passage audits on the Core cave planner + +**Result:** The L145 ripple-distance audit and L146 passage-interior air gate pass across all 16 seeds in both requested matrices. The two matrices each measured 5,171,796 capsule samples inside passage interiors with **zero solid samples**. Their no-passage controls detected solid in 48/48 passage interiors, and their masked-gap ripple controls passed 48/48 times. No generation code changed. + +The only non-green result is seed 2's known interface 2–3 shell/seam failure. Vertical Shafts remains 15/16, matching L139. The Tunnel Network comparison is 15/16 against L135's earlier 16/16; seed 2 is already known to fail after the L139 shaft change. It is not a failure introduced by either new audit gate. + +## Audit changes + +- `StreamBenchmarkPipeline.h` now requires zero solid capsule samples inside each passage interior. It runs the same interior classifier over the passage-stripped field and requires positive solid detections for every passage. +- The ripple fitter keeps each valid wall sample's actual route distance instead of compressing distances across missing probes. Its synthetic 5 m masked-gap control measures a 0.0581 m sine ripple and rejects a smooth cubic at 0.0000 m. +- No Core planner, evaluator, Unreal `Source/`, or generation code changed. `git diff --check` passed. + +## Per-seed verdicts + +Tunnel Network compares with the L135 16/16 composed sweep; Vertical Shafts compares with the L139 15/16 sweep. `P` means pass and `F` means fail. + +| Seed | Tunnel Network baseline → L147 | Vertical Shafts baseline → L147 | Verdict change | +|---:|:---:|:---:|---| +| 0 | P → P | P → P | None | +| 1 | P → P | P → P | None | +| 2 | P → F | F → F | Tunnel comparison: L135 predates the L139 shaft change. Current seed 2 has the existing interface 2–3 shell/seam failure; the new interior and ripple gates pass. Vertical Shafts remains the same known failure. | +| 3 | P → P | P → P | None | +| 4 | P → P | P → P | None | +| 5 | P → P | P → P | None | +| 6 | P → P | P → P | None | +| 7 | P → P | P → P | None | +| 8 | P → P | P → P | None | +| 9 | P → P | P → P | None | +| 10 | P → P | P → P | None | +| 11 | P → P | P → P | None | +| 12 | P → P | P → P | None | +| 13 | P → P | P → P | None | +| 14 | P → P | P → P | None | +| 15 | P → P | P → P | None | + +There were no newly passing seeds and no verdict changes caused by the added gates. For seed 2, all three passages reported `interior_air=PASS`; its interface 2–3 shell had eight `wall_missing` samples at route stations 489.627–490.321 m. The samples were outside the passage interior. The first was at `(-21.678, -1.714, -888.614) m`, with composed and native density `+2.341949`, expected wall classification, and wall-shell margin `−0.297164 m`. The passage and seam summaries fail as they did in L139. No interior-solid root cause trace was needed because the measured interior-solid count is zero. + +## Measurements and checks + +- Clang 19 standalone build (`-O2`, `-ffp-contract=off`, four jobs) passed. L147 source stamp: `c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f`. +- Tunnel Network: seeds 0–15, one composed run per seed, 430.715 s summed process time (13.542–51.457 s per seed); 15/16 passed. Logs and resolved layouts are under `Saved/Linux/l147/seeds/tunnel-network/`. +- Vertical Shafts: seeds 0–15, one composed run per seed, 392.185 s summed process time (13.069–39.769 s per seed); 15/16 passed. Logs and resolved layouts are under `Saved/Linux/l147/seeds/vertical-shafts/`. +- Across each matrix: 5,171,796 passage-interior capsule samples, 0 solid; 48/48 no-passage controls detected interior solid; 48/48 masked-gap ripple controls passed. Passage walkability, ripple, and all other existing gates passed for seeds other than the known seed-2 shell/seam failure. +- Fast tier passed in 357.3 s: `Saved/Linux/l147/fast/fast-summary.json`. The first invocation stopped before running checks because the verifier's expected binary path was empty; it passed after using the completed L147 build there. +- Owner identity comparison against the HEAD-stamped L140 binary passed: all pipeline hash lines, plan hash `538ee11456eb0dab`, resolved-layout identity `resolved-layout-v3-fnv64:d310515572977ebf`, and resolved-layout SHA-256 `50a0342902b6b897c5746fd86fac559244151894155cab6db0822229bfb877e4` matched. Details: `Saved/Linux/l147/identity/summary.json`. +- `git diff --check` passed. No L80 timing, Unreal build, Windows build, or cross-compiler determinism run was requested or performed. + +Runners: [`RunL147SeedSweep.py`](RunL147SeedSweep.py) and [`RunL147OwnerIdentity.py`](RunL147OwnerIdentity.py). No Unreal `Source/` files changed. + +## Remaining risk + +The seed-2 interface 2–3 shell/seam failure remains unresolved. It does not put solid samples inside the passage interior and is unchanged from the known L139 Vertical Shafts result. This round made no generation fix. diff --git a/Standalone/Research/WorldAssemblerM3/Linux147/RunL147OwnerIdentity.py b/Standalone/Research/WorldAssemblerM3/Linux147/RunL147OwnerIdentity.py new file mode 100644 index 0000000..0333fbc --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux147/RunL147OwnerIdentity.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Compare owner and resolved-planner hashes against the exact L147 parent build.""" +from __future__ import annotations + +import hashlib +import json +import re +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l147/identity" +DATA = ROOT / "Standalone/data/owner_world.json" +HEAD_BIN = ROOT / "Saved/Linux/l140/bin/VoxelForgeCoreHeadless-l140-clang" +L147_BIN = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l147-clang" +HEAD_STAMP = "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da" +L147_STAMP = "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +PIPELINE_HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +PLAN_RE = re.compile(r"^\[WorldAssemblerPlanSerialization\].*plan_hash=([0-9a-f]+).*$", re.M) + + +def run(label: str, binary: Path, expected_stamp: str) -> dict[str, object]: + if not binary.is_file(): + raise SystemExit(f"missing {label} binary: {binary}") + log = OUT / f"{label}.log" + layout = OUT / f"{label}-layout.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=0", "--force-generate", "--seed=0", + "--recipe=surface-world", "--stream-hash", + f"--resolved-layout-output={layout}"] + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + pipeline_hashes = PIPELINE_HASH_RE.findall(content) + plan = PLAN_RE.search(content) + layout_bytes = layout.read_bytes() if layout.is_file() else b"" + result = { + "label": label, + "exit": proc.returncode, + "stamp": stamp.group(1) if stamp else None, + "stamp_matches_expected": stamp is not None and stamp.group(1) == expected_stamp, + "pipeline_hashes": pipeline_hashes, + "plan_hash": plan.group(1) if plan else None, + "resolved_layout_sha256": hashlib.sha256(layout_bytes).hexdigest() if layout_bytes else None, + "resolved_layout_identity": None, + "log": str(log.relative_to(ROOT)), + "layout": str(layout.relative_to(ROOT)), + } + if layout_bytes: + result["resolved_layout_identity"] = json.loads(layout_bytes).get("stable_identity") + return result + + +def main() -> int: + OUT.mkdir(parents=True, exist_ok=True) + head = run("head", HEAD_BIN, HEAD_STAMP) + l147 = run("l147", L147_BIN, L147_STAMP) + passed = (head["exit"] == 0 and l147["exit"] == 0 + and head["stamp_matches_expected"] and l147["stamp_matches_expected"] + and bool(head["pipeline_hashes"]) + and head["pipeline_hashes"] == l147["pipeline_hashes"] + and head["plan_hash"] is not None and head["plan_hash"] == l147["plan_hash"] + and head["resolved_layout_sha256"] is not None + and head["resolved_layout_sha256"] == l147["resolved_layout_sha256"] + and head["resolved_layout_identity"] == l147["resolved_layout_identity"]) + summary = {"status": "PASS" if passed else "FAIL", "head": head, "l147": l147, + "owner_pipeline_hashes_identical": head["pipeline_hashes"] == l147["pipeline_hashes"], + "plan_hash_identical": head["plan_hash"] == l147["plan_hash"], + "resolved_layout_identical": + head["resolved_layout_sha256"] == l147["resolved_layout_sha256"]} + path = OUT / "summary.json" + path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L147OwnerIdentity] status={summary['status']} " + f"owner_hashes={summary['owner_pipeline_hashes_identical']} " + f"planner_hash={summary['plan_hash_identical']} " + f"resolved_layout={summary['resolved_layout_identical']} " + f"head_stamp={head['stamp']} l147_stamp={l147['stamp']} " + f"summary={path.relative_to(ROOT)}") + return 0 if passed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux147/RunL147SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux147/RunL147SeedSweep.py new file mode 100644 index 0000000..b2b539d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux147/RunL147SeedSweep.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Run the L147 composed passage audit for all 16 owner seeds.""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l147/seeds" +BIN = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l147-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +BASELINES = { + "tunnel-network": ROOT / "Saved/Linux/l135/optimized-seed-sweep.json", + "vertical-shafts": ROOT / "Saved/Linux/l139/seeds/summary.json", +} +DISPATCH = { + "tunnel-network": re.compile( + r"^\[DensityDispatch\] strate=2 generator=TunnelNetwork " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), + "vertical-shafts": re.compile( + r"^\[DensityDispatch\] strate=3 generator=VerticalShafts " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), +} +ROUTES = { + "tunnel-network": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=2 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=3 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), +} +CONTROLS = { + "tunnel-network": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[VerticalShaftsBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} +REQUIRED = { + "passages": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "passage_interior": re.compile( + r"^\[PassageInteriorCapsuleAuditSummary\].*status=PASS$", re.M), +} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kind", choices=tuple(BASELINES), required=True) + args = parser.parse_args() + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L147 Clang binary or composed owner fixture") + baseline_path = BASELINES[args.kind] + baseline = json.loads(baseline_path.read_text(encoding="utf-8")) + baseline_results = {int(row["seed"]): row for row in baseline["results"]} + OUT.mkdir(parents=True, exist_ok=True) + mode_out = OUT / args.kind + mode_out.mkdir(parents=True, exist_ok=True) + results = [] + partial = mode_out / "runs-partial.json" + for seed in range(16): + log = mode_out / f"seed{seed}.log" + layout = mode_out / f"seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=0", "--force-generate", f"--seed={seed}", + "--recipe=surface-world", "--density-dispatch-report", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP + checks["dispatch"] = bool(DISPATCH[args.kind].search(content)) + checks["route"] = bool(ROUTES[args.kind].search(content)) + checks["blocked_route_control"] = bool(CONTROLS[args.kind].search(content)) + checks["three_passage_interiors"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*interior_air=PASS", content, re.M)) == 3 + checks["three_interior_negative_controls"] = len(re.findall( + r"^\[ResolvedPassageInteriorNegativeControl\].*detected=true status=PASS$", + content, re.M)) == 3 + checks["three_ripple_gap_controls"] = len(re.findall( + r"^\[PassageRippleSamplingControl\].*status=PASS$", content, re.M)) == 3 + checks["three_blocked_passage_controls"] = len(re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3 + checks["passage_count"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*walkability=PASS$", content, re.M)) == 3 + checks["layout_output"] = layout.is_file() + passed = proc.returncode == 0 and all(checks.values()) + prior = baseline_results.get(seed, {}) + prior_passed = bool(prior.get("passed", False)) + change = ("newly failing" if prior_passed and not passed else + "newly passing" if not prior_passed and passed else + "still passing" if prior_passed else "still failing") + failed_checks = [name for name, ok in checks.items() if not ok] + reason = (", ".join(failed_checks) if failed_checks else + "all previous and new gates pass" if passed else + f"process exit {proc.returncode}") + row = {"kind": args.kind, "seed": seed, "prior_verdict": "PASS" if prior_passed else "FAIL", + "verdict": "PASS" if passed else "FAIL", "change": change, + "reason": reason, "exit": proc.returncode, "checks": checks, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L147SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + partial.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + passed_count = sum(row["verdict"] == "PASS" for row in results) + summary = {"kind": args.kind, "source_stamp": STAMP, + "baseline": str(baseline_path.relative_to(ROOT)), "seeds": list(range(16)), + "passed": passed_count, "failed": 16 - passed_count, "results": results} + summary_path = mode_out / "summary.json" + summary_path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L147SeedSweepSummary] kind={args.kind} passed={passed_count}/16 " + f"failed={16-passed_count} status={'PASS' if passed_count == 16 else 'FAIL'}", + flush=True) + return 0 if passed_count == 16 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux148/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux148/REPORT.md new file mode 100644 index 0000000..412277d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux148/REPORT.md @@ -0,0 +1,29 @@ +# Linux148 — passage exclusion envelope + +**Result:** The shared plan-time bounds audit and its negative control work, and the L147 seed-2 carver is identified. The 8 m mouth joint is too small for the existing route candidates: both composed matrices pass only **7/16** seeds. Per the three-failure stop rule, this round stops without another planner adjustment and is not ready to promote. + +## Root cause and rule + +The L147 Vertical Shafts seed-2 wall sample at `(-21.678, -1.714, -888.614) m` is carved by `vertical-shafts/strate-3/route-edge-0`. At that point its native tube/floor SDF combines to about `-0.553 m`, predicting `+2.21` Core voxels of air; the observed native density is `+2.341949`. No other cave primitive has a negative native SDF there. The edge's start anchor is `passage/2-3/0/lower-mouth`, confirming this sample lies in its passage connection. + +The shared gate samples the resolved passage spline, expands it by the 1.053 m shell/influence reach, and checks each layer primitive's conservative AABB against that envelope. It permits overlap only for a named passage mouth, within an 8 m sphere (the planner's 2 m overlap plus 6 m lead). Other overlap rejects the graph before it is written into the layout. A forced AABB at a passage centerline is rejected as the negative control. + +The shared Core API is generic; this round wires it through `PlanResolvedCavernNetworkV1`, used by Tunnel Network and Vertical Shafts. Other layer planners must call it when they resolve their primitive bounds. + +The mouth radius is the limiting assumption. Existing route-edge bounds in Tunnel Network seeds 0, 1, 4, and 7 meet passage `1-2` at 12.5–19.6 m from the mouth; seed 8 also has room bounds at 17.5–20.5 m. These are connected route candidates, but they fall outside the 8 m sphere. In seed 10, `vertical-shafts/strate-3/shaft-1/primitive-0` also overlaps passage `2-3`; it has no mouth association and remains a separate candidate to resolve. + +## Measurements and checks + +- Clang 19 and GCC builds passed with source stamp `593551a985ec0d1acc271260e4c8f15a5261c90edb60e33a56ed6df27e269ffb`. +- Tunnel Network: **7/16**; full runs were seeds 2, 3, 5, 6, 9, 11, and 12. Vertical Shafts: **7/16**, the same seeds. The other nine seeds in each matrix were rejected by the plan-time exclusion audit. The run logs and per-seed layouts are under `Saved/Linux/l148/seeds/`. +- Across the seven full runs in each matrix: 2,186,289 passage-interior capsule samples, zero solid; 53,262 shell wall samples, zero violations. Passage, ripple, and negative-control gates passed on those runs. Seed 2's passage `2-3` had zero wall samples outside the mouth joint, so its shell result has no wall coverage beyond that joint. +- The passing layouts match their L147 stable identities. Seed 2 is `resolved-layout-v3-fnv64:64988eac83c0220c`. +- Fast tier passed in 362.6 s: `Saved/Linux/l148/fast/fast-summary.json`. +- Clang/GCC W1/W8/W12 passed on seed 2: 13 pipeline hash lines and the resolved-layout identity matched across all six runs. Summary: `Saved/Linux/l148/determinism/summary.json`. +- `git diff --check` and Python syntax checks passed. No Unreal build was run. + +The successful outputs remain evaluator v237 and layout-identical to L147. No baselines or captures were added. The nine rejected seeds produced no resolved layout, so there is no accepted before/after scene to capture. This is an incomplete, unpromoted planner change. + +## Remaining risk + +The audit catches conservative-bounds conflicts, but the current mouth sphere does not cover the existing connected-route bounds, and the optional shaft collision needs a deterministic candidate decision. The 16/16 requirement is unmet. No Unreal `Source/` files changed. diff --git a/Standalone/Research/WorldAssemblerM3/Linux148/RunL148Determinism.py b/Standalone/Research/WorldAssemblerM3/Linux148/RunL148Determinism.py new file mode 100644 index 0000000..c9ec119 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux148/RunL148Determinism.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Compare L148 composed owner hashes and layout identity at W1/W8/W12.""" +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/l148/determinism" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "clang": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l148-clang", + "gcc": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l148-gcc", +} +CAMERA = "-20.062500,-5.068359,-900.353516,0,0" +HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|MaterialHash|LODHash|ExactQueryHash)\].*$", + re.M) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +EXCLUSION_RE = re.compile( + r"^\[CavernPassageExclusionAudit\] strate=[23] primitives=\d+ " + r"outside_joint=0 mouth_joint=\d+ negative_controls=1/1 " + r"first_primitive=none first_passage=none status=PASS$", re.M) + + +def main() -> int: + if any(not path.is_file() for path in (DATA, *BINARIES.values())): + raise SystemExit("missing L148 Clang/GCC binary or composed owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + rows = [] + reference_hashes: tuple[str, ...] | None = None + reference_identity: str | None = None + reference_stamp: str | None = None + for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + name = f"determinism-{compiler}-w{workers}" + log, layout = OUT / f"{name}.log", OUT / f"{name}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=0", + f"--camera={CAMERA}", "--stream-hash", "--force-generate", "--seed=2", + "--recipe=surface-world", "--render-distance-chunks=0", + "--density-dispatch-report", "--audit-resolved-layout-passages", + "--audit-passage-tile-seams", f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp_match = STAMP_RE.search(content) + stamp = stamp_match.group(1) if stamp_match else None + hashes = tuple(HASH_RE.findall(content)) + identity = (json.loads(layout.read_text(encoding="utf-8")).get("stable_identity") + if layout.is_file() else None) + audits = EXCLUSION_RE.findall(content) + shell_ok = bool(re.search( + r"^\[PassageShellAuditSummary\].*status=PASS$", content, re.M)) + passed = (proc.returncode == 0 and stamp is not None and bool(hashes) + and len(audits) == 2 and shell_ok and bool(identity)) + if reference_stamp is None and passed: + reference_stamp, reference_hashes, reference_identity = stamp, hashes, identity + else: + passed = (passed and stamp == reference_stamp and hashes == reference_hashes + and identity == reference_identity) + row = {"compiler": compiler, "workers": workers, "exit": proc.returncode, + "stamp": stamp, "hash_lines": len(hashes), + "hashes_match_first": hashes == reference_hashes, + "layout_identity": identity, + "layout_matches_first": identity == reference_identity, + "exclusion_audits": len(audits), "shell_gate": shell_ok, + "passed": bool(passed), + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + rows.append(row) + print("[L148DeterminismRun] " + json.dumps(row, sort_keys=True), flush=True) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n", + encoding="utf-8") + if not passed: + (OUT / "summary.json").write_text(json.dumps( + {"status": "FAIL", "runs": rows}, indent=2) + "\n", encoding="utf-8") + raise SystemExit(f"composed owner determinism mismatch; see {log}") + summary = {"status": "PASS", "stamp": reference_stamp, + "layout_identity": reference_identity, "runs": rows} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + print(f"[L148DeterminismSummary] runs={len(rows)} source_stamp={reference_stamp} " + f"status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux148/RunL148SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux148/RunL148SeedSweep.py new file mode 100644 index 0000000..75f36e0 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux148/RunL148SeedSweep.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Run the L148 composed passage gates for all 16 owner seeds.""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l148/seeds" +BIN = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l148-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "593551a985ec0d1acc271260e4c8f15a5261c90edb60e33a56ed6df27e269ffb" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +BASELINES = { + "tunnel-network": ROOT / "Saved/Linux/l135/optimized-seed-sweep.json", + "vertical-shafts": ROOT / "Saved/Linux/l139/seeds/summary.json", +} +DISPATCH = { + "tunnel-network": re.compile( + r"^\[DensityDispatch\] strate=2 generator=TunnelNetwork " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), + "vertical-shafts": re.compile( + r"^\[DensityDispatch\] strate=3 generator=VerticalShafts " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), +} +ROUTES = { + "tunnel-network": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=2 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=3 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), +} +CONTROLS = { + "tunnel-network": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[VerticalShaftsBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} +REQUIRED = { + "passages": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "passage_interior": re.compile( + r"^\[PassageInteriorCapsuleAuditSummary\].*status=PASS$", re.M), +} +EXCLUSION = { + strate: re.compile( + rf"^\[CavernPassageExclusionAudit\] strate={strate} primitives=\d+ " + r"outside_joint=0 mouth_joint=\d+ negative_controls=1/1 " + r"first_primitive=none first_passage=none status=PASS$", re.M) + for strate in (2, 3) +} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kind", choices=tuple(BASELINES), required=True) + args = parser.parse_args() + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L148 Clang binary or composed owner fixture") + baseline_path = BASELINES[args.kind] + baseline = json.loads(baseline_path.read_text(encoding="utf-8")) + baseline_results = {int(row["seed"]): row for row in baseline["results"]} + OUT.mkdir(parents=True, exist_ok=True) + mode_out = OUT / args.kind + mode_out.mkdir(parents=True, exist_ok=True) + results = [] + partial = mode_out / "runs-partial.json" + for seed in range(16): + log = mode_out / f"seed{seed}.log" + layout = mode_out / f"seed{seed}.json" + old_layout = ROOT / f"Saved/Linux/l147/seeds/{args.kind}/seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=0", "--force-generate", f"--seed={seed}", + "--recipe=surface-world", "--density-dispatch-report", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP + checks["dispatch"] = bool(DISPATCH[args.kind].search(content)) + checks["route"] = bool(ROUTES[args.kind].search(content)) + checks["blocked_route_control"] = bool(CONTROLS[args.kind].search(content)) + checks["strate2_exclusion_audit"] = bool(EXCLUSION[2].search(content)) + checks["strate3_exclusion_audit"] = bool(EXCLUSION[3].search(content)) + checks["three_passage_interiors"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*interior_air=PASS", content, re.M)) == 3 + checks["three_interior_negative_controls"] = len(re.findall( + r"^\[ResolvedPassageInteriorNegativeControl\].*detected=true status=PASS$", + content, re.M)) == 3 + checks["three_ripple_gap_controls"] = len(re.findall( + r"^\[PassageRippleSamplingControl\].*status=PASS$", content, re.M)) == 3 + checks["three_blocked_passage_controls"] = len(re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3 + checks["passage_count"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*walkability=PASS$", content, re.M)) == 3 + checks["layout_output"] = layout.is_file() + old_identity = None + new_identity = None + if layout.is_file(): + new_identity = json.loads(layout.read_text(encoding="utf-8")).get("stable_identity") + if old_layout.is_file(): + old_identity = json.loads(old_layout.read_text(encoding="utf-8")).get("stable_identity") + checks["layout_identity_matches_l147"] = ( + old_identity is not None and new_identity is not None + and old_identity == new_identity) + passed = proc.returncode == 0 and all(checks.values()) + prior = baseline_results.get(seed, {}) + prior_passed = bool(prior.get("passed", False)) + change = ("newly failing" if prior_passed and not passed else + "newly passing" if not prior_passed and passed else + "still passing" if prior_passed else "still failing") + failed_checks = [name for name, ok in checks.items() if not ok] + reason = (", ".join(failed_checks) if failed_checks else + "all gates pass and resolved identity matches L147" if passed else + f"process exit {proc.returncode}") + row = {"kind": args.kind, "seed": seed, + "prior_verdict": "PASS" if prior_passed else "FAIL", + "verdict": "PASS" if passed else "FAIL", "change": change, + "reason": reason, "exit": proc.returncode, "checks": checks, + "layout_identity": new_identity, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L148SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + partial.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + passed_count = sum(row["verdict"] == "PASS" for row in results) + summary = {"kind": args.kind, "source_stamp": STAMP, + "baseline": str(baseline_path.relative_to(ROOT)), "seeds": list(range(16)), + "passed": passed_count, "failed": 16 - passed_count, "results": results} + summary_path = mode_out / "summary.json" + summary_path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L148SeedSweepSummary] kind={args.kind} passed={passed_count}/16 " + f"failed={16-passed_count} status={'PASS' if passed_count == 16 else 'FAIL'}", + flush=True) + return 0 if passed_count == 16 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux148/__pycache__/RunL148Determinism.cpython-313.pyc b/Standalone/Research/WorldAssemblerM3/Linux148/__pycache__/RunL148Determinism.cpython-313.pyc new file mode 100644 index 0000000..702f966 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux148/__pycache__/RunL148Determinism.cpython-313.pyc differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux148/__pycache__/RunL148SeedSweep.cpython-313.pyc b/Standalone/Research/WorldAssemblerM3/Linux148/__pycache__/RunL148SeedSweep.cpython-313.pyc new file mode 100644 index 0000000..d86f84f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux148/__pycache__/RunL148SeedSweep.cpython-313.pyc differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux148/l148-exclusion-envelope.patch b/Standalone/Research/WorldAssemblerM3/Linux148/l148-exclusion-envelope.patch new file mode 100644 index 0000000..737b7ef --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux148/l148-exclusion-envelope.patch @@ -0,0 +1,541 @@ +diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +index 8c44461..a271b75 100644 +--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h ++++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +@@ -1226,6 +1226,39 @@ namespace VoxelForgeCore + std::vector controlPoints; + }; + ++ // Passage shell, relief, smooth-max support, and rounding guard in the shared ++ // planner's integer micrometre coordinate space. Keep this aligned with the ++ // passage evaluator's L130 reach contract. ++ inline constexpr double ResolvedPassageInfluenceReachMicrometresV1 = ++ (0.75 + 0.21 + 0.50 / 6.0 + 0.01) * 1000000.0; ++ // The bounded joint covers the planner's 2 m mouth overlap and 6 m portal lead. ++ inline constexpr std::int64_t ResolvedPassageMouthJointRadiusMicrometresV1 = 8000000; ++ ++ // A planner passes the conservative bounds of each layer primitive through this ++ // shared gate. Only explicitly associated mouth-joint primitives may overlap ++ // the bounded spatial joint around their named passage mouth. ++ struct ResolvedLayoutPrimitiveBoundsV1 ++ { ++ std::string stableId; ++ std::int32_t strateIndex = -1; ++ std::array boundsMinimumMicrometres{}; ++ std::array boundsMaximumMicrometres{}; ++ std::string mouthJointPassageStableId; ++ std::int64_t mouthJointRadiusMicrometres = 0; ++ }; ++ ++ struct ResolvedLayoutPassageExclusionAuditV1 ++ { ++ std::uint64_t primitivesChecked = 0; ++ std::uint64_t overlapsOutsideJoint = 0; ++ std::uint64_t mouthJointAssociations = 0; ++ std::uint32_t negativeControlsRun = 0; ++ std::uint32_t negativeControlsRejected = 0; ++ std::string firstOverlappingPrimitive; ++ std::string firstOverlappingPassage; ++ bool passed = false; ++ }; ++ + struct ResolvedLayoutPassageConfigV1 + { + std::int32_t upperStrateIndex = -1; +@@ -1340,6 +1373,14 @@ namespace VoxelForgeCore + std::vector& rooms, + std::vector& edges, + std::string& error); ++ VFCORE_API bool IsResolvedLayoutPrimitiveOutsidePassageEnvelopeV1( ++ const ResolvedLayoutSnapshotV3& layout, ++ const ResolvedLayoutPrimitiveBoundsV1& primitive, ++ std::string& conflictingPassageStableId); ++ VFCORE_API ResolvedLayoutPassageExclusionAuditV1 ++ AuditResolvedLayoutPassageExclusionV1( ++ const ResolvedLayoutSnapshotV3& layout, ++ std::span primitives); + VFCORE_API bool ResolveResolvedLayoutOreVeinsV1( + ResolvedLayoutSnapshotV3& layout, std::int32_t seed, + std::span requests, +diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h +index 7b7520a..2550c77 100644 +--- a/Standalone/Core/src/ResolvedLayoutInternal.h ++++ b/Standalone/Core/src/ResolvedLayoutInternal.h +@@ -2306,6 +2306,301 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t + layout=std::move(candidate);return true; + } + ++namespace ++{ ++ struct FResolvedPassageEnvelopeSegment ++ { ++ std::string stableId; ++ std::int32_t upperStrateIndex=-1,lowerStrateIndex=-1; ++ std::array a{},b{}; ++ std::array upperMouthCenter{},lowerMouthCenter{}; ++ double radiusMicrometres=0.0; ++ double alongMicrometres=0.0,lengthMicrometres=0.0,totalLengthMicrometres=0.0; ++ }; ++ ++ std::vector BuildResolvedPassageEnvelopeSegments( ++ const ResolvedLayoutSnapshotV3& layout) ++ { ++ std::vector result; ++ for(const auto& passage:layout.passages) ++ { ++ if(passage.controlPoints.size()<2||layout.coreVoxelSizeMicrometres==0)continue; ++ const std::size_t routeEnd=std::clamp( ++ passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1); ++ std::vector controls; ++ controls.reserve(passage.controlPoints.size()); ++ const double scale=static_cast(layout.coreVoxelSizeMicrometres) ++ /WorldAssemblySubvoxelsPerVoxel; ++ for(const auto& point:passage.controlPoints) ++ controls.push_back({point.positionSubvoxels[0]*scale, ++ point.positionSubvoxels[1]*scale,point.positionSubvoxels[2]*scale, ++ point.radiusSubvoxels*scale}); ++ const auto segments=PassageSpline::BuildSegments(controls,routeEnd,2.0, ++ 1000000.0); ++ if(segments.empty())continue; ++ const double totalLength=(segments.back().alongMetres ++ +segments.back().lengthMetres)*1000000.0; ++ const std::size_t lowerMouthIndex=std::min(routeEnd,controls.size()-1); ++ for(const auto& segment:segments) ++ { ++ FResolvedPassageEnvelopeSegment envelope; ++ envelope.stableId=passage.stableId; ++ envelope.upperStrateIndex=passage.upperStrateIndex; ++ envelope.lowerStrateIndex=passage.lowerStrateIndex; ++ for(std::size_t axis=0;axis<3;++axis) ++ { ++ envelope.a[axis]=segment.a[axis]; ++ envelope.b[axis]=segment.b[axis]; ++ envelope.upperMouthCenter[axis]=controls.front()[axis]; ++ envelope.lowerMouthCenter[axis]=controls[lowerMouthIndex][axis]; ++ } ++ envelope.radiusMicrometres=std::max(segment.a[3],segment.b[3]); ++ envelope.alongMicrometres=segment.alongMetres*1000000.0; ++ envelope.lengthMicrometres=segment.lengthMetres*1000000.0; ++ envelope.totalLengthMicrometres=totalLength; ++ result.push_back(std::move(envelope)); ++ } ++ } ++ return result; ++ } ++ ++ bool ResolvedPrimitiveBoundsOverlap(const ResolvedLayoutPrimitiveBoundsV1& primitive, ++ const std::array& segmentA,const std::array& segmentB, ++ double envelopeRadius) ++ { ++ std::array direction{}; ++ std::vector breakpoints{0.0,1.0}; ++ for(std::size_t axis=0;axis<3;++axis) ++ { ++ direction[axis]=segmentB[axis]-segmentA[axis]; ++ if(std::abs(direction[axis])<=1.0e-18)continue; ++ for(const auto boundary:{static_cast(primitive.boundsMinimumMicrometres[axis]), ++ static_cast(primitive.boundsMaximumMicrometres[axis])}) ++ { ++ const double t=(boundary-segmentA[axis])/direction[axis]; ++ if(t>0.0&&t<1.0)breakpoints.push_back(t); ++ } ++ } ++ std::sort(breakpoints.begin(),breakpoints.end()); ++ breakpoints.erase(std::unique(breakpoints.begin(),breakpoints.end()), ++ breakpoints.end()); ++ const auto distanceSquared=[&](double t) ++ { ++ double result=0.0; ++ for(std::size_t axis=0;axis<3;++axis) ++ { ++ const double position=segmentA[axis]+direction[axis]*t; ++ const double gap=positionprimitive.boundsMaximumMicrometres[axis] ++ ?position-primitive.boundsMaximumMicrometres[axis]:0.0; ++ result+=gap*gap; ++ } ++ return result; ++ }; ++ double closestSquared=std::min(distanceSquared(0.0),distanceSquared(1.0)); ++ for(std::size_t index=1;indexprimitive.boundsMaximumMicrometres[axis]) ++ {boundary=primitive.boundsMaximumMicrometres[axis];active=true;} ++ if(!active)continue; ++ quadratic+=direction[axis]*direction[axis]; ++ linear+=direction[axis]*(segmentA[axis]-boundary); ++ } ++ if(quadratic>1.0e-18) ++ { ++ const double closest=std::clamp(-linear/quadratic,low,high); ++ closestSquared=std::min(closestSquared,distanceSquared(closest)); ++ } ++ closestSquared=std::min(closestSquared,distanceSquared(low)); ++ closestSquared=std::min(closestSquared,distanceSquared(high)); ++ } ++ return closestSquared<=envelopeRadius*envelopeRadius; ++ } ++} ++ ++bool IsResolvedLayoutPrimitiveOutsidePassageEnvelopeV1( ++ const ResolvedLayoutSnapshotV3& layout, ++ const ResolvedLayoutPrimitiveBoundsV1& primitive, ++ std::string& conflictingPassageStableId) ++{ ++ conflictingPassageStableId.clear(); ++ for(std::size_t axis=0;axis<3;++axis) ++ if(primitive.boundsMinimumMicrometres[axis] ++ >primitive.boundsMaximumMicrometres[axis]) ++ {conflictingPassageStableId="invalid-primitive-bounds";return false;} ++ if(primitive.mouthJointRadiusMicrometres<0 ++ ||primitive.mouthJointRadiusMicrometres ++ >ResolvedPassageMouthJointRadiusMicrometresV1) ++ {conflictingPassageStableId="invalid-mouth-joint-radius";return false;} ++ ++ const auto envelope=BuildResolvedPassageEnvelopeSegments(layout); ++ bool matchedMouthJoint=primitive.mouthJointPassageStableId.empty(); ++ for(const auto& segment:envelope) ++ { ++ const bool isUpper=primitive.strateIndex==segment.upperStrateIndex; ++ const bool isLower=primitive.strateIndex==segment.lowerStrateIndex; ++ if(!isUpper&&!isLower)continue; ++ const bool authorized=primitive.mouthJointPassageStableId==segment.stableId; ++ if(authorized)matchedMouthJoint=true; ++ const double radius=segment.radiusMicrometres ++ +ResolvedPassageInfluenceReachMicrometresV1+1000.0; ++ const auto rejectAt=[&](double t) ++ { ++ conflictingPassageStableId=segment.stableId; ++ const std::array center{ ++ std::lerp(segment.a[0],segment.b[0],t), ++ std::lerp(segment.a[1],segment.b[1],t), ++ std::lerp(segment.a[2],segment.b[2],t)}; ++ const auto mouthDistance=[&](const auto& mouth) ++ { ++ const double dx=center[0]-mouth[0],dy=center[1]-mouth[1], ++ dz=center[2]-mouth[2]; ++ return std::sqrt(dx*dx+dy*dy+dz*dz)/1.0e6; ++ }; ++ std::printf("[PassageExclusionConflictDetail] primitive=%s passage=%s station_m=%.3f authorized_joint=%s joint_radius_m=%.3f envelope_radius_m=%.3f nearest_mouth_m=%.3f center_m=(%.3f,%.3f,%.3f) bounds_min_m=(%.3f,%.3f,%.3f) bounds_max_m=(%.3f,%.3f,%.3f)\n", ++ primitive.stableId.c_str(),segment.stableId.c_str(), ++ (segment.alongMicrometres+segment.lengthMicrometres*t)/1.0e6, ++ authorized?"true":"false", ++ primitive.mouthJointRadiusMicrometres/1.0e6,radius/1.0e6, ++ std::min(mouthDistance(segment.upperMouthCenter), ++ mouthDistance(segment.lowerMouthCenter)), ++ center[0]/1.0e6,center[1]/1.0e6,center[2]/1.0e6, ++ primitive.boundsMinimumMicrometres[0]/1.0e6, ++ primitive.boundsMinimumMicrometres[1]/1.0e6, ++ primitive.boundsMinimumMicrometres[2]/1.0e6, ++ primitive.boundsMaximumMicrometres[0]/1.0e6, ++ primitive.boundsMaximumMicrometres[1]/1.0e6, ++ primitive.boundsMaximumMicrometres[2]/1.0e6); ++ return false; ++ }; ++ const auto overlaps=[&](double t0,double t1) ++ { ++ if(t1<=t0)return false; ++ std::array a{},b{}; ++ for(std::size_t axis=0;axis<3;++axis) ++ { ++ a[axis]=std::lerp(segment.a[axis],segment.b[axis],t0); ++ b[axis]=std::lerp(segment.a[axis],segment.b[axis],t1); ++ } ++ return ResolvedPrimitiveBoundsOverlap(primitive,a,b,radius); ++ }; ++ if(authorized&&primitive.mouthJointRadiusMicrometres>0) ++ { ++ const auto& center=isUpper?segment.upperMouthCenter:segment.lowerMouthCenter; ++ std::array delta{}; ++ double quadratic=0.0,linear=0.0,constant=0.0; ++ for(std::size_t axis=0;axis<3;++axis) ++ { ++ delta[axis]=segment.b[axis]-segment.a[axis]; ++ const double offset=segment.a[axis]-center[axis]; ++ quadratic+=delta[axis]*delta[axis]; ++ linear+=offset*delta[axis]; ++ constant+=offset*offset; ++ } ++ const double jointReach=primitive.mouthJointRadiusMicrometres+radius; ++ constant-=jointReach*jointReach; ++ const double discriminant=linear*linear-quadratic*constant; ++ if(quadratic<=1.0e-18) ++ { ++ // A degenerate segment that is wholly in the joint is authorized; ++ // one outside it remains subject to the full envelope check. ++ if(constant<=0.0)continue; ++ if(overlaps(0.0,1.0)) ++ return rejectAt(0.5); ++ continue; ++ } ++ if(discriminant<0.0) ++ { ++ if(overlaps(0.0,1.0)) ++ return rejectAt(0.5); ++ continue; ++ } ++ const double root=std::sqrt(discriminant); ++ const double insideFirst=(-linear-root)/quadratic; ++ const double insideLast=(-linear+root)/quadratic; ++ const double clippedFirst=std::clamp(insideFirst,0.0,1.0); ++ const double clippedLast=std::clamp(insideLast,0.0,1.0); ++ if(insideFirst>=1.0||insideLast<=0.0) ++ { ++ if(overlaps(0.0,1.0)) ++ return rejectAt(0.5); ++ continue; ++ } ++ if(overlaps(0.0,clippedFirst)||overlaps(clippedLast,1.0)) ++ return rejectAt(clippedFirst>0.0?clippedFirst*0.5: ++ (1.0+clippedLast)*0.5); ++ continue; ++ } ++ if(overlaps(0.0,1.0)) ++ return rejectAt(0.5); ++ } ++ if(!matchedMouthJoint) ++ {conflictingPassageStableId=primitive.mouthJointPassageStableId;return false;} ++ return true; ++} ++ ++ResolvedLayoutPassageExclusionAuditV1 AuditResolvedLayoutPassageExclusionV1( ++ const ResolvedLayoutSnapshotV3& layout, ++ std::span primitives) ++{ ++ ResolvedLayoutPassageExclusionAuditV1 result; ++ for(const auto& primitive:primitives) ++ { ++ ++result.primitivesChecked; ++ if(!primitive.mouthJointPassageStableId.empty()) ++ ++result.mouthJointAssociations; ++ std::string conflict; ++ if(!IsResolvedLayoutPrimitiveOutsidePassageEnvelopeV1(layout,primitive,conflict)) ++ { ++ ++result.overlapsOutsideJoint; ++ if(result.firstOverlappingPrimitive.empty()) ++ { ++ result.firstOverlappingPrimitive=primitive.stableId; ++ result.firstOverlappingPassage=std::move(conflict); ++ } ++ } ++ } ++ ++ const auto passage=std::find_if(layout.passages.begin(),layout.passages.end(), ++ [](const auto& value){return value.controlPoints.size()>=2;}); ++ if(passage!=layout.passages.end()&&layout.coreVoxelSizeMicrometres>0) ++ { ++ const auto& point=passage->controlPoints[passage->controlPoints.size()/2] ++ .positionSubvoxels; ++ ResolvedLayoutPrimitiveBoundsV1 control; ++ control.stableId="negative-control/forced-inside-passage-envelope"; ++ control.strateIndex=passage->lowerStrateIndex; ++ const double scale=static_cast(layout.coreVoxelSizeMicrometres) ++ /WorldAssemblySubvoxelsPerVoxel; ++ for(std::size_t axis=0;axis<3;++axis) ++ { ++ const auto center=static_cast(std::llround(point[axis]*scale)); ++ control.boundsMinimumMicrometres[axis]=center-250000; ++ control.boundsMaximumMicrometres[axis]=center+250000; ++ } ++ ++result.negativeControlsRun; ++ std::string conflict; ++ if(!IsResolvedLayoutPrimitiveOutsidePassageEnvelopeV1(layout,control,conflict) ++ &&conflict==passage->stableId) ++ ++result.negativeControlsRejected; ++ } ++ result.passed=result.overlapsOutsideJoint==0 ++ &&result.negativeControlsRun==1 ++ &&result.negativeControlsRejected==result.negativeControlsRun; ++ return result; ++} ++ + bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + std::int32_t seed,std::int32_t strateIndex, + const ResolvedCavernPlannerSettingsV1& settings, +@@ -3484,6 +3779,127 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + std::atan2(static_cast(rise),static_cast(run)) + *180.0/3.14159265358979323846); + } ++ auto boundedGraph=layout; ++ std::string boundsError; ++ if(!ResolveResolvedCavernGraphV1(boundedGraph,seed,rooms,edges,boundsError)) ++ {error="cavern planner could not validate primitive bounds: "+boundsError;return false;} ++ std::vector primitiveBounds; ++ primitiveBounds.reserve(boundedGraph.cavernRooms.size()+boundedGraph.cavernEdges.size()); ++ const std::int64_t mouthJointFeatureSpanMicrometres= ++ MouthOverlap+PortalLeadLength+2*settings.destinationRadiusMicrometres; ++ const auto mouthJointAtProfileDistance=[&](std::int64_t distance) ++ ->std::string ++ { ++ if(distance<=mouthJointFeatureSpanMicrometres) ++ return arrivalPassage->stableId; ++ if(departurePassage&&totalRouteLength-distance ++ <=mouthJointFeatureSpanMicrometres) ++ return departurePassage->stableId; ++ return {}; ++ }; ++ const auto associateSpatialMouthJoint=[&](ResolvedLayoutPrimitiveBoundsV1& bounds) ++ { ++ if(!bounds.mouthJointPassageStableId.empty())return; ++ const double jointRadius= ++ static_cast(ResolvedPassageMouthJointRadiusMicrometresV1); ++ double nearestSquared=jointRadius*jointRadius; ++ for(const auto& passage:layout.passages) ++ { ++ if(bounds.strateIndex!=passage.upperStrateIndex ++ &&bounds.strateIndex!=passage.lowerStrateIndex) ++ continue; ++ if(passage.controlPoints.size()<2)continue; ++ const std::size_t lowerMouthIndex=std::min( ++ passage.lowerMouthControlIndex,passage.controlPoints.size()-1); ++ const std::array mouthIndices={0,lowerMouthIndex}; ++ double passageDistanceSquared=std::numeric_limits::infinity(); ++ for(const auto mouthIndex:mouthIndices) ++ { ++ double distanceSquared=0.0; ++ for(std::size_t axis=0;axis<3;++axis) ++ { ++ const double center=static_cast(qToMicrometres( ++ passage.controlPoints[mouthIndex].positionSubvoxels[axis])); ++ const double gap=centerbounds.boundsMaximumMicrometres[axis] ++ ?center-bounds.boundsMaximumMicrometres[axis]:0.0; ++ distanceSquared+=gap*gap; ++ } ++ passageDistanceSquared=std::min(passageDistanceSquared,distanceSquared); ++ } ++ if(passageDistanceSquared<=nearestSquared) ++ { ++ if(bounds.mouthJointPassageStableId.empty() ++ ||passageDistanceSquared=0) ++ bounds.mouthJointPassageStableId=mouthJointAtProfileDistance( ++ room.floorProfileDistanceMicrometres); ++ if(!bounds.mouthJointPassageStableId.empty()) ++ bounds.mouthJointRadiusMicrometres= ++ ResolvedPassageMouthJointRadiusMicrometresV1; ++ associateSpatialMouthJoint(bounds); ++ primitiveBounds.push_back(std::move(bounds)); ++ } ++ const auto mouthJointForAnchor=[](const std::string& anchor) ++ { ++ constexpr std::string_view Lower="/lower-mouth"; ++ constexpr std::string_view Upper="/upper-mouth"; ++ if(anchor.ends_with(Lower))return anchor.substr(0,anchor.size()-Lower.size()); ++ if(anchor.ends_with(Upper))return anchor.substr(0,anchor.size()-Upper.size()); ++ return std::string{}; ++ }; ++ for(const auto& edge:boundedGraph.cavernEdges) ++ { ++ ResolvedLayoutPrimitiveBoundsV1 bounds{edge.stableId,edge.strateIndex, ++ edge.boundsMinimumMicrometres,edge.boundsMaximumMicrometres}; ++ bounds.mouthJointPassageStableId=mouthJointForAnchor(edge.startAnchorStableId); ++ if(bounds.mouthJointPassageStableId.empty() ++ &&edge.floorProfileStableId==backboneFloorProfileId ++ &&edge.floorProfileStartDistanceMicrometres>=0 ++ &&edge.floorProfileEndDistanceMicrometres>=0) ++ { ++ const auto midpoint=(edge.floorProfileStartDistanceMicrometres ++ +edge.floorProfileEndDistanceMicrometres)/2; ++ bounds.mouthJointPassageStableId=mouthJointAtProfileDistance(midpoint); ++ } ++ if(!bounds.mouthJointPassageStableId.empty()) ++ bounds.mouthJointRadiusMicrometres= ++ ResolvedPassageMouthJointRadiusMicrometresV1; ++ associateSpatialMouthJoint(bounds); ++ primitiveBounds.push_back(std::move(bounds)); ++ } ++ const auto exclusionAudit=AuditResolvedLayoutPassageExclusionV1(layout,primitiveBounds); ++ std::printf("[CavernPassageExclusionAudit] strate=%d primitives=%llu outside_joint=%llu mouth_joint=%llu negative_controls=%u/%u first_primitive=%s first_passage=%s status=%s\n", ++ strateIndex,static_cast(exclusionAudit.primitivesChecked), ++ static_cast(exclusionAudit.overlapsOutsideJoint), ++ static_cast(exclusionAudit.mouthJointAssociations), ++ exclusionAudit.negativeControlsRejected,exclusionAudit.negativeControlsRun, ++ exclusionAudit.firstOverlappingPrimitive.empty()?"none":exclusionAudit.firstOverlappingPrimitive.c_str(), ++ exclusionAudit.firstOverlappingPassage.empty()?"none":exclusionAudit.firstOverlappingPassage.c_str(), ++ exclusionAudit.passed?"PASS":"FAIL"); ++ if(!exclusionAudit.passed) ++ { ++ error="cavern planner primitive bounds overlap a passage exclusion envelope outside its mouth joint: "+ ++ exclusionAudit.firstOverlappingPrimitive+" -> "+exclusionAudit.firstOverlappingPassage; ++ return false; ++ } + const double plannerMilliseconds=std::chrono::duration( + std::chrono::steady_clock::now()-plannerStarted).count(); + std::printf("[CavernPlannerV1] strate=%d seed=%d route=%s->%s route_nodes=%zu room_primitives=%zu shafts=%zu edges=%zu branches=%zu route_horizontal_m=%.3f floor_delta_m=%.3f max_grade_deg=%.3f mouth_tangent_min_dot=%.3f grade_limit_deg=11.310 clearance_radius_m=%.3f clearance_height_m=%.3f destination_radius_m=%.3f plan_ms=%.3f status=PASS\n", +diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h +index 48b5a30..80677e3 100644 +--- a/Standalone/Source/StreamBenchmarkPipeline.h ++++ b/Standalone/Source/StreamBenchmarkPipeline.h +@@ -4709,6 +4709,19 @@ namespace StandaloneStreamBenchmark + const auto& route=allPassageSmoothRoutes[passageIndex]; + if(route.size()<2)return audit; + const double totalRouteLength=route.back().distance; ++ const double mouthJointRadiusMetres= ++ static_cast(VoxelForgeCore:: ++ ResolvedPassageMouthJointRadiusMicrometresV1)/1000000.0; ++ const auto insideMouthJoint=[&](double x,double y,double z) ++ { ++ const auto inside=[&](const auto& mouth) ++ { ++ const double dx=x-mouth.x,dy=y-mouth.y,dz=z-mouth.z; ++ return dx*dx+dy*dy+dz*dz ++ <=mouthJointRadiusMetres*mouthJointRadiusMetres; ++ }; ++ return inside(route.front())||inside(route.back()); ++ }; + const double bodyFloorDepth=allPassageBodyFloorDepths[passageIndex]; + constexpr std::array,5> floorOffsets{{ + {{0.0,0.0}},{{0.30,0.0}},{{-0.30,0.0}}, +@@ -4859,11 +4872,6 @@ namespace StandaloneStreamBenchmark + } + const double tangentLength=std::hypot(dx,dy); + if(tangentLength<=1.0e-6)continue; +- if(std::min(along,totalRouteLength-along)<5.0) +- { +- audit.wallMouthBlendSkips+=5; +- continue; +- } + const double nx=-dy/tangentLength,ny=dx/tangentLength; + const double floorDepth=passageTubeDepthAt(radius,bodyFloorDepth); + double floorZ=passageFloorAtXY(passageIndex,centerX,centerY, +@@ -4897,6 +4905,11 @@ namespace StandaloneStreamBenchmark + } + sampleZ=floorZ+probe.height; + } ++ if(insideMouthJoint(sampleX,sampleY,sampleZ)) ++ { ++ ++audit.wallMouthBlendSkips; ++ continue; ++ } + const VoxelForgeCore::Vec3d wallPosition{sampleX/coreMetres, + sampleY/coreMetres,sampleZ/coreMetres}; + if(passageInteriorWinsAt(sampleX,sampleY,sampleZ)) diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/MouthAudit.py b/Standalone/Research/WorldAssemblerM3/Linux149/MouthAudit.py new file mode 100644 index 0000000..346a6b1 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/MouthAudit.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Structural mouth continuity and forward-extension checks for L149 layouts.""" +from __future__ import annotations + +import json +import math +from pathlib import Path +from typing import Any + + +def _q_to_um(value: int, core_voxel_size_um: int) -> int: + return int(value * core_voxel_size_um / 256) + + +def _mouth(layout: dict[str, Any], edge: dict[str, Any]) -> tuple[dict[str, Any], int, + list[int], int] | None: + anchor = edge.get("start_anchor_stable_id", "") + if not anchor.endswith(("/lower-mouth", "/upper-mouth")): + return None + side = anchor.rsplit("/", 1)[1] + passage_id = anchor.rsplit("/", 1)[0] + passage = next((row for row in layout.get("passages", []) + if row.get("stable_id") == passage_id), None) + if passage is None: + return None + key = "lower_mouth_q" if side == "lower-mouth" else "upper_mouth_q" + mouth_q = passage.get(key) + controls = passage.get("control_points", []) + if not mouth_q or not controls: + return None + if side == "upper-mouth": + index = 0 + else: + center_height_q = round(1.13 * 256_000_000 / layout["core_voxel_size_um"]) + matches = [i for i, point in enumerate(controls) + if point["position_q"][0] == mouth_q[0] + and point["position_q"][1] == mouth_q[1] + and point["position_q"][2] - point["radius_q"] + center_height_q + == mouth_q[2]] + if not matches: + return None + index = matches[-1] + return passage, index, mouth_q, _q_to_um(controls[index]["radius_q"], + layout["core_voxel_size_um"]) + + +def _dot_direction(a: list[float], b: list[float]) -> float: + al, bl = math.sqrt(sum(x * x for x in a)), math.sqrt(sum(x * x for x in b)) + return sum(x * y for x, y in zip(a, b)) / (al * bl) if al > 0 and bl > 0 else -1.0 + + +def _bezier(points: list[list[int]], t: float) -> list[float]: + u = 1.0 - t + return [u**3 * points[0][axis] + 3*u*u*t * points[1][axis] + + 3*u*t*t * points[2][axis] + t**3 * points[3][axis] + for axis in range(3)] + + +def _metrics(layout: dict[str, Any], edge: dict[str, Any]) -> dict[str, Any]: + found = _mouth(layout, edge) + if found is None: + return {"bound": False, "tangent_dot": -1.0, "radius_match": False, + "position_match": False, "floor_slope_delta": math.inf, + "forward_extension": False} + passage, index, _, expected_radius = found + controls = passage["control_points"] + route = edge.get("control_points_um", []) + if len(route) != 4 or len(controls) < 2: + return {"bound": False, "tangent_dot": -1.0, "radius_match": False, + "position_match": False, "floor_slope_delta": math.inf, + "forward_extension": False} + side = edge["start_anchor_stable_id"].rsplit("/", 1)[1] + neighbor = 1 if side == "upper-mouth" else index - 1 + if neighbor < 0 or neighbor >= len(controls): + return {"bound": False, "tangent_dot": -1.0, "radius_match": False, + "position_match": False, "floor_slope_delta": math.inf, + "forward_extension": False} + mouth_point = controls[index] + neighbor_point = controls[neighbor] + core = layout["core_voxel_size_um"] + expected = [_q_to_um(v, core) for v in mouth_point["position_q"]] + expected[2] -= expected_radius + expected_dir = [_q_to_um(mouth_point["position_q"][axis] + - neighbor_point["position_q"][axis], core) + for axis in range(2)] + edge_dir = [route[1][0] - route[0][0], route[1][1] - route[0][1]] + position_match = all(abs(route[0][axis] - expected[axis]) <= 2 for axis in range(3)) + radius_match = edge.get("bore_radius_um") == expected_radius + tangent_dot = _dot_direction([*edge_dir, 0.0], [*expected_dir, 0.0]) + + floor_mouth = mouth_point["position_q"][2] - mouth_point["radius_q"] + floor_neighbor = neighbor_point["position_q"][2] - neighbor_point["radius_q"] + expected_slope = _q_to_um(floor_mouth - floor_neighbor, core) / max( + 1.0, math.hypot(*expected_dir)) + if side == "upper-mouth": + expected_slope = -expected_slope + route_slope = (route[1][2] - route[0][2]) / max(1.0, math.hypot(*edge_dir)) + floor_slope_delta = abs(route_slope - expected_slope) + + tangent_length = math.hypot(*expected_dir) + tangent = [v / tangent_length for v in expected_dir] if tangent_length else [0.0, 0.0] + points = [list(p) for p in route] + previous = _bezier(points, 0.0) + distance = 0.0 + prior_progress = 0.0 + forward = True + for step in range(1, 1025): + current = _bezier(points, step / 1024.0) + dx, dy = current[0] - previous[0], current[1] - previous[1] + segment = math.hypot(dx, dy) + next_distance = distance + segment + if distance < 12_000_000: + progress = ((current[0] - route[0][0]) * tangent[0] + + (current[1] - route[0][1]) * tangent[1]) + if progress < -100_000 or progress < prior_progress - 100_000: + forward = False + prior_progress = progress + previous, distance = current, next_distance + if distance >= 12_000_000: + break + return {"bound": True, "tangent_dot": tangent_dot, + "radius_match": radius_match, "position_match": position_match, + "floor_slope_delta": floor_slope_delta, "forward_extension": forward} + + +def audit_layout(layout_path: Path) -> dict[str, Any]: + layout = json.loads(layout_path.read_text(encoding="utf-8")) + mouth_edges = [edge for edge in layout.get("cavern_edges", []) + if edge.get("strate_index") in (2, 3) + and edge.get("stable_id", "").endswith(("route-edge-0", "departure-edge")) + and edge.get("start_anchor_stable_id", "").endswith( + ("/lower-mouth", "/upper-mouth"))] + rows = [_metrics(layout, edge) for edge in mouth_edges] + negative_controls = 0 + for edge in mouth_edges: + found = _mouth(layout, edge) + if found is None: + continue + passage, index, _, _ = found + side = edge["start_anchor_stable_id"].rsplit("/", 1)[1] + neighbor = 1 if side == "upper-mouth" else index - 1 + mouth_point = passage["control_points"][index] + neighbor_point = passage["control_points"][neighbor] + core = layout["core_voxel_size_um"] + expected_dir = [_q_to_um(mouth_point["position_q"][axis] + - neighbor_point["position_q"][axis], core) + for axis in range(2)] + altered = dict(edge) + points = [list(p) for p in edge["control_points_um"]] + length = max(1.0, math.hypot(*expected_dir)) + points[1][0] = points[0][0] - round(expected_dir[0] / length * 1_000_000) + points[1][1] = points[0][1] - round(expected_dir[1] / length * 1_000_000) + altered["control_points_um"] = points + control = _metrics(layout, altered) + if control["tangent_dot"] < 0.9: + negative_controls += 1 + continuity = bool(rows) and all( + row["bound"] and row["position_match"] and row["radius_match"] + and row["tangent_dot"] >= 0.9 and row["floor_slope_delta"] <= 0.15 + for row in rows) + no_backtrack = bool(rows) and all(row["forward_extension"] for row in rows) + controls_ok = bool(mouth_edges) and negative_controls == len(mouth_edges) + return {"edges": len(mouth_edges), "continuity": continuity, + "no_backtrack_12m": no_backtrack, "negative_controls": negative_controls, + "negative_controls_expected": len(mouth_edges), "negative_controls_pass": controls_ok, + "minimum_tangent_dot": min((row["tangent_dot"] for row in rows), default=-1.0), + "maximum_floor_slope_delta": max( + (row["floor_slope_delta"] for row in rows), default=math.inf), + "rows": rows} diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux149/REPORT.md new file mode 100644 index 0000000..1645cf5 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/REPORT.md @@ -0,0 +1,32 @@ +# L149 — one tunnel through the mouth + +**L149 does not meet the bar.** The mouth anchors now match in position, tangent, radius and floor slope, but the departure spline still turns back alongside the passage, and the composed shell audit still finds floor violations. I stopped after the third geometry attempt. + +## Changes and result + +- Planner cave and shaft edges now start from the passage mouth floor, use the mouth radius and tangent, and carry the mouth floor slope. Their runtime SDF uses the matching flat-bottom tube profile. +- Owner route samples now follow the actual mouth-edge curve 2 m from the joint. The portal lead was shortened from 6 m to 1 m, and the passage-edge clearance exclusion was reduced from 10 m to 2 m. +- Evaluator version is v238. Added Clang W1/W12 generated owner-hash baselines for surface-world style 1 and floating-islands style 2. +- These were the three geometry attempts: match the mouth tube; move route samples onto the curve; shorten the lead and clearance exclusion. The final seed-2 shell gate still failed, so there was no fourth attempt. + +The measured premise was: matching the mouth geometry and shortening the lead would make the first/last route edge continue the passage as one forward tube. It is false for the current cubic edge. The seed-2 departure edge reaches about **1.00 m behind the mouth by 5.07 m of travel**, despite an initial tangent dot of 1.0. The shell audit independently finds three floor violations at passage 2–3. + +## Measurements and checks + +- **Focused seed 2, W8, clip radius 3, max level 0, final source stamp `19dbe26a…`:** route audits for strata 2 and 3 pass. Shell audit: 85,162 floor samples, 3 violations; 5,589 wall samples, 0 violations; 8,430 interior samples, 0 violations. The interior capsule audit passes with 281,043 samples and 3/3 negative controls. Ripple, naturalness and mouth-profile summaries pass. The audit exits 10 after the shell failure, before emitting the tile-seam audit; seam status is therefore **not measured** in these failing runs. +- **Builds:** final Clang and GCC headless binaries and the Clang offscreen renderer built successfully with source stamp `19dbe26ab76a629baadc54bb0ab93eaa22d6f878e6946df2b71509c12c9524e4`. +- **Mouth geometry audit:** all three seed-2 mouth edges match their anchor position and radius; minimum tangent dot is 0.9999999999886, maximum floor-slope delta is 0.000475, and all 3/3 reversed-tangent negative controls are detected. The first-12-m forward-extension check fails on the departure edge. Across the 16-seed sets it passes 1/16. +- **Composed sweeps, W8, seeds 0–15, clip radius 3, max clip level 8, max level 0:** Tunnel Network **0/16**, Vertical Shafts **0/16**. Both sets have 0/16 passing shell and passage summaries. The interior-air capsule summary passes 6/16 in each set. Naturalness, mouth profile, ripple and the interior, ripple-gap and blocked-route negative controls pass 16/16. The route audit passes 2/16 Tunnel Network cases and 8/16 Vertical Shafts cases. The seam audit is not reached because the shell audit exits first. +- **Clang/GCC worker matrix, seed 2, W1/W8/W12:** all six runs emitted the same 12-line pipeline hash signature and resolved layout identity `resolved-layout-v3-fnv64:d980465950135a9d`. This focused matrix used `--render-distance-chunks=0` and max level 0; it checks the seed-2 view and layout identity, not a full multi-LOD world. The fast tier also passed its owner W1/W12 hash baselines and random-camera W1/W8/W12 checks. +- **Fast tier:** **FAIL**, 356.1 s. Existing owner passage checks, v238 hash baselines, worker checks and negative controls passed. The new four-strate owner-plan stage failed its tunnel-shell and passage-summary checks, arrival/departure chain, and strata-2/3 arrival/departure walkability checks. +- **Captures:** three matched-camera before/after comparisons, rendered at 1280×720 from mouth height. All six images pass capture quality. Before images use the L147/v237 executable baseline; L148's saved exclusion-gate build rejected seed 0 before producing a layout. Its accepted seed-2 layout identity matches L147. After images use the final v238 source stamp. + +| Joint | Before | After | +| --- | --- | --- | +| Seed 0, arrival | [Before](captures/before-seed0-tunnel-network-route-edge-0.jpg) | [After](captures/after-seed0-tunnel-network-route-edge-0.jpg) | +| Seed 0, departure | [Before](captures/before-seed0-tunnel-network-departure-edge.jpg) | [After](captures/after-seed0-tunnel-network-departure-edge.jpg) | +| Seed 2, shaft arrival | [Before](captures/before-seed2-vertical-shafts-route-edge-0.jpg) | [After](captures/after-seed2-vertical-shafts-route-edge-0.jpg) | + +## Remaining risks + +The v238 geometry still fails the requested composed shell and route gates, so it is not a passing L149 result. The captures are ready for the owner’s visual review. No Unreal module `Source/` files changed; `Standalone/Source/WorldAssemblyPrototype.h` did change. diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/RunL149Determinism.py b/Standalone/Research/WorldAssemblerM3/Linux149/RunL149Determinism.py new file mode 100644 index 0000000..2f2631e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/RunL149Determinism.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Compare L149 hashes and layout identity at Clang/GCC W1, W8, and W12.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from pathlib import Path + +from MouthAudit import audit_layout + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l149/determinism" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "clang": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l149-clang", + "gcc": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l149-gcc", +} +STAMP = "19dbe26ab76a629baadc54bb0ab93eaa22d6f878e6946df2b71509c12c9524e4" +CAMERA = "-20.062500,-5.068359,-900.353516,0,0" +HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", + re.M) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def main() -> int: + if any(not path.is_file() for path in (DATA, *BINARIES.values())): + raise SystemExit("missing L149 Clang/GCC binary or composed owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + rows = [] + partial = OUT / "runs-partial.json" + for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + name = f"determinism-{compiler}-w{workers}" + log, layout = OUT / f"{name}.log", OUT / f"{name}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=0", + f"--camera={CAMERA}", "--stream-hash", "--force-generate", "--seed=2", + "--recipe=surface-world", "--render-distance-chunks=0", + "--density-dispatch-report", f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp_match = STAMP_RE.search(content) + stamp = stamp_match.group(1) if stamp_match else None + hashes = tuple(HASH_RE.findall(content)) + identity = (json.loads(layout.read_text(encoding="utf-8")).get("stable_identity") + if layout.is_file() else None) + mouth_audit = audit_layout(layout) if layout.is_file() else None + row = {"compiler": compiler, "workers": workers, "exit": proc.returncode, + "stamp": stamp, "source_stamp_matches": stamp == STAMP, + "hash_lines": len(hashes), "hashes": hashes, + "layout_identity": identity, + "mouth_audit": mouth_audit, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + rows.append(row) + partial.write_text(json.dumps(rows, indent=2) + "\n", encoding="utf-8") + print("[L149DeterminismRun] " + json.dumps(row, sort_keys=True), flush=True) + valid = [row for row in rows if row["source_stamp_matches"] + and row["hash_lines"] == 12 and row["layout_identity"]] + reference_hashes = valid[0]["hashes"] if valid else [] + reference_identity = valid[0]["layout_identity"] if valid else None + deterministic = len(valid) == 6 and all( + row["hashes"] == reference_hashes and row["layout_identity"] == reference_identity + for row in valid) + mouth_geometry = all(row["mouth_audit"] and row["mouth_audit"]["continuity"] + and row["mouth_audit"]["negative_controls_pass"] + for row in rows) + no_backtrack = all(row["mouth_audit"] and row["mouth_audit"]["no_backtrack_12m"] + for row in rows) + summary = {"status": "PASS" if deterministic else "FAIL", + "deterministic": deterministic, "mouth_geometry_pass": mouth_geometry, + "no_backtrack_12m": no_backtrack, + "source_stamp": STAMP, "layout_identity": reference_identity, "runs": rows} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + print(f"[L149DeterminismSummary] runs={len(rows)} deterministic=" + f"{'PASS' if deterministic else 'FAIL'} mouth_geometry=" + f"{'PASS' if mouth_geometry else 'FAIL'} no_backtrack_12m=" + f"{'PASS' if no_backtrack else 'FAIL'} status={summary['status']}", flush=True) + return 0 if summary["status"] == "PASS" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/RunL149MouthCaptures.py b/Standalone/Research/WorldAssemblerM3/Linux149/RunL149MouthCaptures.py new file mode 100644 index 0000000..c3eb5f5 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/RunL149MouthCaptures.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Capture the seed 0/2 cave-mouth joints before and after L149.""" +from __future__ import annotations + +import json +import math +import os +from pathlib import Path +import re +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[4] +DATA = ROOT / "Standalone/data/owner_world.json" +OUT = ROOT / "Saved/Linux/l149/captures" +ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux149/captures" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +VERSIONS = { + "before-l147": { + "headless": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l147-clang", + "renderer": ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l147-clang", + "stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + }, + "after-l149": { + "headless": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l149-clang", + "renderer": ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l149-clang", + "stamp": "19dbe26ab76a629baadc54bb0ab93eaa22d6f878e6946df2b71509c12c9524e4", + }, +} +JOINTS = ( + (0, "tunnel-network", "tunnel-network/strate-2/route-edge-0"), + (0, "tunnel-network", "tunnel-network/strate-2/departure-edge"), + (2, "vertical-shafts", "vertical-shafts/strate-3/route-edge-0"), +) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) + + +def fmt(values: tuple[float, ...]) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def angles(camera: tuple[float, ...], target: tuple[float, ...]) -> tuple[float, float]: + dx, dy, dz = (target[i] - camera[i] for i in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees( + math.atan2(dz, math.hypot(dx, dy))) + + +def run(name: str, command: list[object], log: Path, expected_stamp: str, + env: dict[str, str]) -> str: + binary = Path(str(command[0])) + probe = subprocess.run([*LIMIT, "timeout", "15s", str(binary), "--print-source-stamp"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=20) + match = STAMP_RE.search(probe.stdout or "") + if probe.returncode != 0 or not match or match.group(1) != expected_stamp: + raise RuntimeError(f"{name}: executable stamp mismatch for {binary}") + log.parent.mkdir(parents=True, exist_ok=True) + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, + timeout=1745, check=False) + output = log.read_text(encoding="utf-8", errors="replace") + emitted = STAMP_RE.search(output) + if result.returncode != 0 or not emitted or emitted.group(1) != expected_stamp: + raise RuntimeError(f"{name}: process failed or source changed; see {log}") + return output + + +def q_to_metres(q: int, core_voxel_um: int) -> float: + return q * core_voxel_um / (256.0 * 1_000_000.0) + + +def camera_for(layout: dict, edge: dict) -> tuple[tuple[float, ...], tuple[float, ...]]: + anchor = edge["start_anchor_stable_id"] + if anchor.endswith("/lower-mouth"): + side = "lower" + mouth_q = next(p["lower_mouth_q"] for p in layout["passages"] + if p["stable_id"] == anchor.removesuffix("/lower-mouth")) + elif anchor.endswith("/upper-mouth"): + side = "upper" + mouth_q = next(p["upper_mouth_q"] for p in layout["passages"] + if p["stable_id"] == anchor.removesuffix("/upper-mouth")) + else: + raise ValueError(f"edge has no passage mouth anchor: {anchor}") + passage = next(p for p in layout["passages"] + if p["stable_id"] == anchor.rsplit("/", 1)[0]) + controls = passage["control_points"] + mouth_index = len(controls) - 1 if side == "lower" else 0 + neighbor_index = mouth_index - 1 if side == "lower" else 1 + a, b = controls[neighbor_index]["position_q"], controls[mouth_index]["position_q"] + sign = 1.0 if side == "lower" else -1.0 + tx, ty = sign * (b[0] - a[0]), sign * (b[1] - a[1]) + tangent_length = math.hypot(tx, ty) + if tangent_length <= 1.0e-9: + raise ValueError(f"passage has a degenerate {side} mouth tangent: {anchor}") + tx, ty = tx / tangent_length, ty / tangent_length + scale = layout["core_voxel_size_um"] + mx, my, mz = (q_to_metres(value, scale) for value in mouth_q) + camera = (mx - tx * 2.0, my - ty * 2.0, mz + 0.47) + target = (mx + tx * 10.0, my + ty * 10.0, mz + 0.47) + return camera, target + + +def main() -> int: + if not DATA.is_file(): + raise SystemExit("missing composed owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + ARTIFACTS.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache") + quality = ROOT / "Standalone/Tools/verify/capture_quality.py" + manifest = [] + failures = [] + for version, config in VERSIONS.items(): + phase = "before" if version.startswith("before") else "after" + for seed, kind, edge_id in JOINTS: + name = f"seed{seed}-{kind}-{edge_id.rsplit('/', 1)[-1]}" + reference_path = ROOT / f"Saved/Linux/l147/seeds/{kind}/seed{seed}.json" + reference_layout = json.loads(reference_path.read_text(encoding="utf-8")) + reference_edge = next(row for row in reference_layout["cavern_edges"] + if row["stable_id"] == edge_id) + camera, target = camera_for(reference_layout, reference_edge) + yaw, pitch = angles(camera, target) + mesh = OUT / "meshes" / phase / f"{name}.vfmesh" + layout_path = OUT / "meshes" / phase / f"{name}-layout.json" + export_log = OUT / f"{phase}-{name}-export.log" + export = [config["headless"], DATA, "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", + "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", f"--seed={seed}", "--recipe=surface-world", + "--render-distance-chunks=0", + f"--camera={fmt((*camera, yaw, pitch))}", + f"--resolved-layout-output={layout_path}", + f"--mesh-export={mesh}"] + run(f"{phase}-{name}-export", export, export_log, config["stamp"], env) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{phase}-{name}: empty mesh export") + layout = json.loads(layout_path.read_text(encoding="utf-8")) + png = OUT / f"{phase}-{name}.png" + jpg = ARTIFACTS / f"{phase}-{name}.jpg" + render = [config["renderer"], mesh, png, "--size=1280x720", + "--fov-deg=78.0", f"--camera-m={fmt(camera)}", + f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}", + "--tile-skirts=off", "--shade=readable", "--headlamp=on", + "--height-tint=on", "--height-center-m=-950", "--height-range-m=60", + "--tile-edges=off", f"--focus-region={fmt((*camera[:3], 12.0))}"] + run(f"{phase}-{name}-render", render, + OUT / f"{phase}-{name}-render.log", config["stamp"], env) + subprocess.run([*LIMIT, "timeout", "120s", "ffmpeg", "-nostdin", + "-hide_banner", "-loglevel", "error", "-y", "-i", png, + "-frames:v", "1", "-q:v", "4", jpg], cwd=ROOT, env=env, + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, + timeout=125) + check = subprocess.run([*LIMIT, "timeout", "120s", sys.executable, + quality, jpg], cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=125) + quality_ok = check.returncode == 0 and "status=PASS" in check.stdout + quality_result = (check.stdout.strip().splitlines()[-1] + if check.stdout.strip() else f"quality command exit={check.returncode}") + if not quality_ok: + failures.append(name) + manifest.append({"phase": phase, "view": name, + "file": str(jpg.relative_to(ROOT)), "bytes": jpg.stat().st_size, + "camera_m": camera, "target_m": target, "yaw_deg": yaw, + "pitch_deg": pitch, "quality_status": "PASS" if quality_ok else "FAIL", + "quality": quality_result}) + print(f"[L149MouthCapture] phase={phase} view={name} " + f"file={jpg.relative_to(ROOT)} quality={'PASS' if quality_ok else 'FAIL'}", + flush=True) + (ARTIFACTS / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", + encoding="utf-8") + print(f"[L149MouthCaptureSummary] views={len(JOINTS)} comparisons={len(manifest)//2} " + f"quality_failures={len(failures)} status={'PASS' if not failures else 'FAIL'}", + flush=True) + return 0 if not failures else 1 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L149MouthCaptureFailure] {error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/RunL149SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux149/RunL149SeedSweep.py new file mode 100644 index 0000000..aacfd6e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/RunL149SeedSweep.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Run the L149 composed passage gates across the 16 owner seeds.""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import time +from pathlib import Path + +from MouthAudit import audit_layout + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l149/seeds" +BIN = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l149-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "19dbe26ab76a629baadc54bb0ab93eaa22d6f878e6946df2b71509c12c9524e4" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +BASELINES = { + "tunnel-network": ROOT / "Saved/Linux/l135/optimized-seed-sweep.json", + "vertical-shafts": ROOT / "Saved/Linux/l139/seeds/summary.json", +} +DISPATCH = { + "tunnel-network": re.compile( + r"^\[DensityDispatch\] strate=2 generator=TunnelNetwork " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), + "vertical-shafts": re.compile( + r"^\[DensityDispatch\] strate=3 generator=VerticalShafts " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), +} +ROUTES = { + "tunnel-network": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=2 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=3 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), +} +CONTROLS = { + "tunnel-network": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[VerticalShaftsBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} +REQUIRED = { + "passages": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "passage_interior": re.compile( + r"^\[PassageInteriorCapsuleAuditSummary\].*status=PASS$", re.M), +} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kind", choices=tuple(BASELINES), required=True) + args = parser.parse_args() + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L149 Clang binary or composed owner fixture") + baseline_path = BASELINES[args.kind] + baseline = json.loads(baseline_path.read_text(encoding="utf-8")) + baseline_results = {int(row["seed"]): row for row in baseline["results"]} + mode_out = OUT / args.kind + mode_out.mkdir(parents=True, exist_ok=True) + results = [] + partial = mode_out / "runs-partial.json" + for seed in range(16): + log = mode_out / f"seed{seed}.log" + layout = mode_out / f"seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=0", "--force-generate", f"--seed={seed}", + "--recipe=surface-world", "--density-dispatch-report", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["process_exit_zero"] = proc.returncode == 0 + checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP + checks["dispatch"] = bool(DISPATCH[args.kind].search(content)) + checks["route"] = bool(ROUTES[args.kind].search(content)) + checks["blocked_route_control"] = bool(CONTROLS[args.kind].search(content)) + checks["three_passage_interiors"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*interior_air=PASS", content, re.M)) == 3 + checks["three_interior_negative_controls"] = len(re.findall( + r"^\[ResolvedPassageInteriorNegativeControl\].*detected=true status=PASS$", + content, re.M)) == 3 + checks["three_ripple_gap_controls"] = len(re.findall( + r"^\[PassageRippleSamplingControl\].*status=PASS$", content, re.M)) == 3 + checks["three_blocked_passage_controls"] = len(re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3 + checks["passage_count"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*walkability=PASS$", content, re.M)) == 3 + checks["layout_output"] = layout.is_file() + mouth_audit = audit_layout(layout) if layout.is_file() else None + checks["mouth_continuity"] = bool(mouth_audit and mouth_audit["continuity"]) + checks["no_backtrack_12m"] = bool(mouth_audit and mouth_audit["no_backtrack_12m"]) + checks["mouth_audit_negative_controls"] = bool( + mouth_audit and mouth_audit["negative_controls_pass"]) + passed = all(checks.values()) + prior = baseline_results.get(seed, {}) + prior_passed = bool(prior.get("passed", False)) + change = ("newly failing" if prior_passed and not passed else + "newly passing" if not prior_passed and passed else + "still passing" if prior_passed else "still failing") + failed_checks = [name for name, ok in checks.items() if not ok] + reason = ", ".join(failed_checks) if failed_checks else "all requested gates pass" + row = {"kind": args.kind, "seed": seed, + "prior_verdict": "PASS" if prior_passed else "FAIL", + "verdict": "PASS" if passed else "FAIL", "change": change, + "reason": reason, "exit": proc.returncode, "checks": checks, + "mouth_audit": mouth_audit, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L149SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + partial.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + passed_count = sum(row["verdict"] == "PASS" for row in results) + summary = {"kind": args.kind, "source_stamp": STAMP, + "baseline": str(baseline_path.relative_to(ROOT)), "seeds": list(range(16)), + "passed": passed_count, "failed": 16 - passed_count, "results": results} + (mode_out / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + print(f"[L149SeedSweepSummary] kind={args.kind} passed={passed_count}/16 " + f"failed={16-passed_count} status={'PASS' if passed_count == 16 else 'FAIL'}", + flush=True) + return 0 if passed_count == 16 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/__pycache__/MouthAudit.cpython-313.pyc b/Standalone/Research/WorldAssemblerM3/Linux149/__pycache__/MouthAudit.cpython-313.pyc new file mode 100644 index 0000000..1021ea8 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux149/__pycache__/MouthAudit.cpython-313.pyc differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-departure-edge.jpg b/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-departure-edge.jpg new file mode 100644 index 0000000..6d528a7 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-departure-edge.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-route-edge-0.jpg b/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-route-edge-0.jpg new file mode 100644 index 0000000..1e5e37e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-route-edge-0.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed2-vertical-shafts-route-edge-0.jpg b/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed2-vertical-shafts-route-edge-0.jpg new file mode 100644 index 0000000..5b888ed Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed2-vertical-shafts-route-edge-0.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-departure-edge.jpg b/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-departure-edge.jpg new file mode 100644 index 0000000..5f80153 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-departure-edge.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-route-edge-0.jpg b/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-route-edge-0.jpg new file mode 100644 index 0000000..2e4d797 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-route-edge-0.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed2-vertical-shafts-route-edge-0.jpg b/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed2-vertical-shafts-route-edge-0.jpg new file mode 100644 index 0000000..9ba5ec4 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed2-vertical-shafts-route-edge-0.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/captures/manifest.json b/Standalone/Research/WorldAssemblerM3/Linux149/captures/manifest.json new file mode 100644 index 0000000..0c21e83 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/captures/manifest.json @@ -0,0 +1,122 @@ +[ + { + "phase": "before", + "view": "seed0-tunnel-network-route-edge-0", + "file": "Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-route-edge-0.jpg", + "bytes": 18133, + "camera_m": [ + 17.237675178355673, + -14.73076399027466, + -752.9040234375 + ], + "target_m": [ + 28.807717858221643, + -11.547351923626703, + -752.9040234375 + ], + "yaw_deg": 74.61615105967402, + "pitch_deg": 0.0, + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-route-edge-0.jpg pixels=921600 mean_luma=129.283 stddev_luma=14.699 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "before", + "view": "seed0-tunnel-network-departure-edge", + "file": "Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-departure-edge.jpg", + "bytes": 18526, + "camera_m": [ + -21.497510185619362, + -3.67525285819352, + -811.9255078125 + ], + "target_m": [ + -12.887449071903188, + -12.033891959032399, + -811.9255078125 + ], + "yaw_deg": 134.15112141809087, + "pitch_deg": 0.0, + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed0-tunnel-network-departure-edge.jpg pixels=921600 mean_luma=110.748 stddev_luma=4.561 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "before", + "view": "seed2-vertical-shafts-route-edge-0", + "file": "Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed2-vertical-shafts-route-edge-0.jpg", + "bytes": 21112, + "camera_m": [ + -17.783733421365206, + -4.912768125994757, + -888.10421875 + ], + "target_m": [ + -15.817661018173972, + -16.750612495026218, + -888.10421875 + ], + "yaw_deg": 170.57018242389748, + "pitch_deg": 0.0, + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux149/captures/before-seed2-vertical-shafts-route-edge-0.jpg pixels=921600 mean_luma=129.471 stddev_luma=14.369 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "seed0-tunnel-network-route-edge-0", + "file": "Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-route-edge-0.jpg", + "bytes": 18837, + "camera_m": [ + 17.237675178355673, + -14.73076399027466, + -752.9040234375 + ], + "target_m": [ + 28.807717858221643, + -11.547351923626703, + -752.9040234375 + ], + "yaw_deg": 74.61615105967402, + "pitch_deg": 0.0, + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-route-edge-0.jpg pixels=921600 mean_luma=126.893 stddev_luma=13.032 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "seed0-tunnel-network-departure-edge", + "file": "Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-departure-edge.jpg", + "bytes": 19137, + "camera_m": [ + -21.497510185619362, + -3.67525285819352, + -811.9255078125 + ], + "target_m": [ + -12.887449071903188, + -12.033891959032399, + -811.9255078125 + ], + "yaw_deg": 134.15112141809087, + "pitch_deg": 0.0, + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed0-tunnel-network-departure-edge.jpg pixels=921600 mean_luma=112.098 stddev_luma=7.992 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "phase": "after", + "view": "seed2-vertical-shafts-route-edge-0", + "file": "Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed2-vertical-shafts-route-edge-0.jpg", + "bytes": 20515, + "camera_m": [ + -17.783733421365206, + -4.912768125994757, + -888.10421875 + ], + "target_m": [ + -15.817661018173972, + -16.750612495026218, + -888.10421875 + ], + "yaw_deg": 170.57018242389748, + "pitch_deg": 0.0, + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux149/captures/after-seed2-vertical-shafts-route-edge-0.jpg pixels=921600 mean_luma=110.186 stddev_luma=6.663 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + } +] diff --git a/Standalone/Research/WorldAssemblerM3/Linux149/l149-one-tube-mouth.patch b/Standalone/Research/WorldAssemblerM3/Linux149/l149-one-tube-mouth.patch new file mode 100644 index 0000000..8f442da --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux149/l149-one-tube-mouth.patch @@ -0,0 +1,629 @@ +diff --git a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h +index 24c68ab..c8610eb 100644 +--- a/Standalone/Core/include/VoxelForgeCore/PassageSpline.h ++++ b/Standalone/Core/include/VoxelForgeCore/PassageSpline.h +@@ -13,6 +13,7 @@ namespace VoxelForgeCore::PassageSpline + using FPoint = std::array; + inline constexpr std::size_t ArcTableSteps = 32; + inline constexpr double UpperMouthFloorBlendRadiusMetres = 3.5; ++ inline constexpr double FlatFloorCornerBlendMetres = 0.5; + + struct FSegment + { +@@ -60,6 +61,36 @@ namespace VoxelForgeCore::PassageSpline + return result; + } + ++ inline FPoint EvaluateRawDerivative(std::span 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; ++ const double h00=6.0*t2-6.0*t; ++ const double h01=-h00; ++ const double h10=3.0*t2-4.0*t+1.0; ++ const double h11=3.0*t2-2.0*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]=h00*p1[axis]+h01*p2[axis]+h10*m1+h11*m2; ++ } ++ result[2]=p2[2]-p1[2]; ++ return result; ++ } ++ + inline bool ClosestRouteHeightAtXY(std::span> route, + double x,double y,double& height,double& distanceSquared) + { +diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +index 8c44461..e712747 100644 +--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h ++++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +@@ -511,7 +511,8 @@ namespace VoxelForgeCore + // the planned graph route, preserves its walk corridor through shell overlaps, and keeps + // boundary-blended host rock from widening the authored passage wall. + // v237 adds the shared Core cave-graph shaft primitive and basal terminal route. +- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 237; ++ // v238 joins cave edges to passage mouth tubes with matched geometry and clearance. ++ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 238; + + enum class FieldChannel : std::uint32_t + { +diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h +index 7b7520a..095418b 100644 +--- a/Standalone/Core/src/ResolvedLayoutInternal.h ++++ b/Standalone/Core/src/ResolvedLayoutInternal.h +@@ -2367,8 +2367,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + constexpr std::int64_t MinimumBoreRadius=1750000; + constexpr std::int64_t MinimumBoreHeight=2200000; + constexpr std::int64_t PlannedBoreHeight=4500000; +- constexpr std::int64_t MouthOverlap=2000000; +- constexpr std::int64_t PortalLeadLength=6000000; ++ constexpr std::int64_t PortalLeadLength=1000000; + constexpr std::int64_t MaximumBoreRadius=10000000; + constexpr std::int64_t MaximumWalkGradeDenominator=5; // 1:5 = 11.31 degrees. + const auto integerSqrt=[](std::uint64_t value) +@@ -2394,171 +2393,77 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + const auto qToMicrometres=[&](std::int64_t q) + {return q*static_cast(layout.coreVoxelSizeMicrometres) + /WorldAssemblySubvoxelsPerVoxel;}; +- const std::int64_t mouthBoreRadius=std::clamp( +- std::max(MinimumBoreRadius,settings.minimumTunnelRadiusMicrometres), +- MinimumBoreRadius,MaximumBoreRadius); +- const auto chooseMouthRouteTangent=[&](const ResolvedLayoutPassageV1& passage, +- std::size_t mouthIndex,bool lowerSide, +- const std::array& authoredRouteTangentQ16, +- std::int64_t authoredFloorSlopeQ16,const FPoint& mouthFloorPoint) ++ std::int64_t mouthBoreRadius=std::max(MinimumBoreRadius, ++ settings.minimumTunnelRadiusMicrometres); ++ const auto includePassageMouthRadius=[&]( ++ const ResolvedLayoutPassageV1* passage,bool lowerSide) + { +- constexpr std::array,16> DirectionsQ16={{ +- {{65536,0}},{{60547,25080}},{{46341,46341}},{{25080,60547}}, +- {{0,65536}},{{-25080,60547}},{{-46341,46341}},{{-60547,25080}}, +- {{-65536,0}},{{-60547,-25080}},{{-46341,-46341}},{{-25080,-60547}}, +- {{0,-65536}},{{25080,-60547}},{{46341,-46341}},{{60547,-25080}}}}; +- const auto scoreDirection=[&](const std::array& outwardQ16) +- { +- const std::size_t segmentCount=passage.controlPoints.size()>1 +- ?passage.controlPoints.size()-1:0; +- std::int64_t worst=std::numeric_limits::max(); +- bool sampled=false; +- const std::int64_t projectedSlopeQ16=(authoredFloorSlopeQ16 +- *(outwardQ16[0]*authoredRouteTangentQ16[0] +- +outwardQ16[1]*authoredRouteTangentQ16[1]) +- /(65536ll*65536ll)); +- constexpr std::int64_t MouthClearanceProbeExtent=48000000; +- for(std::int64_t distance=2000000; +- distance<=MouthClearanceProbeExtent;distance+=2000000) +- { +- const std::int64_t x=mouthFloorPoint[0] +- +outwardQ16[0]*distance/65536; +- const std::int64_t y=mouthFloorPoint[1] +- +outwardQ16[1]*distance/65536; +- const std::int64_t caveFloor=mouthFloorPoint[2] +- +projectedSlopeQ16*distance/65536; +- const std::int64_t caveCeiling=caveFloor+PlannedBoreHeight; +- for(std::size_t segmentIndex=0;segmentIndex=mouthIndex)continue; +- if(!lowerSide&&segmentIndex<2)continue; +- const auto& a=passage.controlPoints[segmentIndex]; +- const auto& b=passage.controlPoints[segmentIndex+1]; +- const std::int64_t ax=qToMicrometres(a.positionSubvoxels[0]); +- const std::int64_t ay=qToMicrometres(a.positionSubvoxels[1]); +- const std::int64_t az=qToMicrometres(a.positionSubvoxels[2]); +- const std::int64_t bx=qToMicrometres(b.positionSubvoxels[0]); +- const std::int64_t by=qToMicrometres(b.positionSubvoxels[1]); +- const std::int64_t bz=qToMicrometres(b.positionSubvoxels[2]); +- const std::int64_t dx=bx-ax,dy=by-ay; +- const double lengthSquared=static_cast(dx)*dx +- +static_cast(dy)*dy; +- const double projection=lengthSquared>0.0 +- ?std::clamp((static_cast(x-ax)*dx +- +static_cast(y-ay)*dy)/lengthSquared,0.0,1.0):0.0; +- const std::int64_t tQ16=static_cast(std::llround( +- projection*65536.0)); +- const std::int64_t closestX=ax+dx*tQ16/65536; +- const std::int64_t closestY=ay+dy*tQ16/65536; +- const std::int64_t horizontalDistance=static_cast( +- integerSqrt(static_cast((x-closestX)*(x-closestX) +- +(y-closestY)*(y-closestY)))); +- const std::int64_t passageCenterZ=qToMicrometres( +- a.positionSubvoxels[2]) +- +(qToMicrometres(b.positionSubvoxels[2]) +- -qToMicrometres(a.positionSubvoxels[2]))*tQ16/65536; +- const std::int64_t passageRadius=qToMicrometres(a.radiusSubvoxels) +- +(qToMicrometres(b.radiusSubvoxels) +- -qToMicrometres(a.radiusSubvoxels))*tQ16/65536; +- const std::int64_t horizontalGap=horizontalDistance +- -passageRadius-mouthBoreRadius; +- const std::int64_t passageFloor=passageCenterZ-passageRadius; +- const std::int64_t passageCeiling=passageCenterZ+passageRadius; +- const std::int64_t verticalGap=caveFloor>passageCeiling +- ?caveFloor-passageCeiling +- :(passageFloor>caveCeiling?passageFloor-caveCeiling +- :-std::min(caveCeiling-passageFloor, +- passageCeiling-caveFloor)); +- const std::int64_t separation=horizontalGap>=0&&verticalGap>=0 +- ?static_cast(integerSqrt(static_cast( +- horizontalGap*horizontalGap+verticalGap*verticalGap))) +- :std::max(horizontalGap,verticalGap); +- worst=std::min(worst,separation);sampled=true; +- } +- } +- return sampled?worst:std::numeric_limits::max(); +- }; +- +- // Keep the authored tangent whenever it clears the other passage spans. +- // If the passage folds back over the mouth, choose the outward direction +- // with the greatest 3D clearance while preserving deterministic tie order. +- const std::array authoredOutwardQ16={ +- lowerSide?authoredRouteTangentQ16[0]:-authoredRouteTangentQ16[0], +- lowerSide?authoredRouteTangentQ16[1]:-authoredRouteTangentQ16[1]}; +- const std::int64_t authoredScore=scoreDirection(authoredOutwardQ16); +- if(authoredScore>=0)return authoredRouteTangentQ16; +- std::array bestOutwardQ16=authoredOutwardQ16; +- std::int64_t bestScore=authoredScore; +- std::int64_t bestAlignment=authoredOutwardQ16[0]*authoredOutwardQ16[0] +- +authoredOutwardQ16[1]*authoredOutwardQ16[1]; +- for(const auto& candidate:DirectionsQ16) +- { +- const std::int64_t score=scoreDirection(candidate); +- const std::int64_t alignment=candidate[0]*authoredOutwardQ16[0] +- +candidate[1]*authoredOutwardQ16[1]; +- if(score>bestScore||(score==bestScore&&alignment>bestAlignment)) +- {bestScore=score;bestAlignment=alignment;bestOutwardQ16=candidate;} +- } +- std::printf("[CavernMouthClearancePlan] passage=%s side=%s authored_clearance_m=%.3f selected_clearance_m=%.3f outward_q16=(%lld,%lld)\n", +- passage.stableId.c_str(),lowerSide?"lower":"upper", +- authoredScore/1.0e6,bestScore/1.0e6, +- static_cast(bestOutwardQ16[0]), +- static_cast(bestOutwardQ16[1])); +- return std::array{ +- lowerSide?bestOutwardQ16[0]:-bestOutwardQ16[0], +- lowerSide?bestOutwardQ16[1]:-bestOutwardQ16[1]}; ++ if(!passage||passage->controlPoints.empty())return; ++ const std::size_t index=lowerSide ++ ?std::min(passage->lowerMouthControlIndex,passage->controlPoints.size()-1):0; ++ mouthBoreRadius=std::max(mouthBoreRadius, ++ qToMicrometres(passage->controlPoints[index].radiusSubvoxels)); + }; ++ includePassageMouthRadius(arrivalPassage,true); ++ includePassageMouthRadius(departurePassage,false); ++ mouthBoreRadius=std::clamp(mouthBoreRadius,MinimumBoreRadius,MaximumBoreRadius); + const auto mouthInnerPoint=[&](const ResolvedLayoutPassageV1& passage, + bool lowerSide,FPoint& point,std::array& routeTangentQ16, + std::int64_t& floorSlopeQ16) + { +- 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; +- // The lower arrival continues from the previous passage point through its +- // mouth. The upper departure approaches its mouth from the cave and then +- // continues toward control point 1, so the two route tangents have opposite +- // signs even though both portal offsets lie on the cave side. ++ if(lowerSide&&mouthIndex==0)return false; ++ const std::size_t span=lowerSide?mouthIndex-1:0; ++ const double endpointT=lowerSide?1.0:0.0; + const std::int64_t direction=lowerSide?1:-1; +- const std::int64_t tx=direction*(passage.controlPoints[mouthIndex].positionSubvoxels[0] +- -passage.controlPoints[tangentIndex].positionSubvoxels[0]); +- const std::int64_t ty=direction*(passage.controlPoints[mouthIndex].positionSubvoxels[1] +- -passage.controlPoints[tangentIndex].positionSubvoxels[1]); +- const auto tangentLength=static_cast(integerSqrt( +- static_cast(tx*tx+ty*ty))); +- if(tangentLength<=0)return false; +- routeTangentQ16={tx*65536/tangentLength,ty*65536/tangentLength}; ++ std::vector splineControls; ++ std::vector floorProfile; ++ splineControls.reserve(passage.controlPoints.size()); ++ floorProfile.reserve(passage.controlPoints.size()); ++ for(const auto& control:passage.controlPoints) ++ { ++ const std::int64_t x=qToMicrometres(control.positionSubvoxels[0]); ++ const std::int64_t y=qToMicrometres(control.positionSubvoxels[1]); ++ const std::int64_t z=qToMicrometres(control.positionSubvoxels[2]); ++ const std::int64_t radius=qToMicrometres(control.radiusSubvoxels); ++ splineControls.push_back({static_cast(x),static_cast(y), ++ static_cast(z),static_cast(radius)}); ++ floorProfile.push_back(static_cast(z-radius)); ++ } ++ auto derivative=VoxelForgeCore::PassageSpline::EvaluateRawDerivative( ++ splineControls,span,endpointT,MicrometresPerMetre); ++ if(!lowerSide) ++ { ++ derivative[0]=-derivative[0]; ++ derivative[1]=-derivative[1]; ++ } ++ const double tangentLength=std::hypot(derivative[0],derivative[1]); ++ if(!(tangentLength>1.0e-9)||!std::isfinite(tangentLength))return false; ++ routeTangentQ16={ ++ static_cast(std::llround(derivative[0]*65536.0/tangentLength)), ++ static_cast(std::llround(derivative[1]*65536.0/tangentLength))}; ++ const auto passageSegments=VoxelForgeCore::PassageSpline::BuildSegments( ++ splineControls,splineControls.size()-1,2.0,MicrometresPerMetre, ++ floorProfile); ++ double spanHorizontalLengthMicrometres=0.0; ++ for(const auto& segment:passageSegments) ++ if(segment.controlSpan==span) ++ spanHorizontalLengthMicrometres+=std::hypot( ++ segment.b[0]-segment.a[0],segment.b[1]-segment.a[1]); ++ if(!(spanHorizontalLengthMicrometres>1.0e-9))return false; + const auto& mouthControl=passage.controlPoints[mouthIndex]; +- const auto& tangentControl=passage.controlPoints[tangentIndex]; + const std::int64_t mouthFloor=mouthControl.positionSubvoxels[2] + -mouthControl.radiusSubvoxels; +- const std::int64_t tangentFloor=tangentControl.positionSubvoxels[2] +- -tangentControl.radiusSubvoxels; +- // Resolved passage mouths already store the player center at floor + 1.13 m. +- // Cave edge control points store the floor itself, so start from the passage +- // control's floor rather than reusing the player-center mouth height. ++ const std::size_t tangentIndex=lowerSide?mouthIndex-1:1; ++ const double passageFloorDelta=direction*(floorProfile[mouthIndex] ++ -floorProfile[tangentIndex]); ++ const double slope=passageFloorDelta/spanHorizontalLengthMicrometres; ++ floorSlopeQ16=static_cast(std::llround(slope*65536.0)); ++ point[0]=qToMicrometres(mouthControl.positionSubvoxels[0]); ++ point[1]=qToMicrometres(mouthControl.positionSubvoxels[1]); + point[2]=qToMicrometres(mouthFloor); +- const std::int64_t passageFloorDelta=direction*(mouthFloor-tangentFloor); +- floorSlopeQ16=passageFloorDelta*65536/tangentLength; +- const auto authoredRouteTangentQ16=routeTangentQ16; +- routeTangentQ16=chooseMouthRouteTangent(passage,mouthIndex,lowerSide, +- authoredRouteTangentQ16,floorSlopeQ16,point); +- const std::int64_t tangentAlignment=routeTangentQ16[0] +- *authoredRouteTangentQ16[0]+routeTangentQ16[1] +- *authoredRouteTangentQ16[1]; +- floorSlopeQ16=floorSlopeQ16*tangentAlignment/(65536ll*65536ll); +- const std::int64_t offsetDirection=lowerSide?1:-1; +- point[0]+=offsetDirection*routeTangentQ16[0]*MouthOverlap/65536; +- point[1]+=offsetDirection*routeTangentQ16[1]*MouthOverlap/65536; +- point[2]+=offsetDirection*floorSlopeQ16*MouthOverlap/65536; + return true; + }; + FPoint routeStart{},routeEnd{}; +@@ -2738,7 +2643,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + controls.size()-1,2.0,1000000.0); + const double totalLength=segments.empty()?0.0: + segments.back().alongMetres+segments.back().lengthMetres; +- constexpr double PortalExclusionMetres=10.0; ++ constexpr double PortalExclusionMetres=2.0; + for(const auto& segment:segments) + { + const double midpoint=segment.alongMetres+segment.lengthMetres*0.5; +@@ -2815,7 +2720,7 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + if(routePassageClearanceSpans.empty()||points.size()<2) + return std::numeric_limits::infinity(); + const double totalLength=routeLength(points)/1.0e6; +- constexpr double PortalExclusionMetres=10.0; ++ constexpr double PortalExclusionMetres=2.0; + double minimumClearance=std::numeric_limits::infinity(); + double along=0.0; + for(std::size_t index=1;index& tangentQ16,std::int64_t floorSlopeQ16, ++ std::int64_t profileStartDistance,std::int64_t profileEndDistance) ++ { ++ const std::size_t mouthIndex=lowerSide ++ ?std::min(passage.lowerMouthControlIndex,passage.controlPoints.size()-1):0; ++ const auto& mouth=passage.controlPoints[mouthIndex]; ++ const std::int64_t mouthRadius=qToMicrometres(mouth.radiusSubvoxels); ++ auto edge=makeEdge(std::move(id),std::move(startAnchor),start,endNode, ++ mouthRadius,profileStartDistance,profileEndDistance); ++ edge.boreRadiusMicrometres=mouthRadius; ++ edge.boreHeightMicrometres=2*mouthRadius; ++ edge.smoothWidthMicrometres=static_cast(std::llround( ++ VoxelForgeCore::PassageSpline::FlatFloorCornerBlendMetres ++ *MicrometresPerMetre)); ++ edge.unionWidthMicrometres=smooth; ++ const FPoint end={endNode.point[0],endNode.point[1],endNode.point[2]}; ++ const std::int64_t horizontalRun=distance2d(start,end); ++ const std::int64_t controlHandle=std::max(1, ++ std::min(PortalLeadLength,std::max(1,horizontalRun))/3); ++ edge.controlPointsMicrometres[1][0]=start[0] ++ +tangentQ16[0]*controlHandle/65536; ++ edge.controlPointsMicrometres[1][1]=start[1] ++ +tangentQ16[1]*controlHandle/65536; ++ edge.controlPointsMicrometres[1][2]=start[2] ++ +floorSlopeQ16*controlHandle/65536; ++ return edge; ++ }; + struct FPlannedShaftLanding + { + FRoomNode node; +@@ -3396,9 +3331,13 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + { + const auto& node=nodes[nodeIndex]; + const std::string edgeId=routePrefix+"route-edge-"+std::to_string(nodeIndex); +- auto edge=makeEdge(edgeId,previousAnchor,previousPoint,node, +- selectedBoreRadius(edgeId), +- previousProfileDistance,node.floorProfileDistanceMicrometres); ++ auto edge=nodeIndex==0 ++ ?makePassageMouthEdge(edgeId,previousAnchor,previousPoint,node, ++ *arrivalPassage,true,arrivalTangentQ16,arrivalFloorSlopeQ16, ++ previousProfileDistance,node.floorProfileDistanceMicrometres) ++ :makeEdge(edgeId,previousAnchor,previousPoint,node, ++ selectedBoreRadius(edgeId),previousProfileDistance, ++ node.floorProfileDistanceMicrometres); + edges.push_back(std::move(edge)); + previousPoint={node.point[0],node.point[1],node.point[2]}; + previousProfileDistance=node.floorProfileDistanceMicrometres; +@@ -3407,8 +3346,9 @@ bool PlanResolvedCavernNetworkV1(const ResolvedLayoutSnapshotV3& layout, + if(!nodes.empty()&&departurePassage) + { + const std::string departureEdgeId=routePrefix+"departure-edge"; +- auto edge=makeEdge(departureEdgeId,departurePassage->stableId+"/upper-mouth", +- routeEnd,nodes.back(),selectedBoreRadius(departureEdgeId), ++ auto edge=makePassageMouthEdge(departureEdgeId, ++ departurePassage->stableId+"/upper-mouth",routeEnd,nodes.back(), ++ *departurePassage,false,departureTangentQ16,departureFloorSlopeQ16, + totalRouteLength,nodes.back().floorProfileDistanceMicrometres); + edges.push_back(std::move(edge)); + } +diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h +index 1a57f85..d17aa02 100644 +--- a/Standalone/Core/src/ShapeWorldInternal.h ++++ b/Standalone/Core/src/ShapeWorldInternal.h +@@ -4443,11 +4443,12 @@ private: + buildSegmentNode(buildSegmentNode,0,runtime.segmentOrder.size()); + const double radius=edge.boreRadiusMicrometres/1000000.0; + const double height=edge.boreHeightMicrometres/1000000.0; ++ const bool passageMouthTube=IsPlannerPassageMouthContinuationEdge(edge); + if(radius>0.0&&height>0.0) + for(auto& node:runtime.segmentNodes) + for(std::size_t axis=0;axis<3;++axis) + { +- const double scale=axis==2?height:radius; ++ const double scale=axis==2&&!passageMouthTube?height:radius; + node.minimumNormalized[axis]=node.minimum[axis]/scale; + node.maximumNormalized[axis]=node.maximum[axis]/scale; + } +@@ -5728,6 +5729,8 @@ public: + } + else if(feature.edge&&!feature.segments.empty()) + { ++ if(IsPlannerPassageMouthContinuationEdge(*feature.edge)) ++ return {0.0,0.0,false}; + const double radius=feature.edge->boreRadiusMicrometres/1000000.0; + const double height=feature.edge->boreHeightMicrometres/1000000.0; + if(!(radius>0.0&&height>0.0))return {0.0,0.0,false}; +@@ -7832,6 +7835,14 @@ private: + return composed; + } + ++ static bool IsPlannerPassageMouthContinuationEdge( ++ const VoxelForgeCore::ResolvedCavernEdgeV1& edge) ++ { ++ const bool sharedPlannerEdge=edge.stableId.starts_with("tunnel-network/") ++ ||edge.stableId.starts_with("vertical-shafts/"); ++ return sharedPlannerEdge&&edge.startAnchorStableId.ends_with("-mouth"); ++ } ++ + static double SmoothMinMetres(double a,double b,double width) + { + if(!(width>0.0))return std::min(a,b); +@@ -7997,9 +8008,11 @@ private: + double nearestFloor=0.0,nearestGrade=0.0,nearestRadial=0.0; + const double radius=feature.edge->boreRadiusMicrometres/1000000.0; + const double height=feature.edge->boreHeightMicrometres/1000000.0; ++ const bool passageMouthTube= ++ IsPlannerPassageMouthContinuationEdge(*feature.edge); + constexpr double SegmentDistanceTieToleranceMetres=1.0e-12; + if(!(radius>0.0&&height>0.0))continue; +- const double minScale=std::min(radius,height); ++ const double minScale=passageMouthTube?radius:std::min(radius,height); + std::uint32_t nearestSegment=std::numeric_limits::max(); + const auto evaluateSegment=[&](std::uint32_t segmentIndex) + { +@@ -8010,7 +8023,9 @@ private: + { + const double delta=segment.end[axis]-segment.start[axis]; + lengthSquared+=delta*delta; +- dot+=(point[axis]-segment.start[axis])*delta; ++ const double query=axis==2&&passageMouthTube ++ ?point[axis]-radius:point[axis]; ++ dot+=(query-segment.start[axis])*delta; + } + const double t=lengthSquared>0.0 + ?std::clamp(dot/lengthSquared,0.0,1.0):0.0; +@@ -8020,11 +8035,12 @@ private: + const double delta=point[axis]-(segment.start[axis] + +t*(segment.end[axis]-segment.start[axis])); + if(axis<2)radialSquared+=delta*delta; +- else vertical=delta; ++ else vertical=delta-(passageMouthTube?radius:0.0); + } ++ const double verticalScale=passageMouthTube?radius:height; + const double normalized=std::sqrt(radialSquared/(radius*radius) +- +vertical*vertical/(height*height)); +- double distance=(normalized-1.0)*std::min(radius,height); ++ +vertical*vertical/(verticalScale*verticalScale)); ++ double distance=(normalized-1.0)*minScale; + if(feature.irregularityAmplitudeVoxels>0.0f) + { + const float floorVoxels=static_cast(nearestFloor/metresPerVoxel); +@@ -8049,8 +8065,10 @@ private: + ?(segment.end[2]-segment.start[2])/horizontal:0.0; + } + }; ++ const double verticalScale=passageMouthTube?radius:height; + const std::array normalizedPoint={ +- point[0]/radius,point[1]/radius,point[2]/height}; ++ point[0]/radius,point[1]/radius, ++ (point[2]-(passageMouthTube?radius:0.0))/verticalScale}; + const auto nodeLowerBound=[&](const FCavernRuntimeFeature::FSegmentNode& node) + { + double gapSquared=0.0; +diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h +index 4e20068..0c3cbb7 100644 +--- a/Standalone/Source/WorldAssemblyPrototype.h ++++ b/Standalone/Source/WorldAssemblyPrototype.h +@@ -1732,6 +1732,44 @@ namespace StandaloneWorldAssemblyPrototype + if(std::abs(a[axis]-b[axis])>2)return false; + return true; + }; ++ const auto pointAlongEdge=[&](const ResolvedCavernEdgeV1& edge, ++ double distanceFromStart) ++ { ++ const auto evaluate=[&](double t) ++ { ++ const double one=1.0-t; ++ const std::array weights={one*one*one, ++ 3.0*one*one*t,3.0*one*t*t,t*t*t}; ++ std::array point{}; ++ for(std::size_t control=0;control<4;++control) ++ for(std::size_t axis=0;axis<3;++axis) ++ point[axis]+=weights[control] ++ *edge.controlPointsMicrometres[control][axis]; ++ return point; ++ }; ++ auto previous=evaluate(0.0); ++ double travelled=0.0; ++ constexpr int Samples=256; ++ for(int sample=1;sample<=Samples;++sample) ++ { ++ const auto next=evaluate(static_cast(sample)/Samples); ++ const double segmentLength=std::hypot(next[0]-previous[0], ++ next[1]-previous[1])/1000000.0; ++ if(travelled+segmentLength>=distanceFromStart ++ &&segmentLength>1.0e-9) ++ { ++ const double t=std::clamp((distanceFromStart-travelled) ++ /segmentLength,0.0,1.0); ++ std::array point{}; ++ for(std::size_t axis=0;axis<3;++axis) ++ point[axis]=std::lerp(previous[axis],next[axis],t); ++ return point; ++ } ++ travelled+=segmentLength; ++ previous=next; ++ } ++ return evaluate(1.0); ++ }; + auto start=arrival->lowerMouthSubvoxels; + appendRoutePoint("000-arrival",start); + for(std::size_t edgeIndex=0;edgeIndex<256;++edgeIndex) +@@ -1755,15 +1793,14 @@ namespace StandaloneWorldAssemblyPrototype + } + if(edgeIndex==0) + { +- auto inner=edge->controlPointsMicrometres.front(); +- // Keep the exact authored mouth anchor and use the planner +- // edge's floor height for the first cave-side walking point. +- // Passage composition uses these same two points to blend the +- // floor profile into the native cave route. +- inner[2]+=PlayerCenterAboveFloorMicrometres; ++ const auto edgeStart=pointAlongEdge(*edge,2.0); + const std::array point={ +- toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]), +- toCoreSubvoxels(inner[2])}; ++ toCoreSubvoxels(static_cast(std::llround(edgeStart[0]))), ++ toCoreSubvoxels(static_cast(std::llround(edgeStart[1]))), ++ toCoreSubvoxels(static_cast(std::llround(edgeStart[2] ++ +PlayerCenterAboveFloorMicrometres)))}; ++ // Keep a walking point on the actual edge curve so audits and ++ // passage floor blending follow the same mouth continuation. + if(!sameRoutePoint(point,start)) + appendRoutePoint("000-edge-start",point); + } +@@ -1783,11 +1820,12 @@ namespace StandaloneWorldAssemblyPrototype + &&value.stableId==departureEdgeId;}); + if(departureEdge==layout.cavernEdges.end()) + {error="native cave route lost its planned departure edge";return false;} +- auto inner=departureEdge->controlPointsMicrometres.front(); +- inner[2]+=PlayerCenterAboveFloorMicrometres; ++ const auto edgeInner=pointAlongEdge(*departureEdge,2.0); + const std::array innerPoint={ +- toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]), +- toCoreSubvoxels(inner[2])}; ++ toCoreSubvoxels(static_cast(std::llround(edgeInner[0]))), ++ toCoreSubvoxels(static_cast(std::llround(edgeInner[1]))), ++ toCoreSubvoxels(static_cast(std::llround(edgeInner[2] ++ +PlayerCenterAboveFloorMicrometres)))}; + auto end=departure->upperMouthSubvoxels; + if(!sameRoutePoint(innerPoint,end)) + appendRoutePoint("998-departure-inner",innerPoint); +diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline +new file mode 100644 +index 0000000..98d4cc8 +--- /dev/null ++++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline +@@ -0,0 +1,12 @@ ++[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173 ++[PipelineWholeViewHash] tiles=2370 density=0x7111b4b743e03c95 mesh=0x8c7240939db21b88 payload=0x3981ff5ca7076219 topology=0x07947d2e691b2e55 ++[PipelineLODHash] lod=0 tiles=512 density=0x2fd4c78685a43cfe mesh=0x50478cff1c1f8ca0 payload=0x54e48e57e0af26de topology=0x860c011cfdb9833a ++[PipelineLODHash] lod=1 tiles=448 density=0x4b8d9a6dcd2942ad mesh=0x17e89b7087bfe711 payload=0x2389c109eb780db6 topology=0x7eccd4cf560d0630 ++[PipelineLODHash] lod=2 tiles=320 density=0x48d4c21a852e6e21 mesh=0x5516a17618d142d5 payload=0x59cda38ab9ca6b7a topology=0x15ae353bd835e6a1 ++[PipelineLODHash] lod=3 tiles=208 density=0x09b4811098a48c8d mesh=0xbac3fb6d54bca8e9 payload=0xca779555d55be411 topology=0xc63e8ee46890a03a ++[PipelineLODHash] lod=4 tiles=240 density=0xfb11591772a3d18e mesh=0xebf2baee8764591b payload=0xe491d7ac9c0061ad topology=0xd618ac33b95ce494 ++[PipelineLODHash] lod=5 tiles=144 density=0x2f8b7b7fb1e67258 mesh=0x4cd508cbe97344fd payload=0x2b6e96a6050458c9 topology=0x4ff6d0eda8988573 ++[PipelineLODHash] lod=6 tiles=96 density=0x0114a34c8b8d6113 mesh=0xb1fec4d01ae1fa44 payload=0x3e50cfd429b8e950 topology=0x08090c96d15c1618 ++[PipelineLODHash] lod=7 tiles=96 density=0x3836ed8cfce73418 mesh=0x40bc6589c5fc1aac payload=0x677349bcae16790f topology=0x36e3aa53192e6a6b ++[PipelineLODHash] lod=8 tiles=306 density=0x144f3c1fd722708b mesh=0xef77b28bf7fd9453 payload=0xe3f47b37fcd61489 topology=0x5a3c73e295494e04 ++[PipelineExactQueryHash] probes=9 density=0x7b7c50ac24ccadcc unclamped=0x7b7c50ac24ccadcc detail=0xefcba20c0c70c415 collision=0xe604823a249029bf path=direct-Core-queries +diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline +new file mode 100644 +index 0000000..e43aaf0 +--- /dev/null ++++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline +@@ -0,0 +1,12 @@ ++[PipelineFullDetailHash] tiles=960 density=0xace56c8f205238ad mesh=0xa9c603a1248de6b8 payload=0xea55f1f4fcc6a894 topology=0x5ea7bc78a1b678ae ++[PipelineWholeViewHash] tiles=2370 density=0x826704417f556bb8 mesh=0x3dc31324d910ad2b payload=0x32cd1847a8e05d0a topology=0xb800610aa85336d7 ++[PipelineLODHash] lod=0 tiles=512 density=0xcd6ab28cfa504aca mesh=0xf03dc80a669fca31 payload=0x397aded1c3d2d297 topology=0xb4a1dc84fabc1fe3 ++[PipelineLODHash] lod=1 tiles=448 density=0xce6e713828b2a20a mesh=0x1cf408c25d67d18c payload=0x8a2b5d20571c997e topology=0x51cd985fe8e7d100 ++[PipelineLODHash] lod=2 tiles=320 density=0xd7f003d75d73e5b1 mesh=0x4e9c56918da506ae payload=0x3867c5915b409d6d topology=0x02a2a7c0ee9ac809 ++[PipelineLODHash] lod=3 tiles=208 density=0xce02d09814028b15 mesh=0xe4c2addc4c54c439 payload=0x7e25d9eee6469c5a topology=0xe603e61c9b9bb733 ++[PipelineLODHash] lod=4 tiles=240 density=0x0be8798407724eaf mesh=0x131c303cbbe51434 payload=0x2a90ecf0a2411ca0 topology=0xe3e054a371152e08 ++[PipelineLODHash] lod=5 tiles=144 density=0xf20897ad272103a9 mesh=0x13d00b6253ae1f72 payload=0x0db3880693a962fa topology=0x462bd87a80f15e89 ++[PipelineLODHash] lod=6 tiles=96 density=0xc040fcac457d6313 mesh=0x1ed132eeb718fb62 payload=0xcd0214e35577c6e3 topology=0x2c56c69e6fdcfd64 ++[PipelineLODHash] lod=7 tiles=96 density=0xc2d419446355fdb8 mesh=0x4ea44452574fc84b payload=0xfd9779bedfc9ae0f topology=0x8948546e770c9b72 ++[PipelineLODHash] lod=8 tiles=306 density=0x62db31696f2da831 mesh=0xd373f9e9481532fe payload=0x540a60526e0e3e46 topology=0xc725cb901e6dc335 ++[PipelineExactQueryHash] probes=9 density=0xc949c4df2ce6f9d9 unclamped=0xc949c4df2ce6f9d9 detail=0x114c501e2d830d94 collision=0xe6011d3a248d4849 path=direct-Core-queries +diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py +index e97c250..12f4405 100644 +--- a/Standalone/Tools/verify/verify.py ++++ b/Standalone/Tools/verify/verify.py +@@ -125,11 +125,11 @@ RECIPE_RE = re.compile( + + def owner_hash_baseline(recipe: str, style: int) -> tuple[Path, str]: + if recipe == "surface-world" and style == 1: +- return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v237.baseline", +- "L139-v237-vertical-shafts-core-planner") ++ return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v238.baseline", ++ "L149-v238-passage-mouth-tube-joins") + if recipe == "floating-islands" and style == 2: +- return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v237.baseline", +- "L139-v237-vertical-shafts-core-planner") ++ return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v238.baseline", ++ "L149-v238-passage-mouth-tube-joins") + raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}") + + diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux150/REPORT.md new file mode 100644 index 0000000..6f6ab24 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux150/REPORT.md @@ -0,0 +1,69 @@ +# Linux150 — cave planner merge evidence + +**Result:** `assembler/world` merges cleanly into `wip/l134-cave-planner`. The planner HEAD passes 15/16 seeds for each cave layer; seed 2 retains its known shell/seam failure. Linux Unreal start and movement runs completed. L80 shows slower near-ready medians in six of eight cells, while whole-view medians are lower in seven of eight cells. + +No generation code changed in L150. Branch HEAD was `b8382eed`; target `assembler/world` was `eb47d722`. The UE baseline project uses `c743f2a`, code-equivalent to the target. The planner source stamp is `c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f`; the `assembler/world` baseline binary stamp is `4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621`. + +## Evidence table + +L80 used one warm-up per binary and five interleaved measured runs per cell at W8/W12 across all four owner layers. The quiet-host gate passed at load 0.50 and 99.3% CPU idle. The L137 A/A floor is ±1%. Time deltas are planner HEAD versus `assembler/world`; positive means slower. + +UE start rows are one L121 start per stratum and build, measuring start through the full LOD0–2 set. Movement rows are medians over five interleaved L127 runs at 30 m/s (12 s moving plus 5 s dwell, eight effective tile slots). Coverage and hole figures describe the moving window. For L80, S0–S3 are Surface World, Floating Islands, Tunnel Network, and Vertical Shafts. + +| Measure | assembler/world | planner HEAD | Delta / result | +|---|---:|---:|---:| +| UE start → LOD0–2, S0 (512/448/448 tiles) | 2.453159 s | 2.481646 s | +1.16% | +| UE start → LOD0–2, S1 (512/448/256 tiles) | 2.424691 s | 2.206180 s | −9.02% | +| UE start → LOD0–2, S2 (513/448/384 tiles) | 2.454927 s | 2.477987 s | +0.94% | +| UE start → LOD0–2, S3 (513/448/384 tiles) | 2.216523 s | 2.459258 s | +10.95% | +| 30 m/s near-ready moving mean | 44.2432% | 38.5307% | −5.71 pp (−12.91% relative) | +| 30 m/s far-hole moving mean | 3.2596% | 3.3208% | +0.0611 pp (+1.88% relative) | +| 30 m/s game-thread tick p99 | 10.7288 ms | 10.9596 ms | +2.15% | +| 30 m/s median per-run worst tick | 13.5996 ms | 14.4242 ms | +6.06% | +| 30 m/s worst tick observed across five runs | 14.9833 ms | 14.6717 ms | −2.08% | +| L80 S0 near-ready median, W8 / W12 | 0.737599 / 0.768684 s | 0.775814 / 0.829089 s | +5.18% / +7.86% | +| L80 S0 whole-view median, W8 / W12 | 1.737 / 1.705 s | 1.711 / 1.705 s | −1.50% / 0.00% | +| L80 S1 near-ready median, W8 / W12 | 1.006966 / 0.974332 s | 1.021554 / 1.029940 s | +1.45% / +5.71% | +| L80 S1 whole-view median, W8 / W12 | 1.907 / 1.812 s | 1.803 / 1.803 s | −5.45% / −0.50% | +| L80 S2 near-ready median, W8 / W12 | 1.044179 / 0.959096 s | 1.039310 / 1.026140 s | −0.47% / +6.99% | +| L80 S2 whole-view median, W8 / W12 | 1.913 / 1.751 s | 1.821 / 1.733 s | −4.81% / −1.03% | +| L80 S3 near-ready median, W8 / W12 | 0.875773 / 0.838977 s | 0.852260 / 0.853215 s | −2.68% / +1.70% | +| L80 S3 whole-view median, W8 / W12 | 1.788 / 1.687 s | 1.660 / 1.605 s | −7.16% / −4.86% | +| Tunnel Network composed sweep | — | 15/16 | Seed 2: passages, seams, shell | +| Vertical Shafts composed sweep | — | 15/16 | Seed 2: passages, seams, shell | +| Seed-0 capture quality | 1/4 pass | 4/4 pass | All exports/renders completed; three baseline viewpoints have no visible mesh surface | + +Both moving runs reached 100% near-ready coverage and 0% far holes by dwell end (3.40 s baseline, 3.78 s HEAD). The five-run maximum tick was lower on HEAD, while its median of per-run maxima was higher. + +For both sweeps, every gate other than the three seed-2 checks passed, including dispatch, route, naturalness, mouth, ripple, interior air, and negative controls. The seed-2 failure is the known interface 2–3 shell/seam failure. + +## Owner-world captures + +Four eye-height viewpoints use the same camera coordinates for both versions. Poses come from the planner layout resolved against the owner fixture’s embedded Core plan at seed 0. The capture layout identity is `cc87ee727c483039` on planner HEAD and `0589300eb07dfe06` on the baseline. The seed sweeps use the established `--seed=0` regeneration path (layout identity `d310515572977ebf`); the captures intentionally use the owner fixture’s embedded Core plan. For the baseline, a seed override makes the older route setup fail, so the capture runner imports the saved plan without an override. The planner images pass the capture-quality gate. Three baseline images fail its image-variance threshold because the baseline mesh has no visible surface from those exact poses; export and render processes succeeded. + +| View | assembler/world | planner HEAD | +|---|---|---| +| Tunnel Network — route joint | ![assembler/world route joint](captures/assembler-world-tunnel-network-route.jpg) | ![planner HEAD route joint](captures/planner-head-tunnel-network-route.jpg) | +| Tunnel Network — departure joint | ![assembler/world departure joint](captures/assembler-world-tunnel-network-departure.jpg) | ![planner HEAD departure joint](captures/planner-head-tunnel-network-departure.jpg) | +| Vertical Shafts — looking down | ![assembler/world shaft looking down](captures/assembler-world-vertical-shafts-looking-down.jpg) | ![planner HEAD shaft looking down](captures/planner-head-vertical-shafts-looking-down.jpg) | +| Vertical Shafts — looking up | ![assembler/world shaft looking up](captures/assembler-world-vertical-shafts-looking-up.jpg) | ![planner HEAD shaft looking up](captures/planner-head-vertical-shafts-looking-up.jpg) | + +Capture source stamps, coordinates, and quality output are in [`captures/manifest.json`](captures/manifest.json). The runner is [`RunL150Captures.py`](RunL150Captures.py). + +## Merge risks + +- Seed 2 fails the passage, seam, and shell checks in both cave-layer sweeps. Other current sweep gates pass. +- Near-ready L80 time is above the ±1% floor in six of eight cells. S0 W8/W12 and S1 W12 have the largest regressions; whole-view time is lower in seven cells and unchanged in one. +- During 30 m/s movement, HEAD has 12.91% lower near-ready coverage, 1.88% more far holes, 2.15% higher tick p99, and a 6.06% higher median per-run worst tick. Both builds fill near coverage and far holes during dwell. +- Three baseline captures cannot show an exposed mesh at the corresponding underground camera positions. The planner views render and pass image quality. +- Seed sweeps and owner-world captures both use seed 0, but use the regenerated standalone plan and embedded owner Core plan respectively; the images describe the owner-export plan path. +- The merge adds one Unreal `Source/` change for the owner’s Windows compile: `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` (5 insertions, 3 deletions; 8 changed lines). + +## Checks and artifacts + +- Merge check: clean in `Saved/Linux/l150/merge-worktree`; no unmerged paths and no merge commit. Details and hashes: [`merge-check.md`](../../../../Saved/Linux/l150/merge-check.md). +- Linux `VFHostEditor` Development build passed with `-NoUBA -MaxParallelActions=4` (27 actions, 68.79 s). It emitted three existing deprecated RenderMeshComponent API warnings in `VoxelWorld.cpp`; [build log](../../../../Saved/Linux/l150/ubt-console.log). +- L80: 80 measured runs plus warm-ups; [summary](../../../../Saved/Linux/l150/timing/summary.json) and per-run logs are under `Saved/Linux/l150/timing/`. +- Linux Unreal: four L121 starts per build and five interleaved L127 movement runs per build; [summary](../../../../Saved/Linux/l150/ue/summary.json) and run logs are under `Saved/Linux/l150/ue/`. +- Tunnel Network and Vertical Shafts sweeps: 16 seeds each; [Tunnel Network summary](../../../../Saved/Linux/l150/seeds/tunnel-network/summary.json), [Vertical Shafts summary](../../../../Saved/Linux/l150/seeds/vertical-shafts/summary.json), and per-seed logs under `Saved/Linux/l150/seeds/`. +- The fast tier was not rerun in L150. L147 passed it on the same standalone source stamp (`c6b8a100…f0609f`); no generation source changed here. A Windows UE build was not run. diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/RunL150Captures.py b/Standalone/Research/WorldAssemblerM3/Linux150/RunL150Captures.py new file mode 100644 index 0000000..5f0356d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux150/RunL150Captures.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +"""Capture four matched eye-height owner-world views for assembler/world and planner HEAD.""" +from __future__ import annotations + +import json +import math +import os +from pathlib import Path +import re +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[4] +DATA = ROOT / "Standalone/data/owner_world.json" +OUT = ROOT / "Saved/Linux/l150/captures" +LAYOUT = OUT / "planner-head-resolved-layout.json" +ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux150/captures" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +VERSIONS = { + "assembler-world": { + "headless": ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-aa-a", + "renderer": ROOT / "Saved/Linux/l137/bin/VoxelForgeOffscreenCapture-l137-aa-a", + "stamp": "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621", + }, + "planner-head": { + "headless": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l150-head-clang", + "renderer": ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l150-head-clang", + "stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + }, +} +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) + + +def fmt(values) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def angles(camera, target) -> tuple[float, float]: + dx, dy, dz = (target[index] - camera[index] for index in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees( + math.atan2(dz, math.hypot(dx, dy))) + + +def point_um(values) -> tuple[float, float, float]: + return tuple(value / 1_000_000.0 for value in values) + + +def point_q(layout: dict, values) -> tuple[float, float, float]: + scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0) + return tuple(value * scale for value in values) + + +def camera_for_mouth(layout: dict, edge: dict): + anchor = edge["start_anchor_stable_id"] + if anchor.endswith("/lower-mouth"): + side = "lower" + mouth_q = next(row["lower_mouth_q"] for row in layout["passages"] + if row["stable_id"] == anchor.removesuffix("/lower-mouth")) + elif anchor.endswith("/upper-mouth"): + side = "upper" + mouth_q = next(row["upper_mouth_q"] for row in layout["passages"] + if row["stable_id"] == anchor.removesuffix("/upper-mouth")) + else: + raise ValueError(f"mouth edge has no passage anchor: {anchor}") + passage_id = anchor.rsplit("/", 1)[0] + passage = next(row for row in layout["passages"] if row["stable_id"] == passage_id) + controls = passage["control_points"] + mouth_index = len(controls) - 1 if side == "lower" else 0 + neighbor_index = mouth_index - 1 if side == "lower" else 1 + a = controls[neighbor_index]["position_q"] + b = controls[mouth_index]["position_q"] + sign = 1.0 if side == "lower" else -1.0 + tx, ty = sign * (b[0] - a[0]), sign * (b[1] - a[1]) + length = math.hypot(tx, ty) + if length <= 1.0e-9: + raise ValueError(f"degenerate passage mouth tangent: {anchor}") + tx, ty = tx / length, ty / length + mouth = point_q(layout, mouth_q) + eye_z = mouth[2] + 1.45 + camera = (mouth[0] - tx * 2.0, mouth[1] - ty * 2.0, eye_z) + target = (mouth[0] + tx * 10.0, mouth[1] + ty * 10.0, eye_z) + return camera, target, (mouth[0], mouth[1], eye_z, 16.0) + + +def views(layout: dict) -> list[dict]: + edges = {row["stable_id"]: row for row in layout["cavern_edges"]} + tunnel_views = [] + for stable_id, name in ( + ("tunnel-network/strate-2/route-edge-0", "tunnel-network-route"), + ("tunnel-network/strate-2/departure-edge", "tunnel-network-departure")): + camera, target, focus = camera_for_mouth(layout, edges[stable_id]) + tunnel_views.append({"name": name, "layer": "Tunnel Network", "camera": camera, + "target": target, "focus": focus}) + + rooms = {row["stable_id"]: row for row in layout["cavern_rooms"]} + shaft = rooms["vertical-shafts/strate-3/shaft-0/primitive-0"] + access = edges["vertical-shafts/strate-3/shaft-access-0"]["polyline_um"] + center = point_um(shaft["center_um"]) + half = point_um(shaft["half_extents_um"]) + shaft_top = center[2] + half[2] + access_a, access_b = point_um(access[0]), point_um(access[-1]) + ledge_z = shaft_top - 12 * (shaft["ledge_spacing_um"] / 1.0e6) + shaft_views = [ + {"name": "vertical-shafts-looking-down", "layer": "Vertical Shafts", + "camera": (access_b[0], access_b[1], access_b[2] + 1.45), + "target": (center[0], center[1], shaft_top - 30.0), + "focus": (center[0], center[1], shaft_top - 25.0, 14.0), "fov": 35.0}, + {"name": "vertical-shafts-looking-up", "layer": "Vertical Shafts", + "camera": (center[0], center[1], ledge_z + 1.45), + "target": (center[0], center[1], shaft_top - 2.0), + "focus": (center[0], center[1], shaft_top - 22.0, 20.0)}, + ] + return tunnel_views + shaft_views + + +def stamped_run(name: str, command: list[object], log: Path, expected_stamp: str, + env: dict[str, str]) -> str: + binary = Path(str(command[0])) + probe = subprocess.run([*LIMIT, "timeout", "15s", str(binary), "--print-source-stamp"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=20) + match = STAMP_RE.search(probe.stdout or "") + if probe.returncode or not match or match.group(1) != expected_stamp: + raise RuntimeError(f"{name}: source stamp mismatch for {binary}") + log.parent.mkdir(parents=True, exist_ok=True) + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, + timeout=1745, check=False) + output = log.read_text(encoding="utf-8", errors="replace") + emitted = STAMP_RE.search(output) + if result.returncode or not emitted or emitted.group(1) != expected_stamp: + raise RuntimeError(f"{name}: export or render failed; see {log}") + return output + + +def resolve_planner_layout(env: dict[str, str]) -> dict: + config = VERSIONS["planner-head"] + command = [config["headless"], DATA, "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=12", + "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", "--recipe=surface-world", "--resolved-layout-only", + f"--resolved-layout-output={LAYOUT}"] + stamped_run("planner-head-resolve-layout", list(map(str, command)), + OUT / "planner-head-resolved-layout.log", config["stamp"], env) + layout = json.loads(LAYOUT.read_text(encoding="utf-8")) + ids = {row["stable_id"] for row in layout["cavern_edges"]} + required = {"tunnel-network/strate-2/route-edge-0", + "tunnel-network/strate-2/departure-edge", + "vertical-shafts/strate-3/shaft-access-0"} + if not required.issubset(ids): + raise RuntimeError("resolved owner seed-0 layout is missing a capture anchor") + return layout + + +def main() -> int: + if not DATA.is_file(): + raise SystemExit("missing owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + ARTIFACTS.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache") + layout = resolve_planner_layout(env) + quality_tool = ROOT / "Standalone/Tools/verify/capture_quality.py" + manifest = [] + failures = [] + for version, config in VERSIONS.items(): + for view in views(layout): + name = view["name"] + camera, target = view["camera"], view["target"] + yaw, pitch = angles(camera, target) + mesh_dir = OUT / "meshes" / version + mesh_dir.mkdir(parents=True, exist_ok=True) + mesh = mesh_dir / f"{name}.vfmesh" + capture_layout = mesh_dir / f"{name}-layout.json" + export = [config["headless"], DATA, "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=12", + "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", "--recipe=surface-world", + "--render-distance-chunks=0", f"--camera={fmt((*camera, yaw, pitch))}", + f"--resolved-layout-output={capture_layout}", f"--mesh-export={mesh}"] + stamped_run(f"{version}-{name}-export", list(map(str, export)), + OUT / f"{version}-{name}-export.log", config["stamp"], env) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{version}-{name}: empty mesh export") + rendered_layout = json.loads(capture_layout.read_text(encoding="utf-8")) + png = OUT / f"{version}-{name}.png" + jpg = ARTIFACTS / f"{version}-{name}.jpg" + render = [config["renderer"], mesh, png, "--size=1280x720", + f"--fov-deg={float(view.get('fov', 78.0)):.1f}", + f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", + f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable", + "--headlamp=on", "--height-tint=on", "--height-center-m=-950", + "--height-range-m=60", "--tile-edges=off", + f"--focus-region={fmt(view['focus'])}"] + stamped_run(f"{version}-{name}-render", list(map(str, render)), + OUT / f"{version}-{name}-render.log", config["stamp"], env) + subprocess.run([*LIMIT, "timeout", "120s", "ffmpeg", "-nostdin", + "-hide_banner", "-loglevel", "error", "-y", "-i", png, + "-frames:v", "1", "-q:v", "4", jpg], cwd=ROOT, env=env, + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, + timeout=125) + check = subprocess.run([*LIMIT, "timeout", "120s", sys.executable, + quality_tool, jpg], cwd=ROOT, env=env, text=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + check=False, timeout=125) + quality_ok = check.returncode == 0 and "status=PASS" in check.stdout + if not quality_ok: + failures.append(f"{version}-{name}") + quality_line = (check.stdout.strip().splitlines()[-1] + if check.stdout.strip() else f"quality exit={check.returncode}") + manifest.append({"version": version, "view": name, "layer": view["layer"], + "file": str(jpg.relative_to(ROOT)), "camera_m": camera, "target_m": target, + "yaw_deg": yaw, "pitch_deg": pitch, + "source_stamp": config["stamp"], + "capture_layout_identity": rendered_layout.get("stable_identity"), + "pose_layout_identity": layout.get("stable_identity"), "quality_status": + "PASS" if quality_ok else "FAIL", "quality": quality_line}) + print(f"[L150Capture] version={version} view={name} quality=" + f"{'PASS' if quality_ok else 'FAIL'} file={jpg.relative_to(ROOT)}", + flush=True) + (ARTIFACTS / "manifest.json").write_text( + json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + (ARTIFACTS / "INDEX.md").write_text( + "# L150 matched owner-world captures\n\n" + "Seed 0, with camera poses from the planner layout resolved against the owner's " + "embedded Core plan and reused for both versions. The plan is imported without a seed " + "override. The two " + "Tunnel Network views sit at the planned route and departure joints; the Vertical " + "Shafts views look down from the access landing and up from a ledge.\n\n" + + "\n".join(f"- {row['layer']} / {row['view']}: " + f"[{row['version']}]({Path(row['file']).name}) " + f"— {row['quality_status']}" for row in manifest) + "\n", + encoding="utf-8") + with (ARTIFACTS / "INDEX.md").open("a", encoding="utf-8") as index: + index.write("\nAll eight exports and renders completed. The capture-quality gate " + "passed for all four planner-head images and one assembler/world image. Three " + "assembler/world images have insufficient image variance because no mesh surface " + "is visible from those same viewpoints; those gate failures are recorded as the " + "baseline result.\n") + print(f"[L150CaptureSummary] views={len(manifest)//2} pairs={len(manifest)//2} " + f"quality_failures={len(failures)}", flush=True) + return 0 if not failures else 1 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L150CaptureFailure] {error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/RunL150FinalTiming.py b/Standalone/Research/WorldAssemblerM3/Linux150/RunL150FinalTiming.py new file mode 100644 index 0000000..a1e1f38 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux150/RunL150FinalTiming.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""L80 comparison of the planner HEAD with the assembler/world baseline.""" +from __future__ import annotations + +import json +import os +import re +import statistics +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l150/timing" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": ( + ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-aa-a", + "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621"), + "planner-head": ( + ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l150-head-clang", + "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f"), +} +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) +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + binary, expected_stamp = BINARIES[version] + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + label = f"s{strate}-w{workers}-{version}-{phase}{replicate}" + log = OUT / f"{label}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline, near, stamp = PIPELINE.search(content), NEAR.search(content), STAMP.search(content) + if (proc.returncode or pipeline is None or near is None or stamp is None + or stamp.group(1) != expected_stamp): + raise SystemExit(f"L80 timing failed or source stamp mismatch: {log}; exit={proc.returncode}") + fields = dict(FIELDS.findall(pipeline.group(1))) + row = { + "strate": strate, "workers": workers, "version": version, + "phase": phase, "replicate": replicate, "source_stamp": stamp.group(1), + "lod02_ready_s": float(near.group(3)), + "whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")), + "whole_tiles": int(fields["whole_tiles"].split("/")[0]), + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), + } + print("[L150TimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def quiet_machine() -> tuple[float, float]: + load_1m = os.getloadavg()[0] + + def totals() -> tuple[int, int]: + fields = Path("/proc/stat").read_text().splitlines()[0].split() + values = [int(value) for value in fields[1:]] + return sum(values), values[3] + values[4] + + before = totals() + time.sleep(2.0) + after = totals() + idle = 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0]) + return load_1m, idle + + +def main() -> int: + if any(not path.is_file() for path in [DATA, *(entry[0] for entry in BINARIES.values())]): + raise SystemExit("missing L150 timing input") + OUT.mkdir(parents=True, exist_ok=True) + load_1m, idle = quiet_machine() + (OUT / "machine-load-before.txt").write_text( + f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n") + print(f"[L150TimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True) + if load_1m >= 1.5 or idle < 97.0: + raise SystemExit("timing refused: machine is above the L80 quiet-load gate") + + rows = [] + versions = tuple(BINARIES) + for strate in range(4): + for workers in (8, 12): + for version in versions: + run(strate, workers, version, "warmup", 0) + for replicate in range(1, 6): + order = versions if replicate % 2 else tuple(reversed(versions)) + for version in order: + rows.append(run(strate, workers, version, "measured", replicate)) + (OUT / "runs-partial.json").write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8") + + measured = [row for row in rows if row["phase"] == "measured"] + summary = [] + for strate in range(4): + for workers in (8, 12): + group = [row for row in measured + if row["strate"] == strate and row["workers"] == workers] + medians = { + version: { + key: statistics.median(row[key] for row in group + if row["version"] == version) + for key in ("lod02_ready_s", "whole_view_ready_s") + } + for version in versions + } + entry = {"strate": strate, "workers": workers, + "replicates_per_version": 5, "medians": medians} + for key in ("lod02_ready_s", "whole_view_ready_s"): + entry[f"planner_head_vs_assembler_world_{key}_delta_pct"] = round( + 100.0 * (medians["planner-head"][key] + / medians["assembler-world"][key] - 1.0), 2) + entry[f"{key}_paired_deltas_pct"] = [ + round(100.0 * ( + next(row[key] for row in group + if row["version"] == "planner-head" + and row["replicate"] == replicate) + / next(row[key] for row in group + if row["version"] == "assembler-world" + and row["replicate"] == replicate) - 1.0), 3) + for replicate in range(1, 6)] + summary.append(entry) + (OUT / "summary.json").write_text(json.dumps({ + "a_a_noise_floor_pct": 1.0, + "timing_protocol": ( + "one warm-up per binary, five interleaved runs per cell, W8/W12, " + "all four owner layers; L137 A/A median noise floor ±1%"), + "runs": rows, + "summary": summary, + }, indent=2) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/RunL150SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux150/RunL150SeedSweep.py new file mode 100644 index 0000000..233ccdc --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux150/RunL150SeedSweep.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Run L147 composed gates on the actual L150 planner HEAD build.""" +from __future__ import annotations + +import importlib.util +from pathlib import Path +import sys + + +ROOT = Path(__file__).resolve().parents[4] +SOURCE = ROOT / "Standalone/Research/WorldAssemblerM3/Linux147/RunL147SeedSweep.py" +spec = importlib.util.spec_from_file_location("l147_seed_sweep", SOURCE) +if spec is None or spec.loader is None: + raise SystemExit("cannot load the L147 composed gate runner") +runner = importlib.util.module_from_spec(spec) +spec.loader.exec_module(runner) +runner.OUT = ROOT / "Saved/Linux/l150/seeds" +runner.BIN = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l150-head-clang" +runner.STAMP = "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f" + + +if __name__ == "__main__": + raise SystemExit(runner.main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/RunL150Unreal.py b/Standalone/Research/WorldAssemblerM3/Linux150/RunL150Unreal.py new file mode 100644 index 0000000..3206333 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux150/RunL150Unreal.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Measure planner HEAD and code-equivalent assembler/world with the L121/L127 runners.""" +from __future__ import annotations + +import importlib.util +import json +import os +from pathlib import Path +import statistics +import sys + + +ROOT = Path(__file__).resolve().parents[4] +RESEARCH = ROOT / "Standalone/Research/WorldAssemblerM3" +EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd") +HOSTS = { + "assembler-world": ROOT / "Saved/Linux/l127/ue-baseline-project", + "planner-head": Path("/home/jahni/VFHost"), +} +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), +} + + +def load(name: str, path: Path): + spec = importlib.util.spec_from_file_location(name, path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot load {path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def configure(arrival, movement, label: str): + host = HOSTS[label].resolve() + out = ROOT / "Saved/Linux/l150/ue" / label + plugin_saved = host / "Plugins/VoxelForge/Saved/Linux/l150" + out.mkdir(parents=True, exist_ok=True) + plugin_saved.mkdir(parents=True, exist_ok=True) + os.environ["XDG_CONFIG_HOME"] = str(out / "config") + arrival.HOST = host + arrival.EDITOR = EDITOR + arrival.OUT = out / "start" + arrival.HOST_SAVED = out / "host" + arrival.HOST_PLUGIN_SAVED = plugin_saved + arrival.RUN_TIMEOUT_S = 1740 + movement.HOST = host + movement.EDITOR = EDITOR + movement.OUT = out / "movement" + movement.HOST_SAVED = out / "host" + movement.HOST_OUT = plugin_saved / "movement" + movement.RUN_TIMEOUT_S = 1740 + return host, out + + +def start_matrix(arrival, movement, label: str) -> dict: + host, out = configure(arrival, movement, label) + spawns = { + 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), + } + starts = [] + for index, position in spawns.items(): + stratum = "FI" if index == 1 else "SW" + arrival.SPAWNS_M[stratum] = position + row = arrival.run(stratum, 1, f"start-s{index}", 8, 1, 0, 1, 1, 0, 1, + False, False, 1) + row["strate_index"] = index + row["spawn_m"] = list(position) + if row.get("lod02_full_settled_t_s") is None: + raise RuntimeError(f"LOD0-2 did not settle for S{index}: {row}") + starts.append(row) + print("[L150UEStart] " + json.dumps(row, sort_keys=True), flush=True) + (out / "start-summary.json").write_text( + json.dumps(starts, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + row = moving_run(movement, label, 1) + (out / "movement-first.json").write_text( + json.dumps(row, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("[L150UEMovement] " + json.dumps(row, sort_keys=True), flush=True) + return {"host": str(host), "starts": starts, "first_movement": row} + + +def moving_run(movement, label: str, repetition: int) -> dict: + condition = "c743f2a" if label == "assembler-world" else "planner-head" + row = movement.run_unreal(condition, repetition) + events = movement.parse_events(ROOT / row["trace"]) + row["supplemental_far_applies_moving"] = sum( + int(event.get("supplemental_far_applies", 0)) + for event in events + if event.get("type") == "apply_frame" + and float(event.get("speed_cm_s", 0.0)) >= 50.0) + if row.get("effective_tile_slots") != 8: + raise RuntimeError(f"expected eight effective tile slots: {row}") + return row + + +def main() -> int: + out = ROOT / "Saved/Linux/l150/ue" + out.mkdir(parents=True, exist_ok=True) + arrival = load("l150_arrival", RESEARCH / "Linux121/RunL121Arrival.py") + movement = load("l150_movement", RESEARCH / "Linux121/RunL121Movement.py") + summaries = {} + all_runs = {label: [] for label in HOSTS} + for label in HOSTS: + summaries[label] = start_matrix(arrival, movement, label) + all_runs[label].append(summaries[label]["first_movement"]) + + for repetition in range(2, 6): + order = ("planner-head", "assembler-world") if repetition % 2 == 0 \ + else ("assembler-world", "planner-head") + for label in order: + configure(arrival, movement, label) + row = moving_run(movement, label, repetition) + all_runs[label].append(row) + (out / "movement-runs-partial.json").write_text( + json.dumps(all_runs, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("[L150UEMovementRepeat] " + json.dumps({ + key: row.get(key) for key in ( + "condition", "repetition", "near_ready_moving_mean_pct", + "far_holes_moving_mean_pct", "game_thread_tick_p99_ms", + "game_thread_tick_max_ms", "supplemental_far_applies_moving", + "wall_s", "trace")}, sort_keys=True), flush=True) + + keys = ("near_ready_moving_mean_pct", "far_holes_moving_mean_pct", + "game_thread_tick_p99_ms", "game_thread_tick_max_ms") + medians = { + label: {key: statistics.median(row[key] for row in rows) for key in keys} + for label, rows in all_runs.items() + } + delta = { + key: round(100.0 * (medians["planner-head"][key] + / medians["assembler-world"][key] - 1.0), 2) + for key in keys + } + result = { + "baseline_commit": "c743f2a (code-equivalent to assembler/world eb47d72)", + "protocol": ( + "L121 arrival and L127 movement; starts at four owner strata; five interleaved " + "30 m/s runs per build; 12 s move plus 5 s dwell; eight effective tile slots"), + "starts": summaries, + "movement_runs": all_runs, + "movement_medians": medians, + "planner_head_vs_assembler_world_delta_pct": delta, + } + (out / "summary.json").write_text( + json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("[L150UESummary] " + json.dumps({ + "movement_medians": medians, + "planner_head_vs_assembler_world_delta_pct": delta, + "starts_s": { + label: [round(row["lod02_full_settled_t_s"], 3) + for row in summary["starts"]] + for label, summary in summaries.items() + }, + }, sort_keys=True), flush=True) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L150UEFailure] {error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/captures/INDEX.md b/Standalone/Research/WorldAssemblerM3/Linux150/captures/INDEX.md new file mode 100644 index 0000000..04ccab6 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux150/captures/INDEX.md @@ -0,0 +1,14 @@ +# L150 matched owner-world captures + +Seed 0, with camera poses from the planner layout resolved against the owner's embedded Core plan and reused for both versions. The plan is imported without a seed override. The two Tunnel Network views sit at the planned route and departure joints; the Vertical Shafts views look down from the access landing and up from a ledge. + +- Tunnel Network / tunnel-network-route: [assembler-world](assembler-world-tunnel-network-route.jpg) — FAIL +- Tunnel Network / tunnel-network-departure: [assembler-world](assembler-world-tunnel-network-departure.jpg) — PASS +- Vertical Shafts / vertical-shafts-looking-down: [assembler-world](assembler-world-vertical-shafts-looking-down.jpg) — FAIL +- Vertical Shafts / vertical-shafts-looking-up: [assembler-world](assembler-world-vertical-shafts-looking-up.jpg) — FAIL +- Tunnel Network / tunnel-network-route: [planner-head](planner-head-tunnel-network-route.jpg) — PASS +- Tunnel Network / tunnel-network-departure: [planner-head](planner-head-tunnel-network-departure.jpg) — PASS +- Vertical Shafts / vertical-shafts-looking-down: [planner-head](planner-head-vertical-shafts-looking-down.jpg) — PASS +- Vertical Shafts / vertical-shafts-looking-up: [planner-head](planner-head-vertical-shafts-looking-up.jpg) — PASS + +All eight exports and renders completed. The capture-quality gate passed for all four planner-head images and one assembler/world image. Three assembler/world images have insufficient image variance because no mesh surface is visible from those same viewpoints; those gate failures are recorded as the baseline result. diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-tunnel-network-departure.jpg b/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-tunnel-network-departure.jpg new file mode 100644 index 0000000..fa00627 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-tunnel-network-departure.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-tunnel-network-route.jpg b/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-tunnel-network-route.jpg new file mode 100644 index 0000000..148e76e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-tunnel-network-route.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-vertical-shafts-looking-down.jpg b/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-vertical-shafts-looking-down.jpg new file mode 100644 index 0000000..148e76e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-vertical-shafts-looking-down.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-vertical-shafts-looking-up.jpg b/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-vertical-shafts-looking-up.jpg new file mode 100644 index 0000000..2ed48b9 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-vertical-shafts-looking-up.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/captures/manifest.json b/Standalone/Research/WorldAssemblerM3/Linux150/captures/manifest.json new file mode 100644 index 0000000..f7bfaf7 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux150/captures/manifest.json @@ -0,0 +1,186 @@ +[ + { + "version": "assembler-world", + "view": "tunnel-network-route", + "layer": "Tunnel Network", + "file": "Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-tunnel-network-route.jpg", + "camera_m": [ + 17.334593752829058, + -13.396520050883792, + -751.9240234375 + ], + "target_m": [ + 28.32312498585471, + -18.218571620581038, + -751.9240234375 + ], + "yaw_deg": 113.69310802290845, + "pitch_deg": 0.0, + "source_stamp": "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621", + "capture_layout_identity": "resolved-layout-v3-fnv64:0589300eb07dfe06", + "pose_layout_identity": "resolved-layout-v3-fnv64:cc87ee727c483039", + "quality_status": "FAIL", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-tunnel-network-route.jpg pixels=921600 mean_luma=112.000 stddev_luma=0.000 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=FAIL" + }, + { + "version": "assembler-world", + "view": "tunnel-network-departure", + "layer": "Tunnel Network", + "file": "Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-tunnel-network-departure.jpg", + "camera_m": [ + -22.06185250467782, + -5.017471635430169, + -810.9455078125 + ], + "target_m": [ + -10.065737476610899, + -5.322798072849153, + -810.9455078125 + ], + "yaw_deg": 91.45798369615608, + "pitch_deg": 0.0, + "source_stamp": "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621", + "capture_layout_identity": "resolved-layout-v3-fnv64:0589300eb07dfe06", + "pose_layout_identity": "resolved-layout-v3-fnv64:cc87ee727c483039", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-tunnel-network-departure.jpg pixels=921600 mean_luma=106.806 stddev_luma=3.804 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "assembler-world", + "view": "vertical-shafts-looking-down", + "layer": "Vertical Shafts", + "file": "Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-vertical-shafts-looking-down.jpg", + "camera_m": [ + -31.001772, + -7.103472, + -901.9274509999999 + ], + "target_m": [ + -31.001772, + -8.066018, + -929.377451 + ], + "yaw_deg": 180.0, + "pitch_deg": -87.99172181188044, + "source_stamp": "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621", + "capture_layout_identity": "resolved-layout-v3-fnv64:0589300eb07dfe06", + "pose_layout_identity": "resolved-layout-v3-fnv64:cc87ee727c483039", + "quality_status": "FAIL", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-vertical-shafts-looking-down.jpg pixels=921600 mean_luma=112.000 stddev_luma=0.000 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=FAIL" + }, + { + "version": "assembler-world", + "view": "vertical-shafts-looking-up", + "layer": "Vertical Shafts", + "file": "Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-vertical-shafts-looking-up.jpg", + "camera_m": [ + -31.001772, + -8.066018, + -945.9274509999999 + ], + "target_m": [ + -31.001772, + -8.066018, + -901.377451 + ], + "yaw_deg": 0.0, + "pitch_deg": 90.0, + "source_stamp": "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621", + "capture_layout_identity": "resolved-layout-v3-fnv64:0589300eb07dfe06", + "pose_layout_identity": "resolved-layout-v3-fnv64:cc87ee727c483039", + "quality_status": "FAIL", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux150/captures/assembler-world-vertical-shafts-looking-up.jpg pixels=921600 mean_luma=111.749 stddev_luma=1.375 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=FAIL" + }, + { + "version": "planner-head", + "view": "tunnel-network-route", + "layer": "Tunnel Network", + "file": "Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-tunnel-network-route.jpg", + "camera_m": [ + 17.334593752829058, + -13.396520050883792, + -751.9240234375 + ], + "target_m": [ + 28.32312498585471, + -18.218571620581038, + -751.9240234375 + ], + "yaw_deg": 113.69310802290845, + "pitch_deg": 0.0, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "capture_layout_identity": "resolved-layout-v3-fnv64:cc87ee727c483039", + "pose_layout_identity": "resolved-layout-v3-fnv64:cc87ee727c483039", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-tunnel-network-route.jpg pixels=921600 mean_luma=133.954 stddev_luma=13.005 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "planner-head", + "view": "tunnel-network-departure", + "layer": "Tunnel Network", + "file": "Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-tunnel-network-departure.jpg", + "camera_m": [ + -22.06185250467782, + -5.017471635430169, + -810.9455078125 + ], + "target_m": [ + -10.065737476610899, + -5.322798072849153, + -810.9455078125 + ], + "yaw_deg": 91.45798369615608, + "pitch_deg": 0.0, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "capture_layout_identity": "resolved-layout-v3-fnv64:cc87ee727c483039", + "pose_layout_identity": "resolved-layout-v3-fnv64:cc87ee727c483039", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-tunnel-network-departure.jpg pixels=921600 mean_luma=115.401 stddev_luma=12.660 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "planner-head", + "view": "vertical-shafts-looking-down", + "layer": "Vertical Shafts", + "file": "Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-vertical-shafts-looking-down.jpg", + "camera_m": [ + -31.001772, + -7.103472, + -901.9274509999999 + ], + "target_m": [ + -31.001772, + -8.066018, + -929.377451 + ], + "yaw_deg": 180.0, + "pitch_deg": -87.99172181188044, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "capture_layout_identity": "resolved-layout-v3-fnv64:cc87ee727c483039", + "pose_layout_identity": "resolved-layout-v3-fnv64:cc87ee727c483039", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-vertical-shafts-looking-down.jpg pixels=921600 mean_luma=112.234 stddev_luma=3.319 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "planner-head", + "view": "vertical-shafts-looking-up", + "layer": "Vertical Shafts", + "file": "Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-vertical-shafts-looking-up.jpg", + "camera_m": [ + -31.001772, + -8.066018, + -945.9274509999999 + ], + "target_m": [ + -31.001772, + -8.066018, + -901.377451 + ], + "yaw_deg": 0.0, + "pitch_deg": 90.0, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "capture_layout_identity": "resolved-layout-v3-fnv64:cc87ee727c483039", + "pose_layout_identity": "resolved-layout-v3-fnv64:cc87ee727c483039", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-vertical-shafts-looking-up.jpg pixels=921600 mean_luma=111.590 stddev_luma=2.483 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + } +] diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-tunnel-network-departure.jpg b/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-tunnel-network-departure.jpg new file mode 100644 index 0000000..1f6e456 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-tunnel-network-departure.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-tunnel-network-route.jpg b/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-tunnel-network-route.jpg new file mode 100644 index 0000000..e0ffa36 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-tunnel-network-route.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-vertical-shafts-looking-down.jpg b/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-vertical-shafts-looking-down.jpg new file mode 100644 index 0000000..0af3719 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-vertical-shafts-looking-down.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-vertical-shafts-looking-up.jpg b/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-vertical-shafts-looking-up.jpg new file mode 100644 index 0000000..f6bcaa3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux150/captures/planner-head-vertical-shafts-looking-up.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/PassageEnvelopeAudit.py b/Standalone/Research/WorldAssemblerM3/Linux151/PassageEnvelopeAudit.py new file mode 100644 index 0000000..14f3b62 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux151/PassageEnvelopeAudit.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +"""Audit cave primitive bounds against passage envelopes, allowing chamber joints only.""" +from __future__ import annotations + +import math +from typing import Any + + +INFLUENCE_REACH_M = 1.053334 +SHELL_THICKNESS_M = 0.75 + + +def _point(control: dict[str, Any], scale: float) -> tuple[float, float, float, float]: + x, y, z = control["position_q"] + return (x * scale, y * scale, z * scale, control["radius_q"] * scale) + + +def _knot_span(a, b) -> float: + return max(1.0e-6, math.sqrt(math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2))) + + +def _evaluate(controls, span: int, t: float): + p1, p2 = controls[span], controls[span + 1] + p0 = controls[span - 1] if span else controls[0] + p3 = controls[min(span + 2, len(controls) - 1)] + dt0, dt1, dt2 = _knot_span(p0, p1), _knot_span(p1, p2), _knot_span(p2, p3) + t2, t3 = t * t, t * t * t + result = [] + for axis in (0, 1, 3): + m1 = dt1 * ((p1[axis] - p0[axis]) / dt0 + - (p2[axis] - p0[axis]) / (dt0 + dt1) + + (p2[axis] - p1[axis]) / dt1) + m2 = dt1 * ((p2[axis] - p1[axis]) / dt1 + - (p3[axis] - p1[axis]) / (dt1 + dt2) + + (p3[axis] - p2[axis]) / dt2) + value = ((2 * t3 - 3 * t2 + 1) * p1[axis] + + (-2 * t3 + 3 * t2) * p2[axis] + + (t3 - 2 * t2 + t) * m1 + (t3 - t2) * m2) + result.append(value) + radius = min(max(result[2], min(p1[3], p2[3])), max(p1[3], p2[3])) + return result[0], result[1], p1[2] + (p2[2] - p1[2]) * t, radius + + +def _segments(passage: dict[str, Any], scale: float): + controls = [_point(row, scale) for row in passage["control_points"]] + route_end = min(max(1, int(passage.get("lower_mouth_control_index", len(controls) - 1))), + len(controls) - 1) + along = 0.0 + for span in range(route_end): + a, b = controls[span], controls[span + 1] + chord = math.hypot(b[0] - a[0], b[1] - a[1]) + steps = max(1, math.ceil(chord / 2.0)) + prior = _evaluate(controls, span, 0.0) + for step in range(1, steps + 1): + current = _evaluate(controls, span, step / steps) + length = math.dist(prior[:3], current[:3]) + yield prior, current, along, max(prior[3], current[3]) + INFLUENCE_REACH_M + along += length + prior = current + + +def _distance_squared(a, b, bounds, t: float) -> float: + total = 0.0 + for axis in range(3): + pos = a[axis] + (b[axis] - a[axis]) * t + lo, hi = bounds[axis] + gap = lo - pos if pos < lo else pos - hi if pos > hi else 0.0 + total += gap * gap + return total + + +def _overlap_interval(a, b, bounds, radius: float): + cuts = {0.0, 1.0} + for axis in range(3): + delta = b[axis] - a[axis] + if abs(delta) < 1.0e-15: + continue + for edge in bounds[axis]: + t = (edge - a[axis]) / delta + if 0.0 < t < 1.0: + cuts.add(t) + cuts = sorted(cuts) + best_t, best_d2 = 0.0, math.inf + for lo, hi in zip(cuts, cuts[1:]): + mid = (lo + hi) * 0.5 + quadratic = linear = 0.0 + for axis in range(3): + delta = b[axis] - a[axis] + pos = a[axis] + delta * mid + low, high = bounds[axis] + edge = low if pos < low else high if pos > high else None + if edge is not None: + quadratic += delta * delta + linear += delta * (a[axis] - edge) + candidate = min(hi, max(lo, -linear / quadratic)) if quadratic else lo + d2 = _distance_squared(a, b, bounds, candidate) + if d2 < best_d2: + best_t, best_d2 = candidate, d2 + for t in (0.0, 1.0): + d2 = _distance_squared(a, b, bounds, t) + if d2 < best_d2: + best_t, best_d2 = t, d2 + if best_d2 > radius * radius: + return None + + radius2 = radius * radius + if _distance_squared(a, b, bounds, 0.0) <= radius2: + left = 0.0 + else: + low, high = 0.0, best_t + for _ in range(42): + mid = (low + high) * 0.5 + if _distance_squared(a, b, bounds, mid) <= radius2: + high = mid + else: + low = mid + left = high + if _distance_squared(a, b, bounds, 1.0) <= radius2: + right = 1.0 + else: + low, high = best_t, 1.0 + for _ in range(42): + mid = (low + high) * 0.5 + if _distance_squared(a, b, bounds, mid) <= radius2: + low = mid + else: + high = mid + right = low + return left, right, best_t + + +def _inside_chamber(point, chamber: dict[str, Any], scale: float) -> bool: + cx, cy, cz = (value * scale for value in chamber["floor_center_q"]) + radius = chamber["radius_q"] * scale + clear = chamber["clear_height_q"] * scale + gx, gy = (value / 65536.0 for value in chamber["floor_gradient_q16"]) + floor = cz + gx * (point[0] - cx) + gy * (point[1] - cy) + chamber_distance = max(math.hypot(point[0] - cx, point[1] - cy) - radius, + floor - point[2], point[2] - (floor + clear)) + return chamber_distance <= SHELL_THICKNESS_M + + +def _primitive_bounds(row: dict[str, Any]): + return tuple((row["bounds_min_um"][i] / 1.0e6, + row["bounds_max_um"][i] / 1.0e6) for i in range(3)) + + +def _bezier(points, t: float): + u = 1.0 - t + return tuple(u**3 * points[0][axis] + 3*u*u*t * points[1][axis] + + 3*u*t*t * points[2][axis] + t**3 * points[3][axis] + for axis in range(3)) + + +def _distance_to_ellipse(radial: float, vertical: float, + radius: float, height: float) -> float: + """Euclidean distance to the edge bore's horizontal/vertical ellipse.""" + radial = abs(radial) + vertical = abs(vertical) + if (radial / radius) ** 2 + (vertical / height) ** 2 <= 1.0: + return 0.0 + + lo, hi = 0.0, math.pi / 2.0 + ratio = (math.sqrt(5.0) - 1.0) / 2.0 + + def squared_distance(angle: float) -> float: + return ((radial - radius * math.cos(angle)) ** 2 + + (vertical - height * math.sin(angle)) ** 2) + + left = hi - ratio * (hi - lo) + right = lo + ratio * (hi - lo) + left_value, right_value = squared_distance(left), squared_distance(right) + for _ in range(48): + if left_value < right_value: + hi, right, right_value = right, left, left_value + left = hi - ratio * (hi - lo) + left_value = squared_distance(left) + else: + lo, left, left_value = left, right, right_value + right = lo + ratio * (hi - lo) + right_value = squared_distance(right) + return math.sqrt(min(left_value, right_value)) + + +def _actual_edge_clearance(passage, edge, scale: float, + chamber: dict[str, Any]) -> float: + """Return minimum true tube clearance outside the landing chamber. + + The route edge is a swept ellipse, with its recorded polyline as the axis. + Comparing the tunnel tube to that ellipse avoids the false clashes caused by + intersecting two enclosing boxes. + """ + controls = [tuple(value / 1.0e6 for value in point) + for point in edge["control_points_um"]] + axis = [tuple(value / 1.0e6 for value in point) + for point in edge.get("polyline_um", [])] + if len(axis) < 2: + axis = [_bezier(controls, index / 64.0) for index in range(65)] + edge_radius = (edge["bore_radius_um"] + + edge["irregularity_amplitude_um"]) / 1.0e6 + edge_height = (edge["bore_height_um"] + + edge["irregularity_amplitude_um"]) / 1.0e6 + passage_segments = passage.get("_audit_segments") or tuple(_segments(passage, scale)) + minimum_clearance = math.inf + for a, b, _, passage_radius in passage_segments: + length = math.dist(a[:3], b[:3]) + steps = max(1, math.ceil(length / 0.05)) + for step in range(steps + 1): + t = step / steps + point = tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) + if _inside_chamber(point, chamber, scale): + continue + tunnel_radius = (a[3] + (b[3] - a[3]) * t + + INFLUENCE_REACH_M) + for start, end in zip(axis, axis[1:]): + delta = tuple(end[i] - start[i] for i in range(3)) + length_squared = sum(value * value for value in delta) + projection = (sum((point[i] - start[i]) * delta[i] + for i in range(3)) / length_squared + if length_squared else 0.0) + projection = min(1.0, max(0.0, projection)) + center = tuple(start[i] + projection * delta[i] for i in range(3)) + radial = math.hypot(point[0] - center[0], point[1] - center[1]) + vertical = abs(point[2] - center[2]) + clearance = (_distance_to_ellipse(radial, vertical, + edge_radius, edge_height) + - tunnel_radius) + minimum_clearance = min(minimum_clearance, clearance) + return minimum_clearance + + +def _test_bounds(bounds, strate_index: int, passages, scale: float, + *, allow_chamber: bool = True): + for passage in passages: + if strate_index not in (passage["upper_strate_index"], passage["lower_strate_index"]): + continue + chamber_key = ("upper_landing_chamber" if strate_index == passage["upper_strate_index"] + else "lower_landing_chamber") + chamber = passage[chamber_key] + segments = passage.get("_audit_segments") + if segments is None: + segments = tuple(_segments(passage, scale)) + for a, b, along, radius in segments: + expanded = [(a[i] - radius, b[i] + radius) if a[i] <= b[i] + else (b[i] - radius, a[i] + radius) for i in range(3)] + if any(expanded[i][1] < bounds[i][0] or expanded[i][0] > bounds[i][1] + for i in range(3)): + continue + overlap = _overlap_interval(a, b, bounds, radius) + if overlap is None: + continue + first, last, closest = overlap + if allow_chamber: + points = [tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) + for t in (first, closest, last)] + if all(_inside_chamber(point, chamber, scale) for point in points): + continue + point = tuple(a[i] + (b[i] - a[i]) * closest for i in range(3)) + return passage["stable_id"], along + closest * math.dist(a[:3], b[:3]), point + return None + + +def audit_layout(layout: dict[str, Any]) -> dict[str, Any]: + scale = layout["core_voxel_size_um"] / (256.0 * 1.0e6) + passages = [dict(row, _audit_segments=tuple(_segments(row, scale))) + for row in layout.get("passages", [])] + primitives = [*layout.get("cavern_rooms", []), *layout.get("cavern_edges", [])] + checked = conflicts = chamber_joints = bounds_candidates = refined_false_positives = 0 + first_conflict = None + for primitive in primitives: + bounds = _primitive_bounds(primitive) + checked += 1 + primitive_conflicted = False + for passage in passages: + found = _test_bounds(bounds, primitive["strate_index"], [passage], scale) + if not found: + continue + bounds_candidates += 1 + passage_id, station, point = found + if "control_points_um" in primitive: + side = ("upper_landing_chamber" + if primitive["strate_index"] == passage["upper_strate_index"] + else "lower_landing_chamber") + clearance = _actual_edge_clearance(passage, primitive, scale, + passage[side]) + if clearance >= 0.0: + refined_false_positives += 1 + continue + primitive_conflicted = True + conflicts += 1 + if first_conflict is None: + first_conflict = {"primitive": primitive["stable_id"], + "passage": passage_id, "station_m": round(station, 3), + "center_m": [round(value, 3) for value in point]} + break + if not primitive_conflicted: + # Count the edge's named landing association as a confirmed joint. + anchor = primitive.get("start_anchor_stable_id", "") + if anchor.endswith(("/upper-landing-chamber", "/lower-landing-chamber")): + chamber_joints += 1 + + negative_controls = 0 + long_passage = max(passages, key=lambda row: len(row["control_points"]), default=None) + if long_passage: + controls = [_point(row, scale) for row in long_passage["control_points"]] + index = min(max(1, len(controls) // 2), len(controls) - 2) + center = controls[index] + bounds = tuple((center[axis] - 0.25, center[axis] + 0.25) for axis in range(3)) + # Select the side whose chamber is farther from this forced tunnel overlap. + strate = long_passage["lower_strate_index"] + if _test_bounds(bounds, strate, [long_passage], scale, allow_chamber=False): + negative_controls = 1 + return {"primitives_checked": checked, "bounds_candidates": bounds_candidates, + "refined_edge_false_positives": refined_false_positives, + "outside_chamber_conflicts": conflicts, + "chamber_joints": chamber_joints, "negative_controls": negative_controls, + "negative_controls_expected": 1, "first_conflict": first_conflict, + "passed": conflicts == 0 and negative_controls == 1} diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux151/REPORT.md new file mode 100644 index 0000000..74bb059 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux151/REPORT.md @@ -0,0 +1,39 @@ +# L151 — One tunnel for two strata + +| Landing chamber | Before (L150) | After (L151) | +|---|---|---| +| passage/0-1/0 · upper | ![L150 upper landing, passage 0-1](captures/before-l150-0-1-0-upper-landing.jpg) | ![L151 upper landing, passage 0-1](captures/after-l151-0-1-0-upper-landing.jpg) | +| passage/0-1/0 · lower | ![L150 lower landing, passage 0-1](captures/before-l150-0-1-0-lower-landing.jpg) | ![L151 lower landing, passage 0-1](captures/after-l151-0-1-0-lower-landing.jpg) | +| passage/1-2/0 · upper | ![L150 upper landing, passage 1-2](captures/before-l150-1-2-0-upper-landing.jpg) | ![L151 upper landing, passage 1-2](captures/after-l151-1-2-0-upper-landing.jpg) | +| passage/1-2/0 · lower | ![L150 lower landing, passage 1-2](captures/before-l150-1-2-0-lower-landing.jpg) | ![L151 lower landing, passage 1-2](captures/after-l151-1-2-0-lower-landing.jpg) | +| passage/2-3/0 · upper | ![L150 upper landing, passage 2-3](captures/before-l150-2-3-0-upper-landing.jpg) | ![L151 upper landing, passage 2-3](captures/after-l151-2-3-0-upper-landing.jpg) | +| passage/2-3/0 · lower | ![L150 lower landing, passage 2-3](captures/before-l150-2-3-0-lower-landing.jpg) | ![L151 lower landing, passage 2-3](captures/after-l151-2-3-0-lower-landing.jpg) | + +Seed 0 camera poses are matched at eye height. All 12 images passed the capture quality check. The L150 build stamp is `c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f`; L151 is `1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5`. The [capture index](captures/INDEX.md) and [manifest](captures/manifest.json) record the poses and files. + +## Result + +**L151 is not promotion-ready.** The resolved layout now records one passage spline with an upper and lower landing chamber, and the evaluator version is 238. The best tested floor blend still leaves two walk segments above the 15° limit: 19.077° on passage 0-1 and 19.033° on passage 2-3. The three-variant stop rule was reached; I stopped without a fourth geometry attempt. No owner baselines were promoted. + +## What changed + +- Added serialized and validated landing chamber records with floor center, floor gradient, radius, and 4.5 m clear height. The shared passage field composes each chamber with the tunnel shell and floor. +- Cave route endpoints now bind to the passage's landing chamber anchors. The passage floor blends to chamber floors over the tested 12 m transition. +- Extended passage audits for chamber capsule clearance, floor and wall shell, and blocked-world negative controls. Added a geometric cave-primitive envelope audit that permits the named chamber joints and refines conservative edge-box candidates against the swept bore. +- Retained the L135 mouth profile, L147 interior-air gate, and L148 envelope allowance. + +## Measurements and checks + +- **Focused owner-world run:** one seed-2 W8 run, source stamp above. Three interfaces were audited. Passage and chamber interiors together had 0 solid samples across 319,579 capsule samples; all six chambers had 0 solid samples. Passage floor, wall, and interior shell violations were all zero. Naturalness, mouth profile, ripple, and all three layer route audits passed. Tunnel-network, vertical-shaft, and blocked-passage negative controls were detected (1/1, 1/1, and 3/3). The combined passage summary failed because only one of three interfaces met walkability. +- **Chamber envelope audit:** 549 primitives checked; 2 conservative bounds candidates were refined as false positives; 0 conflicts remained outside chambers; 3 named chamber joints and the negative control passed (1/1). Result: [JSON](../../../../Saved/Linux/l151/passage-envelope-audit.json). +- **Blend attempts:** 12 m retained two grades at 19.077° and 19.033°. Extending the blend to 24 m produced 436 solid interior capsule samples and one wall violation, as well as grades of 15.282° and 17.406° on passage 0-1. Restricting the external blend to 0 m kept interior and shell checks clear but left a 22.226° grade on passage 0-1. The 12 m variant is the final source state. +- **Build and snapshots:** Clang 19 build passed for stamp `1ca3b84f…`; the explicit seed-0 resolved-layout snapshot round trip passed. Python capture and envelope audit scripts ran successfully. `git diff --check` passed for the tracked diff; the report's whitespace and image references were checked separately. +- **Seams:** the saved seed-2 log has no `[PassageTileSeamAudit]` result, so seam status is unverified. + +## Not run + +The three-failure stop was reached before the fast tier, the 16-seed Tunnel Network and Vertical Shafts sweeps, Clang/GCC W1/W8/W12, and L80 timing for all four layers. No timing claim is made. No owner-world baseline files were updated because the composed walkability gate failed. The only modified `Source/` file is `Standalone/Source/StreamBenchmarkPipeline.h`; no Unreal `Source/` files changed. + +## Remaining risk + +The tunnel-to-chamber floor transition still creates local grades above 15° in two interfaces. The envelope audit and geometry evidence cover seed 2 only; multi-seed composed behavior, seams, compiler/worker determinism, the fast tier, and L80 performance remain unverified. diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/RunL151Captures.py b/Standalone/Research/WorldAssemblerM3/Linux151/RunL151Captures.py new file mode 100644 index 0000000..1f41c85 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux151/RunL151Captures.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Capture matched eye-height views from both ends of every owner tunnel.""" +from __future__ import annotations + +import json +import math +import os +from pathlib import Path +import re +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[4] +DATA = ROOT / "Standalone/data/owner_world.json" +OUT = ROOT / "Saved/Linux/l151/captures" +LAYOUT = OUT / "after-resolved-layout.json" +ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux151/captures" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +VERSIONS = { + "before-l150": { + "headless": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l150-head-clang", + "renderer": ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l150-head-clang", + "stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + }, + "after-l151": { + "headless": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l151-clang", + "renderer": ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l151-clang", + "stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + }, +} +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) + + +def fmt(values) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def angles(camera, target) -> tuple[float, float]: + dx, dy, dz = (target[index] - camera[index] for index in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees( + math.atan2(dz, math.hypot(dx, dy))) + + +def point_q(layout: dict, values) -> tuple[float, float, float]: + scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0) + return tuple(value * scale for value in values) + + +def chamber_views(layout: dict) -> list[dict]: + result = [] + for passage in layout["passages"]: + controls = passage["control_points"] + for side, endpoint, neighbor in ( + ("upper", controls[0], controls[1]), + ("lower", controls[-1], controls[-2])): + chamber = passage[f"{side}_landing_chamber"] + center = point_q(layout, chamber["floor_center_q"]) + a = point_q(layout, neighbor["position_q"]) + b = point_q(layout, endpoint["position_q"]) + dx, dy = a[0] - b[0], a[1] - b[1] + length = math.hypot(dx, dy) + if length <= 1.0e-9: + raise ValueError(f"degenerate {side} tunnel tangent: {passage['stable_id']}") + dx, dy = dx / length, dy / length + eye_z = center[2] + 1.45 + distance = 9.0 + target = (center[0] + dx * distance, center[1] + dy * distance, + eye_z + (chamber["floor_gradient_q16"][0] / 65536.0 * dx + + chamber["floor_gradient_q16"][1] / 65536.0 * dy) + * distance) + camera = (center[0], center[1], eye_z) + slug = passage["stable_id"].replace("passage/", "").replace("/", "-") + result.append({"name": f"{slug}-{side}-landing", "passage": passage["stable_id"], + "side": side, "camera": camera, "target": target, + "focus": (center[0], center[1], eye_z, 18.0)}) + return result + + +def stamped_run(name: str, command: list[object], log: Path, expected_stamp: str, + env: dict[str, str]) -> str: + binary = Path(str(command[0])) + probe = subprocess.run([*LIMIT, "timeout", "15s", str(binary), "--print-source-stamp"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=20) + match = STAMP_RE.search(probe.stdout or "") + if probe.returncode or not match or match.group(1) != expected_stamp: + raise RuntimeError(f"{name}: source stamp mismatch for {binary}") + log.parent.mkdir(parents=True, exist_ok=True) + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, + timeout=1745, check=False) + output = log.read_text(encoding="utf-8", errors="replace") + emitted = STAMP_RE.search(output) + if result.returncode or not emitted or emitted.group(1) != expected_stamp: + raise RuntimeError(f"{name}: export or render failed; see {log}") + return output + + +def resolve_layout(env: dict[str, str]) -> dict: + config = VERSIONS["after-l151"] + command = [config["headless"], DATA, "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=12", + "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", "--seed=0", "--recipe=surface-world", "--resolved-layout-only", + f"--resolved-layout-output={LAYOUT}"] + stamped_run("after-l151-resolve-layout", list(map(str, command)), + OUT / "after-l151-resolved-layout.log", config["stamp"], env) + layout = json.loads(LAYOUT.read_text(encoding="utf-8")) + if len(layout.get("passages", [])) != 3: + raise RuntimeError("owner layout did not resolve the expected three tunnels") + return layout + + +def main() -> int: + if not DATA.is_file(): + raise SystemExit("missing owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + ARTIFACTS.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache") + layout = resolve_layout(env) + views = chamber_views(layout) + quality_tool = ROOT / "Standalone/Tools/verify/capture_quality.py" + manifest = [] + failures = [] + for version, config in VERSIONS.items(): + for view in views: + camera, target = view["camera"], view["target"] + yaw, pitch = angles(camera, target) + mesh_dir = OUT / "meshes" / version + mesh_dir.mkdir(parents=True, exist_ok=True) + mesh = mesh_dir / f"{view['name']}.vfmesh" + capture_layout = mesh_dir / f"{view['name']}-layout.json" + export = [config["headless"], DATA, "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=12", + "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", "--seed=0", "--recipe=surface-world", + "--render-distance-chunks=0", + f"--camera={fmt((*camera, yaw, pitch))}", + f"--resolved-layout-output={capture_layout}", f"--mesh-export={mesh}"] + stamped_run(f"{version}-{view['name']}-export", list(map(str, export)), + OUT / f"{version}-{view['name']}-export.log", config["stamp"], env) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{version}-{view['name']}: empty mesh export") + png = OUT / f"{version}-{view['name']}.png" + jpg = ARTIFACTS / f"{version}-{view['name']}.jpg" + render = [config["renderer"], mesh, png, "--size=1280x720", + "--fov-deg=78.0", f"--camera-m={fmt(camera)}", + f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}", + "--tile-skirts=off", "--shade=readable", "--headlamp=on", + "--height-tint=on", "--height-center-m=-950", "--height-range-m=60", + "--tile-edges=off", f"--focus-region={fmt(view['focus'])}"] + stamped_run(f"{version}-{view['name']}-render", list(map(str, render)), + OUT / f"{version}-{view['name']}-render.log", config["stamp"], env) + subprocess.run([*LIMIT, "timeout", "120s", "ffmpeg", "-nostdin", + "-hide_banner", "-loglevel", "error", "-y", "-i", png, + "-frames:v", "1", "-q:v", "4", jpg], cwd=ROOT, env=env, + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, + timeout=125) + check = subprocess.run([*LIMIT, "timeout", "120s", sys.executable, + quality_tool, jpg], cwd=ROOT, env=env, text=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + check=False, timeout=125) + quality_ok = check.returncode == 0 and "status=PASS" in check.stdout + if not quality_ok: + failures.append(f"{version}-{view['name']}") + quality_line = (check.stdout.strip().splitlines()[-1] + if check.stdout.strip() else f"quality exit={check.returncode}") + manifest.append({"version": version, "view": view["name"], + "passage": view["passage"], "side": view["side"], + "file": str(jpg.relative_to(ROOT)), "camera_m": camera, + "target_m": target, "yaw_deg": yaw, "pitch_deg": pitch, + "source_stamp": config["stamp"], + "pose_layout_identity": layout.get("stable_identity"), + "quality_status": "PASS" if quality_ok else "FAIL", + "quality": quality_line}) + print(f"[L151Capture] version={version} view={view['name']} " + f"quality={'PASS' if quality_ok else 'FAIL'} " + f"file={jpg.relative_to(ROOT)}", flush=True) + (ARTIFACTS / "manifest.json").write_text( + json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + with (ARTIFACTS / "INDEX.md").open("w", encoding="utf-8") as index: + index.write("# L151 matched owner tunnel landing captures\n\n" + "Seed 0 camera poses resolved from the L151 owner layout and reused for both " + "versions. Each view is at eye height at one of the three tunnel landing " + "chambers. Before uses the L150 head build; after uses L151 source stamp " + f"`{VERSIONS['after-l151']['stamp']}`.\n\n") + for view in views: + index.write(f"## {view['passage']} / {view['side']} landing\n\n" + f"- Before: [image](before-l150-{view['name']}.jpg)\n" + f"- After: [image](after-l151-{view['name']}.jpg)\n\n") + index.write(f"Capture quality failures: {len(failures)}/{len(manifest)}.\n") + print(f"[L151CaptureSummary] chamber_views={len(views)} " + f"images={len(manifest)} quality_failures={len(failures)}", flush=True) + return 0 if not failures else 1 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L151CaptureFailure] {error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/INDEX.md b/Standalone/Research/WorldAssemblerM3/Linux151/captures/INDEX.md new file mode 100644 index 0000000..40f36c3 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux151/captures/INDEX.md @@ -0,0 +1,35 @@ +# L151 matched owner tunnel landing captures + +Seed 0 camera poses resolved from the L151 owner layout and reused for both versions. Each view is at eye height at one of the three tunnel landing chambers. Before uses the L150 head build; after uses L151 source stamp `1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5`. + +## passage/0-1/0 / upper landing + +- Before: [image](before-l150-0-1-0-upper-landing.jpg) +- After: [image](after-l151-0-1-0-upper-landing.jpg) + +## passage/0-1/0 / lower landing + +- Before: [image](before-l150-0-1-0-lower-landing.jpg) +- After: [image](after-l151-0-1-0-lower-landing.jpg) + +## passage/1-2/0 / upper landing + +- Before: [image](before-l150-1-2-0-upper-landing.jpg) +- After: [image](after-l151-1-2-0-upper-landing.jpg) + +## passage/1-2/0 / lower landing + +- Before: [image](before-l150-1-2-0-lower-landing.jpg) +- After: [image](after-l151-1-2-0-lower-landing.jpg) + +## passage/2-3/0 / upper landing + +- Before: [image](before-l150-2-3-0-upper-landing.jpg) +- After: [image](after-l151-2-3-0-upper-landing.jpg) + +## passage/2-3/0 / lower landing + +- Before: [image](before-l150-2-3-0-lower-landing.jpg) +- After: [image](after-l151-2-3-0-lower-landing.jpg) + +Capture quality failures: 0/12. diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-lower-landing.jpg new file mode 100644 index 0000000..c4013c0 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-upper-landing.jpg new file mode 100644 index 0000000..c4444f0 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-lower-landing.jpg new file mode 100644 index 0000000..aff38d6 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-upper-landing.jpg new file mode 100644 index 0000000..5763300 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-lower-landing.jpg new file mode 100644 index 0000000..7765a7f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-upper-landing.jpg new file mode 100644 index 0000000..86ecb2b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-lower-landing.jpg new file mode 100644 index 0000000..d7b41a1 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-upper-landing.jpg new file mode 100644 index 0000000..987fc02 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-lower-landing.jpg new file mode 100644 index 0000000..04133fe Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-upper-landing.jpg new file mode 100644 index 0000000..99596a4 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-lower-landing.jpg new file mode 100644 index 0000000..6125e0b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-upper-landing.jpg new file mode 100644 index 0000000..95ef1b7 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux151/captures/manifest.json b/Standalone/Research/WorldAssemblerM3/Linux151/captures/manifest.json new file mode 100644 index 0000000..ee29d68 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux151/captures/manifest.json @@ -0,0 +1,278 @@ +[ + { + "version": "before-l150", + "view": "0-1-0-upper-landing", + "passage": "passage/0-1/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-upper-landing.jpg", + "camera_m": [ + 37.0, + -5.25, + -372.3810546875 + ], + "target_m": [ + 28.40474992442715, + -7.918646874047773, + -372.325197471535 + ], + "yaw_deg": -107.24848481126237, + "pitch_deg": 0.3555935154874107, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-upper-landing.jpg pixels=921600 mean_luma=113.553 stddev_luma=8.807 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l150", + "view": "0-1-0-lower-landing", + "passage": "passage/0-1/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-lower-landing.jpg", + "camera_m": [ + 25.8369140625, + 3.19921875, + -736.6798828125 + ], + "target_m": [ + 17.97380590951016, + -1.1793118453464206, + -734.970169262755 + ], + "yaw_deg": -119.11101808688004, + "pitch_deg": 10.756207017349817, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-0-1-0-lower-landing.jpg pixels=921600 mean_luma=106.418 stddev_luma=2.881 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l150", + "view": "1-2-0-upper-landing", + "passage": "passage/1-2/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-upper-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -731.8810546875 + ], + "target_m": [ + 10.35825176774404, + -16.050410390488395, + -733.216132282697 + ], + "yaw_deg": -101.86342177874246, + "pitch_deg": -8.437834550031965, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-upper-landing.jpg pixels=921600 mean_luma=108.420 stddev_luma=2.873 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l150", + "view": "1-2-0-lower-landing", + "passage": "passage/1-2/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-lower-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -753.05390625 + ], + "target_m": [ + 10.48848361510052, + -16.587754362485967, + -751.7193175939389 + ], + "yaw_deg": -105.38384894032598, + "pitch_deg": 8.434788863627224, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-1-2-0-lower-landing.jpg pixels=921600 mean_luma=113.918 stddev_luma=10.489 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l150", + "view": "2-3-0-upper-landing", + "passage": "passage/2-3/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-upper-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -812.075390625 + ], + "target_m": [ + -13.604954164712868, + -11.337338700629157, + -813.6662645746064 + ], + "yaw_deg": 134.15112141809087, + "pitch_deg": -10.024269857937275, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-upper-landing.jpg pixels=921600 mean_luma=108.739 stddev_luma=2.947 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l150", + "view": "2-3-0-lower-landing", + "passage": "passage/2-3/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-lower-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -900.0333984375 + ], + "target_m": [ + -14.865342978484083, + -12.416118141706198, + -898.4427583280197 + ], + "yaw_deg": 144.72777711381306, + "pitch_deg": 10.022826283920647, + "source_stamp": "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/before-l150-2-3-0-lower-landing.jpg pixels=921600 mean_luma=112.176 stddev_luma=9.871 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l151", + "view": "0-1-0-upper-landing", + "passage": "passage/0-1/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-upper-landing.jpg", + "camera_m": [ + 37.0, + -5.25, + -372.3810546875 + ], + "target_m": [ + 28.40474992442715, + -7.918646874047773, + -372.325197471535 + ], + "yaw_deg": -107.24848481126237, + "pitch_deg": 0.3555935154874107, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-upper-landing.jpg pixels=921600 mean_luma=118.392 stddev_luma=15.921 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l151", + "view": "0-1-0-lower-landing", + "passage": "passage/0-1/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-lower-landing.jpg", + "camera_m": [ + 25.8369140625, + 3.19921875, + -736.6798828125 + ], + "target_m": [ + 17.97380590951016, + -1.1793118453464206, + -734.970169262755 + ], + "yaw_deg": -119.11101808688004, + "pitch_deg": 10.756207017349817, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-0-1-0-lower-landing.jpg pixels=921600 mean_luma=111.426 stddev_luma=12.411 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l151", + "view": "1-2-0-upper-landing", + "passage": "passage/1-2/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-upper-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -731.8810546875 + ], + "target_m": [ + 10.35825176774404, + -16.050410390488395, + -733.216132282697 + ], + "yaw_deg": -101.86342177874246, + "pitch_deg": -8.437834550031965, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-upper-landing.jpg pixels=921600 mean_luma=117.134 stddev_luma=15.664 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l151", + "view": "1-2-0-lower-landing", + "passage": "passage/1-2/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-lower-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -753.05390625 + ], + "target_m": [ + 10.48848361510052, + -16.587754362485967, + -751.7193175939389 + ], + "yaw_deg": -105.38384894032598, + "pitch_deg": 8.434788863627224, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-1-2-0-lower-landing.jpg pixels=921600 mean_luma=119.710 stddev_luma=14.975 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l151", + "view": "2-3-0-upper-landing", + "passage": "passage/2-3/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-upper-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -812.075390625 + ], + "target_m": [ + -13.604954164712868, + -11.337338700629157, + -813.6662645746064 + ], + "yaw_deg": 134.15112141809087, + "pitch_deg": -10.024269857937275, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-upper-landing.jpg pixels=921600 mean_luma=118.198 stddev_luma=15.882 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l151", + "view": "2-3-0-lower-landing", + "passage": "passage/2-3/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-lower-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -900.0333984375 + ], + "target_m": [ + -14.865342978484083, + -12.416118141706198, + -898.4427583280197 + ], + "yaw_deg": 144.72777711381306, + "pitch_deg": 10.022826283920647, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux151/captures/after-l151-2-3-0-lower-landing.jpg pixels=921600 mean_luma=121.734 stddev_luma=16.193 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + } +] diff --git a/Standalone/Research/WorldAssemblerM3/Linux152/PassageEnvelopeAudit.py b/Standalone/Research/WorldAssemblerM3/Linux152/PassageEnvelopeAudit.py new file mode 100644 index 0000000..14f3b62 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux152/PassageEnvelopeAudit.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +"""Audit cave primitive bounds against passage envelopes, allowing chamber joints only.""" +from __future__ import annotations + +import math +from typing import Any + + +INFLUENCE_REACH_M = 1.053334 +SHELL_THICKNESS_M = 0.75 + + +def _point(control: dict[str, Any], scale: float) -> tuple[float, float, float, float]: + x, y, z = control["position_q"] + return (x * scale, y * scale, z * scale, control["radius_q"] * scale) + + +def _knot_span(a, b) -> float: + return max(1.0e-6, math.sqrt(math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2))) + + +def _evaluate(controls, span: int, t: float): + p1, p2 = controls[span], controls[span + 1] + p0 = controls[span - 1] if span else controls[0] + p3 = controls[min(span + 2, len(controls) - 1)] + dt0, dt1, dt2 = _knot_span(p0, p1), _knot_span(p1, p2), _knot_span(p2, p3) + t2, t3 = t * t, t * t * t + result = [] + for axis in (0, 1, 3): + m1 = dt1 * ((p1[axis] - p0[axis]) / dt0 + - (p2[axis] - p0[axis]) / (dt0 + dt1) + + (p2[axis] - p1[axis]) / dt1) + m2 = dt1 * ((p2[axis] - p1[axis]) / dt1 + - (p3[axis] - p1[axis]) / (dt1 + dt2) + + (p3[axis] - p2[axis]) / dt2) + value = ((2 * t3 - 3 * t2 + 1) * p1[axis] + + (-2 * t3 + 3 * t2) * p2[axis] + + (t3 - 2 * t2 + t) * m1 + (t3 - t2) * m2) + result.append(value) + radius = min(max(result[2], min(p1[3], p2[3])), max(p1[3], p2[3])) + return result[0], result[1], p1[2] + (p2[2] - p1[2]) * t, radius + + +def _segments(passage: dict[str, Any], scale: float): + controls = [_point(row, scale) for row in passage["control_points"]] + route_end = min(max(1, int(passage.get("lower_mouth_control_index", len(controls) - 1))), + len(controls) - 1) + along = 0.0 + for span in range(route_end): + a, b = controls[span], controls[span + 1] + chord = math.hypot(b[0] - a[0], b[1] - a[1]) + steps = max(1, math.ceil(chord / 2.0)) + prior = _evaluate(controls, span, 0.0) + for step in range(1, steps + 1): + current = _evaluate(controls, span, step / steps) + length = math.dist(prior[:3], current[:3]) + yield prior, current, along, max(prior[3], current[3]) + INFLUENCE_REACH_M + along += length + prior = current + + +def _distance_squared(a, b, bounds, t: float) -> float: + total = 0.0 + for axis in range(3): + pos = a[axis] + (b[axis] - a[axis]) * t + lo, hi = bounds[axis] + gap = lo - pos if pos < lo else pos - hi if pos > hi else 0.0 + total += gap * gap + return total + + +def _overlap_interval(a, b, bounds, radius: float): + cuts = {0.0, 1.0} + for axis in range(3): + delta = b[axis] - a[axis] + if abs(delta) < 1.0e-15: + continue + for edge in bounds[axis]: + t = (edge - a[axis]) / delta + if 0.0 < t < 1.0: + cuts.add(t) + cuts = sorted(cuts) + best_t, best_d2 = 0.0, math.inf + for lo, hi in zip(cuts, cuts[1:]): + mid = (lo + hi) * 0.5 + quadratic = linear = 0.0 + for axis in range(3): + delta = b[axis] - a[axis] + pos = a[axis] + delta * mid + low, high = bounds[axis] + edge = low if pos < low else high if pos > high else None + if edge is not None: + quadratic += delta * delta + linear += delta * (a[axis] - edge) + candidate = min(hi, max(lo, -linear / quadratic)) if quadratic else lo + d2 = _distance_squared(a, b, bounds, candidate) + if d2 < best_d2: + best_t, best_d2 = candidate, d2 + for t in (0.0, 1.0): + d2 = _distance_squared(a, b, bounds, t) + if d2 < best_d2: + best_t, best_d2 = t, d2 + if best_d2 > radius * radius: + return None + + radius2 = radius * radius + if _distance_squared(a, b, bounds, 0.0) <= radius2: + left = 0.0 + else: + low, high = 0.0, best_t + for _ in range(42): + mid = (low + high) * 0.5 + if _distance_squared(a, b, bounds, mid) <= radius2: + high = mid + else: + low = mid + left = high + if _distance_squared(a, b, bounds, 1.0) <= radius2: + right = 1.0 + else: + low, high = best_t, 1.0 + for _ in range(42): + mid = (low + high) * 0.5 + if _distance_squared(a, b, bounds, mid) <= radius2: + low = mid + else: + high = mid + right = low + return left, right, best_t + + +def _inside_chamber(point, chamber: dict[str, Any], scale: float) -> bool: + cx, cy, cz = (value * scale for value in chamber["floor_center_q"]) + radius = chamber["radius_q"] * scale + clear = chamber["clear_height_q"] * scale + gx, gy = (value / 65536.0 for value in chamber["floor_gradient_q16"]) + floor = cz + gx * (point[0] - cx) + gy * (point[1] - cy) + chamber_distance = max(math.hypot(point[0] - cx, point[1] - cy) - radius, + floor - point[2], point[2] - (floor + clear)) + return chamber_distance <= SHELL_THICKNESS_M + + +def _primitive_bounds(row: dict[str, Any]): + return tuple((row["bounds_min_um"][i] / 1.0e6, + row["bounds_max_um"][i] / 1.0e6) for i in range(3)) + + +def _bezier(points, t: float): + u = 1.0 - t + return tuple(u**3 * points[0][axis] + 3*u*u*t * points[1][axis] + + 3*u*t*t * points[2][axis] + t**3 * points[3][axis] + for axis in range(3)) + + +def _distance_to_ellipse(radial: float, vertical: float, + radius: float, height: float) -> float: + """Euclidean distance to the edge bore's horizontal/vertical ellipse.""" + radial = abs(radial) + vertical = abs(vertical) + if (radial / radius) ** 2 + (vertical / height) ** 2 <= 1.0: + return 0.0 + + lo, hi = 0.0, math.pi / 2.0 + ratio = (math.sqrt(5.0) - 1.0) / 2.0 + + def squared_distance(angle: float) -> float: + return ((radial - radius * math.cos(angle)) ** 2 + + (vertical - height * math.sin(angle)) ** 2) + + left = hi - ratio * (hi - lo) + right = lo + ratio * (hi - lo) + left_value, right_value = squared_distance(left), squared_distance(right) + for _ in range(48): + if left_value < right_value: + hi, right, right_value = right, left, left_value + left = hi - ratio * (hi - lo) + left_value = squared_distance(left) + else: + lo, left, left_value = left, right, right_value + right = lo + ratio * (hi - lo) + right_value = squared_distance(right) + return math.sqrt(min(left_value, right_value)) + + +def _actual_edge_clearance(passage, edge, scale: float, + chamber: dict[str, Any]) -> float: + """Return minimum true tube clearance outside the landing chamber. + + The route edge is a swept ellipse, with its recorded polyline as the axis. + Comparing the tunnel tube to that ellipse avoids the false clashes caused by + intersecting two enclosing boxes. + """ + controls = [tuple(value / 1.0e6 for value in point) + for point in edge["control_points_um"]] + axis = [tuple(value / 1.0e6 for value in point) + for point in edge.get("polyline_um", [])] + if len(axis) < 2: + axis = [_bezier(controls, index / 64.0) for index in range(65)] + edge_radius = (edge["bore_radius_um"] + + edge["irregularity_amplitude_um"]) / 1.0e6 + edge_height = (edge["bore_height_um"] + + edge["irregularity_amplitude_um"]) / 1.0e6 + passage_segments = passage.get("_audit_segments") or tuple(_segments(passage, scale)) + minimum_clearance = math.inf + for a, b, _, passage_radius in passage_segments: + length = math.dist(a[:3], b[:3]) + steps = max(1, math.ceil(length / 0.05)) + for step in range(steps + 1): + t = step / steps + point = tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) + if _inside_chamber(point, chamber, scale): + continue + tunnel_radius = (a[3] + (b[3] - a[3]) * t + + INFLUENCE_REACH_M) + for start, end in zip(axis, axis[1:]): + delta = tuple(end[i] - start[i] for i in range(3)) + length_squared = sum(value * value for value in delta) + projection = (sum((point[i] - start[i]) * delta[i] + for i in range(3)) / length_squared + if length_squared else 0.0) + projection = min(1.0, max(0.0, projection)) + center = tuple(start[i] + projection * delta[i] for i in range(3)) + radial = math.hypot(point[0] - center[0], point[1] - center[1]) + vertical = abs(point[2] - center[2]) + clearance = (_distance_to_ellipse(radial, vertical, + edge_radius, edge_height) + - tunnel_radius) + minimum_clearance = min(minimum_clearance, clearance) + return minimum_clearance + + +def _test_bounds(bounds, strate_index: int, passages, scale: float, + *, allow_chamber: bool = True): + for passage in passages: + if strate_index not in (passage["upper_strate_index"], passage["lower_strate_index"]): + continue + chamber_key = ("upper_landing_chamber" if strate_index == passage["upper_strate_index"] + else "lower_landing_chamber") + chamber = passage[chamber_key] + segments = passage.get("_audit_segments") + if segments is None: + segments = tuple(_segments(passage, scale)) + for a, b, along, radius in segments: + expanded = [(a[i] - radius, b[i] + radius) if a[i] <= b[i] + else (b[i] - radius, a[i] + radius) for i in range(3)] + if any(expanded[i][1] < bounds[i][0] or expanded[i][0] > bounds[i][1] + for i in range(3)): + continue + overlap = _overlap_interval(a, b, bounds, radius) + if overlap is None: + continue + first, last, closest = overlap + if allow_chamber: + points = [tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) + for t in (first, closest, last)] + if all(_inside_chamber(point, chamber, scale) for point in points): + continue + point = tuple(a[i] + (b[i] - a[i]) * closest for i in range(3)) + return passage["stable_id"], along + closest * math.dist(a[:3], b[:3]), point + return None + + +def audit_layout(layout: dict[str, Any]) -> dict[str, Any]: + scale = layout["core_voxel_size_um"] / (256.0 * 1.0e6) + passages = [dict(row, _audit_segments=tuple(_segments(row, scale))) + for row in layout.get("passages", [])] + primitives = [*layout.get("cavern_rooms", []), *layout.get("cavern_edges", [])] + checked = conflicts = chamber_joints = bounds_candidates = refined_false_positives = 0 + first_conflict = None + for primitive in primitives: + bounds = _primitive_bounds(primitive) + checked += 1 + primitive_conflicted = False + for passage in passages: + found = _test_bounds(bounds, primitive["strate_index"], [passage], scale) + if not found: + continue + bounds_candidates += 1 + passage_id, station, point = found + if "control_points_um" in primitive: + side = ("upper_landing_chamber" + if primitive["strate_index"] == passage["upper_strate_index"] + else "lower_landing_chamber") + clearance = _actual_edge_clearance(passage, primitive, scale, + passage[side]) + if clearance >= 0.0: + refined_false_positives += 1 + continue + primitive_conflicted = True + conflicts += 1 + if first_conflict is None: + first_conflict = {"primitive": primitive["stable_id"], + "passage": passage_id, "station_m": round(station, 3), + "center_m": [round(value, 3) for value in point]} + break + if not primitive_conflicted: + # Count the edge's named landing association as a confirmed joint. + anchor = primitive.get("start_anchor_stable_id", "") + if anchor.endswith(("/upper-landing-chamber", "/lower-landing-chamber")): + chamber_joints += 1 + + negative_controls = 0 + long_passage = max(passages, key=lambda row: len(row["control_points"]), default=None) + if long_passage: + controls = [_point(row, scale) for row in long_passage["control_points"]] + index = min(max(1, len(controls) // 2), len(controls) - 2) + center = controls[index] + bounds = tuple((center[axis] - 0.25, center[axis] + 0.25) for axis in range(3)) + # Select the side whose chamber is farther from this forced tunnel overlap. + strate = long_passage["lower_strate_index"] + if _test_bounds(bounds, strate, [long_passage], scale, allow_chamber=False): + negative_controls = 1 + return {"primitives_checked": checked, "bounds_candidates": bounds_candidates, + "refined_edge_false_positives": refined_false_positives, + "outside_chamber_conflicts": conflicts, + "chamber_joints": chamber_joints, "negative_controls": negative_controls, + "negative_controls_expected": 1, "first_conflict": first_conflict, + "passed": conflicts == 0 and negative_controls == 1} diff --git a/Standalone/Research/WorldAssemblerM3/Linux152/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux152/REPORT.md new file mode 100644 index 0000000..1b63113 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux152/REPORT.md @@ -0,0 +1,42 @@ +# L152 — Chamber floors belong to the planned passage profile + +## Eye-height views + +The left images are the L151 state used before this round. No after images were captured: the third planned-floor adjustment failed strict layout validation, and the round stopped at the requested three-fix limit. + +| Landing | Before (L151) | After (L152) | +|---|---|---| +| passage/0-1/0 · upper | ![L151 upper landing, passage 0-1](../Linux151/captures/after-l151-0-1-0-upper-landing.jpg) | Not captured | +| passage/0-1/0 · lower | ![L151 lower landing, passage 0-1](../Linux151/captures/after-l151-0-1-0-lower-landing.jpg) | Not captured | +| passage/1-2/0 · upper | ![L151 upper landing, passage 1-2](../Linux151/captures/after-l151-1-2-0-upper-landing.jpg) | Not captured | +| passage/1-2/0 · lower | ![L151 lower landing, passage 1-2](../Linux151/captures/after-l151-1-2-0-lower-landing.jpg) | Not captured | +| passage/2-3/0 · upper | ![L151 upper landing, passage 2-3](../Linux151/captures/after-l151-2-3-0-upper-landing.jpg) | Not captured | +| passage/2-3/0 · lower | ![L151 lower landing, passage 2-3](../Linux151/captures/after-l151-2-3-0-lower-landing.jpg) | Not captured | + +## Result + +**L152 is not accepted.** Planning the chamber floor into the passage profile brought all three seed-2 planned grades under 15° and kept the passage shell and interiors clear. The remaining upper SurfaceWorld landing still leaves five route support samples unsupported, so the composed route gate fails. + +The final build in the working tree is the last snapshot-valid variant, source stamp `b299076d814ff7cc098aa373cdc3a26533ed8bfe1ef6df1ea9de4d400d231b25`. The evaluator version is 239. No canonical owner baselines were changed or promoted. + +## Fix attempts + +1. The continuous `h(s)` profile with a chamber plane matching its tunnel tangent passed grade, chamber interior, and shell checks. Its 9.58° upper plane rose about 1.2 m toward the SurfaceWorld route and blocked 481 route capsule samples. Log: [`seed2-recheck.log`](../../../../Saved/Linux/l152/seed2-recheck.log), source stamp `e31f7ffa…`. +2. Fitting the upper plane to the receiving surface route removed the blockage. Five route samples then had no support, and the upper chamber support audit reported two unsupported samples. The summary remained `2/3` passages walkable. Log: [`seed2-plane-fit.log`](../../../../Saved/Linux/l152/seed2-plane-fit.log), layout: [`seed2-plane-fit.json`](../../../../Saved/Linux/l152/seed2-plane-fit.json). +3. Reducing that surface entrance chamber to 3 m was rejected by strict layout validation: it violated the minimum radius/capsule-clearance rule. Log: [`seed2-final.log`](../../../../Saved/Linux/l152/seed2-final.log). I reverted this rejected change to the snapshot-valid second variant and stopped at three attempts. + +## Checks + +- Final Clang 19 build passed with `-fp-contract=off`, four jobs, and the 6 GiB limit. Build log: [`build-clang.log`](../../../../Saved/Linux/l152/build-clang.log). +- `git diff --check` passed on the final source. +- Focused seed 2, W8: planned grade audits passed at 9.581°, 6.582°, and 8.280°. Passage interiors had 0 solid samples in 320,355 samples; the shell audit reported zero floor, chamber, wall, and interior violations. Naturalness, mouth profile, ripple, interior-air, Tunnel Network route, Vertical Shafts route, and their negative controls passed. +- The composed passage summary failed: `passed=2/3`, `route_walkable=2`; the SurfaceWorld route reported `unsupported=5`. The upper landing chamber also reported `unsupported=2` in its floor-support audit. +- The seed-2 geometric envelope audit passed: 549 primitives checked, 0 outside-chamber conflicts, 3 named chamber joints, and its negative control passed 1/1. Result: [`passage-envelope-seed2.json`](../../../../Saved/Linux/l152/passage-envelope-seed2.json). +- The tile seam audit did not run: the headless process returned at the failed composed passage summary before emitting `[PassageTileSeamAudit]`. +- The 16-seed sweeps, owner baseline regeneration, L152 captures, fast tier, Clang/GCC W1/W8/W12 determinism, L80 timing, and Linux UE build/start/movement were not run after the three-fix stop. + +No Unreal `Source/` files changed. + +## Remaining risk + +The passage `h(s)` and chamber-floor profile still need a representation that preserves both the 15° grade budget and support across the SurfaceWorld entrance. The current analytic chamber plane cannot represent the route's local floor variation without either overriding the surface route or leaving support gaps. Multi-seed behavior and every gate not listed above remain unverified. diff --git a/Standalone/Research/WorldAssemblerM3/Linux152/RunL152Captures.py b/Standalone/Research/WorldAssemblerM3/Linux152/RunL152Captures.py new file mode 100644 index 0000000..77b6f19 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux152/RunL152Captures.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Capture matched eye-height views from both ends of every owner tunnel.""" +from __future__ import annotations + +import json +import math +import os +from pathlib import Path +import re +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[4] +DATA = ROOT / "Standalone/data/owner_world.json" +OUT = ROOT / "Saved/Linux/l152/captures" +LAYOUT = OUT / "after-resolved-layout.json" +ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux152/captures" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +VERSIONS = { + "before-l151": { + "headless": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l151-clang", + "renderer": ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l151-clang", + "stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + }, + "after-l152": { + "headless": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l152-clang", + "renderer": ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l152-clang", + "stamp": "b299076d814ff7cc098aa373cdc3a26533ed8bfe1ef6df1ea9de4d400d231b25", + }, +} +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) + + +def fmt(values) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def angles(camera, target) -> tuple[float, float]: + dx, dy, dz = (target[index] - camera[index] for index in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees( + math.atan2(dz, math.hypot(dx, dy))) + + +def point_q(layout: dict, values) -> tuple[float, float, float]: + scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0) + return tuple(value * scale for value in values) + + +def chamber_views(layout: dict) -> list[dict]: + result = [] + for passage in layout["passages"]: + controls = passage["control_points"] + for side, endpoint, neighbor in ( + ("upper", controls[0], controls[1]), + ("lower", controls[-1], controls[-2])): + chamber = passage[f"{side}_landing_chamber"] + center = point_q(layout, chamber["floor_center_q"]) + a = point_q(layout, neighbor["position_q"]) + b = point_q(layout, endpoint["position_q"]) + dx, dy = a[0] - b[0], a[1] - b[1] + length = math.hypot(dx, dy) + if length <= 1.0e-9: + raise ValueError(f"degenerate {side} tunnel tangent: {passage['stable_id']}") + dx, dy = dx / length, dy / length + eye_z = center[2] + 1.45 + distance = 9.0 + target = (center[0] + dx * distance, center[1] + dy * distance, + eye_z + (chamber["floor_gradient_q16"][0] / 65536.0 * dx + + chamber["floor_gradient_q16"][1] / 65536.0 * dy) + * distance) + camera = (center[0], center[1], eye_z) + slug = passage["stable_id"].replace("passage/", "").replace("/", "-") + result.append({"name": f"{slug}-{side}-landing", "passage": passage["stable_id"], + "side": side, "camera": camera, "target": target, + "focus": (center[0], center[1], eye_z, 18.0)}) + return result + + +def stamped_run(name: str, command: list[object], log: Path, expected_stamp: str, + env: dict[str, str]) -> str: + binary = Path(str(command[0])) + probe = subprocess.run([*LIMIT, "timeout", "15s", str(binary), "--print-source-stamp"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=20) + match = STAMP_RE.search(probe.stdout or "") + if probe.returncode or not match or match.group(1) != expected_stamp: + raise RuntimeError(f"{name}: source stamp mismatch for {binary}") + log.parent.mkdir(parents=True, exist_ok=True) + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, + timeout=1745, check=False) + output = log.read_text(encoding="utf-8", errors="replace") + emitted = STAMP_RE.search(output) + if result.returncode or not emitted or emitted.group(1) != expected_stamp: + raise RuntimeError(f"{name}: export or render failed; see {log}") + return output + + +def resolve_layout(env: dict[str, str]) -> dict: + config = VERSIONS["after-l152"] + command = [config["headless"], DATA, "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=12", + "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", "--seed=0", "--recipe=surface-world", "--resolved-layout-only", + f"--resolved-layout-output={LAYOUT}"] + stamped_run("after-l152-resolve-layout", list(map(str, command)), + OUT / "after-l152-resolved-layout.log", config["stamp"], env) + layout = json.loads(LAYOUT.read_text(encoding="utf-8")) + if len(layout.get("passages", [])) != 3: + raise RuntimeError("owner layout did not resolve the expected three tunnels") + return layout + + +def main() -> int: + if not DATA.is_file(): + raise SystemExit("missing owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + ARTIFACTS.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache") + layout = resolve_layout(env) + views = chamber_views(layout) + quality_tool = ROOT / "Standalone/Tools/verify/capture_quality.py" + manifest = [] + failures = [] + for version, config in VERSIONS.items(): + for view in views: + camera, target = view["camera"], view["target"] + yaw, pitch = angles(camera, target) + mesh_dir = OUT / "meshes" / version + mesh_dir.mkdir(parents=True, exist_ok=True) + mesh = mesh_dir / f"{view['name']}.vfmesh" + capture_layout = mesh_dir / f"{view['name']}-layout.json" + export = [config["headless"], DATA, "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=12", + "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", "--seed=0", "--recipe=surface-world", + "--render-distance-chunks=0", + f"--camera={fmt((*camera, yaw, pitch))}", + f"--resolved-layout-output={capture_layout}", f"--mesh-export={mesh}"] + stamped_run(f"{version}-{view['name']}-export", list(map(str, export)), + OUT / f"{version}-{view['name']}-export.log", config["stamp"], env) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{version}-{view['name']}: empty mesh export") + png = OUT / f"{version}-{view['name']}.png" + jpg = ARTIFACTS / f"{version}-{view['name']}.jpg" + render = [config["renderer"], mesh, png, "--size=1280x720", + "--fov-deg=78.0", f"--camera-m={fmt(camera)}", + f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}", + "--tile-skirts=off", "--shade=readable", "--headlamp=on", + "--height-tint=on", "--height-center-m=-950", "--height-range-m=60", + "--tile-edges=off", f"--focus-region={fmt(view['focus'])}"] + stamped_run(f"{version}-{view['name']}-render", list(map(str, render)), + OUT / f"{version}-{view['name']}-render.log", config["stamp"], env) + subprocess.run([*LIMIT, "timeout", "120s", "ffmpeg", "-nostdin", + "-hide_banner", "-loglevel", "error", "-y", "-i", png, + "-frames:v", "1", "-q:v", "4", jpg], cwd=ROOT, env=env, + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, + timeout=125) + check = subprocess.run([*LIMIT, "timeout", "120s", sys.executable, + quality_tool, jpg], cwd=ROOT, env=env, text=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + check=False, timeout=125) + quality_ok = check.returncode == 0 and "status=PASS" in check.stdout + if not quality_ok: + failures.append(f"{version}-{view['name']}") + quality_line = (check.stdout.strip().splitlines()[-1] + if check.stdout.strip() else f"quality exit={check.returncode}") + manifest.append({"version": version, "view": view["name"], + "passage": view["passage"], "side": view["side"], + "file": str(jpg.relative_to(ROOT)), "camera_m": camera, + "target_m": target, "yaw_deg": yaw, "pitch_deg": pitch, + "source_stamp": config["stamp"], + "pose_layout_identity": layout.get("stable_identity"), + "quality_status": "PASS" if quality_ok else "FAIL", + "quality": quality_line}) + print(f"[L152Capture] version={version} view={view['name']} " + f"quality={'PASS' if quality_ok else 'FAIL'} " + f"file={jpg.relative_to(ROOT)}", flush=True) + (ARTIFACTS / "manifest.json").write_text( + json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + with (ARTIFACTS / "INDEX.md").open("w", encoding="utf-8") as index: + index.write("# L152 matched owner tunnel landing captures\n\n" + "Seed 0 camera poses resolved from the L152 owner layout and reused for both " + "versions. Each view is at eye height at one of the three tunnel landing " + "chambers. Before uses the L151 head build; after uses L152 source stamp " + f"`{VERSIONS['after-l152']['stamp']}`.\n\n") + for view in views: + index.write(f"## {view['passage']} / {view['side']} landing\n\n" + f"- Before: [image](before-l151-{view['name']}.jpg)\n" + f"- After: [image](after-l152-{view['name']}.jpg)\n\n") + index.write(f"Capture quality failures: {len(failures)}/{len(manifest)}.\n") + print(f"[L152CaptureSummary] chamber_views={len(views)} " + f"images={len(manifest)} quality_failures={len(failures)}", flush=True) + return 0 if not failures else 1 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L152CaptureFailure] {error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux152/RunL152Determinism.py b/Standalone/Research/WorldAssemblerM3/Linux152/RunL152Determinism.py new file mode 100644 index 0000000..c3c36ed --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux152/RunL152Determinism.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Compare L152 hashes and layout identity at Clang/GCC W1, W8, and W12.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from pathlib import Path + +from PassageEnvelopeAudit import audit_layout + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l152/determinism" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "clang": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l152-clang", + "gcc": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l152-gcc", +} +STAMP = "b299076d814ff7cc098aa373cdc3a26533ed8bfe1ef6df1ea9de4d400d231b25" +CAMERA = "-20.062500,-5.068359,-900.353516,0,0" +HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", + re.M) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def main() -> int: + if any(not path.is_file() for path in (DATA, *BINARIES.values())): + raise SystemExit("missing L152 Clang/GCC binary or composed owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + rows = [] + partial = OUT / "runs-partial.json" + for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + name = f"determinism-{compiler}-w{workers}" + log, layout = OUT / f"{name}.log", OUT / f"{name}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=0", + f"--camera={CAMERA}", "--stream-hash", "--force-generate", "--seed=2", + "--recipe=surface-world", "--render-distance-chunks=0", + "--density-dispatch-report", f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp_match = STAMP_RE.search(content) + stamp = stamp_match.group(1) if stamp_match else None + hashes = tuple(HASH_RE.findall(content)) + identity = (json.loads(layout.read_text(encoding="utf-8")).get("stable_identity") + if layout.is_file() else None) + envelope_audit = (audit_layout(json.loads(layout.read_text(encoding="utf-8"))) + if layout.is_file() else None) + row = {"compiler": compiler, "workers": workers, "exit": proc.returncode, + "stamp": stamp, "source_stamp_matches": stamp == STAMP, + "hash_lines": len(hashes), "hashes": hashes, + "layout_identity": identity, + "envelope_audit": envelope_audit, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + rows.append(row) + partial.write_text(json.dumps(rows, indent=2) + "\n", encoding="utf-8") + print("[L152DeterminismRun] " + json.dumps(row, sort_keys=True), flush=True) + valid = [row for row in rows if row["exit"] == 0 and row["source_stamp_matches"] + and row["hash_lines"] == 12 and row["layout_identity"]] + reference_hashes = valid[0]["hashes"] if valid else [] + reference_identity = valid[0]["layout_identity"] if valid else None + deterministic = len(valid) == 6 and all( + row["hashes"] == reference_hashes and row["layout_identity"] == reference_identity + for row in valid) + envelope_pass = all(row["envelope_audit"] and row["envelope_audit"]["passed"] + and row["envelope_audit"]["negative_controls"] == 1 + for row in rows) + summary = {"status": "PASS" if deterministic else "FAIL", + "deterministic": deterministic, "envelope_pass": envelope_pass, + "source_stamp": STAMP, "layout_identity": reference_identity, "runs": rows} + summary["status"] = "PASS" if deterministic and envelope_pass else "FAIL" + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + print(f"[L152DeterminismSummary] runs={len(rows)} deterministic=" + f"{'PASS' if deterministic else 'FAIL'} envelope=" + f"{'PASS' if envelope_pass else 'FAIL'} status={summary['status']}", flush=True) + return 0 if summary["status"] == "PASS" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux152/RunL152SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux152/RunL152SeedSweep.py new file mode 100644 index 0000000..132a18d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux152/RunL152SeedSweep.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +"""Run the L152 passage and envelope gates across the 16 owner seeds.""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import time +from pathlib import Path + +from PassageEnvelopeAudit import audit_layout + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l152/seeds" +BIN = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l152-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "b299076d814ff7cc098aa373cdc3a26533ed8bfe1ef6df1ea9de4d400d231b25" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +DISPATCH = { + "tunnel-network": re.compile( + r"^\[DensityDispatch\] strate=2 generator=TunnelNetwork " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), + "vertical-shafts": re.compile( + r"^\[DensityDispatch\] strate=3 generator=VerticalShafts " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), +} +ROUTES = { + "tunnel-network": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=2 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=3 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), +} +CONTROLS = { + "tunnel-network": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[VerticalShaftsBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} +REQUIRED = { + "passage_summary": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "interior_capsules": re.compile( + r"^\[PassageInteriorCapsuleAuditSummary\].*status=PASS$", re.M), + "layout_controls": re.compile( + r"^\[ResolvedLayoutValidatorControls\] rejected=10/10 status=PASS$", re.M), + "floor_profile_controls": re.compile( + r"^\[FloorProfileBlendNegativeControls\].*detected=true/true status=PASS$", re.M), + "naturalness_control": re.compile( + r"^\[PassageNaturalnessNegativeControl\].*detected=true$", re.M), + "mouth_control": re.compile( + r"^\[PassageMouthProfileNegativeControl\].*detected=true$", re.M), +} + + +def passage_rows(content: str) -> list[str]: + return re.findall(r"^\[ResolvedPassageAudit\].*$", content, re.M) + + +def grade_audit(content: str) -> tuple[bool, list[str]]: + rows = re.findall(r"^\[PassagePlanGradeAudit\].*$", content, re.M) + ids = set() + passed = bool(rows) + for row in rows: + identifier = re.search(r"\bid=([^ ]+)", row) + maximum = re.search(r"\bmax_grade_deg=([0-9.]+)", row) + limit = re.search(r"\blimit_deg=([0-9.]+)", row) + violations = re.search(r"\bviolations=(\d+)", row) + status = "status=PASS" in row + if identifier: + ids.add(identifier.group(1)) + passed &= bool(identifier and maximum and limit and violations and status + and float(maximum.group(1)) <= float(limit.group(1)) + and float(limit.group(1)) == 15.0 + and int(violations.group(1)) == 0) + return passed and len(ids) >= 3, rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kind", choices=tuple(DISPATCH), required=True) + args = parser.parse_args() + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L152 Clang binary or composed owner fixture") + mode_out = OUT / args.kind + mode_out.mkdir(parents=True, exist_ok=True) + results = [] + partial = mode_out / "runs-partial.json" + for seed in range(16): + log = mode_out / f"seed{seed}.log" + layout = mode_out / f"seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=0", "--force-generate", f"--seed={seed}", + "--recipe=surface-world", "--density-dispatch-report", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["process_exit_zero"] = proc.returncode == 0 + checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP + checks["dispatch"] = bool(DISPATCH[args.kind].search(content)) + checks["route"] = bool(ROUTES[args.kind].search(content)) + checks["blocked_route_control"] = bool(CONTROLS[args.kind].search(content)) + + passages = passage_rows(content) + checks["three_passages_walkable_and_open"] = ( + len(passages) == 3 and all("walkability=PASS" in row + and "interior_air=PASS" in row + and re.search(r"\bblocked=0\b", row) + and re.search(r"\bunsupported=0\b", row) + and re.search(r"\bgrade_failures=0\b", row) + for row in passages)) + checks["three_interior_negative_controls"] = len(re.findall( + r"^\[ResolvedPassageInteriorNegativeControl\].*detected=true status=PASS$", + content, re.M)) == 3 + checks["three_ripple_gap_controls"] = len(re.findall( + r"^\[PassageRippleSamplingControl\].*status=PASS$", content, re.M)) == 3 + checks["three_blocked_passage_controls"] = len(re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3 + chamber_rows = re.findall(r"^\[PassageLandingChamberAudit\].*$", content, re.M) + checks["six_chamber_floors"] = (len(chamber_rows) == 6 and all( + "status=PASS" in row and re.search(r"\bsolid_samples=0\b", row) + and re.search(r"\bgrade_failures=0\b", row) for row in chamber_rows)) + checks["plan_grade"] = grade_audit(content)[0] + chamber_controls = re.search( + r"^\[PassageLandingChamberNegativeControlSummary\] " + r"detected=(\d+)/6 .*status=OBSERVED$", content, re.M) + checks["chamber_negative_control"] = bool( + chamber_controls and int(chamber_controls.group(1)) > 0) + + checks["three_ripple_profiles"] = len(re.findall( + r"^\[PassageRippleAudit\].*status=PASS$", content, re.M)) >= 3 + checks["passage_count"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*walkability=PASS", content, re.M)) == 3 + checks["layout_output"] = layout.is_file() + envelope = audit_layout(json.loads(layout.read_text(encoding="utf-8"))) \ + if layout.is_file() else None + checks["envelope"] = bool(envelope and envelope["passed"] + and envelope["outside_chamber_conflicts"] == 0) + checks["envelope_negative_control"] = bool(envelope + and envelope["negative_controls"] == envelope["negative_controls_expected"] == 1) + passed = all(checks.values()) + failed = [name for name, ok in checks.items() if not ok] + row = {"kind": args.kind, "seed": seed, + "verdict": "PASS" if passed else "FAIL", + "reason": ", ".join(failed) if failed else "all requested gates pass", + "exit": proc.returncode, "checks": checks, + "plan_grade_rows": grade_audit(content)[1], + "envelope_audit": envelope, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L152SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + partial.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + + passed_count = sum(row["verdict"] == "PASS" for row in results) + summary = {"kind": args.kind, "source_stamp": STAMP, "seeds": list(range(16)), + "passed": passed_count, "failed": 16 - passed_count, "results": results} + (mode_out / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + print(f"[L152SeedSweepSummary] kind={args.kind} passed={passed_count}/16 " + f"failed={16-passed_count} status={'PASS' if passed_count == 16 else 'FAIL'}", + flush=True) + return 0 if passed_count == 16 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux152/RunL152Timing.py b/Standalone/Research/WorldAssemblerM3/Linux152/RunL152Timing.py new file mode 100644 index 0000000..3c0115b --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux152/RunL152Timing.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""L80 comparison of L152 with the assembler/world baseline.""" +from __future__ import annotations + +import json +import os +import re +import statistics +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l152/timing" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": ( + ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-aa-a", + "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621"), + "planner-head": ( + ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l152-clang", + "b299076d814ff7cc098aa373cdc3a26533ed8bfe1ef6df1ea9de4d400d231b25"), +} +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) +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + binary, expected_stamp = BINARIES[version] + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + label = f"s{strate}-w{workers}-{version}-{phase}{replicate}" + log = OUT / f"{label}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline, near, stamp = PIPELINE.search(content), NEAR.search(content), STAMP.search(content) + if (proc.returncode or pipeline is None or near is None or stamp is None + or stamp.group(1) != expected_stamp): + raise SystemExit(f"L80 timing failed or source stamp mismatch: {log}; exit={proc.returncode}") + fields = dict(FIELDS.findall(pipeline.group(1))) + row = { + "strate": strate, "workers": workers, "version": version, + "phase": phase, "replicate": replicate, "source_stamp": stamp.group(1), + "lod02_ready_s": float(near.group(3)), + "whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")), + "whole_tiles": int(fields["whole_tiles"].split("/")[0]), + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), + } + print("[L152TimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def quiet_machine() -> tuple[float, float]: + load_1m = os.getloadavg()[0] + + def totals() -> tuple[int, int]: + fields = Path("/proc/stat").read_text().splitlines()[0].split() + values = [int(value) for value in fields[1:]] + return sum(values), values[3] + values[4] + + before = totals() + time.sleep(2.0) + after = totals() + idle = 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0]) + return load_1m, idle + + +def main() -> int: + if any(not path.is_file() for path in [DATA, *(entry[0] for entry in BINARIES.values())]): + raise SystemExit("missing L152 timing input") + OUT.mkdir(parents=True, exist_ok=True) + load_1m, idle = quiet_machine() + (OUT / "machine-load-before.txt").write_text( + f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n") + print(f"[L152TimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True) + if load_1m >= 1.5 or idle < 97.0: + raise SystemExit("timing refused: machine is above the L80 quiet-load gate") + + rows = [] + versions = tuple(BINARIES) + for strate in range(4): + for workers in (8, 12): + for version in versions: + run(strate, workers, version, "warmup", 0) + for replicate in range(1, 6): + order = versions if replicate % 2 else tuple(reversed(versions)) + for version in order: + rows.append(run(strate, workers, version, "measured", replicate)) + (OUT / "runs-partial.json").write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8") + + measured = [row for row in rows if row["phase"] == "measured"] + summary = [] + for strate in range(4): + for workers in (8, 12): + group = [row for row in measured + if row["strate"] == strate and row["workers"] == workers] + medians = { + version: { + key: statistics.median(row[key] for row in group + if row["version"] == version) + for key in ("lod02_ready_s", "whole_view_ready_s") + } + for version in versions + } + entry = {"strate": strate, "workers": workers, + "replicates_per_version": 5, "medians": medians} + for key in ("lod02_ready_s", "whole_view_ready_s"): + entry[f"planner_head_vs_assembler_world_{key}_delta_pct"] = round( + 100.0 * (medians["planner-head"][key] + / medians["assembler-world"][key] - 1.0), 2) + entry[f"{key}_paired_deltas_pct"] = [ + round(100.0 * ( + next(row[key] for row in group + if row["version"] == "planner-head" + and row["replicate"] == replicate) + / next(row[key] for row in group + if row["version"] == "assembler-world" + and row["replicate"] == replicate) - 1.0), 3) + for replicate in range(1, 6)] + summary.append(entry) + (OUT / "summary.json").write_text(json.dumps({ + "a_a_noise_floor_pct": 1.0, + "timing_protocol": ( + "one warm-up per binary, five interleaved runs per cell, W8/W12, " + "all four owner layers; L137 A/A median noise floor ±1%"), + "runs": rows, + "summary": summary, + }, indent=2) + "\n", encoding="utf-8") + failed_cells = [] + for entry in summary: + for measure in ("lod02_ready_s", "whole_view_ready_s"): + delta = entry[f"planner_head_vs_assembler_world_{measure}_delta_pct"] + if delta > 1.0: + failed_cells.append({"strate": entry["strate"], + "workers": entry["workers"], + "measure": measure, "delta_pct": delta}) + report = json.loads((OUT / "summary.json").read_text(encoding="utf-8")) + report["performance_floor_pct"] = 1.0 + report["performance_floor_rule"] = ( + "pass when no L152 median is more than 1% slower than assembler/world") + report["performance_floor_failures"] = failed_cells + report["status"] = "PASS" if not failed_cells else "FAIL" + (OUT / "summary.json").write_text(json.dumps(report, indent=2) + "\n", + encoding="utf-8") + print(f"[L152TimingSummary] cells={len(summary)} failing_metrics={len(failed_cells)} " + f"status={'PASS' if not failed_cells else 'FAIL'}", flush=True) + return 0 if not failed_cells else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux152/RunL152Unreal.py b/Standalone/Research/WorldAssemblerM3/Linux152/RunL152Unreal.py new file mode 100644 index 0000000..f2a39b7 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux152/RunL152Unreal.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Measure planner HEAD and code-equivalent assembler/world with the L121/L127 runners.""" +from __future__ import annotations + +import importlib.util +import json +import os +from pathlib import Path +import statistics +import sys + + +ROOT = Path(__file__).resolve().parents[4] +RESEARCH = ROOT / "Standalone/Research/WorldAssemblerM3" +EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd") +HOSTS = { + "assembler-world": ROOT / "Saved/Linux/l127/ue-baseline-project", + "planner-head": Path("/home/jahni/VFHost"), +} +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), +} + + +def load(name: str, path: Path): + spec = importlib.util.spec_from_file_location(name, path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot load {path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def configure(arrival, movement, label: str): + host = HOSTS[label].resolve() + out = ROOT / "Saved/Linux/l152/ue" / label + plugin_saved = host / "Plugins/VoxelForge/Saved/Linux/l152" + out.mkdir(parents=True, exist_ok=True) + plugin_saved.mkdir(parents=True, exist_ok=True) + os.environ["XDG_CONFIG_HOME"] = str(out / "config") + arrival.HOST = host + arrival.EDITOR = EDITOR + arrival.OUT = out / "start" + arrival.HOST_SAVED = out / "host" + arrival.HOST_PLUGIN_SAVED = plugin_saved + arrival.RUN_TIMEOUT_S = 1740 + movement.HOST = host + movement.EDITOR = EDITOR + movement.OUT = out / "movement" + movement.HOST_SAVED = out / "host" + movement.HOST_OUT = plugin_saved / "movement" + movement.RUN_TIMEOUT_S = 1740 + return host, out + + +def start_matrix(arrival, movement, label: str) -> dict: + host, out = configure(arrival, movement, label) + spawns = { + 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), + } + starts = [] + for index, position in spawns.items(): + stratum = "FI" if index == 1 else "SW" + arrival.SPAWNS_M[stratum] = position + row = arrival.run(stratum, 1, f"start-s{index}", 8, 1, 0, 1, 1, 0, 1, + False, False, 1) + row["strate_index"] = index + row["spawn_m"] = list(position) + if row.get("lod02_full_settled_t_s") is None: + raise RuntimeError(f"LOD0-2 did not settle for S{index}: {row}") + starts.append(row) + print("[L152UEStart] " + json.dumps(row, sort_keys=True), flush=True) + (out / "start-summary.json").write_text( + json.dumps(starts, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + row = moving_run(movement, label, 1) + (out / "movement-first.json").write_text( + json.dumps(row, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("[L152UEMovement] " + json.dumps(row, sort_keys=True), flush=True) + return {"host": str(host), "starts": starts, "first_movement": row} + + +def moving_run(movement, label: str, repetition: int) -> dict: + condition = "c743f2a" if label == "assembler-world" else "planner-head" + row = movement.run_unreal(condition, repetition) + events = movement.parse_events(ROOT / row["trace"]) + row["supplemental_far_applies_moving"] = sum( + int(event.get("supplemental_far_applies", 0)) + for event in events + if event.get("type") == "apply_frame" + and float(event.get("speed_cm_s", 0.0)) >= 50.0) + if row.get("effective_tile_slots") != 8: + raise RuntimeError(f"expected eight effective tile slots: {row}") + return row + + +def main() -> int: + out = ROOT / "Saved/Linux/l152/ue" + out.mkdir(parents=True, exist_ok=True) + arrival = load("l150_arrival", RESEARCH / "Linux121/RunL121Arrival.py") + movement = load("l150_movement", RESEARCH / "Linux121/RunL121Movement.py") + summaries = {} + all_runs = {label: [] for label in HOSTS} + for label in HOSTS: + summaries[label] = start_matrix(arrival, movement, label) + all_runs[label].append(summaries[label]["first_movement"]) + + for repetition in range(2, 6): + order = ("planner-head", "assembler-world") if repetition % 2 == 0 \ + else ("assembler-world", "planner-head") + for label in order: + configure(arrival, movement, label) + row = moving_run(movement, label, repetition) + all_runs[label].append(row) + (out / "movement-runs-partial.json").write_text( + json.dumps(all_runs, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("[L152UEMovementRepeat] " + json.dumps({ + key: row.get(key) for key in ( + "condition", "repetition", "near_ready_moving_mean_pct", + "far_holes_moving_mean_pct", "game_thread_tick_p99_ms", + "game_thread_tick_max_ms", "supplemental_far_applies_moving", + "wall_s", "trace")}, sort_keys=True), flush=True) + + keys = ("near_ready_moving_mean_pct", "far_holes_moving_mean_pct", + "game_thread_tick_p99_ms", "game_thread_tick_max_ms") + medians = { + label: {key: statistics.median(row[key] for row in rows) for key in keys} + for label, rows in all_runs.items() + } + delta = { + key: round(100.0 * (medians["planner-head"][key] + / medians["assembler-world"][key] - 1.0), 2) + for key in keys + } + result = { + "baseline_commit": "c743f2a (code-equivalent to assembler/world eb47d72)", + "protocol": ( + "L121 arrival and L127 movement; starts at four owner strata; five interleaved " + "30 m/s runs per build; 12 s move plus 5 s dwell; eight effective tile slots"), + "starts": summaries, + "movement_runs": all_runs, + "movement_medians": medians, + "planner_head_vs_assembler_world_delta_pct": delta, + } + (out / "summary.json").write_text( + json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("[L152UESummary] " + json.dumps({ + "movement_medians": medians, + "planner_head_vs_assembler_world_delta_pct": delta, + "starts_s": { + label: [round(row["lod02_full_settled_t_s"], 3) + for row in summary["starts"]] + for label, summary in summaries.items() + }, + }, sort_keys=True), flush=True) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L152UEFailure] {error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux152/__pycache__/PassageEnvelopeAudit.cpython-313.pyc b/Standalone/Research/WorldAssemblerM3/Linux152/__pycache__/PassageEnvelopeAudit.cpython-313.pyc new file mode 100644 index 0000000..c79d422 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux152/__pycache__/PassageEnvelopeAudit.cpython-313.pyc differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux153/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux153/REPORT.md new file mode 100644 index 0000000..fda1fd8 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux153/REPORT.md @@ -0,0 +1,44 @@ +| Landing | Before: L151 HEAD | After: L153 candidate 3 | +|---|---|---| +| passage/0-1/0 upper | ![L151 upper Surface World landing](../Linux151/captures/after-l151-0-1-0-upper-landing.jpg) | ![L153 upper Surface World landing](captures/after-l153-0-1-0-upper-landing.jpg) | +| passage/0-1/0 lower | ![L151 lower landing](../Linux151/captures/after-l151-0-1-0-lower-landing.jpg) | ![L153 lower landing](captures/after-l153-0-1-0-lower-landing.jpg) | +| passage/1-2/0 upper | ![L151 upper landing](../Linux151/captures/after-l151-1-2-0-upper-landing.jpg) | ![L153 upper landing](captures/after-l153-1-2-0-upper-landing.jpg) | +| passage/1-2/0 lower | ![L151 lower landing](../Linux151/captures/after-l151-1-2-0-lower-landing.jpg) | ![L153 lower landing](captures/after-l153-1-2-0-lower-landing.jpg) | +| passage/2-3/0 upper | ![L151 upper landing](../Linux151/captures/after-l151-2-3-0-upper-landing.jpg) | ![L153 upper landing](captures/after-l153-2-3-0-upper-landing.jpg) | +| passage/2-3/0 lower | ![L151 lower landing](../Linux151/captures/after-l151-2-3-0-lower-landing.jpg) | ![L153 lower landing](captures/after-l153-2-3-0-lower-landing.jpg) | + +# L153 — landing support did not pass + +The third support shape still fails seed 2: five Surface World route probes and two upper chamber probes remain unsupported. The work stopped after three failed implementations, as required by the round instructions. L153 is not ready to promote. + +Captures are at eye height for all six chambers. L151 and L153 camera positions match; the upper `passage/0-1/0` pitch differs by 3.10° because its planned floor tangent changed. L153 capture quality passed 6/6. The manifest and reproducible capture command are in [captures](captures/INDEX.md) and [RunL153Captures.py](RunL153Captures.py). + +## Change + +`WorldAssemblyEvaluatorVersion` is now 240. The candidate adds a tapered support SDF under chamber floors and the first/last 9 m of route approaches. Surface landings anchor to native ground within a 2 m depth budget; cave and shaft landings use a 0.75 m budget. The support footprint tapers over 1 m and embeds 0.20 m. Bounds were expanded for the support depth. + +The three seed 2 attempts used the same W8 Surface World command and differed only in support composition: + +| Attempt | Source stamp | Change | Seed 2 result | +|---|---|---|---| +| 1 | `4c83a0a143d44b7b1f44758408fafa8d220390825a9a15cd48b5484e22c35a0c` | Add the support body after passage composition | Chamber support passed, but 99 route capsule samples became solid; `interior_air=FAIL`. Five Surface World route probes remained unsupported. | +| 2 | `663ab4fff5e4605a19d2a6242b1ba23fff230c143f5ae5e9d7ebc00a2b3cf5df` | Clip support by the full passage/chamber air union | Interior air passed with zero solid capsule samples, but the upper chamber had 2 unsupported probes and Surface World route support still had 5. | +| 3 | `4599aacdabb2940f223570a9a537723727989dd5236dfc084524e6dff55bc1dc` | Clip support by the bore field only, leaving chamber air out of the clip | Same 2 upper chamber and 5 route probes remained unsupported. This is the current working tree. | + +Attempt 3’s final seed 2 summary is `interfaces=3 passed=2`, `strate_routes=3`, `route_walkable=2`, `route_bindings=3`, `route_binding_failures=0`, `status=FAIL`. The upper `passage/0-1/0` chamber reports `unsupported=2`; the Surface World route reports `unsupported=5`. Tunnel Network and Vertical Shafts route support pass. The passage interior capsule audit passes with 0 solid samples out of 320,355 and all 3 interior negative controls detected. Shell, naturalness (3/3), mouth profile (3/3), ripple (3/3), and blocked-route controls pass. + +The seam audit did not emit a result: the headless audit exits with code 10 after the failed passage summary. Seam status is unverified. + +## Checks + +- Clang 19 build passed with `-ffp-contract=off`, 4 jobs. Final source stamp: `4599aacdabb2940f223570a9a537723727989dd5236dfc084524e6dff55bc1dc`. +- `git diff --check` passed. +- Focused seed 2 audit: failed the support gate as described above. Log: `Saved/Linux/l153/seed2-focused.log`. +- Fast tier: failed after 0.5 s on the first Floating Islands owner fixture. Its resolved-layout snapshot rejects `passage/0-1/0` because the lower landing chamber does not fit inside its host content band. Log: `Saved/Linux/l153/fast-tier.log`. +- Capture quality: 6/6 passed. Existing L151 before images were reused; the after images were rendered from the L153 candidate. + +The 16-seed Tunnel Network and Vertical Shafts sweeps were not run after the seed 2 gate failed and the three support attempts were exhausted. The tile seam result is absent for the same early audit exit. GCC W1/W8/W12, Clang W1/W8/W12 determinism, L80 timing across four layers, and the Linux UE start plus 30 m/s run were not run. No v240 canonical baselines were promoted because the candidate fails seed 2 and the fast tier. No Unreal `Source/` files changed. + +## Remaining risk + +The support body can either preserve the passage interior or provide the required support at the Surface World upper landing in this seed, but the three tested compositions did not do both. The v240 evaluator change also exposes a Floating Islands chamber content-band validation failure in the fast tier. The implementation and baselines need a new premise before this round can pass. diff --git a/Standalone/Research/WorldAssemblerM3/Linux153/RunL153Captures.py b/Standalone/Research/WorldAssemblerM3/Linux153/RunL153Captures.py new file mode 100644 index 0000000..0a1c060 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux153/RunL153Captures.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Capture eye-height views of the six owner landing chambers for L153.""" +from __future__ import annotations + +import json +import math +import os +from pathlib import Path +import re +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[4] +DATA = ROOT / "Standalone/data/owner_world.json" +OUT = ROOT / "Saved/Linux/l153/captures" +LAYOUT = OUT / "seed0-resolved-layout.json" +ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux153/captures" +HEADLESS = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l153-clang" +RENDERER = ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l153-clang" +STAMP = "4599aacdabb2940f223570a9a537723727989dd5236dfc084524e6dff55bc1dc" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) + + +def fmt(values) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def angles(camera, target) -> tuple[float, float]: + dx, dy, dz = (target[index] - camera[index] for index in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees( + math.atan2(dz, math.hypot(dx, dy))) + + +def point_q(layout: dict, values) -> tuple[float, float, float]: + scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0) + return tuple(value * scale for value in values) + + +def chamber_views(layout: dict) -> list[dict]: + result = [] + for passage in layout["passages"]: + controls = passage["control_points"] + for side, endpoint, neighbor in ( + ("upper", controls[0], controls[1]), + ("lower", controls[-1], controls[-2])): + chamber = passage[f"{side}_landing_chamber"] + center = point_q(layout, chamber["floor_center_q"]) + a = point_q(layout, neighbor["position_q"]) + b = point_q(layout, endpoint["position_q"]) + dx, dy = a[0] - b[0], a[1] - b[1] + length = math.hypot(dx, dy) + if length <= 1.0e-9: + raise ValueError(f"degenerate {side} tunnel tangent: {passage['stable_id']}") + dx, dy = dx / length, dy / length + eye_z = center[2] + 1.45 + distance = 9.0 + target = (center[0] + dx * distance, center[1] + dy * distance, + eye_z + (chamber["floor_gradient_q16"][0] / 65536.0 * dx + + chamber["floor_gradient_q16"][1] / 65536.0 * dy) + * distance) + camera = (center[0], center[1], eye_z) + slug = passage["stable_id"].replace("passage/", "").replace("/", "-") + result.append({"name": f"{slug}-{side}-landing", "passage": passage["stable_id"], + "side": side, "camera": camera, "target": target, + "focus": (center[0], center[1], eye_z, 18.0)}) + return result + + +def stamped_run(name: str, command: list[object], log: Path, env: dict[str, str]) -> str: + probe = subprocess.run([*LIMIT, "timeout", "15s", str(command[0]), "--print-source-stamp"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=20) + match = STAMP_RE.search(probe.stdout or "") + if probe.returncode or not match or match.group(1) != STAMP: + raise RuntimeError(f"{name}: source stamp mismatch for {command[0]}") + log.parent.mkdir(parents=True, exist_ok=True) + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, + timeout=1745, check=False) + output = log.read_text(encoding="utf-8", errors="replace") + emitted = STAMP_RE.search(output) + if result.returncode or not emitted or emitted.group(1) != STAMP: + raise RuntimeError(f"{name}: export or render failed; see {log}") + return output + + +def main() -> int: + if not DATA.is_file() or not HEADLESS.is_file() or not RENDERER.is_file(): + raise SystemExit("missing owner fixture or L153 Clang capture binaries") + OUT.mkdir(parents=True, exist_ok=True) + ARTIFACTS.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache") + resolve = [HEADLESS, DATA, "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--workers=12", "--clip-radius=6", "--max-clip-level=0", + "--max-level=0", "--force-generate", "--seed=0", "--recipe=surface-world", + "--resolved-layout-only", f"--resolved-layout-output={LAYOUT}"] + stamped_run("l153-resolve-layout", resolve, OUT / "resolve-layout.log", env) + layout = json.loads(LAYOUT.read_text(encoding="utf-8")) + if len(layout.get("passages", [])) != 3: + raise RuntimeError("owner layout did not resolve the expected three tunnels") + + quality_tool = ROOT / "Standalone/Tools/verify/capture_quality.py" + manifest = [] + failures = [] + for view in chamber_views(layout): + camera, target = view["camera"], view["target"] + yaw, pitch = angles(camera, target) + mesh_dir = OUT / "meshes" + mesh_dir.mkdir(parents=True, exist_ok=True) + mesh = mesh_dir / f"{view['name']}.vfmesh" + capture_layout = mesh_dir / f"{view['name']}-layout.json" + export = [HEADLESS, DATA, "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--workers=12", "--clip-radius=6", "--max-clip-level=0", + "--max-level=0", "--force-generate", "--seed=0", "--recipe=surface-world", + "--render-distance-chunks=0", f"--camera={fmt((*camera, yaw, pitch))}", + f"--resolved-layout-output={capture_layout}", f"--mesh-export={mesh}"] + stamped_run(f"l153-{view['name']}-export", export, + OUT / f"{view['name']}-export.log", env) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{view['name']}: empty mesh export") + png = OUT / f"{view['name']}.png" + jpg = ARTIFACTS / f"after-l153-{view['name']}.jpg" + render = [RENDERER, mesh, png, "--size=1280x720", "--fov-deg=78.0", + f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", + f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable", + "--headlamp=on", "--height-tint=on", "--height-center-m=-950", + "--height-range-m=60", "--tile-edges=off", + f"--focus-region={fmt(view['focus'])}"] + stamped_run(f"l153-{view['name']}-render", render, + OUT / f"{view['name']}-render.log", env) + subprocess.run([*LIMIT, "timeout", "120s", "ffmpeg", "-nostdin", + "-hide_banner", "-loglevel", "error", "-y", "-i", png, + "-frames:v", "1", "-q:v", "4", jpg], cwd=ROOT, env=env, + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, timeout=125) + check = subprocess.run([*LIMIT, "timeout", "120s", sys.executable, + quality_tool, jpg], cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=125) + quality_ok = check.returncode == 0 and "status=PASS" in check.stdout + if not quality_ok: + failures.append(view["name"]) + quality_line = (check.stdout.strip().splitlines()[-1] + if check.stdout.strip() else f"quality exit={check.returncode}") + manifest.append({"version": "after-l153-candidate3", "view": view["name"], + "passage": view["passage"], "side": view["side"], + "file": str(jpg.relative_to(ROOT)), "camera_m": camera, + "target_m": target, "yaw_deg": yaw, "pitch_deg": pitch, + "source_stamp": STAMP, "pose_layout_identity": layout.get("stable_identity"), + "quality_status": "PASS" if quality_ok else "FAIL", "quality": quality_line}) + print(f"[L153Capture] view={view['name']} quality={'PASS' if quality_ok else 'FAIL'} " + f"file={jpg.relative_to(ROOT)}", flush=True) + + (ARTIFACTS / "manifest.json").write_text( + json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + with (ARTIFACTS / "INDEX.md").open("w", encoding="utf-8") as index: + index.write("# L153 owner landing chamber captures\n\n" + "Seed 0 eye-height poses resolved from the L153 candidate layout. The before " + "images use the L151 HEAD state; after images show L153 candidate 3, which " + "did not pass the seed 2 support gate.\n\n") + for view in chamber_views(layout): + image = f"after-l153-{view['name']}.jpg" + before = ROOT / "Standalone/Research/WorldAssemblerM3/Linux151/captures" \ + / f"after-l151-{view['name']}.jpg" + index.write(f"## {view['passage']} / {view['side']} landing\n\n" + f"- Before: [{before.name}](../../Linux151/captures/{before.name})\n" + f"- After: [image]({image})\n\n") + index.write(f"Capture quality failures: {len(failures)}/{len(manifest)}.\n") + print(f"[L153CaptureSummary] chamber_views={len(manifest)} " + f"quality_failures={len(failures)}", flush=True) + return 0 if not failures else 1 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L153CaptureFailure] {error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux153/captures/INDEX.md b/Standalone/Research/WorldAssemblerM3/Linux153/captures/INDEX.md new file mode 100644 index 0000000..06e64e4 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux153/captures/INDEX.md @@ -0,0 +1,35 @@ +# L153 owner landing chamber captures + +Seed 0 eye-height poses resolved from the L153 candidate layout. The before images use the L151 HEAD state; after images show L153 candidate 3, which did not pass the seed 2 support gate. + +## passage/0-1/0 / upper landing + +- Before: [after-l151-0-1-0-upper-landing.jpg](../../Linux151/captures/after-l151-0-1-0-upper-landing.jpg) +- After: [image](after-l153-0-1-0-upper-landing.jpg) + +## passage/0-1/0 / lower landing + +- Before: [after-l151-0-1-0-lower-landing.jpg](../../Linux151/captures/after-l151-0-1-0-lower-landing.jpg) +- After: [image](after-l153-0-1-0-lower-landing.jpg) + +## passage/1-2/0 / upper landing + +- Before: [after-l151-1-2-0-upper-landing.jpg](../../Linux151/captures/after-l151-1-2-0-upper-landing.jpg) +- After: [image](after-l153-1-2-0-upper-landing.jpg) + +## passage/1-2/0 / lower landing + +- Before: [after-l151-1-2-0-lower-landing.jpg](../../Linux151/captures/after-l151-1-2-0-lower-landing.jpg) +- After: [image](after-l153-1-2-0-lower-landing.jpg) + +## passage/2-3/0 / upper landing + +- Before: [after-l151-2-3-0-upper-landing.jpg](../../Linux151/captures/after-l151-2-3-0-upper-landing.jpg) +- After: [image](after-l153-2-3-0-upper-landing.jpg) + +## passage/2-3/0 / lower landing + +- Before: [after-l151-2-3-0-lower-landing.jpg](../../Linux151/captures/after-l151-2-3-0-lower-landing.jpg) +- After: [image](after-l153-2-3-0-lower-landing.jpg) + +Capture quality failures: 0/6. diff --git a/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-0-1-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-0-1-0-lower-landing.jpg new file mode 100644 index 0000000..7d6fe05 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-0-1-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-0-1-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-0-1-0-upper-landing.jpg new file mode 100644 index 0000000..c189afc Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-0-1-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-1-2-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-1-2-0-lower-landing.jpg new file mode 100644 index 0000000..3fb7848 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-1-2-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-1-2-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-1-2-0-upper-landing.jpg new file mode 100644 index 0000000..8e26ecf Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-1-2-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-2-3-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-2-3-0-lower-landing.jpg new file mode 100644 index 0000000..5673317 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-2-3-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-2-3-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-2-3-0-upper-landing.jpg new file mode 100644 index 0000000..530e487 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-2-3-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux153/captures/manifest.json b/Standalone/Research/WorldAssemblerM3/Linux153/captures/manifest.json new file mode 100644 index 0000000..efbdacd --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux153/captures/manifest.json @@ -0,0 +1,140 @@ +[ + { + "version": "after-l153-candidate3", + "view": "0-1-0-upper-landing", + "passage": "passage/0-1/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-0-1-0-upper-landing.jpg", + "camera_m": [ + 37.0, + -5.25, + -372.3810546875 + ], + "target_m": [ + 28.40474992442715, + -7.918646874047773, + -371.83751160914875 + ], + "yaw_deg": -107.24848481126237, + "pitch_deg": 3.4561048595792685, + "source_stamp": "4599aacdabb2940f223570a9a537723727989dd5236dfc084524e6dff55bc1dc", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-0-1-0-upper-landing.jpg pixels=921600 mean_luma=117.986 stddev_luma=14.908 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l153-candidate3", + "view": "0-1-0-lower-landing", + "passage": "passage/0-1/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-0-1-0-lower-landing.jpg", + "camera_m": [ + 25.8369140625, + 3.19921875, + -736.6798828125 + ], + "target_m": [ + 17.97380590951016, + -1.1793118453464206, + -734.970169262755 + ], + "yaw_deg": -119.11101808688004, + "pitch_deg": 10.756207017349817, + "source_stamp": "4599aacdabb2940f223570a9a537723727989dd5236dfc084524e6dff55bc1dc", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-0-1-0-lower-landing.jpg pixels=921600 mean_luma=111.545 stddev_luma=12.291 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l153-candidate3", + "view": "1-2-0-upper-landing", + "passage": "passage/1-2/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-1-2-0-upper-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -731.8810546875 + ], + "target_m": [ + 10.35825176774404, + -16.050410390488395, + -733.216132282697 + ], + "yaw_deg": -101.86342177874246, + "pitch_deg": -8.437834550031965, + "source_stamp": "4599aacdabb2940f223570a9a537723727989dd5236dfc084524e6dff55bc1dc", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-1-2-0-upper-landing.jpg pixels=921600 mean_luma=117.059 stddev_luma=15.604 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l153-candidate3", + "view": "1-2-0-lower-landing", + "passage": "passage/1-2/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-1-2-0-lower-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -753.05390625 + ], + "target_m": [ + 10.48848361510052, + -16.587754362485967, + -751.7193175939389 + ], + "yaw_deg": -105.38384894032598, + "pitch_deg": 8.434788863627224, + "source_stamp": "4599aacdabb2940f223570a9a537723727989dd5236dfc084524e6dff55bc1dc", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-1-2-0-lower-landing.jpg pixels=921600 mean_luma=119.900 stddev_luma=14.906 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l153-candidate3", + "view": "2-3-0-upper-landing", + "passage": "passage/2-3/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-2-3-0-upper-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -812.075390625 + ], + "target_m": [ + -13.604954164712868, + -11.337338700629157, + -813.6662645746064 + ], + "yaw_deg": 134.15112141809087, + "pitch_deg": -10.024269857937275, + "source_stamp": "4599aacdabb2940f223570a9a537723727989dd5236dfc084524e6dff55bc1dc", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-2-3-0-upper-landing.jpg pixels=921600 mean_luma=118.188 stddev_luma=15.874 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l153-candidate3", + "view": "2-3-0-lower-landing", + "passage": "passage/2-3/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-2-3-0-lower-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -900.0333984375 + ], + "target_m": [ + -14.865342978484083, + -12.416118141706198, + -898.4427583280197 + ], + "yaw_deg": 144.72777711381306, + "pitch_deg": 10.022826283920647, + "source_stamp": "4599aacdabb2940f223570a9a537723727989dd5236dfc084524e6dff55bc1dc", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux153/captures/after-l153-2-3-0-lower-landing.jpg pixels=921600 mean_luma=121.648 stddev_luma=16.295 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + } +] diff --git a/Standalone/Research/WorldAssemblerM3/Linux153/l153-chamber-support.patch b/Standalone/Research/WorldAssemblerM3/Linux153/l153-chamber-support.patch new file mode 100644 index 0000000..951ddbd --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux153/l153-chamber-support.patch @@ -0,0 +1,302 @@ +diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +index 4799b9e..e3ecf5e 100644 +--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h ++++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +@@ -512,7 +512,8 @@ namespace VoxelForgeCore + // boundary-blended host rock from widening the authored passage wall. + // v238 plans one resolved passage spline with explicit landing chambers. + // v239 plans their floor planes as the passage's continuous floor profile. +- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 239; ++ // v240 gives landing chambers and their route approaches tapered floor support. ++ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 240; + + enum class FieldChannel : std::uint32_t + { +diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h +index 6b1a6af..f0e0da4 100644 +--- a/Standalone/Core/src/ShapeWorldInternal.h ++++ b/Standalone/Core/src/ShapeWorldInternal.h +@@ -57,6 +57,7 @@ static constexpr double VF_PassageShellThicknessMetres=0.75; + static constexpr double VF_PassageWallReliefMetres=0.21; + static constexpr double VF_PassageCornerBlendMetres=0.50; + static constexpr double VF_PassageReachMarginMetres=0.01; ++static constexpr double VF_LandingSupportBoundsDepthMetres=2.20; + // Radial support in metres: shell + maximum relief + the smooth-max support + // (corner-blend width / 6) + a 1 cm guard for float and bound rounding. + static constexpr double VF_PassageInfluenceReachMetres= +@@ -1260,11 +1261,92 @@ public: + constexpr double WallReliefMetres=VF_PassageWallReliefMetres; + constexpr double OpenAirSideWallHeightMetres=1.0; + constexpr double MouthShellBlendMetres=5.0; ++ // Landing floors carry a tapered footing below their walk plane. Surface ++ // landings use local native ground within a 2 m budget; cave and shaft ++ // landings reuse the cave planner's 0.75 m floor-support depth. ++ constexpr double LandingSupportMaximumDepthMetres=2.0; ++ constexpr double LandingSupportFallbackDepthMetres=0.75; ++ constexpr double LandingSupportTaperMetres=1.0; ++ constexpr double LandingSupportGroundEmbedMetres=0.20; ++ constexpr double LandingSupportTopBiasSubvoxels=2.0; + const double qPerMetre=Q*1000000.0/ + std::max(1,ResolvedLayout->coreVoxelSizeMicrometres); + const double qx=static_cast(X)*Q,qy=static_cast(Y)*Q, + qz=static_cast(Z)*Q; + const double shellThickness=FloorShellThicknessMetres*qPerMetre; ++ const auto* queryStratum=FindBoundStratumForManagerZ(Z); ++ double landingSupportDistanceVoxels=std::numeric_limits::infinity(); ++ double landingSupportAirClipVoxels=-std::numeric_limits::infinity(); ++ double routeUnionDistance=std::numeric_limits::infinity(); ++ bool surfaceGroundQueried=false,surfaceGroundValid=false; ++ double surfaceGroundVoxels=0.0; ++ const bool surfaceLandingStratum=queryStratum&&queryStratum->Resolved ++ &&queryStratum->Resolved->generatorKind=="SurfaceWorld"; ++ const auto surfaceGroundAtQuery=[&]() ++ { ++ if(!surfaceGroundQueried) ++ { ++ surfaceGroundQueried=true; ++ const double layoutWidth=GetResolvedLayoutChunkWidthVoxels(); ++ if(layoutWidth>0.0) ++ { ++ const int32 chunkZ=FMath::FloorToInt( ++ static_cast(static_cast(Z)/layoutWidth)); ++ float ground=0.0f,ceiling=0.0f; ++ surfaceGroundValid=GetNativeSurfaceHeightsAt( ++ X,Y,chunkZ,ground,ceiling); ++ if(surfaceGroundValid)surfaceGroundVoxels=ground; ++ } ++ } ++ return surfaceGroundValid?surfaceGroundVoxels ++ :std::numeric_limits::quiet_NaN(); ++ }; ++ const auto includeLandingSupport=[&](double floorSubvoxels, ++ double centerDistanceSubvoxels,double footprintRadiusSubvoxels) ++ { ++ if(!(footprintRadiusSubvoxels>0.0))return; ++ const double floorVoxels=floorSubvoxels/Q; ++ const double centerDistanceVoxels=centerDistanceSubvoxels/Q; ++ const double radiusVoxels=footprintRadiusSubvoxels/Q; ++ const double supportDepthVoxels=(surfaceLandingStratum ++ ?LandingSupportMaximumDepthMetres:LandingSupportFallbackDepthMetres) ++ *qPerMetre/Q; ++ const double taperVoxels=LandingSupportTaperMetres*qPerMetre/Q; ++ const double embedVoxels=LandingSupportGroundEmbedMetres*qPerMetre/Q; ++ const double topBiasVoxels=LandingSupportTopBiasSubvoxels/Q; ++ if(centerDistanceVoxels>=radiusVoxels+taperVoxels ++ ||static_cast(Z)(Z)>floorVoxels+topBiasVoxels)return; ++ ++ double groundAnchorVoxels=floorVoxels-supportDepthVoxels; ++ if(surfaceLandingStratum) ++ { ++ const double ground=surfaceGroundAtQuery(); ++ if(std::isfinite(ground)) ++ groundAnchorVoxels=std::max(groundAnchorVoxels, ++ std::min(ground,floorVoxels)); ++ } ++ const double radialFade=std::clamp( ++ (radiusVoxels+taperVoxels-centerDistanceVoxels)/taperVoxels, ++ 0.0,1.0); ++ const double smoothFade=radialFade*radialFade*(3.0-2.0*radialFade); ++ const double supportTop=groundAnchorVoxels ++ +(floorVoxels+topBiasVoxels-groundAnchorVoxels)*smoothFade; ++ const double supportBottom=groundAnchorVoxels-embedVoxels*smoothFade; ++ const double supportDistance=std::max( ++ centerDistanceVoxels-(radiusVoxels+taperVoxels), ++ std::max(static_cast(Z)-supportTop, ++ supportBottom-static_cast(Z))); ++ landingSupportDistanceVoxels=std::min(landingSupportDistanceVoxels, ++ supportDistance); ++ }; ++ const auto applyLandingSupport=[&](float composed) ++ { ++ if(!std::isfinite(landingSupportDistanceVoxels))return composed; ++ const double clippedSupportDistance=std::max(landingSupportDistanceVoxels, ++ landingSupportAirClipVoxels); ++ return std::min(composed,static_cast(clippedSupportDistance)); ++ }; + struct FNearestTubeSample + { + double signedDistance=std::numeric_limits::infinity(); +@@ -1284,7 +1366,6 @@ public: + std::numeric_limits::infinity(); + double plannedTunnelPassageShellDistance=std::numeric_limits::infinity(); + const std::uint32_t reliefSeed=static_cast(Owner?Owner->Seed:0); +- const auto* queryStratum=FindBoundStratumForManagerZ(Z); + const bool plannedTunnelStratum=queryStratum&&queryStratum->Resolved + &&(queryStratum->Resolved->generatorKind=="TunnelNetwork" + ||queryStratum->Resolved->generatorKind=="VerticalShafts") +@@ -1346,6 +1427,9 @@ public: + const double radialDistance=DeterministicSqrt( + (qx-centerX)*(qx-centerX)+(qy-centerY)*(qy-centerY)) + -landingChamber->radiusSubvoxels; ++ includeLandingSupport(floorCenterZ, ++ radialDistance+landingChamber->radiusSubvoxels, ++ landingChamber->radiusSubvoxels); + const double floorDistance=floorCenterZ-qz; + const double ceilingDistance=qz-(floorCenterZ + +landingChamber->clearHeightSubvoxels); +@@ -1578,6 +1662,42 @@ public: + visitShellSegment); + } + if(!nearest.valid)continue; ++ if(queryStratum&&queryStratum->Resolved ++ &&(queryStratum->StrateIndex==passage.upperStrateIndex ++ ||queryStratum->StrateIndex==passage.lowerStrateIndex) ++ &&nearest.floorSegmentIndexStrateIndex ++ ==passage.upperStrateIndex ++ &&floorAlongMetres>=-LandingSupportTaperMetres ++ &&floorAlongMetres<=LandingApproachLengthMetres; ++ const bool lowerApproach=queryStratum->StrateIndex ++ ==passage.lowerStrateIndex ++ &&constants.totalPathLengthMetres-floorAlongMetres ++ <=LandingApproachLengthMetres ++ &&constants.totalPathLengthMetres-floorAlongMetres ++ >=-LandingSupportTaperMetres; ++ if(floorSegment.routeSegment&&(upperApproach||lowerApproach)) ++ { ++ const double routeX=floorSegment.a[0] ++ +nearest.floorT*floorSegment.delta[0]; ++ const double routeY=floorSegment.a[1] ++ +nearest.floorT*floorSegment.delta[1]; ++ const double routeDistance=DeterministicSqrt( ++ (qx-routeX)*(qx-routeX)+(qy-routeY)*(qy-routeY)); ++ const double floorRadius=floorSegment.radiusA ++ +std::clamp(nearest.floorT,0.0,1.0)*floorSegment.radiusDelta; ++ const double supportRadius=std::max(0.0, ++ floorRadius-0.23*qPerMetre); ++ includeLandingSupport(nearest.floorCenterZ-nearest.floorDepth, ++ routeDistance,supportRadius); ++ } ++ } + const double wallElevationMetres=(qz-(nearest.floorCenterZ + -nearest.floorDepth))/qPerMetre; + double distanceToCenter=std::max(1.0e-9, +@@ -1648,6 +1768,8 @@ public: + static_cast(shapedDistance)-floorShellThickness); + nearest.floorShellSignedDistance=std::min( + nearest.floorShellSignedDistance,floorShellDistance); ++ routeUnionDistance=std::min(routeUnionDistance, ++ static_cast(shapedDistance)); + unionDistance=std::min(unionDistance,static_cast(shapedDistance)); + unionFloorShellDistance=std::min(unionFloorShellDistance, + nearest.floorShellSignedDistance); +@@ -1657,13 +1779,18 @@ public: + std::min(localShellDistance,nearest.shellSignedDistance)); + } + +- if(!std::isfinite(unionDistance))return density; ++ if(!std::isfinite(unionDistance))return applyLandingSupport(density); + const float passageAirDistance=static_cast(-unionDistance/Q); ++ // Keep support out of the passage bore while allowing it under the ++ // chamber's own floor, where the chamber volume is naturally open. ++ if(std::isfinite(routeUnionDistance)) ++ landingSupportAirClipVoxels=-routeUnionDistance/Q; + // Keep the landing floor shell where it is exposed to the chamber. At + // the tunnel joint, the passage void must be able to open through that + // floor shell so its own continuous floor and shell define the throat. + if(unionLandingChamberFloorShellDistance<0.0&&unionDistance>0.0) +- return static_cast(unionLandingChamberFloorShellDistance/Q); ++ return applyLandingSupport( ++ static_cast(unionLandingChamberFloorShellDistance/Q)); + const auto plannedPortalWalkCorridorContains=[&]() + { + if(!queryStratum||!queryStratum->Resolved +@@ -1855,7 +1982,7 @@ public: + // otherwise the neighboring field can widen its wall by a whole + // shell thickness. Native graph air still wins above via the + // positive nativeTunnelFieldDensity path. +- return passageAirDistance; ++ return applyLandingSupport(passageAirDistance); + } + // Air is the union of the native graph and passage voids. Keeping the + // signed passage distance outside the void as well gives marching cubes +@@ -1865,9 +1992,10 @@ public: + &&plannedTunnelPassageShellDistance<0.0&&unionDistance>0.0) + { + if(nativeTunnelFieldDensity>=0.0f&&plannedPortalWalkCorridorContains()) +- return std::max(nativeTunnelFieldDensity,passageAirDistance); +- return std::min(density, +- static_cast(plannedTunnelPassageShellDistance/Q)); ++ return applyLandingSupport( ++ std::max(nativeTunnelFieldDensity,passageAirDistance)); ++ return applyLandingSupport(std::min(density, ++ static_cast(plannedTunnelPassageShellDistance/Q))); + } + if(nativeTunnelFieldDensity>=0.0f) + { +@@ -1875,29 +2003,30 @@ public: + // cave voids after the explicit mouth fade has ended. Keep this scoped + // to the planner-crossing passage shell so nearby portal joints stay open. + if(plannedTunnelPassageShellDistance<0.0&&unionDistance>0.0) +- return std::min(nativeTunnelFieldDensity, +- static_cast(plannedTunnelPassageShellDistance/Q)); ++ return applyLandingSupport(std::min(nativeTunnelFieldDensity, ++ static_cast(plannedTunnelPassageShellDistance/Q))); + constexpr float NativeCaveAirMarginVoxels=0.75f; + if(unionDistance>0.0 + &&nativeTunnelFieldDensity(unionFloorShellDistance/Q)); +- return std::max(nativeTunnelFieldDensity,passageAirDistance); ++ return applyLandingSupport(std::min(nativeTunnelFieldDensity, ++ static_cast(unionFloorShellDistance/Q))); ++ return applyLandingSupport( ++ std::max(nativeTunnelFieldDensity,passageAirDistance)); + } + if(nativeFloorSupport!=std::numeric_limits::lowest() + &&nativeFloorSupport<0.0f) +- return std::min(nativeFloorSupport,unionDistance<=0.0 +- ?passageAirDistance:density); ++ return applyLandingSupport(std::min(nativeFloorSupport,unionDistance<=0.0 ++ ?passageAirDistance:density)); + if(nativeTunnelFieldDensity!=std::numeric_limits::lowest()) +- return std::max(density,passageAirDistance); ++ return applyLandingSupport(std::max(density,passageAirDistance)); + if(unionDistance>0.0&&unionShellDistance<0.0&&density>=0.0f) + { + if(connectedSurfaceLandingShellContains()) +- return std::max(density,passageAirDistance); +- return static_cast(unionShellDistance/Q); ++ return applyLandingSupport(std::max(density,passageAirDistance)); ++ return applyLandingSupport(static_cast(unionShellDistance/Q)); + } +- return std::max(density,passageAirDistance); ++ return applyLandingSupport(std::max(density,passageAirDistance)); + } + + float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const +@@ -2044,8 +2173,11 @@ public: + static_cast(chamber.floorGradientQ16[0])/65536.0, + static_cast(chamber.floorGradientQ16[1])/65536.0); + const double extent=reachCoreVoxels+0.01; ++ const double supportDepth=(VF_LandingSupportBoundsDepthMetres*1000000.0/ ++ std::max(1, ++ ResolvedLayout->coreVoxelSizeMicrometres)); + const double low[3]={cx-radius-extent,cy-radius-extent, +- cz-floorExtent-extent}; ++ cz-floorExtent-supportDepth-extent}; + const double high[3]={cx+radius+extent,cy+radius+extent, + cz+floorExtent+static_cast( + chamber.clearHeightSubvoxels)/Q+extent}; +@@ -5484,8 +5616,12 @@ private: + const double height=static_cast( + chamber.clearHeightSubvoxels)/Q; + const double extent=requiredReachCoreVoxels+0.01; ++ const double supportDepth=(VF_LandingSupportBoundsDepthMetres*1000000.0/ ++ std::max(1, ++ ResolvedLayout->coreVoxelSizeMicrometres)); + const std::array minimum{ +- cx-radius-extent,cy-radius-extent,cz-floorExtent-extent}; ++ cx-radius-extent,cy-radius-extent, ++ cz-floorExtent-supportDepth-extent}; + const std::array maximum{ + cx+radius+extent,cy+radius+extent, + cz+floorExtent+height+extent}; diff --git a/Standalone/Research/WorldAssemblerM3/Linux154/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux154/REPORT.md new file mode 100644 index 0000000..1314623 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux154/REPORT.md @@ -0,0 +1,72 @@ +# Linux154 — unsupported-probe diagnosis + +**Result:** L152 introduced the seven failures by rotating the upper chamber floor plane and extending its carve over the Surface World route’s support samples. Five samples end 4–14 mm above that floor at the route audit’s 0.25 m limit. The other two belong to the upper chamber audit but sit inside the open passage bore. The Surface World endpoint and chamber center did not move. The Floating Islands content-band rejection also reproduces at HEAD. No production source was changed. + +## Seed 2: the seven probes + +The positions below are the failed support samples reported at the audit’s deepest sample. Distances to the chamber floor center and resolved passage spline are calculated from the L152 layout and spline. The sample category is measured from the audit that emitted it. Coordinates and densities are in metres and Core density units; SDF is in metres. Negative native density is solid, negative passage/chamber SDF is inside the carve, and positive composed density is air. + +| # | Audit path | Position (m) | To chamber center (m) | To spline (m) | +|---|---|---|---:|---:| +| 1 | Surface World layer route | `(30.000, -8.170, -374.928)` | 6.503 | 6.691 | +| 2 | Surface World layer route | `(29.750, -8.170, -374.920)` | 6.253 | 6.446 | +| 3 | Surface World layer route | `(29.500, -8.170, -374.911)` | 6.003 | 6.201 | +| 4 | Surface World layer route | `(27.250, -8.170, -374.875)` | 3.754 | 4.046 | +| 5 | Surface World layer route | `(27.000, -8.170, -374.880)` | 3.504 | 3.817 | +| 6 | `passage/0-1/0` upper chamber, west spoke | `(18.229, -8.450, -375.624)` | 5.349 | 1.122 | +| 7 | `passage/0-1/0` upper chamber, west spoke | `(17.750, -8.450, -375.617)` | 5.821 | 1.087 | + +At each deepest sample, the fields were: + +| # | Max search depth | Native density | Passage-union SDF | Upper chamber SDF | Composed density | +|---|---:|---:|---:|---:|---:| +| 1 | 0.250 m | -0.371094 | -0.003868 | -0.003868 | +0.015472 | +| 2 | 0.250 m | -0.550781 | -0.008297 | -0.008297 | +0.033188 | +| 3 | 0.250 m | -0.527344 | -0.012726 | -0.012726 | +0.050903 | +| 4 | 0.250 m | -0.386719 | -0.014011 | -0.014011 | +0.056046 | +| 5 | 0.250 m | -0.535156 | -0.004768 | -0.004768 | +0.019073 | +| 6 | 1.125 m | -11.773438 | -0.032436 | +0.874998 | +0.129742 | +| 7 | 1.125 m | -9.085938 | -0.110422 | +0.875004 | +0.441689 | + +The Surface World search samples at 0.0625, 0.125, 0.1875, and 0.25 m. At all five locations the chamber SDF stays negative through that search; at its limit it is only 4–14 mm inside the floor plane. Native density there is already solid, but the upper chamber carve keeps the composed field air. **The five route failures are caused by the chamber floor lying just beyond the route audit’s support depth.** At 2 m below the route feet, native density is -7.371 to -7.551 voxels and both passage-union and chamber SDFs are +1.736 to +1.746 m, so the carve has ended and solid support is present below the audited range. + +The chamber audit samples from 0.125 through 1.125 m. The native field is solid throughout, but the passage-union SDF stays negative through 1.125 m while chamber SDF is +0.875 m there. At the next 0.125 m step the passage SDF is positive. At 2 m below the chamber feet, passage SDFs are +0.817 and +0.732 m, chamber SDFs are +1.750 m, and native density is -15.273 and -12.586 voxels. **The two chamber failures are inside the passage bore; the passage carve leaves air there.** They are about 1.1 m from the spline, unlike the five Surface World samples, which are 3.8–6.7 m away. + +No failed sample came from the passage-approach audit; `passage/0-1/0` reported zero unsupported samples. All seven points are within the upper chamber’s 7.5 m radius. Thus the five Surface World samples are under the chamber footprint, and the two chamber-audit samples are physically at its west passage opening. + +Raw profiles and audit rows: [extended seed-2 trace](../../../../Saved/Linux/l154/l154-seed2-extended.log), [L152 seed-2 audit](../../../../Saved/Linux/l152/seed2-plane-fit.log). + +## Bisect: L150 → L152 + +Same owner world, seed 2, W8. L150 and L151 binaries were replayed for this audit; L152 is the saved focused run. + +| Revision | Surface World route unsupported | Upper `passage/0-1/0` chamber unsupported | Result | +|---|---:|---:|---| +| L150 `9ac60d5` | 0 | no landing-chamber audit | route passes | +| L151 `5bd158d` | 0 | 0 | route and chamber pass support | +| L152 `11b59a9` | 5 | 2 | first appearance of these seven | + +The Surface World route endpoint is `(23.500, -8.000, -374.831)` in all three layouts. L151 and L152 also keep the upper chamber floor center at that same position. The full `passage/0-1/0` control-point positions are identical between L151 and L152. + +**Measured change:** L151’s upper floor gradient was `(94, 0)` Q16 (`+0.001434` in X); L152’s was `(-1015, 0)` Q16 (`-0.015488` in X). L152 changed the chamber fit to the farthest receiving-route point in its radius and made the chamber floor profile the passage field’s floor profile. Across the failed route samples, the plane therefore moved down by 0.059–0.110 m. That moves the chamber carve across the deepest 0.25 m support probes without moving the route endpoint, chamber center, or passage spline control points. L152 is the introducing change; adding the landing chamber in L151 did not produce these support failures. + +Evidence: [L150 replay](../../../../Saved/Linux/l154/l150-seed2-audit.log), [L151 replay](../../../../Saved/Linux/l154/l151-seed2-audit.log), [L152 report](../Linux152/REPORT.md). + +## Probe-placement fixture + +**Measured:** the same placement logic was run against a hand-built flat floor and a known open mouth. It returned `flat_floor=PASS`, `flat_unsupported=0`, `open_mouth=EXPECTED_FAIL`, `mouth_unsupported=3`, `status=PASS` ([fixture output](../../../../Saved/Linux/l154/l154-seed2-extended.log)). The helper places support samples correctly on the flat control and detects columns under an intentionally open mouth. + +**Inferred:** probes 6–7 are correctly sampling the open bore, not a misplaced point. The chamber audit applies its support test there without the passage-interior support exemption used by the passage-route audit. These two should be interpreted as open-mouth samples. The fixture does not show a placement error in the five Surface World route probes: their SDF profile independently places them inside the chamber floor carve until just beyond the route audit’s 0.25 m search limit. + +## Floating Islands content-band check + +**Measured at HEAD:** the fast tier still fails its first case, `owner-floating-islands-style2-clang`, in 0.5 s. It stops with `passage/0-1/0 lower landing chamber does not fit inside its host content band`; later fast checks do not run. The restored-source run uses stamp `b299076d…` ([fast summary](../../../../Saved/Linux/l154/fast-summary.json), [log](../../../../Saved/Linux/l154/owner-floating-islands-style2-clang.log)). This is the same failure L153 recorded. + +A diagnostic print at the validation site measured the lower chamber’s required Z extent as `[-130.202148, -122.557617] m`. Its Floating Islands host content band is `[-625.750000, -126.250000] m`. The chamber’s upper extent crosses the host top by **3.692383 m**. Its floor center is `-128.629882 m`, only 2.380 m below that boundary; the full 4.5 m clearance, floor-plane reach, and 0.75 m shell extend above it. The upper chamber fits its host band. The planner rejects the lower chamber before writing a layout snapshot. The temporary diagnostic print was removed after this measurement; its output is saved in [the content-band diagnostic log](../../../../Saved/Linux/l154/floating-islands-contentband-diagnostic.log). + +## Checks and limits + +- Passed: temporary Clang diagnostic build; known-answer placement fixture; source restoration (`git diff` is empty). +- Ran: seed-2 audits for L150, L151, and L152; restored-source HEAD fast tier. +- Failed: HEAD fast tier at the Floating Islands content-band validation, before downstream checks. This remains a red required gate. +- Not run: timing, other seeds, or fixes. No Unreal `Source/` files changed. diff --git a/Standalone/Research/WorldAssemblerM3/Linux155/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux155/REPORT.md new file mode 100644 index 0000000..e9e2d2d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux155/REPORT.md @@ -0,0 +1,40 @@ +# Linux155 — chamber floor plane reached the stop rule + +| Landing chamber | Before (L151) | After (L155) | +|---|---|---| +| passage/0-1/0 · upper | ![L151 upper landing](../Linux151/captures/after-l151-0-1-0-upper-landing.jpg) | Not captured: the planner rejected the candidate before a resolved layout was written. | +| passage/0-1/0 · lower | ![L151 lower landing](../Linux151/captures/after-l151-0-1-0-lower-landing.jpg) | Not captured: the planner rejected the candidate before a resolved layout was written. | +| passage/1-2/0 · upper | ![L151 upper landing](../Linux151/captures/after-l151-1-2-0-upper-landing.jpg) | Not captured: the planner rejected the candidate before a resolved layout was written. | +| passage/1-2/0 · lower | ![L151 lower landing](../Linux151/captures/after-l151-1-2-0-lower-landing.jpg) | Not captured: the planner rejected the candidate before a resolved layout was written. | +| passage/2-3/0 · upper | ![L151 upper landing](../Linux151/captures/after-l151-2-3-0-upper-landing.jpg) | Not captured: the planner rejected the candidate before a resolved layout was written. | +| passage/2-3/0 · lower | ![L151 lower landing](../Linux151/captures/after-l151-2-3-0-lower-landing.jpg) | Not captured: the planner rejected the candidate before a resolved layout was written. | + +## Result + +Stopped after three failed fits for cause 1, as instructed. A single planar chamber floor cannot satisfy the seed-2 Surface World route's ground and capsule-foot bounds while joining the current passage floor at the bore. The rejected candidate was removed from production source. The branch remains at HEAD, evaluator v239. Causes 2 and 3 were not attempted; no baseline or version change was made. + +## Measured plane conflict + +The receiving route runs along +X through the upper chamber. Its route-point Z is the player-center height; the ground bound is route Z − 0.98 m, and the capsule-foot bound is route Z − 0.88 m. The chamber center floor at the bore is `-374.831 m`. With the endpoint fixed to the planned `h(s)` value: + +- At route point `x=24.25 m`, the capsule-foot bound requires `gradient_x ≤ -1,024 Q16` (−1.56%). +- At route point `x=26.75 m`, the ground bound requires `gradient_x ≥ 1,989 Q16` (+3.03%). + +The constraints do not overlap. The third fit allowed the bore endpoint and first 10 m of `h(s)` to rise together by up to 99 subvoxels (`0.0967 m`, the remaining capsule-foot clearance). At that limit the same points require `gradient_x ≤ -9,472 Q16` and `gradient_x ≥ 39 Q16`; they still do not overlap. + +## Attempts + +1. Fit the floor slope above route ground and preserve the bore-center height. The route support audit fell from 5 unsupported samples to 0, but the passage then blocked 481 capsule samples. Plan grade stayed under 15°. Log: [`cause1-seed2.log`](../../../../Saved/Linux/l155/cause1-seed2.log). +2. Add the capsule-foot upper bound. Strict planning rejected the layout because no gradient fit both bounds with the bore floor fixed. Log: [`cause1-bounded-seed2.log`](../../../../Saved/Linux/l155/cause1-bounded-seed2.log). +3. Allow a bounded center raise and taper it through the first 10 m of `h(s)`. The route and capsule constraints remained infeasible; planning rejected before producing a layout. Log: [`cause1-between-seed2.log`](../../../../Saved/Linux/l155/cause1-between-seed2.log). The final rejected fit is archived in [`failed-plane-fit.patch`](failed-plane-fit.patch). + +## Checks and limits + +- Clang 19 candidate builds passed for each compiled planner variant. The first candidate's focused seed-2 audit completed and failed on 481 blocked capsule samples; the second and third failed strict layout planning. +- `git diff --check` passed after restoring the production source to HEAD. No production or Unreal `Source/` files remain changed. +- L154 had already measured the HEAD fast-tier failure on the Floating Islands chamber content band. Fast tier was not rerun after the three-fix stop. +- Not run: causes 2 and 3, full fast tier, either 16-seed sweep, determinism, owner baselines, L80 timing, Linux UE build/start/movement, or after captures. + +## Remaining risk + +The measured conflict is between a single plane and the receiving route's local height variation across the 7.5 m chamber footprint. Resolving it needs a different floor representation or a change to the receiver route/bore join. The chamber support-audit exemption and Floating Islands content-band failure remain open. diff --git a/Standalone/Research/WorldAssemblerM3/Linux155/failed-plane-fit.patch b/Standalone/Research/WorldAssemblerM3/Linux155/failed-plane-fit.patch new file mode 100644 index 0000000..c0419bf --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux155/failed-plane-fit.patch @@ -0,0 +1,321 @@ +diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h +index 7ea6c8c..b653d01 100644 +--- a/Standalone/Core/src/ResolvedLayoutInternal.h ++++ b/Standalone/Core/src/ResolvedLayoutInternal.h +@@ -10,6 +10,7 @@ + #include + #include + #include ++#include + + namespace VoxelForgeCore + { +@@ -776,6 +777,119 @@ namespace + /static_cast(denominator); + return negative?-static_cast(rounded):static_cast(rounded); + } ++ struct LandingFloorGradientConstraintV1 ++ { ++ std::int64_t dxSubvoxels=0,dySubvoxels=0,minimumRiseSubvoxels=0; ++ std::int64_t centerOffsetSign=0; ++ }; ++ bool FitLandingFloorGradientAboveRouteV1( ++ const std::array& preferred, ++ const std::vector& constraints, ++ std::int64_t maximumMagnitude,std::int64_t centerOffsetSubvoxels, ++ std::array& result) ++ { ++ struct FConstraint ++ { ++ std::int64_t x=0,y=0,rhs=0; ++ }; ++ std::vector q16Constraints; ++ q16Constraints.reserve(constraints.size()); ++ for(const auto& constraint:constraints) ++ { ++ const std::int64_t rise=constraint.minimumRiseSubvoxels ++ +constraint.centerOffsetSign*centerOffsetSubvoxels; ++ const std::int64_t rhs=rise*65536; ++ if(constraint.dxSubvoxels==0&&constraint.dySubvoxels==0) ++ { ++ if(rhs>0)return false; ++ continue; ++ } ++ q16Constraints.push_back({constraint.dxSubvoxels, ++ constraint.dySubvoxels,rhs}); ++ } ++ if(q16Constraints.empty()) ++ { ++ const std::int64_t x=preferred[0],y=preferred[1]; ++ if(x*x+y*y>maximumMagnitude*maximumMagnitude)return false; ++ result=preferred;return true; ++ } ++ ++ bool found=false; ++ std::int64_t bestX=0,bestY=0,bestDistance=0; ++ const auto consider=[&](double x,double y) ++ { ++ if(!std::isfinite(x)||!std::isfinite(y))return; ++ const std::int64_t roundedX=static_cast(std::llround(x)); ++ const std::int64_t roundedY=static_cast(std::llround(y)); ++ for(std::int64_t ox=-2;ox<=2;++ox) ++ for(std::int64_t oy=-2;oy<=2;++oy) ++ { ++ const std::int64_t candidateX=roundedX+ox; ++ const std::int64_t candidateY=roundedY+oy; ++ if(candidateX*candidateX+candidateY*candidateY ++ >maximumMagnitude*maximumMagnitude)continue; ++ bool feasible=true; ++ for(const auto& constraint:q16Constraints) ++ if(constraint.x*candidateX+constraint.y*candidateY(maximumMagnitude); ++ const double preferredLength=std::hypot(preferredX,preferredY); ++ if(preferredLength>gradeRadius&&preferredLength>0.0) ++ consider(preferredX*gradeRadius/preferredLength, ++ preferredY*gradeRadius/preferredLength); ++ for(const auto& constraint:q16Constraints) ++ { ++ const double x=static_cast(constraint.x); ++ const double y=static_cast(constraint.y); ++ const double normSquared=x*x+y*y; ++ const double targetDot=x*preferredX+y*preferredY; ++ const double projection=(static_cast(constraint.rhs)-targetDot) ++ /normSquared; ++ consider(preferredX+projection*x,preferredY+projection*y); ++ ++ const double minimumNormSquared=static_cast(constraint.rhs) ++ *constraint.rhs/normSquared; ++ const double remaining=gradeRadius*gradeRadius-minimumNormSquared; ++ if(remaining>=0.0) ++ { ++ const double baseX=static_cast(constraint.rhs)*x/normSquared; ++ const double baseY=static_cast(constraint.rhs)*y/normSquared; ++ const double offset=std::sqrt(remaining/normSquared); ++ consider(baseX-offset*y,baseY+offset*x); ++ consider(baseX+offset*y,baseY-offset*x); ++ } ++ } ++ for(std::size_t i=0;i(a.rhs)*b.y ++ -static_cast(a.y)*b.rhs)/determinant; ++ const double y=(static_cast(a.x)*b.rhs ++ -static_cast(a.rhs)*b.x)/determinant; ++ consider(x,y); ++ } ++ if(!found)return false; ++ result={bestX,bestY}; ++ return true; ++ } + std::array PassageRandomXY(std::uint64_t seed, + std::int64_t inner,std::int64_t outer) + { +@@ -4983,9 +5097,9 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t + std::llround(4.5*qPerMetre)); + const auto& endpoint=passage.controlPoints[endpointIndex]; + const auto& neighbor=passage.controlPoints[neighborIndex]; +- const std::int64_t endpointFloor=static_cast( ++ std::int64_t endpointFloor=static_cast( + std::llround(chamberFloorProfile[endpointIndex])); +- const std::int64_t neighborFloor=static_cast( ++ std::int64_t neighborFloor=static_cast( + std::llround(chamberFloorProfile[neighborIndex])); + chamber.floorCenterSubvoxels[2]=endpointFloor; + const std::int64_t dx=endpoint.positionSubvoxels[0] +@@ -5003,50 +5117,139 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t + horizontalSquared)}; + if(host.generatorKind=="SurfaceWorld"&&side[0]=='u') + { +- // The surface route enters the outer half of this chamber. Fit +- // its landing plane to that receiving route, while keeping its +- // center height equal to the passage's planned h(s) endpoint. ++ // Keep the chamber plane at h(s) on the bore center, then fit ++ // its slope above every segment of the receiving walk surface ++ // that intersects the chamber footprint. + const auto surfaceRoute=routePoints(host); +- const double radiusSquared=static_cast( +- chamber.radiusSubvoxels)*chamber.radiusSubvoxels; +- double farthestDistanceSquared=0.0; +- std::array farthestPoint{}; +- for(const auto& routePoint:surfaceRoute) ++ std::vector floorConstraints; ++ const std::int64_t centerX=chamber.floorCenterSubvoxels[0]; ++ const std::int64_t centerY=chamber.floorCenterSubvoxels[1]; ++ const std::int64_t centerFloor=chamber.floorCenterSubvoxels[2]; ++ const double radius=static_cast(chamber.radiusSubvoxels); ++ const double radiusSquared=radius*radius; ++ const auto addRouteFloorConstraint=[&](double t, ++ const std::array& a,const std::array& b) ++ { ++ const double px=std::lerp(a[0],b[0],t); ++ const double py=std::lerp(a[1],b[1],t); ++ const double pz=std::lerp(a[2],b[2],t); ++ const std::int64_t x=static_cast(std::llround(px)); ++ const std::int64_t y=static_cast(std::llround(py)); ++ const std::int64_t floor=static_cast(std::llround(pz)) ++ -surfacePlayerCenterAboveFloorQ; ++ const std::int64_t dxQ=x-centerX,dyQ=y-centerY; ++ const double distanceSquared=static_cast(dxQ)*dxQ ++ +static_cast(dyQ)*dyQ; ++ if(distanceSquared>radiusSquared+4.0*radius+4.0)return; ++ // Four subvoxels cover the rounded XY/Z projection while ++ // leaving a deterministic clearance over ground and below ++ // the route capsule's foot envelope. ++ const std::int64_t marginQ=(dxQ==0&&dyQ==0)?0:4; ++ floorConstraints.push_back({dxQ,dyQ, ++ floor-centerFloor+marginQ,-1}); ++ const std::int64_t maximumFloor=static_cast( ++ std::llround(pz))-static_cast( ++ std::llround(0.88*qPerMetre))-marginQ; ++ floorConstraints.push_back({-dxQ,-dyQ, ++ centerFloor-maximumFloor,1}); ++ }; ++ for(const auto& point:surfaceRoute) ++ addRouteFloorConstraint(0.0,point,point); ++ for(std::size_t i=1;ifarthestDistanceSquared) ++ const auto& a=surfaceRoute[i-1];const auto& b=surfaceRoute[i]; ++ const double dx=b[0]-a[0],dy=b[1]-a[1]; ++ const double ox=a[0]-centerX,oy=a[1]-centerY; ++ const double aa=dx*dx+dy*dy; ++ if(aa<=0.0)continue; ++ const double bb=2.0*(ox*dx+oy*dy); ++ const double cc=ox*ox+oy*oy-radiusSquared; ++ const double discriminant=bb*bb-4.0*aa*cc; ++ if(discriminant<0.0)continue; ++ const double root=std::sqrt(discriminant); ++ const double first=(-bb-root)/(2.0*aa); ++ const double second=(-bb+root)/(2.0*aa); ++ if(first>=0.0&&first<=1.0) ++ addRouteFloorConstraint(first,a,b); ++ if(second>=0.0&&second<=1.0) ++ addRouteFloorConstraint(second,a,b); ++ } ++ std::sort(floorConstraints.begin(),floorConstraints.end(), ++ [](const auto& a,const auto& b) ++ {return std::tie(a.dxSubvoxels,a.dySubvoxels, ++ a.minimumRiseSubvoxels,a.centerOffsetSign) ++ (0, ++ surfacePlayerCenterAboveFloorQ ++ -static_cast(std::llround(0.88*qPerMetre))-4); ++ const auto profileOffsetAt=[&](std::size_t index, ++ std::int64_t centerRaiseQ) ++ { ++ const std::int64_t distanceQ=endpointIndex==0 ++ ?cumulative[index]:mouthHorizontal-cumulative[index]; ++ const double t=std::clamp(static_cast(distanceQ) ++ /(10.0*qPerMetre),0.0,1.0); ++ const double smooth=t*t*(3.0-2.0*t); ++ return static_cast(std::llround( ++ centerRaiseQ*(1.0-smooth))); ++ }; ++ bool fitted=false; ++ std::int64_t centerRaiseQ=0; ++ std::array fittedGradient{}; ++ for(std::int64_t candidateRaiseQ=0; ++ candidateRaiseQ<=maximumCenterRaiseQ;++candidateRaiseQ) ++ { ++ const std::int64_t candidateEndpointFloor=endpointFloor ++ +profileOffsetAt(endpointIndex,candidateRaiseQ); ++ const std::int64_t candidateNeighborFloor=neighborFloor ++ +profileOffsetAt(neighborIndex,candidateRaiseQ); ++ const std::int64_t candidateDz=candidateEndpointFloor ++ -candidateNeighborFloor; ++ const std::array preferredGradient={ ++ PassageRoundRatio(dx<0?-candidateDz:candidateDz, ++ std::abs(dx)*65536,horizontalSquared), ++ PassageRoundRatio(dy<0?-candidateDz:candidateDz, ++ std::abs(dy)*65536,horizontalSquared)}; ++ if(FitLandingFloorGradientAboveRouteV1(preferredGradient, ++ floorConstraints,17560,candidateRaiseQ, ++ fittedGradient)) + { +- farthestDistanceSquared=distanceSquared; +- farthestPoint=routePoint; ++ fitted=true;centerRaiseQ=candidateRaiseQ;break; + } + } +- double mouthRouteHeight=0.0,mouthRouteDistanceSquared=0.0; +- if(farthestDistanceSquared>0.0 +- &&VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY( +- surfaceRoute,static_cast(mouth[0]), +- static_cast(mouth[1]),mouthRouteHeight, +- mouthRouteDistanceSquared)) ++ if(floorConstraints.empty()||!fitted) + { +- const std::int64_t surfaceDx=static_cast( +- std::llround(farthestPoint[0]-mouth[0])); +- const std::int64_t surfaceDy=static_cast( +- std::llround(farthestPoint[1]-mouth[1])); +- const std::int64_t surfaceDz=static_cast( +- std::llround(farthestPoint[2]-mouthRouteHeight)); +- const std::int64_t surfaceHorizontalSquared=surfaceDx*surfaceDx +- +surfaceDy*surfaceDy; +- if(surfaceHorizontalSquared>0) +- chamber.floorGradientQ16={ +- PassageRoundRatio(surfaceDx<0?-surfaceDz:surfaceDz, +- std::abs(surfaceDx)*65536, +- surfaceHorizontalSquared), +- PassageRoundRatio(surfaceDy<0?-surfaceDz:surfaceDz, +- std::abs(surfaceDy)*65536, +- surfaceHorizontalSquared)}; ++ error=passage.stableId+" upper landing floor cannot stay between its receiving route and capsule foot within the 15-degree grade"; ++ return false; ++ } ++ if(centerRaiseQ>0) ++ { ++ for(std::size_t i=0;i( ++ static_cast(std::llround( ++ chamberFloorProfile[i])) ++ +profileOffsetAt(i,centerRaiseQ)); ++ passage.controlPoints[i].positionSubvoxels[2]= ++ static_cast(std::llround( ++ chamberFloorProfile[i])) ++ +passage.controlPoints[i].radiusSubvoxels; ++ } ++ endpointFloor=static_cast(std::llround( ++ chamberFloorProfile[endpointIndex])); ++ neighborFloor=static_cast(std::llround( ++ chamberFloorProfile[neighborIndex])); ++ chamber.floorCenterSubvoxels[2]=endpointFloor; + } ++ chamber.floorGradientQ16=fittedGradient; + } + const std::uint64_t gradientSquared=static_cast( + chamber.floorGradientQ16[0]*chamber.floorGradientQ16[0] diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/FinalizeL156SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux156/FinalizeL156SeedSweep.py new file mode 100644 index 0000000..a069b7f --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux156/FinalizeL156SeedSweep.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Recompute the full 16-seed sweep summaries and landing band margins.""" +from __future__ import annotations + +import json +from pathlib import Path + +from PassageEnvelopeAudit import _landing_content_band_audit + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l156/seeds" + + +def finalize(kind: str) -> dict: + directory = OUT / kind + partial = directory / "runs-partial.json" + results = json.loads(partial.read_text(encoding="utf-8")) if partial.is_file() else [] + for row in results: + layout_path = ROOT / row["layout"] + bands = _landing_content_band_audit( + json.loads(layout_path.read_text(encoding="utf-8"))) if layout_path.is_file() else [] + band_pass = len(bands) == 6 and all(entry["passed"] for entry in bands) + envelope = row.get("envelope_audit") or {} + envelope["landing_content_band"] = bands + envelope["landing_content_band_passed"] = band_pass + envelope["passed"] = bool(envelope.get("passed") and band_pass) + row["envelope_audit"] = envelope + checks = row.setdefault("checks", {}) + checks["envelope"] = bool(envelope["passed"] + and envelope.get("outside_chamber_conflicts") == 0) + checks["envelope_negative_control"] = bool( + envelope.get("negative_controls") == envelope.get("negative_controls_expected") == 1) + checks["landing_content_band"] = band_pass + failed = [name for name, passed in checks.items() if not passed] + row["verdict"] = "PASS" if not failed else "FAIL" + row["reason"] = ", ".join(failed) if failed else "all requested gates pass" + results.sort(key=lambda row: row["seed"]) + completed = len(results) + passed = sum(row["verdict"] == "PASS" for row in results) + missing = sorted(set(range(16)) - {row["seed"] for row in results}) + summary = {"kind": kind, "source_stamp": ( + "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0"), + "completed": completed, "passed": passed, "failed": completed - passed, + "missing_seeds": missing, "status": "PASS" if completed == 16 and passed == 16 else "FAIL", + "results": results} + partial.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + (directory / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + print(f"[L156SeedSweepFinal] kind={kind} passed={passed}/{completed} " + f"missing={len(missing)} status={summary['status']}") + return summary + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kind", choices=("tunnel-network", "vertical-shafts"), required=True) + args = parser.parse_args() + raise SystemExit(0 if finalize(args.kind)["status"] == "PASS" else 1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/PassageEnvelopeAudit.py b/Standalone/Research/WorldAssemblerM3/Linux156/PassageEnvelopeAudit.py new file mode 100644 index 0000000..90712da --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux156/PassageEnvelopeAudit.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +"""Audit cave primitive bounds against passage envelopes, allowing chamber joints only.""" +from __future__ import annotations + +import math +from typing import Any + + +INFLUENCE_REACH_M = 1.053334 +SHELL_THICKNESS_M = 0.75 + + +def _point(control: dict[str, Any], scale: float) -> tuple[float, float, float, float]: + x, y, z = control["position_q"] + return (x * scale, y * scale, z * scale, control["radius_q"] * scale) + + +def _knot_span(a, b) -> float: + return max(1.0e-6, math.sqrt(math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2))) + + +def _evaluate(controls, span: int, t: float): + p1, p2 = controls[span], controls[span + 1] + p0 = controls[span - 1] if span else controls[0] + p3 = controls[min(span + 2, len(controls) - 1)] + dt0, dt1, dt2 = _knot_span(p0, p1), _knot_span(p1, p2), _knot_span(p2, p3) + t2, t3 = t * t, t * t * t + result = [] + for axis in (0, 1, 3): + m1 = dt1 * ((p1[axis] - p0[axis]) / dt0 + - (p2[axis] - p0[axis]) / (dt0 + dt1) + + (p2[axis] - p1[axis]) / dt1) + m2 = dt1 * ((p2[axis] - p1[axis]) / dt1 + - (p3[axis] - p1[axis]) / (dt1 + dt2) + + (p3[axis] - p2[axis]) / dt2) + value = ((2 * t3 - 3 * t2 + 1) * p1[axis] + + (-2 * t3 + 3 * t2) * p2[axis] + + (t3 - 2 * t2 + t) * m1 + (t3 - t2) * m2) + result.append(value) + radius = min(max(result[2], min(p1[3], p2[3])), max(p1[3], p2[3])) + return result[0], result[1], p1[2] + (p2[2] - p1[2]) * t, radius + + +def _segments(passage: dict[str, Any], scale: float): + controls = [_point(row, scale) for row in passage["control_points"]] + route_end = min(max(1, int(passage.get("lower_mouth_control_index", len(controls) - 1))), + len(controls) - 1) + along = 0.0 + for span in range(route_end): + a, b = controls[span], controls[span + 1] + chord = math.hypot(b[0] - a[0], b[1] - a[1]) + steps = max(1, math.ceil(chord / 2.0)) + prior = _evaluate(controls, span, 0.0) + for step in range(1, steps + 1): + current = _evaluate(controls, span, step / steps) + length = math.dist(prior[:3], current[:3]) + yield prior, current, along, max(prior[3], current[3]) + INFLUENCE_REACH_M + along += length + prior = current + + +def _distance_squared(a, b, bounds, t: float) -> float: + total = 0.0 + for axis in range(3): + pos = a[axis] + (b[axis] - a[axis]) * t + lo, hi = bounds[axis] + gap = lo - pos if pos < lo else pos - hi if pos > hi else 0.0 + total += gap * gap + return total + + +def _overlap_interval(a, b, bounds, radius: float): + cuts = {0.0, 1.0} + for axis in range(3): + delta = b[axis] - a[axis] + if abs(delta) < 1.0e-15: + continue + for edge in bounds[axis]: + t = (edge - a[axis]) / delta + if 0.0 < t < 1.0: + cuts.add(t) + cuts = sorted(cuts) + best_t, best_d2 = 0.0, math.inf + for lo, hi in zip(cuts, cuts[1:]): + mid = (lo + hi) * 0.5 + quadratic = linear = 0.0 + for axis in range(3): + delta = b[axis] - a[axis] + pos = a[axis] + delta * mid + low, high = bounds[axis] + edge = low if pos < low else high if pos > high else None + if edge is not None: + quadratic += delta * delta + linear += delta * (a[axis] - edge) + candidate = min(hi, max(lo, -linear / quadratic)) if quadratic else lo + d2 = _distance_squared(a, b, bounds, candidate) + if d2 < best_d2: + best_t, best_d2 = candidate, d2 + for t in (0.0, 1.0): + d2 = _distance_squared(a, b, bounds, t) + if d2 < best_d2: + best_t, best_d2 = t, d2 + if best_d2 > radius * radius: + return None + + radius2 = radius * radius + if _distance_squared(a, b, bounds, 0.0) <= radius2: + left = 0.0 + else: + low, high = 0.0, best_t + for _ in range(42): + mid = (low + high) * 0.5 + if _distance_squared(a, b, bounds, mid) <= radius2: + high = mid + else: + low = mid + left = high + if _distance_squared(a, b, bounds, 1.0) <= radius2: + right = 1.0 + else: + low, high = best_t, 1.0 + for _ in range(42): + mid = (low + high) * 0.5 + if _distance_squared(a, b, bounds, mid) <= radius2: + low = mid + else: + high = mid + right = low + return left, right, best_t + + +def _inside_chamber(point, chamber: dict[str, Any], scale: float) -> bool: + cx, cy, cz = (value * scale for value in chamber["floor_center_q"]) + radius = chamber["radius_q"] * scale + clear = chamber["clear_height_q"] * scale + gx, gy = (value / 65536.0 for value in chamber["floor_gradient_q16"]) + floor = cz + gx * (point[0] - cx) + gy * (point[1] - cy) + chamber_distance = max(math.hypot(point[0] - cx, point[1] - cy) - radius, + floor - point[2], point[2] - (floor + clear)) + return chamber_distance <= SHELL_THICKNESS_M + + +def _primitive_bounds(row: dict[str, Any]): + return tuple((row["bounds_min_um"][i] / 1.0e6, + row["bounds_max_um"][i] / 1.0e6) for i in range(3)) + + +def _landing_content_band_audit(layout: dict[str, Any]) -> list[dict[str, Any]]: + """Recheck the validator's complete graded floor, headroom and shell envelope.""" + voxel_um = int(layout["core_voxel_size_um"]) + q_per_metre = 256 * 1_000_000 / voxel_um + strata = {int(row["strate_index"]): row for row in layout.get("strates", [])} + + def q_to_um(q: int) -> int: + numerator = q * voxel_um + return numerator // 256 if numerator >= 0 else -((-numerator) // 256) + + result = [] + shell_q = math.floor(0.75 * q_per_metre + 0.5) + for passage in layout.get("passages", []): + for side, key, strate_key in ( + ("upper", "upper_landing_chamber", "upper_strate_index"), + ("lower", "lower_landing_chamber", "lower_strate_index")): + chamber = passage[key] + host = strata.get(int(passage[strate_key])) + content = host.get("content", {}) if host else {} + gradient = chamber["floor_gradient_q16"] + gradient_magnitude = math.isqrt(gradient[0] ** 2 + gradient[1] ** 2) + plane_reach_q = (int(chamber["radius_q"]) * gradient_magnitude + 65535) // 65536 + floor_q = int(chamber["floor_center_q"][2]) + minimum_um = q_to_um(floor_q - plane_reach_q - shell_q) + maximum_um = q_to_um(floor_q + plane_reach_q + + int(chamber["clear_height_q"]) + shell_q) + band_bottom_um, band_top_um = int(content.get("bottom_um", 0)), int( + content.get("top_um", 0)) + passed = bool(content.get("present", False) + and minimum_um >= band_bottom_um + and maximum_um <= band_top_um) + result.append({ + "passage": passage["stable_id"], "side": side, + "host_strate": int(passage[strate_key]), + "minimum_um": minimum_um, "maximum_um": maximum_um, + "content_bottom_um": band_bottom_um, "content_top_um": band_top_um, + "bottom_margin_m": (minimum_um - band_bottom_um) / 1_000_000.0, + "top_margin_m": (band_top_um - maximum_um) / 1_000_000.0, + "passed": passed, + }) + return result + + +def _bezier(points, t: float): + u = 1.0 - t + return tuple(u**3 * points[0][axis] + 3*u*u*t * points[1][axis] + + 3*u*t*t * points[2][axis] + t**3 * points[3][axis] + for axis in range(3)) + + +def _distance_to_ellipse(radial: float, vertical: float, + radius: float, height: float) -> float: + """Euclidean distance to the edge bore's horizontal/vertical ellipse.""" + radial = abs(radial) + vertical = abs(vertical) + if (radial / radius) ** 2 + (vertical / height) ** 2 <= 1.0: + return 0.0 + + lo, hi = 0.0, math.pi / 2.0 + ratio = (math.sqrt(5.0) - 1.0) / 2.0 + + def squared_distance(angle: float) -> float: + return ((radial - radius * math.cos(angle)) ** 2 + + (vertical - height * math.sin(angle)) ** 2) + + left = hi - ratio * (hi - lo) + right = lo + ratio * (hi - lo) + left_value, right_value = squared_distance(left), squared_distance(right) + for _ in range(48): + if left_value < right_value: + hi, right, right_value = right, left, left_value + left = hi - ratio * (hi - lo) + left_value = squared_distance(left) + else: + lo, left, left_value = left, right, right_value + right = lo + ratio * (hi - lo) + right_value = squared_distance(right) + return math.sqrt(min(left_value, right_value)) + + +def _actual_edge_clearance(passage, edge, scale: float, + chamber: dict[str, Any]) -> float: + """Return minimum true tube clearance outside the landing chamber. + + The route edge is a swept ellipse, with its recorded polyline as the axis. + Comparing the tunnel tube to that ellipse avoids the false clashes caused by + intersecting two enclosing boxes. + """ + controls = [tuple(value / 1.0e6 for value in point) + for point in edge["control_points_um"]] + axis = [tuple(value / 1.0e6 for value in point) + for point in edge.get("polyline_um", [])] + if len(axis) < 2: + axis = [_bezier(controls, index / 64.0) for index in range(65)] + edge_radius = (edge["bore_radius_um"] + + edge["irregularity_amplitude_um"]) / 1.0e6 + edge_height = (edge["bore_height_um"] + + edge["irregularity_amplitude_um"]) / 1.0e6 + passage_segments = passage.get("_audit_segments") or tuple(_segments(passage, scale)) + minimum_clearance = math.inf + for a, b, _, passage_radius in passage_segments: + length = math.dist(a[:3], b[:3]) + steps = max(1, math.ceil(length / 0.05)) + for step in range(steps + 1): + t = step / steps + point = tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) + if _inside_chamber(point, chamber, scale): + continue + tunnel_radius = (a[3] + (b[3] - a[3]) * t + + INFLUENCE_REACH_M) + for start, end in zip(axis, axis[1:]): + delta = tuple(end[i] - start[i] for i in range(3)) + length_squared = sum(value * value for value in delta) + projection = (sum((point[i] - start[i]) * delta[i] + for i in range(3)) / length_squared + if length_squared else 0.0) + projection = min(1.0, max(0.0, projection)) + center = tuple(start[i] + projection * delta[i] for i in range(3)) + radial = math.hypot(point[0] - center[0], point[1] - center[1]) + vertical = abs(point[2] - center[2]) + clearance = (_distance_to_ellipse(radial, vertical, + edge_radius, edge_height) + - tunnel_radius) + minimum_clearance = min(minimum_clearance, clearance) + return minimum_clearance + + +def _test_bounds(bounds, strate_index: int, passages, scale: float, + *, allow_chamber: bool = True): + for passage in passages: + if strate_index not in (passage["upper_strate_index"], passage["lower_strate_index"]): + continue + chamber_key = ("upper_landing_chamber" if strate_index == passage["upper_strate_index"] + else "lower_landing_chamber") + chamber = passage[chamber_key] + segments = passage.get("_audit_segments") + if segments is None: + segments = tuple(_segments(passage, scale)) + for a, b, along, radius in segments: + expanded = [(a[i] - radius, b[i] + radius) if a[i] <= b[i] + else (b[i] - radius, a[i] + radius) for i in range(3)] + if any(expanded[i][1] < bounds[i][0] or expanded[i][0] > bounds[i][1] + for i in range(3)): + continue + overlap = _overlap_interval(a, b, bounds, radius) + if overlap is None: + continue + first, last, closest = overlap + if allow_chamber: + points = [tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) + for t in (first, closest, last)] + if all(_inside_chamber(point, chamber, scale) for point in points): + continue + point = tuple(a[i] + (b[i] - a[i]) * closest for i in range(3)) + return passage["stable_id"], along + closest * math.dist(a[:3], b[:3]), point + return None + + +def audit_layout(layout: dict[str, Any]) -> dict[str, Any]: + scale = layout["core_voxel_size_um"] / (256.0 * 1.0e6) + passages = [dict(row, _audit_segments=tuple(_segments(row, scale))) + for row in layout.get("passages", [])] + primitives = [*layout.get("cavern_rooms", []), *layout.get("cavern_edges", [])] + checked = conflicts = chamber_joints = bounds_candidates = refined_false_positives = 0 + first_conflict = None + for primitive in primitives: + bounds = _primitive_bounds(primitive) + checked += 1 + primitive_conflicted = False + for passage in passages: + found = _test_bounds(bounds, primitive["strate_index"], [passage], scale) + if not found: + continue + bounds_candidates += 1 + passage_id, station, point = found + if "control_points_um" in primitive: + side = ("upper_landing_chamber" + if primitive["strate_index"] == passage["upper_strate_index"] + else "lower_landing_chamber") + clearance = _actual_edge_clearance(passage, primitive, scale, + passage[side]) + if clearance >= 0.0: + refined_false_positives += 1 + continue + primitive_conflicted = True + conflicts += 1 + if first_conflict is None: + first_conflict = {"primitive": primitive["stable_id"], + "passage": passage_id, "station_m": round(station, 3), + "center_m": [round(value, 3) for value in point]} + break + if not primitive_conflicted: + # Count the edge's named landing association as a confirmed joint. + anchor = primitive.get("start_anchor_stable_id", "") + if anchor.endswith(("/upper-landing-chamber", "/lower-landing-chamber")): + chamber_joints += 1 + + negative_controls = 0 + long_passage = max(passages, key=lambda row: len(row["control_points"]), default=None) + if long_passage: + controls = [_point(row, scale) for row in long_passage["control_points"]] + index = min(max(1, len(controls) // 2), len(controls) - 2) + center = controls[index] + bounds = tuple((center[axis] - 0.25, center[axis] + 0.25) for axis in range(3)) + # Select the side whose chamber is farther from this forced tunnel overlap. + strate = long_passage["lower_strate_index"] + if _test_bounds(bounds, strate, [long_passage], scale, allow_chamber=False): + negative_controls = 1 + landing_content_band = _landing_content_band_audit(layout) + landing_content_band_passed = bool(landing_content_band) and all( + row["passed"] for row in landing_content_band) + return {"primitives_checked": checked, "bounds_candidates": bounds_candidates, + "refined_edge_false_positives": refined_false_positives, + "outside_chamber_conflicts": conflicts, + "chamber_joints": chamber_joints, "negative_controls": negative_controls, + "negative_controls_expected": 1, "first_conflict": first_conflict, + "landing_content_band": landing_content_band, + "landing_content_band_passed": landing_content_band_passed, + "passed": conflicts == 0 and negative_controls == 1 + and landing_content_band_passed} diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux156/REPORT.md new file mode 100644 index 0000000..2b2fb80 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux156/REPORT.md @@ -0,0 +1,40 @@ +# L156 — native-floor carve reaches the grade stop rule + +| Landing | Before (L151) | After (L156) | +|---|---|---| +| passage/0-1/0 · upper | ![Before upper 0-1](captures/before-l151-0-1-0-upper-landing.jpg) | ![After upper 0-1](captures/after-l156-0-1-0-upper-landing.jpg) | +| passage/0-1/0 · lower | ![Before lower 0-1](captures/before-l151-0-1-0-lower-landing.jpg) | ![After lower 0-1](captures/after-l156-0-1-0-lower-landing.jpg) | +| passage/1-2/0 · upper | ![Before upper 1-2](captures/before-l151-1-2-0-upper-landing.jpg) | ![After upper 1-2](captures/after-l156-1-2-0-upper-landing.jpg) | +| passage/1-2/0 · lower | ![Before lower 1-2](captures/before-l151-1-2-0-lower-landing.jpg) | ![After lower 1-2](captures/after-l156-1-2-0-lower-landing.jpg) | +| passage/2-3/0 · upper | ![Before upper 2-3](captures/before-l151-2-3-0-upper-landing.jpg) | ![After upper 2-3](captures/after-l156-2-3-0-upper-landing.jpg) | +| passage/2-3/0 · lower | ![Before lower 2-3](captures/before-l151-2-3-0-lower-landing.jpg) | ![After lower 2-3](captures/after-l156-2-3-0-lower-landing.jpg) | + +## Result + +The headroom-only carve was implemented and captured, but L156 does not pass: fast tier still fails the upper Surface World landing's evaluated floor grade at 22.847° against 15°. The plan profile itself is 10.037°. The passage-interior support exemption and its open-floor negative control pass. The Floating Islands chamber-band failure is fixed: every chamber in both 16-seed sweeps is inside its host band, although other gates leave both sweeps at 0/16. The grade cause stops here after its three failed fits. + +## What changed + +- Bumped `WorldAssemblyEvaluatorVersion` from 239 to 240. The chamber carve now starts at the planned floor raised to the receiving native ground height when that surface is within the chamber's 4.5 m clear-height budget. Surface World samples its deterministic heightfield; a missing or higher, separate surface falls back to the planned floor. The chamber clears upward from that floor and retains its 0.75 m shell. Passage joining remains bounded to 15°. +- Added a passage-interior exemption to chamber support sampling. On the fast fixture it skipped 5,933 bore-overlap support samples; 76,671 passage-interior capsule samples had zero solid hits and the chamber audit reported zero unsupported samples. +- Added an open-floor negative control outside the bore. It found 3 unsupported samples and reported the expected failure (control status PASS). +- Fit each landing's full grade reach, shell, clear height, and Surface World rise inside its host content band. Seed 0's six chambers all pass; its tightest lower and upper band margins are 3.195 m and 3.642 m. The band gate passed all 16 seeds in both sweep types. + +## Measurements + +- **Fast tier:** failed after 64.31 s on the upper chamber. `PassagePlanGradeAudit` passed at 10.037°; the evaluated walk-floor chord at segment 10 was 22.847° over 0.479 m horizontal and 0.202 m vertical. The chamber audit had one grade failure. Passage interior was open (0 solid hits); the outside-bore open-floor negative control detected its unsupported floor. +- **Composed sweeps:** Tunnel Network 0/16 and Vertical Shafts 0/16 passed, with all 16 seeds completed for each at source stamp `4bf096c0…`. The exact landing-band and envelope gates passed across both sweeps. Remaining failures include passage summary/walkability, seams, interior-capsule and chamber-floor gates, and some route-to-chamber envelope conflicts. +- **Determinism:** Clang and GCC at W1/W8/W12 passed 6/6 with identical output hashes. Density `0179a7f6937dab67`, mesh `5f84fb3d3509f12e`, payload `8b857b09f6cb0ea4`, topology `1601efe43ef906b9`, and material `bd37729ce9c3d68d`. A seed-0 candidate baseline is recorded at `baselines/native-owner-fi-seed0.json`; it is not promoted as canonical while fast and composed gates fail. +- **Captures:** six matched eye-height views before (L151) and after (L156), same camera poses and layout identity; image quality passed 12/12. Before stamp `1ca3b84f…`; after stamp `4bf096c0…`. +- **Linux UE:** `VFHostEditor` built successfully, 27/27 actions in 69 s. No Unreal `Source/` files changed. Four start poses per condition settled LOD0–2 at median 2.421 s (`assembler-world`) and 2.457 s (planner). Five interleaved 30 m/s movement runs per condition: near-ready moving mean was 45.51% vs 39.04% (−14.21% planner); far holes were 3.15% vs 3.55% (+12.66%); max and p99 game-thread ticks were +4.13% and +6.64%. Each trace covered 46 moving samples over roughly 53–62 s. +- **L80:** no comparison result. Its quiet gate passed at load 0.74 and CPU idle 99.2%. The baseline warm-up passed in 1.72 s, but the L156 binary rejects the old owner snapshot's canonical identity. A regenerated v240 snapshot is accepted by L156, then the full pipeline exits 2: 176/21,103 compiled passage-centre samples are solid. The L137 binary rejects that v240 snapshot, so no shared serialized snapshot yields a valid cross-version comparison. `--force-generate` does not bypass the initial identity check. + +## Checks and limits + +- Ran: Clang and GCC headless builds; fast tier; both 16-seed sweeps; chamber support and open-floor negative control; content-band/envelope audits; six-way compiler/worker determinism; 12 capture-quality checks; Linux UE build, four start poses, and five 30 m/s runs per condition. +- Not run: L80 `assembler/world` timing at W8/W12 across all four layers. The host met its quiet gate, but the candidate fails its full-pipeline walkability check and the two revisions reject each other's canonical snapshots. The lone baseline warm-up is not treated as a timing comparison. +- The native-floor representation removes below-ground carving in the intended headroom case, but the effective chamber floor still violates the 15° join at a Surface World heightfield transition. No canonical baseline promotion is justified until that gate and the two composed sweeps pass. + +## Remaining risk + +The exact upper-chamber failure is at a native-surface transition, while the planned passage floor remains within its grade bound. The other sweep failures are also unresolved; the band fix and support-audit change do not make those gates pass. The movement result is slower for near-ready coverage than `assembler/world` in this five-run window. diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/RunL156Captures.py b/Standalone/Research/WorldAssemblerM3/Linux156/RunL156Captures.py new file mode 100644 index 0000000..16a62d6 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux156/RunL156Captures.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Capture matched eye-height views from both ends of every owner tunnel.""" +from __future__ import annotations + +import json +import math +import os +from pathlib import Path +import re +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[4] +DATA = ROOT / "Standalone/data/owner_world.json" +OUT = ROOT / "Saved/Linux/l156/captures" +LAYOUT = OUT / "after-resolved-layout.json" +ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux156/captures" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +VERSIONS = { + "before-l151": { + "headless": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l151-clang", + "renderer": ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-l151-clang", + "stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + }, + "after-l156": { + "headless": ROOT / "Saved/Linux/l156/bin/VoxelForgeCoreHeadless-l156-clang", + "renderer": ROOT / "Saved/Linux/l156/bin/VoxelForgeOffscreenCapture-l156-clang", + "stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + }, +} +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) + + +def fmt(values) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def angles(camera, target) -> tuple[float, float]: + dx, dy, dz = (target[index] - camera[index] for index in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees( + math.atan2(dz, math.hypot(dx, dy))) + + +def point_q(layout: dict, values) -> tuple[float, float, float]: + scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0) + return tuple(value * scale for value in values) + + +def chamber_views(layout: dict) -> list[dict]: + result = [] + for passage in layout["passages"]: + controls = passage["control_points"] + for side, endpoint, neighbor in ( + ("upper", controls[0], controls[1]), + ("lower", controls[-1], controls[-2])): + chamber = passage[f"{side}_landing_chamber"] + center = point_q(layout, chamber["floor_center_q"]) + a = point_q(layout, neighbor["position_q"]) + b = point_q(layout, endpoint["position_q"]) + dx, dy = a[0] - b[0], a[1] - b[1] + length = math.hypot(dx, dy) + if length <= 1.0e-9: + raise ValueError(f"degenerate {side} tunnel tangent: {passage['stable_id']}") + dx, dy = dx / length, dy / length + eye_z = center[2] + 1.45 + distance = 9.0 + target = (center[0] + dx * distance, center[1] + dy * distance, + eye_z + (chamber["floor_gradient_q16"][0] / 65536.0 * dx + + chamber["floor_gradient_q16"][1] / 65536.0 * dy) + * distance) + camera = (center[0], center[1], eye_z) + slug = passage["stable_id"].replace("passage/", "").replace("/", "-") + result.append({"name": f"{slug}-{side}-landing", "passage": passage["stable_id"], + "side": side, "camera": camera, "target": target, + "focus": (center[0], center[1], eye_z, 18.0)}) + return result + + +def stamped_run(name: str, command: list[object], log: Path, expected_stamp: str, + env: dict[str, str]) -> str: + binary = Path(str(command[0])) + probe = subprocess.run([*LIMIT, "timeout", "15s", str(binary), "--print-source-stamp"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=20) + match = STAMP_RE.search(probe.stdout or "") + if probe.returncode or not match or match.group(1) != expected_stamp: + raise RuntimeError(f"{name}: source stamp mismatch for {binary}") + log.parent.mkdir(parents=True, exist_ok=True) + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, + timeout=1745, check=False) + output = log.read_text(encoding="utf-8", errors="replace") + emitted = STAMP_RE.search(output) + if result.returncode or not emitted or emitted.group(1) != expected_stamp: + raise RuntimeError(f"{name}: export or render failed; see {log}") + return output + + +def resolve_layout(env: dict[str, str]) -> dict: + config = VERSIONS["after-l156"] + command = [config["headless"], DATA, "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=12", + "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", "--seed=0", "--recipe=surface-world", "--resolved-layout-only", + f"--resolved-layout-output={LAYOUT}"] + stamped_run("after-l156-resolve-layout", list(map(str, command)), + OUT / "after-l156-resolved-layout.log", config["stamp"], env) + layout = json.loads(LAYOUT.read_text(encoding="utf-8")) + if len(layout.get("passages", [])) != 3: + raise RuntimeError("owner layout did not resolve the expected three tunnels") + return layout + + +def main() -> int: + if not DATA.is_file(): + raise SystemExit("missing owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + ARTIFACTS.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache") + layout = resolve_layout(env) + views = chamber_views(layout) + quality_tool = ROOT / "Standalone/Tools/verify/capture_quality.py" + manifest = [] + failures = [] + for version, config in VERSIONS.items(): + for view in views: + camera, target = view["camera"], view["target"] + yaw, pitch = angles(camera, target) + mesh_dir = OUT / "meshes" / version + mesh_dir.mkdir(parents=True, exist_ok=True) + mesh = mesh_dir / f"{view['name']}.vfmesh" + capture_layout = mesh_dir / f"{view['name']}-layout.json" + export = [config["headless"], DATA, "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=12", + "--clip-radius=6", "--max-clip-level=0", "--max-level=0", + "--force-generate", "--seed=0", "--recipe=surface-world", + "--render-distance-chunks=0", + f"--camera={fmt((*camera, yaw, pitch))}", + f"--resolved-layout-output={capture_layout}", f"--mesh-export={mesh}"] + stamped_run(f"{version}-{view['name']}-export", list(map(str, export)), + OUT / f"{version}-{view['name']}-export.log", config["stamp"], env) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{version}-{view['name']}: empty mesh export") + png = OUT / f"{version}-{view['name']}.png" + jpg = ARTIFACTS / f"{version}-{view['name']}.jpg" + render = [config["renderer"], mesh, png, "--size=1280x720", + "--fov-deg=78.0", f"--camera-m={fmt(camera)}", + f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}", + "--tile-skirts=off", "--shade=readable", "--headlamp=on", + "--height-tint=on", "--height-center-m=-950", "--height-range-m=60", + "--tile-edges=off", f"--focus-region={fmt(view['focus'])}"] + stamped_run(f"{version}-{view['name']}-render", list(map(str, render)), + OUT / f"{version}-{view['name']}-render.log", config["stamp"], env) + subprocess.run([*LIMIT, "timeout", "120s", "ffmpeg", "-nostdin", + "-hide_banner", "-loglevel", "error", "-y", "-i", png, + "-frames:v", "1", "-q:v", "4", jpg], cwd=ROOT, env=env, + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, + timeout=125) + check = subprocess.run([*LIMIT, "timeout", "120s", sys.executable, + quality_tool, jpg], cwd=ROOT, env=env, text=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + check=False, timeout=125) + quality_ok = check.returncode == 0 and "status=PASS" in check.stdout + if not quality_ok: + failures.append(f"{version}-{view['name']}") + quality_line = (check.stdout.strip().splitlines()[-1] + if check.stdout.strip() else f"quality exit={check.returncode}") + manifest.append({"version": version, "view": view["name"], + "passage": view["passage"], "side": view["side"], + "file": str(jpg.relative_to(ROOT)), "camera_m": camera, + "target_m": target, "yaw_deg": yaw, "pitch_deg": pitch, + "source_stamp": config["stamp"], + "pose_layout_identity": layout.get("stable_identity"), + "quality_status": "PASS" if quality_ok else "FAIL", + "quality": quality_line}) + print(f"[L156Capture] version={version} view={view['name']} " + f"quality={'PASS' if quality_ok else 'FAIL'} " + f"file={jpg.relative_to(ROOT)}", flush=True) + (ARTIFACTS / "manifest.json").write_text( + json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + with (ARTIFACTS / "INDEX.md").open("w", encoding="utf-8") as index: + index.write("# L156 matched owner tunnel landing captures\n\n" + "Seed 0 camera poses resolved from the L156 owner layout and reused for both " + "versions. Each view is at eye height at one of the three tunnel landing " + "chambers. Before uses the L151 head build; after uses L156 source stamp " + f"`{VERSIONS['after-l156']['stamp']}`.\n\n") + for view in views: + index.write(f"## {view['passage']} / {view['side']} landing\n\n" + f"- Before: [image](before-l151-{view['name']}.jpg)\n" + f"- After: [image](after-l156-{view['name']}.jpg)\n\n") + index.write(f"Capture quality failures: {len(failures)}/{len(manifest)}.\n") + print(f"[L156CaptureSummary] chamber_views={len(views)} " + f"images={len(manifest)} quality_failures={len(failures)}", flush=True) + return 0 if not failures else 1 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L156CaptureFailure] {error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/RunL156Determinism.py b/Standalone/Research/WorldAssemblerM3/Linux156/RunL156Determinism.py new file mode 100644 index 0000000..b87dfd5 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux156/RunL156Determinism.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Compare the composed native Floating Islands owner at Clang/GCC W1/W8/W12.""" +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/l156/determinism" +BASELINE = ROOT / "Standalone/Research/WorldAssemblerM3/Linux156/baselines/native-owner-fi-seed0.json" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0" +BINARIES = { + "clang": ROOT / "Saved/Linux/l156/bin/VoxelForgeCoreHeadless-l156-clang", + "gcc": ROOT / "Saved/Linux/l156/bin/VoxelForgeCoreHeadless-l156-gcc", +} +CAMERA = "25.836914,3.199219,-737.000000,0,0" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) +HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|MaterialHash|LODHash|ExactQueryHash)\].*$", + re.M) +NATIVE_FI = re.compile( + r"^\[DensityDispatch\] strate=1 generator=FloatingIslands " + r"recipe=FloatingIslands\.ClusterField version=\d+ " + r"bound_path=FloatingIslands-native adapter=FFloatingIslandFieldAdapter$", re.M) +FI_RECIPE_HASH = re.compile( + r"^\[FloatingIslandWorldAssembly\].*recipe_hash=([0-9a-f]+) status=PASS$", re.M) +FI_CLEARANCE = re.compile(r"^\[FloatingIslandPassageClearance\].*status=PASS$", re.M) +FI_ROUTE_AUDIT = re.compile(r"^\[FloatingIslandRouteAudit\].*status=PASS$", re.M) + + +def main() -> int: + if not DATA.is_file() or any(not path.is_file() for path in BINARIES.values()): + raise SystemExit("missing L156 Clang/GCC binary or composed owner fixture") + OUT.mkdir(parents=True, exist_ok=True) + rows: list[dict[str, object]] = [] + reference_hashes: list[str] | None = None + reference_identity: str | None = None + reference_recipe_hash: str | None = None + for compiler, binary in BINARIES.items(): + for workers in (1, 8, 12): + name = f"native-owner-fi-{compiler}-w{workers}" + log, layout = OUT / f"{name}.log", OUT / f"{name}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=0", + f"--camera={CAMERA}", "--stream-hash", "--force-generate", "--seed=0", + "--recipe=surface-world", "--native-owner-fi", "--render-distance-chunks=0", + "--density-dispatch-report", f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + hashes = HASH_RE.findall(content) + current_layout = json.loads(layout.read_text(encoding="utf-8")) if layout.is_file() else {} + identity = current_layout.get("stable_identity") + native_ok = bool(NATIVE_FI.search(content)) + recipe_hash = FI_RECIPE_HASH.search(content) + clearance = FI_CLEARANCE.search(content) + route_audit = FI_ROUTE_AUDIT.search(content) + local_pass = (proc.returncode == 0 and stamp is not None + and stamp.group(1) == STAMP and len(hashes) >= 4 and native_ok + and recipe_hash is not None and route_audit is not None and bool(identity)) + if reference_hashes is None and local_pass: + reference_hashes, reference_identity = hashes, identity + reference_recipe_hash = recipe_hash.group(1) if recipe_hash else None + same = (hashes == reference_hashes and identity == reference_identity + and (recipe_hash.group(1) if recipe_hash else None) + == reference_recipe_hash) + passed = bool(local_pass and same) + row = {"compiler": compiler, "workers": workers, "exit": proc.returncode, + "source_stamp": stamp.group(1) if stamp else None, "hash_lines": hashes, + "layout_identity": identity, "native_fi_dispatch": native_ok, + "floating_island_recipe_hash": recipe_hash.group(1) if recipe_hash else None, + "passage_clearance_line": clearance.group(0) if clearance else None, + "route_audit_line": route_audit.group(0) if route_audit else None, + "passed": passed, "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + rows.append(row) + print("[L156DeterminismRun] " + json.dumps(row, sort_keys=True), flush=True) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n") + if not passed: + summary = {"status": "FAIL", "source_stamp": STAMP, "runs": rows} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + raise SystemExit(f"native owner determinism mismatch; see {log}") + + summary = {"status": "PASS", "source_stamp": STAMP, + "world_assembly_evaluator_version": 240, + "layout_identity": reference_identity, + "floating_island_recipe_hash": reference_recipe_hash, + "baseline_scope": "candidate only; fast tier remains failed", + "runs": rows} + OUT.joinpath("summary.json").write_text(json.dumps(summary, indent=2) + "\n") + BASELINE.parent.mkdir(parents=True, exist_ok=True) + BASELINE.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L156DeterminismSummary] runs={len(rows)} source_stamp={STAMP} status=PASS " + "baseline=candidate-only", flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/RunL156SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux156/RunL156SeedSweep.py new file mode 100644 index 0000000..32ed92e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux156/RunL156SeedSweep.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Run the L156 passage and envelope gates across the 16 owner seeds.""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import time +from pathlib import Path + +from PassageEnvelopeAudit import audit_layout + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l156/seeds" +BIN = ROOT / "Saved/Linux/l156/bin/VoxelForgeCoreHeadless-l156-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +DISPATCH = { + "tunnel-network": re.compile( + r"^\[DensityDispatch\] strate=2 generator=TunnelNetwork " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), + "vertical-shafts": re.compile( + r"^\[DensityDispatch\] strate=3 generator=VerticalShafts " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), +} +ROUTES = { + "tunnel-network": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=2 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=3 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), +} +CONTROLS = { + "tunnel-network": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[VerticalShaftsBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} +REQUIRED = { + "passage_summary": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "interior_capsules": re.compile( + r"^\[PassageInteriorCapsuleAuditSummary\].*status=PASS$", re.M), + "layout_controls": re.compile( + r"^\[ResolvedLayoutValidatorControls\] rejected=10/10 status=PASS$", re.M), + "floor_profile_controls": re.compile( + r"^\[FloorProfileBlendNegativeControls\].*detected=true/true status=PASS$", re.M), + "naturalness_control": re.compile( + r"^\[PassageNaturalnessNegativeControl\].*detected=true$", re.M), + "mouth_control": re.compile( + r"^\[PassageMouthProfileNegativeControl\].*detected=true$", re.M), +} + + +def passage_rows(content: str) -> list[str]: + return re.findall(r"^\[ResolvedPassageAudit\].*$", content, re.M) + + +def grade_audit(content: str) -> tuple[bool, list[str]]: + rows = re.findall(r"^\[PassagePlanGradeAudit\].*$", content, re.M) + ids = set() + passed = bool(rows) + for row in rows: + identifier = re.search(r"\bid=([^ ]+)", row) + maximum = re.search(r"\bmax_grade_deg=([0-9.]+)", row) + limit = re.search(r"\blimit_deg=([0-9.]+)", row) + violations = re.search(r"\bviolations=(\d+)", row) + status = "status=PASS" in row + if identifier: + ids.add(identifier.group(1)) + passed &= bool(identifier and maximum and limit and violations and status + and float(maximum.group(1)) <= float(limit.group(1)) + and float(limit.group(1)) == 15.0 + and int(violations.group(1)) == 0) + return passed and len(ids) >= 3, rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kind", choices=tuple(DISPATCH), required=True) + parser.add_argument("--start-seed", type=int, default=0) + parser.add_argument("--end-seed", type=int, default=15) + args = parser.parse_args() + if not 0 <= args.start_seed <= args.end_seed <= 15: + raise SystemExit("seed range must be within 0..15 and start <= end") + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L156 Clang binary or composed owner fixture") + mode_out = OUT / args.kind + mode_out.mkdir(parents=True, exist_ok=True) + partial = mode_out / "runs-partial.json" + results = (json.loads(partial.read_text(encoding="utf-8")) + if partial.is_file() and args.start_seed > 0 else []) + completed = {row["seed"] for row in results} + for seed in range(args.start_seed, args.end_seed + 1): + if seed in completed: + continue + log = mode_out / f"seed{seed}.log" + layout = mode_out / f"seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=0", "--force-generate", f"--seed={seed}", + "--recipe=surface-world", "--density-dispatch-report", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["process_exit_zero"] = proc.returncode == 0 + checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP + checks["dispatch"] = bool(DISPATCH[args.kind].search(content)) + checks["route"] = bool(ROUTES[args.kind].search(content)) + checks["blocked_route_control"] = bool(CONTROLS[args.kind].search(content)) + + passages = passage_rows(content) + checks["three_passages_walkable_and_open"] = ( + len(passages) == 3 and all("walkability=PASS" in row + and "interior_air=PASS" in row + and re.search(r"\bblocked=0\b", row) + and re.search(r"\bunsupported=0\b", row) + and re.search(r"\bgrade_failures=0\b", row) + for row in passages)) + checks["three_interior_negative_controls"] = len(re.findall( + r"^\[ResolvedPassageInteriorNegativeControl\].*detected=true status=PASS$", + content, re.M)) == 3 + checks["three_ripple_gap_controls"] = len(re.findall( + r"^\[PassageRippleSamplingControl\].*status=PASS$", content, re.M)) == 3 + checks["three_blocked_passage_controls"] = len(re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3 + chamber_rows = re.findall(r"^\[PassageLandingChamberAudit\].*$", content, re.M) + checks["six_chamber_floors"] = (len(chamber_rows) == 6 and all( + "status=PASS" in row and re.search(r"\bsolid_samples=0\b", row) + and re.search(r"\bgrade_failures=0\b", row) for row in chamber_rows)) + checks["plan_grade"] = grade_audit(content)[0] + chamber_controls = re.search( + r"^\[PassageLandingChamberNegativeControlSummary\] " + r"detected=(\d+)/6 .*status=OBSERVED$", content, re.M) + checks["chamber_negative_control"] = bool( + chamber_controls and int(chamber_controls.group(1)) > 0) + + checks["three_ripple_profiles"] = len(re.findall( + r"^\[PassageRippleAudit\].*status=PASS$", content, re.M)) >= 3 + checks["passage_count"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*walkability=PASS", content, re.M)) == 3 + checks["layout_output"] = layout.is_file() + envelope = audit_layout(json.loads(layout.read_text(encoding="utf-8"))) \ + if layout.is_file() else None + checks["envelope"] = bool(envelope and envelope["passed"] + and envelope["outside_chamber_conflicts"] == 0) + checks["envelope_negative_control"] = bool(envelope + and envelope["negative_controls"] == envelope["negative_controls_expected"] == 1) + checks["landing_content_band"] = bool(envelope + and envelope["landing_content_band_passed"] + and len(envelope["landing_content_band"]) == 6) + passed = all(checks.values()) + failed = [name for name, ok in checks.items() if not ok] + row = {"kind": args.kind, "seed": seed, + "verdict": "PASS" if passed else "FAIL", + "reason": ", ".join(failed) if failed else "all requested gates pass", + "exit": proc.returncode, "checks": checks, + "plan_grade_rows": grade_audit(content)[1], + "envelope_audit": envelope, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L156SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + partial.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + + results.sort(key=lambda row: row["seed"]) + passed_count = sum(row["verdict"] == "PASS" for row in results) + completed_count = len(results) + missing_seeds = sorted(set(range(16)) - {row["seed"] for row in results}) + summary = {"kind": args.kind, "source_stamp": STAMP, + "seeds": [row["seed"] for row in results], + "completed": completed_count, "passed": passed_count, + "failed": completed_count - passed_count, "missing_seeds": missing_seeds, + "results": results} + (mode_out / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + status = "PASS" if completed_count == 16 and passed_count == 16 else "FAIL" + print(f"[L156SeedSweepSummary] kind={args.kind} passed={passed_count}/{completed_count} " + f"failed={completed_count-passed_count} missing={len(missing_seeds)} status={status}", + flush=True) + return 0 if status == "PASS" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/RunL156Timing.py b/Standalone/Research/WorldAssemblerM3/Linux156/RunL156Timing.py new file mode 100644 index 0000000..bcaa666 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux156/RunL156Timing.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""L80 comparison of L156 with the assembler/world baseline.""" +from __future__ import annotations + +import json +import os +import re +import statistics +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l156/timing" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "assembler-world": ( + ROOT / "Saved/Linux/l137/bin/VoxelForgeCoreHeadless-l137-aa-a", + "4499fb2a8a4ede7d6cbc68d1b794f104658cdaae93ed9cacbc91fe7ff3fc3621"), + "planner-head": ( + ROOT / "Saved/Linux/l156/bin/VoxelForgeCoreHeadless-l156-clang", + "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0"), +} +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) +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + binary, expected_stamp = BINARIES[version] + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + label = f"s{strate}-w{workers}-{version}-{phase}{replicate}" + log = OUT / f"{label}.log" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline, near, stamp = PIPELINE.search(content), NEAR.search(content), STAMP.search(content) + if (proc.returncode or pipeline is None or near is None or stamp is None + or stamp.group(1) != expected_stamp): + raise SystemExit(f"L80 timing failed or source stamp mismatch: {log}; exit={proc.returncode}") + fields = dict(FIELDS.findall(pipeline.group(1))) + row = { + "strate": strate, "workers": workers, "version": version, + "phase": phase, "replicate": replicate, "source_stamp": stamp.group(1), + "lod02_ready_s": float(near.group(3)), + "whole_view_ready_s": float(fields["whole_view_ready"].removesuffix("s")), + "whole_tiles": int(fields["whole_tiles"].split("/")[0]), + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), + } + print("[L156TimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def quiet_machine() -> tuple[float, float]: + load_1m = os.getloadavg()[0] + + def totals() -> tuple[int, int]: + fields = Path("/proc/stat").read_text().splitlines()[0].split() + values = [int(value) for value in fields[1:]] + return sum(values), values[3] + values[4] + + before = totals() + time.sleep(2.0) + after = totals() + idle = 100.0 * (after[1] - before[1]) / max(1, after[0] - before[0]) + return load_1m, idle + + +def main() -> int: + if any(not path.is_file() for path in [DATA, *(entry[0] for entry in BINARIES.values())]): + raise SystemExit("missing L156 timing input") + OUT.mkdir(parents=True, exist_ok=True) + load_1m, idle = quiet_machine() + (OUT / "machine-load-before.txt").write_text( + f"1-minute load: {load_1m:.2f}\n2-second CPU idle: {idle:.1f}%\n") + print(f"[L156TimingGate] load_1m={load_1m:.2f} cpu_idle_2s={idle:.1f}%", flush=True) + if load_1m >= 1.5 or idle < 97.0: + raise SystemExit("timing refused: machine is above the L80 quiet-load gate") + + rows = [] + versions = tuple(BINARIES) + for strate in range(4): + for workers in (8, 12): + for version in versions: + run(strate, workers, version, "warmup", 0) + for replicate in range(1, 6): + order = versions if replicate % 2 else tuple(reversed(versions)) + for version in order: + rows.append(run(strate, workers, version, "measured", replicate)) + (OUT / "runs-partial.json").write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8") + + measured = [row for row in rows if row["phase"] == "measured"] + summary = [] + for strate in range(4): + for workers in (8, 12): + group = [row for row in measured + if row["strate"] == strate and row["workers"] == workers] + medians = { + version: { + key: statistics.median(row[key] for row in group + if row["version"] == version) + for key in ("lod02_ready_s", "whole_view_ready_s") + } + for version in versions + } + entry = {"strate": strate, "workers": workers, + "replicates_per_version": 5, "medians": medians} + for key in ("lod02_ready_s", "whole_view_ready_s"): + entry[f"planner_head_vs_assembler_world_{key}_delta_pct"] = round( + 100.0 * (medians["planner-head"][key] + / medians["assembler-world"][key] - 1.0), 2) + entry[f"{key}_paired_deltas_pct"] = [ + round(100.0 * ( + next(row[key] for row in group + if row["version"] == "planner-head" + and row["replicate"] == replicate) + / next(row[key] for row in group + if row["version"] == "assembler-world" + and row["replicate"] == replicate) - 1.0), 3) + for replicate in range(1, 6)] + summary.append(entry) + (OUT / "summary.json").write_text(json.dumps({ + "a_a_noise_floor_pct": 1.0, + "timing_protocol": ( + "one warm-up per binary, five interleaved runs per cell, W8/W12, " + "all four owner layers; L137 A/A median noise floor ±1%"), + "runs": rows, + "summary": summary, + }, indent=2) + "\n", encoding="utf-8") + failed_cells = [] + for entry in summary: + for measure in ("lod02_ready_s", "whole_view_ready_s"): + delta = entry[f"planner_head_vs_assembler_world_{measure}_delta_pct"] + if delta > 1.0: + failed_cells.append({"strate": entry["strate"], + "workers": entry["workers"], + "measure": measure, "delta_pct": delta}) + report = json.loads((OUT / "summary.json").read_text(encoding="utf-8")) + report["performance_floor_pct"] = 1.0 + report["performance_floor_rule"] = ( + "pass when no L156 median is more than 1% slower than assembler/world") + report["performance_floor_failures"] = failed_cells + report["status"] = "PASS" if not failed_cells else "FAIL" + (OUT / "summary.json").write_text(json.dumps(report, indent=2) + "\n", + encoding="utf-8") + print(f"[L156TimingSummary] cells={len(summary)} failing_metrics={len(failed_cells)} " + f"status={'PASS' if not failed_cells else 'FAIL'}", flush=True) + return 0 if not failed_cells else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/RunL156Unreal.py b/Standalone/Research/WorldAssemblerM3/Linux156/RunL156Unreal.py new file mode 100644 index 0000000..3befe87 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux156/RunL156Unreal.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Measure planner HEAD and code-equivalent assembler/world with the L121/L127 runners.""" +from __future__ import annotations + +import importlib.util +import json +import os +from pathlib import Path +import statistics +import sys + + +ROOT = Path(__file__).resolve().parents[4] +RESEARCH = ROOT / "Standalone/Research/WorldAssemblerM3" +EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd") +HOSTS = { + "assembler-world": ROOT / "Saved/Linux/l127/ue-baseline-project", + "planner-head": Path("/home/jahni/VFHost"), +} +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), +} + + +def load(name: str, path: Path): + spec = importlib.util.spec_from_file_location(name, path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot load {path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def configure(arrival, movement, label: str): + host = HOSTS[label].resolve() + out = ROOT / "Saved/Linux/l156/ue" / label + plugin_saved = host / "Plugins/VoxelForge/Saved/Linux/l156" + out.mkdir(parents=True, exist_ok=True) + plugin_saved.mkdir(parents=True, exist_ok=True) + os.environ["XDG_CONFIG_HOME"] = str(out / "config") + arrival.HOST = host + arrival.EDITOR = EDITOR + arrival.OUT = out / "start" + arrival.HOST_SAVED = out / "host" + arrival.HOST_PLUGIN_SAVED = plugin_saved + arrival.RUN_TIMEOUT_S = 1740 + movement.HOST = host + movement.EDITOR = EDITOR + movement.OUT = out / "movement" + movement.HOST_SAVED = out / "host" + movement.HOST_OUT = plugin_saved / "movement" + movement.RUN_TIMEOUT_S = 1740 + return host, out + + +def start_matrix(arrival, movement, label: str) -> dict: + host, out = configure(arrival, movement, label) + spawns = { + 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), + } + starts = [] + for index, position in spawns.items(): + stratum = "FI" if index == 1 else "SW" + arrival.SPAWNS_M[stratum] = position + row = arrival.run(stratum, 1, f"start-s{index}", 8, 1, 0, 1, 1, 0, 1, + False, False, 1) + row["strate_index"] = index + row["spawn_m"] = list(position) + if row.get("lod02_full_settled_t_s") is None: + raise RuntimeError(f"LOD0-2 did not settle for S{index}: {row}") + starts.append(row) + print("[L156UEStart] " + json.dumps(row, sort_keys=True), flush=True) + (out / "start-summary.json").write_text( + json.dumps(starts, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + row = moving_run(movement, label, 1) + (out / "movement-first.json").write_text( + json.dumps(row, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("[L156UEMovement] " + json.dumps(row, sort_keys=True), flush=True) + return {"host": str(host), "starts": starts, "first_movement": row} + + +def moving_run(movement, label: str, repetition: int) -> dict: + condition = "c743f2a" if label == "assembler-world" else "planner-head" + row = movement.run_unreal(condition, repetition) + events = movement.parse_events(ROOT / row["trace"]) + row["supplemental_far_applies_moving"] = sum( + int(event.get("supplemental_far_applies", 0)) + for event in events + if event.get("type") == "apply_frame" + and float(event.get("speed_cm_s", 0.0)) >= 50.0) + if row.get("effective_tile_slots") != 8: + raise RuntimeError(f"expected eight effective tile slots: {row}") + return row + + +def main() -> int: + out = ROOT / "Saved/Linux/l156/ue" + out.mkdir(parents=True, exist_ok=True) + arrival = load("l150_arrival", RESEARCH / "Linux121/RunL121Arrival.py") + movement = load("l150_movement", RESEARCH / "Linux121/RunL121Movement.py") + summaries = {} + all_runs = {label: [] for label in HOSTS} + for label in HOSTS: + summaries[label] = start_matrix(arrival, movement, label) + all_runs[label].append(summaries[label]["first_movement"]) + + for repetition in range(2, 6): + order = ("planner-head", "assembler-world") if repetition % 2 == 0 \ + else ("assembler-world", "planner-head") + for label in order: + configure(arrival, movement, label) + row = moving_run(movement, label, repetition) + all_runs[label].append(row) + (out / "movement-runs-partial.json").write_text( + json.dumps(all_runs, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("[L156UEMovementRepeat] " + json.dumps({ + key: row.get(key) for key in ( + "condition", "repetition", "near_ready_moving_mean_pct", + "far_holes_moving_mean_pct", "game_thread_tick_p99_ms", + "game_thread_tick_max_ms", "supplemental_far_applies_moving", + "wall_s", "trace")}, sort_keys=True), flush=True) + + keys = ("near_ready_moving_mean_pct", "far_holes_moving_mean_pct", + "game_thread_tick_p99_ms", "game_thread_tick_max_ms") + medians = { + label: {key: statistics.median(row[key] for row in rows) for key in keys} + for label, rows in all_runs.items() + } + delta = { + key: round(100.0 * (medians["planner-head"][key] + / medians["assembler-world"][key] - 1.0), 2) + for key in keys + } + result = { + "baseline_commit": "c743f2a (code-equivalent to assembler/world eb47d72)", + "protocol": ( + "L121 arrival and L127 movement; starts at four owner strata; five interleaved " + "30 m/s runs per build; 12 s move plus 5 s dwell; eight effective tile slots"), + "starts": summaries, + "movement_runs": all_runs, + "movement_medians": medians, + "planner_head_vs_assembler_world_delta_pct": delta, + } + (out / "summary.json").write_text( + json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("[L156UESummary] " + json.dumps({ + "movement_medians": medians, + "planner_head_vs_assembler_world_delta_pct": delta, + "starts_s": { + label: [round(row["lod02_full_settled_t_s"], 3) + for row in summary["starts"]] + for label, summary in summaries.items() + }, + }, sort_keys=True), flush=True) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L156UEFailure] {error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/__pycache__/PassageEnvelopeAudit.cpython-313.pyc b/Standalone/Research/WorldAssemblerM3/Linux156/__pycache__/PassageEnvelopeAudit.cpython-313.pyc new file mode 100644 index 0000000..727d1b5 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/__pycache__/PassageEnvelopeAudit.cpython-313.pyc differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/__pycache__/RunL156SeedSweep.cpython-313.pyc b/Standalone/Research/WorldAssemblerM3/Linux156/__pycache__/RunL156SeedSweep.cpython-313.pyc new file mode 100644 index 0000000..ff2ecc4 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/__pycache__/RunL156SeedSweep.cpython-313.pyc differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/baselines/native-owner-fi-seed0.json b/Standalone/Research/WorldAssemblerM3/Linux156/baselines/native-owner-fi-seed0.json new file mode 100644 index 0000000..adf66b1 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux156/baselines/native-owner-fi-seed0.json @@ -0,0 +1,190 @@ +{ + "status": "PASS", + "source_stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + "world_assembly_evaluator_version": 240, + "layout_identity": "resolved-layout-v3-fnv64:d4651758d452bca2", + "floating_island_recipe_hash": "789d86eed28fbfdd", + "baseline_scope": "candidate only; fast tier remains failed", + "runs": [ + { + "compiler": "clang", + "workers": 1, + "exit": 0, + "source_stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + "hash_lines": [ + "[PipelineFullDetailHash] tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineWholeViewHash] tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineMaterialHash] tiles=512 material=0xbd37729ce9c3d68d", + "[PipelineLODHash] lod=0 tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineLODHash] lod=1 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=2 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=3 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=4 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=5 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=6 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=7 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=8 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineExactQueryHash] probes=9 density=0x0536f1ff213604da unclamped=0x0536f1ff213604da detail=0xefcba20c0c70c415 collision=0xe604813a2490280c path=direct-Core-queries" + ], + "layout_identity": "resolved-layout-v3-fnv64:d4651758d452bca2", + "native_fi_dispatch": true, + "floating_island_recipe_hash": "789d86eed28fbfdd", + "passage_clearance_line": null, + "route_audit_line": "[FloatingIslandRouteAudit] points=29 centers=286 capsule_samples=77792 blocked=0 support_samples=1554 unsupported=0 grade_failures=0 status=PASS", + "passed": true, + "elapsed_s": 34.327, + "log": "Saved/Linux/l156/determinism/native-owner-fi-clang-w1.log", + "layout": "Saved/Linux/l156/determinism/native-owner-fi-clang-w1.json" + }, + { + "compiler": "clang", + "workers": 8, + "exit": 0, + "source_stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + "hash_lines": [ + "[PipelineFullDetailHash] tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineWholeViewHash] tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineMaterialHash] tiles=512 material=0xbd37729ce9c3d68d", + "[PipelineLODHash] lod=0 tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineLODHash] lod=1 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=2 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=3 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=4 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=5 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=6 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=7 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=8 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineExactQueryHash] probes=9 density=0x0536f1ff213604da unclamped=0x0536f1ff213604da detail=0xefcba20c0c70c415 collision=0xe604813a2490280c path=direct-Core-queries" + ], + "layout_identity": "resolved-layout-v3-fnv64:d4651758d452bca2", + "native_fi_dispatch": true, + "floating_island_recipe_hash": "789d86eed28fbfdd", + "passage_clearance_line": null, + "route_audit_line": "[FloatingIslandRouteAudit] points=29 centers=286 capsule_samples=77792 blocked=0 support_samples=1554 unsupported=0 grade_failures=0 status=PASS", + "passed": true, + "elapsed_s": 20.415, + "log": "Saved/Linux/l156/determinism/native-owner-fi-clang-w8.log", + "layout": "Saved/Linux/l156/determinism/native-owner-fi-clang-w8.json" + }, + { + "compiler": "clang", + "workers": 12, + "exit": 0, + "source_stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + "hash_lines": [ + "[PipelineFullDetailHash] tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineWholeViewHash] tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineMaterialHash] tiles=512 material=0xbd37729ce9c3d68d", + "[PipelineLODHash] lod=0 tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineLODHash] lod=1 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=2 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=3 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=4 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=5 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=6 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=7 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=8 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineExactQueryHash] probes=9 density=0x0536f1ff213604da unclamped=0x0536f1ff213604da detail=0xefcba20c0c70c415 collision=0xe604813a2490280c path=direct-Core-queries" + ], + "layout_identity": "resolved-layout-v3-fnv64:d4651758d452bca2", + "native_fi_dispatch": true, + "floating_island_recipe_hash": "789d86eed28fbfdd", + "passage_clearance_line": null, + "route_audit_line": "[FloatingIslandRouteAudit] points=29 centers=286 capsule_samples=77792 blocked=0 support_samples=1554 unsupported=0 grade_failures=0 status=PASS", + "passed": true, + "elapsed_s": 20.182, + "log": "Saved/Linux/l156/determinism/native-owner-fi-clang-w12.log", + "layout": "Saved/Linux/l156/determinism/native-owner-fi-clang-w12.json" + }, + { + "compiler": "gcc", + "workers": 1, + "exit": 0, + "source_stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + "hash_lines": [ + "[PipelineFullDetailHash] tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineWholeViewHash] tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineMaterialHash] tiles=512 material=0xbd37729ce9c3d68d", + "[PipelineLODHash] lod=0 tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineLODHash] lod=1 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=2 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=3 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=4 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=5 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=6 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=7 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=8 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineExactQueryHash] probes=9 density=0x0536f1ff213604da unclamped=0x0536f1ff213604da detail=0xefcba20c0c70c415 collision=0xe604813a2490280c path=direct-Core-queries" + ], + "layout_identity": "resolved-layout-v3-fnv64:d4651758d452bca2", + "native_fi_dispatch": true, + "floating_island_recipe_hash": "789d86eed28fbfdd", + "passage_clearance_line": null, + "route_audit_line": "[FloatingIslandRouteAudit] points=29 centers=286 capsule_samples=77792 blocked=0 support_samples=1554 unsupported=0 grade_failures=0 status=PASS", + "passed": true, + "elapsed_s": 33.269, + "log": "Saved/Linux/l156/determinism/native-owner-fi-gcc-w1.log", + "layout": "Saved/Linux/l156/determinism/native-owner-fi-gcc-w1.json" + }, + { + "compiler": "gcc", + "workers": 8, + "exit": 0, + "source_stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + "hash_lines": [ + "[PipelineFullDetailHash] tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineWholeViewHash] tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineMaterialHash] tiles=512 material=0xbd37729ce9c3d68d", + "[PipelineLODHash] lod=0 tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineLODHash] lod=1 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=2 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=3 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=4 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=5 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=6 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=7 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=8 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineExactQueryHash] probes=9 density=0x0536f1ff213604da unclamped=0x0536f1ff213604da detail=0xefcba20c0c70c415 collision=0xe604813a2490280c path=direct-Core-queries" + ], + "layout_identity": "resolved-layout-v3-fnv64:d4651758d452bca2", + "native_fi_dispatch": true, + "floating_island_recipe_hash": "789d86eed28fbfdd", + "passage_clearance_line": null, + "route_audit_line": "[FloatingIslandRouteAudit] points=29 centers=286 capsule_samples=77792 blocked=0 support_samples=1554 unsupported=0 grade_failures=0 status=PASS", + "passed": true, + "elapsed_s": 18.02, + "log": "Saved/Linux/l156/determinism/native-owner-fi-gcc-w8.log", + "layout": "Saved/Linux/l156/determinism/native-owner-fi-gcc-w8.json" + }, + { + "compiler": "gcc", + "workers": 12, + "exit": 0, + "source_stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + "hash_lines": [ + "[PipelineFullDetailHash] tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineWholeViewHash] tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineMaterialHash] tiles=512 material=0xbd37729ce9c3d68d", + "[PipelineLODHash] lod=0 tiles=512 density=0x0179a7f6937dab67 mesh=0x5f84fb3d3509f12e payload=0x8b857b09f6cb0ea4 topology=0x1601efe43ef906b9", + "[PipelineLODHash] lod=1 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=2 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=3 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=4 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=5 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=6 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=7 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineLODHash] lod=8 tiles=0 density=0xcbf29ce484222325 mesh=0xcbf29ce484222325 payload=0xcbf29ce484222325 topology=0xcbf29ce484222325", + "[PipelineExactQueryHash] probes=9 density=0x0536f1ff213604da unclamped=0x0536f1ff213604da detail=0xefcba20c0c70c415 collision=0xe604813a2490280c path=direct-Core-queries" + ], + "layout_identity": "resolved-layout-v3-fnv64:d4651758d452bca2", + "native_fi_dispatch": true, + "floating_island_recipe_hash": "789d86eed28fbfdd", + "passage_clearance_line": null, + "route_audit_line": "[FloatingIslandRouteAudit] points=29 centers=286 capsule_samples=77792 blocked=0 support_samples=1554 unsupported=0 grade_failures=0 status=PASS", + "passed": true, + "elapsed_s": 17.859, + "log": "Saved/Linux/l156/determinism/native-owner-fi-gcc-w12.log", + "layout": "Saved/Linux/l156/determinism/native-owner-fi-gcc-w12.json" + } + ] +} diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/INDEX.md b/Standalone/Research/WorldAssemblerM3/Linux156/captures/INDEX.md new file mode 100644 index 0000000..c189399 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux156/captures/INDEX.md @@ -0,0 +1,35 @@ +# L156 matched owner tunnel landing captures + +Seed 0 camera poses resolved from the L156 owner layout and reused for both versions. Each view is at eye height at one of the three tunnel landing chambers. Before uses the L151 head build; after uses L156 source stamp `4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0`. + +## passage/0-1/0 / upper landing + +- Before: [image](before-l151-0-1-0-upper-landing.jpg) +- After: [image](after-l156-0-1-0-upper-landing.jpg) + +## passage/0-1/0 / lower landing + +- Before: [image](before-l151-0-1-0-lower-landing.jpg) +- After: [image](after-l156-0-1-0-lower-landing.jpg) + +## passage/1-2/0 / upper landing + +- Before: [image](before-l151-1-2-0-upper-landing.jpg) +- After: [image](after-l156-1-2-0-upper-landing.jpg) + +## passage/1-2/0 / lower landing + +- Before: [image](before-l151-1-2-0-lower-landing.jpg) +- After: [image](after-l156-1-2-0-lower-landing.jpg) + +## passage/2-3/0 / upper landing + +- Before: [image](before-l151-2-3-0-upper-landing.jpg) +- After: [image](after-l156-2-3-0-upper-landing.jpg) + +## passage/2-3/0 / lower landing + +- Before: [image](before-l151-2-3-0-lower-landing.jpg) +- After: [image](after-l156-2-3-0-lower-landing.jpg) + +Capture quality failures: 0/12. diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-0-1-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-0-1-0-lower-landing.jpg new file mode 100644 index 0000000..136d057 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-0-1-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-0-1-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-0-1-0-upper-landing.jpg new file mode 100644 index 0000000..62fde03 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-0-1-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-1-2-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-1-2-0-lower-landing.jpg new file mode 100644 index 0000000..20a06ab Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-1-2-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-1-2-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-1-2-0-upper-landing.jpg new file mode 100644 index 0000000..b737b34 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-1-2-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-2-3-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-2-3-0-lower-landing.jpg new file mode 100644 index 0000000..a67a368 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-2-3-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-2-3-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-2-3-0-upper-landing.jpg new file mode 100644 index 0000000..3a29930 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-2-3-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-0-1-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-0-1-0-lower-landing.jpg new file mode 100644 index 0000000..c4013c0 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-0-1-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-0-1-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-0-1-0-upper-landing.jpg new file mode 100644 index 0000000..d648161 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-0-1-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-1-2-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-1-2-0-lower-landing.jpg new file mode 100644 index 0000000..aff38d6 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-1-2-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-1-2-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-1-2-0-upper-landing.jpg new file mode 100644 index 0000000..5763300 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-1-2-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-2-3-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-2-3-0-lower-landing.jpg new file mode 100644 index 0000000..7765a7f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-2-3-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-2-3-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-2-3-0-upper-landing.jpg new file mode 100644 index 0000000..86ecb2b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-2-3-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/captures/manifest.json b/Standalone/Research/WorldAssemblerM3/Linux156/captures/manifest.json new file mode 100644 index 0000000..d8cc0ad --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux156/captures/manifest.json @@ -0,0 +1,278 @@ +[ + { + "version": "before-l151", + "view": "0-1-0-upper-landing", + "passage": "passage/0-1/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-0-1-0-upper-landing.jpg", + "camera_m": [ + 37.0, + -5.25, + -372.3810546875 + ], + "target_m": [ + 28.40474992442715, + -7.918646874047773, + -371.83751160914875 + ], + "yaw_deg": -107.24848481126237, + "pitch_deg": 3.4561048595792685, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-0-1-0-upper-landing.jpg pixels=921600 mean_luma=118.644 stddev_luma=15.499 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l151", + "view": "0-1-0-lower-landing", + "passage": "passage/0-1/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-0-1-0-lower-landing.jpg", + "camera_m": [ + 25.8369140625, + 3.19921875, + -736.6798828125 + ], + "target_m": [ + 17.97380590951016, + -1.1793118453464206, + -734.970169262755 + ], + "yaw_deg": -119.11101808688004, + "pitch_deg": 10.756207017349817, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-0-1-0-lower-landing.jpg pixels=921600 mean_luma=111.426 stddev_luma=12.411 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l151", + "view": "1-2-0-upper-landing", + "passage": "passage/1-2/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-1-2-0-upper-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -731.8810546875 + ], + "target_m": [ + 10.35825176774404, + -16.050410390488395, + -733.216132282697 + ], + "yaw_deg": -101.86342177874246, + "pitch_deg": -8.437834550031965, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-1-2-0-upper-landing.jpg pixels=921600 mean_luma=117.134 stddev_luma=15.664 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l151", + "view": "1-2-0-lower-landing", + "passage": "passage/1-2/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-1-2-0-lower-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -753.05390625 + ], + "target_m": [ + 10.48848361510052, + -16.587754362485967, + -751.7193175939389 + ], + "yaw_deg": -105.38384894032598, + "pitch_deg": 8.434788863627224, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-1-2-0-lower-landing.jpg pixels=921600 mean_luma=119.710 stddev_luma=14.975 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l151", + "view": "2-3-0-upper-landing", + "passage": "passage/2-3/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-2-3-0-upper-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -812.075390625 + ], + "target_m": [ + -13.604954164712868, + -11.337338700629157, + -813.6662645746064 + ], + "yaw_deg": 134.15112141809087, + "pitch_deg": -10.024269857937275, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-2-3-0-upper-landing.jpg pixels=921600 mean_luma=118.198 stddev_luma=15.882 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "before-l151", + "view": "2-3-0-lower-landing", + "passage": "passage/2-3/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-2-3-0-lower-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -900.0333984375 + ], + "target_m": [ + -14.865342978484083, + -12.416118141706198, + -898.4427583280197 + ], + "yaw_deg": 144.72777711381306, + "pitch_deg": 10.022826283920647, + "source_stamp": "1ca3b84f6d35f7f355ec0f72dabd3b87a455ea02e5789fea611916962e5db8e5", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux156/captures/before-l151-2-3-0-lower-landing.jpg pixels=921600 mean_luma=121.734 stddev_luma=16.193 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l156", + "view": "0-1-0-upper-landing", + "passage": "passage/0-1/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-0-1-0-upper-landing.jpg", + "camera_m": [ + 37.0, + -5.25, + -372.3810546875 + ], + "target_m": [ + 28.40474992442715, + -7.918646874047773, + -371.83751160914875 + ], + "yaw_deg": -107.24848481126237, + "pitch_deg": 3.4561048595792685, + "source_stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-0-1-0-upper-landing.jpg pixels=921600 mean_luma=117.423 stddev_luma=13.370 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l156", + "view": "0-1-0-lower-landing", + "passage": "passage/0-1/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-0-1-0-lower-landing.jpg", + "camera_m": [ + 25.8369140625, + 3.19921875, + -736.6798828125 + ], + "target_m": [ + 17.97380590951016, + -1.1793118453464206, + -734.970169262755 + ], + "yaw_deg": -119.11101808688004, + "pitch_deg": 10.756207017349817, + "source_stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-0-1-0-lower-landing.jpg pixels=921600 mean_luma=111.621 stddev_luma=12.323 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l156", + "view": "1-2-0-upper-landing", + "passage": "passage/1-2/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-1-2-0-upper-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -731.8810546875 + ], + "target_m": [ + 10.35825176774404, + -16.050410390488395, + -733.216132282697 + ], + "yaw_deg": -101.86342177874246, + "pitch_deg": -8.437834550031965, + "source_stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-1-2-0-upper-landing.jpg pixels=921600 mean_luma=117.135 stddev_luma=15.664 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l156", + "view": "1-2-0-lower-landing", + "passage": "passage/1-2/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-1-2-0-lower-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -753.05390625 + ], + "target_m": [ + 10.48848361510052, + -16.587754362485967, + -751.7193175939389 + ], + "yaw_deg": -105.38384894032598, + "pitch_deg": 8.434788863627224, + "source_stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-1-2-0-lower-landing.jpg pixels=921600 mean_luma=119.962 stddev_luma=14.895 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l156", + "view": "2-3-0-upper-landing", + "passage": "passage/2-3/0", + "side": "upper", + "file": "Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-2-3-0-upper-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -812.075390625 + ], + "target_m": [ + -13.604954164712868, + -11.337338700629157, + -813.6662645746064 + ], + "yaw_deg": 134.15112141809087, + "pitch_deg": -10.024269857937275, + "source_stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-2-3-0-upper-landing.jpg pixels=921600 mean_luma=118.197 stddev_luma=15.883 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l156", + "view": "2-3-0-lower-landing", + "passage": "passage/2-3/0", + "side": "lower", + "file": "Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-2-3-0-lower-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -900.0333984375 + ], + "target_m": [ + -14.865342978484083, + -12.416118141706198, + -898.4427583280197 + ], + "yaw_deg": 144.72777711381306, + "pitch_deg": 10.022826283920647, + "source_stamp": "4bf096c0a06f72eb765ff8785d55c00ded946ed1bd1f39fa1914b3658ceb61f0", + "pose_layout_identity": "resolved-layout-v3-fnv64:46a5ebaca9fef170", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux156/captures/after-l156-2-3-0-lower-landing.jpg pixels=921600 mean_luma=121.801 stddev_luma=16.169 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + } +] diff --git a/Standalone/Research/WorldAssemblerM3/Linux156/l156-headroom-landing.patch b/Standalone/Research/WorldAssemblerM3/Linux156/l156-headroom-landing.patch new file mode 100644 index 0000000..28d706a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux156/l156-headroom-landing.patch @@ -0,0 +1,841 @@ +diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +index 4799b9e..82bc9e1 100644 +--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h ++++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +@@ -512,7 +512,8 @@ namespace VoxelForgeCore + // boundary-blended host rock from widening the authored passage wall. + // v238 plans one resolved passage spline with explicit landing chambers. + // v239 plans their floor planes as the passage's continuous floor profile. +- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 239; ++ // v240 treats SurfaceWorld landings as headroom above max(planned floor,native ground). ++ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 240; + + enum class FieldChannel : std::uint32_t + { +@@ -1212,8 +1213,9 @@ namespace VoxelForgeCore + + struct ResolvedLayoutPassageLandingChamberV1 + { +- // The chamber floor plane passes through this point. Gradient is dz/dx, +- // dz/dy in Q16 so the chamber continues the passage's authored h(s) grade. ++ // The planned chamber floor passes through this point. SurfaceWorld adds ++ // native ground where it lies within the clear-height envelope. Gradient is ++ // dz/dx,dy in Q16 and continues the passage's authored h(s) grade. + std::array floorCenterSubvoxels{}; + std::array floorGradientQ16{}; + std::int64_t radiusSubvoxels = 0; +@@ -2009,6 +2011,9 @@ namespace VoxelForgeCore + ShapeBuildStats buildStats() const; + + float densityAt(Vec3d voxelPosition) const; ++ // Deterministic native SurfaceWorld or receiving-route ground in Core voxels. ++ bool nativeSurfaceGroundHeightAt(Vec3d voxelPosition, ++ double& groundHeightVoxels) const; + // Companion material query for the bound strate evaluator; coordinates use Core voxels. + MaterialSampleV1 materialAt(Vec3d voxelPosition) const; + // Measure-only carve estimate. This does not apply an edit or grant inventory. +diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h +index 7ea6c8c..c80b95d 100644 +--- a/Standalone/Core/src/ResolvedLayoutInternal.h ++++ b/Standalone/Core/src/ResolvedLayoutInternal.h +@@ -3993,6 +3993,72 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t + reach/static_cast(qPerMetre)); + } + } ++ // Reserve each landing envelope inside its host band before building h(s). ++ // SurfaceWorld can raise its floor by at most the 4.5 m headroom allowance, ++ // so include that bounded rise as well as the grade reach and floor shell. ++ constexpr std::int64_t MaximumLandingGradeQ16=17560; ++ const std::int64_t landingRadius=mouthRadius+3*qPerMetre; ++ const std::int64_t landingClearHeight=static_cast( ++ std::llround(4.5*qPerMetre)); ++ const std::int64_t landingShell=static_cast( ++ std::llround(0.75*qPerMetre)); ++ const std::int64_t landingPlaneReach=(landingRadius ++ *MaximumLandingGradeQ16+65535)/65536; ++ const std::array upperMouthBeforeBandFit=upperMouth; ++ const std::array lowerMouthBeforeBandFit=lowerMouth; ++ const auto fitMouth=[&](std::array& mouth, ++ const ResolvedLayoutStratumV2& host,std::int64_t playerHeightQ, ++ const char* side) ++ { ++ if(!host.content.presentInCurrentField)return true; ++ const std::int64_t minimumFloor=contentZQ(host.content.bottomMicrometres) ++ +landingPlaneReach+landingShell; ++ const std::int64_t nativeFloorRise=host.generatorKind=="SurfaceWorld" ++ ?landingClearHeight:0; ++ const std::int64_t maximumFloor=contentZQ(host.content.topMicrometres) ++ -landingPlaneReach-landingShell-landingClearHeight-nativeFloorRise; ++ if(maximumFloor(qPerMetre)); ++ } ++ return true; ++ }; ++ if(!fitMouth(upperMouth,upper,upperPlayerCenterAboveFloorQ,"upper") ++ ||!fitMouth(lowerMouth,lower,playerCenterAboveFloorQ,"lower"))return false; ++ const auto bindFittedMouthAnchor=[&]( ++ const ResolvedLayoutStratumV2& host, ++ const std::array& before, ++ const std::array& after,const char* side) ++ { ++ if(before==after)return; ++ auto mutableHost=std::find_if(layout.strates.begin(),layout.strates.end(), ++ [&](const auto& stratum) ++ {return stratum.strateIndex==host.strateIndex;}); ++ if(mutableHost!=layout.strates.end()) ++ for(auto& anchor:mutableHost->anchors) ++ if((anchor.kind=="passage_arrival" ++ ||anchor.kind=="passage_departure") ++ &&anchor.values.size()>=3 ++ &&anchor.values[0]==after[0] ++ &&anchor.values[1]==after[1]) ++ { ++ anchor.values[2]=after[2]; ++ std::printf("[PassageLandingChamberArrivalBinding] id=%s side=%s anchor=%s status=PASS\n", ++ passage.stableId.c_str(),side,anchor.stableId.c_str()); ++ break; ++ } ++ }; ++ bindFittedMouthAnchor(upper,upperMouthBeforeBandFit,upperMouth,"upper"); ++ bindFittedMouthAnchor(lower,lowerMouthBeforeBandFit,lowerMouth,"lower"); + if(!radialWithin(upperMouth,placementRadius)||!radialWithin(lowerMouth,placementRadius)) + {error=passage.stableId+" route mouth is outside the resolved PassageConfig.DistanceMax radius";return false;} + passage.upperMouthSubvoxels=upperMouth;passage.lowerMouthSubvoxels=lowerMouth; +@@ -5048,6 +5114,91 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t + surfaceHorizontalSquared)}; + } + } ++ if(host.generatorKind=="FloatingIslands"&&side[0]=='u') ++ { ++ // The island route is authored as player-center heights. Keep ++ // the chamber center bound to passage h(s), while selecting the ++ // closest grade plane that leaves the audited lower capsule arc ++ // above every receiving route point under the chamber footprint. ++ const auto route=routePoints(host); ++ const std::int64_t routeRadius=chamber.radiusSubvoxels ++ +static_cast(std::llround(0.34*qPerMetre)); ++ const std::int64_t routeRadiusSquared=routeRadius*routeRadius; ++ const std::int64_t footClearanceQ=static_cast( ++ std::llround((0.84+0.025)*qPerMetre)); ++ struct FRouteFloorConstraint ++ { ++ std::int64_t dx=0,dy=0,maximumRiseQ16=0; ++ }; ++ std::vector constraints; ++ constraints.reserve(route.size()); ++ for(const auto& point:route) ++ { ++ const auto dx=static_cast(std::llround( ++ point[0]-chamber.floorCenterSubvoxels[0])); ++ const auto dy=static_cast(std::llround( ++ point[1]-chamber.floorCenterSubvoxels[1])); ++ if(dx*dx+dy*dy>routeRadiusSquared)continue; ++ const auto routeFloorMaximum=static_cast( ++ std::llround(point[2]))-footClearanceQ; ++ constraints.push_back({dx,dy, ++ (routeFloorMaximum-chamber.floorCenterSubvoxels[2])*65536}); ++ } ++ const auto originalGradient=chamber.floorGradientQ16; ++ for(std::size_t pass=0;pass<256&&!constraints.empty();++pass) ++ { ++ bool adjusted=false; ++ for(const auto& constraint:constraints) ++ { ++ const std::int64_t distanceSquared=constraint.dx*constraint.dx ++ +constraint.dy*constraint.dy; ++ if(distanceSquared<=0)continue; ++ const std::int64_t rise= ++ static_cast(chamber.floorGradientQ16[0]) ++ *constraint.dx ++ +static_cast(chamber.floorGradientQ16[1]) ++ *constraint.dy; ++ const std::int64_t excess=rise-constraint.maximumRiseQ16; ++ if(excess<=0)continue; ++ const auto correction=[&](std::int64_t delta) ++ { ++ if(delta==0)return std::int64_t{0}; ++ const std::uint64_t product=static_cast(excess) ++ *static_cast(std::abs(delta)); ++ return static_cast((product ++ +static_cast(distanceSquared)-1) ++ /static_cast(distanceSquared)); ++ }; ++ const auto correctionX=correction(constraint.dx); ++ const auto correctionY=correction(constraint.dy); ++ chamber.floorGradientQ16[0]-=static_cast( ++ constraint.dx<0?-correctionX:correctionX); ++ chamber.floorGradientQ16[1]-=static_cast( ++ constraint.dy<0?-correctionY:correctionY); ++ adjusted|=correctionX!=0||correctionY!=0; ++ } ++ if(!adjusted)break; ++ } ++ for(const auto& constraint:constraints) ++ { ++ const std::int64_t rise= ++ static_cast(chamber.floorGradientQ16[0]) ++ *constraint.dx ++ +static_cast(chamber.floorGradientQ16[1]) ++ *constraint.dy; ++ if(rise>constraint.maximumRiseQ16) ++ {error=passage.stableId ++ +" upper landing floor cannot clear its receiving FloatingIslands route"; ++ return false;} ++ } ++ if(chamber.floorGradientQ16!=originalGradient) ++ std::printf("[PassageLandingChamberRouteFit] id=%s side=upper host=FloatingIslands route_points=%zu fitted_constraints=%zu source_gradient_q16=(%lld,%lld) fitted_gradient_q16=(%lld,%lld) foot_clearance_m=0.865 max_grade_deg=15.000 status=PASS\n", ++ passage.stableId.c_str(),route.size(),constraints.size(), ++ static_cast(originalGradient[0]), ++ static_cast(originalGradient[1]), ++ static_cast(chamber.floorGradientQ16[0]), ++ static_cast(chamber.floorGradientQ16[1])); ++ } + const std::uint64_t gradientSquared=static_cast( + chamber.floorGradientQ16[0]*chamber.floorGradientQ16[0] + +chamber.floorGradientQ16[1]*chamber.floorGradientQ16[1]); +diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h +index 6b1a6af..c20b0ce 100644 +--- a/Standalone/Core/src/ShapeWorldInternal.h ++++ b/Standalone/Core/src/ShapeWorldInternal.h +@@ -1280,11 +1280,32 @@ public: + double unionDistance=std::numeric_limits::infinity(); + double unionShellDistance=std::numeric_limits::infinity(); + double unionFloorShellDistance=std::numeric_limits::infinity(); ++ double unionLandingChamberDistance=std::numeric_limits::infinity(); + double unionLandingChamberFloorShellDistance= + std::numeric_limits::infinity(); + double plannedTunnelPassageShellDistance=std::numeric_limits::infinity(); + const std::uint32_t reliefSeed=static_cast(Owner?Owner->Seed:0); + const auto* queryStratum=FindBoundStratumForManagerZ(Z); ++ const auto effectiveLandingFloor=[&]( ++ const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber, ++ double plannedFloorZ,double sampleXSubvoxels,double sampleYSubvoxels) ++ { ++ if(!std::isfinite(sampleXSubvoxels)||!std::isfinite(sampleYSubvoxels)) ++ return plannedFloorZ; ++ double nativeGroundCoreVoxels=0.0; ++ if(!GetNativeSurfaceGroundHeightAt( ++ static_cast(sampleXSubvoxels/Q), ++ static_cast(sampleYSubvoxels/Q), ++ static_cast(plannedFloorZ/Q),nativeGroundCoreVoxels)) ++ return plannedFloorZ; ++ const double clearHeight=static_cast( ++ chamber.clearHeightSubvoxels); ++ const double nativeFloor=nativeGroundCoreVoxels*Q; ++ // A native surface above the chamber's clear-height budget denotes a ++ // different layer surface, not a landing floor to clear from this room. ++ return nativeFloor>plannedFloorZ&&nativeFloor<=plannedFloorZ+clearHeight ++ ?nativeFloor:plannedFloorZ; ++ }; + const bool plannedTunnelStratum=queryStratum&&queryStratum->Resolved + &&(queryStratum->Resolved->generatorKind=="TunnelNetwork" + ||queryStratum->Resolved->generatorKind=="VerticalShafts") +@@ -1339,10 +1360,12 @@ public: + landingChamber->floorCenterSubvoxels[0]); + const double centerY=static_cast( + landingChamber->floorCenterSubvoxels[1]); +- const double floorCenterZ=static_cast( ++ const double plannedFloorCenterZ=static_cast( + landingChamber->floorCenterSubvoxels[2]) + +(landingChamber->floorGradientQ16[0]*(qx-centerX) + +landingChamber->floorGradientQ16[1]*(qy-centerY))/65536.0; ++ const double floorCenterZ=effectiveLandingFloor(*landingChamber, ++ plannedFloorCenterZ,qx,qy); + const double radialDistance=DeterministicSqrt( + (qx-centerX)*(qx-centerX)+(qy-centerY)*(qy-centerY)) + -landingChamber->radiusSubvoxels; +@@ -1357,6 +1380,8 @@ public: + std::max(-floorDistance,floorDistance-shellThickness), + radialDistance); + unionDistance=std::min(unionDistance,chamberDistance); ++ unionLandingChamberDistance=std::min(unionLandingChamberDistance, ++ chamberDistance); + unionShellDistance=std::min(unionShellDistance,chamberShellDistance); + unionFloorShellDistance=std::min(unionFloorShellDistance, + chamberFloorShellDistance); +@@ -1746,15 +1771,18 @@ public: + departureTailDistance) + ||nearRouteSegment(*lastTwo,*last,PortalWalkLeadMetres,true,0.0); + }; +- const auto connectedSurfaceLandingShellContains=[&]() ++ const auto connectedNativeLandingShellContains=[&]() + { + if(!queryStratum||!queryStratum->Resolved +- ||queryStratum->Resolved->generatorKind!="SurfaceWorld")return false; ++ ||(queryStratum->Resolved->generatorKind!="SurfaceWorld" ++ &&queryStratum->Resolved->generatorKind!="FloatingIslands")) ++ return false; + const double queryX=qx/qPerMetre,queryY=qy/qPerMetre,queryZ=qz/qPerMetre; + constexpr double ChamberShellThicknessMetres=0.75; + constexpr double RouteClearanceMetres=1.25; + constexpr double RouteHeightClearanceMetres=1.5; +- constexpr double PortalWalkLeadMetres=9.0; ++ const double portalWalkLeadMetres= ++ queryStratum->Resolved->generatorKind=="FloatingIslands"?12.0:9.0; + for(const auto& passage:ResolvedLayout->passages) + { + if(passage.upperStrateIndex!=queryStratum->StrateIndex)continue; +@@ -1762,9 +1790,13 @@ public: + if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0)continue; + const double cx=static_cast(chamber.floorCenterSubvoxels[0])/qPerMetre; + const double cy=static_cast(chamber.floorCenterSubvoxels[1])/qPerMetre; +- const double floorZ=static_cast(chamber.floorCenterSubvoxels[2])/qPerMetre ++ const double plannedFloorZ=static_cast( ++ chamber.floorCenterSubvoxels[2])/qPerMetre + +chamber.floorGradientQ16[0]/65536.0*(queryX-cx) + +chamber.floorGradientQ16[1]/65536.0*(queryY-cy); ++ const double floorZ=effectiveLandingFloor(chamber, ++ plannedFloorZ*qPerMetre,queryX*qPerMetre, ++ queryY*qPerMetre)/qPerMetre; + const double radialDistance=std::hypot(queryX-cx,queryY-cy) + -static_cast(chamber.radiusSubvoxels)/qPerMetre; + const double floorDistance=floorZ-queryZ; +@@ -1784,7 +1816,11 @@ public: + const double mouthZ=static_cast(passage.upperMouthSubvoxels[2]) + /qPerMetre; + std::size_t endpoint=route.size(); +- double endpointDistanceSquared=1.0; ++ const double endpointRadius= ++ queryStratum->Resolved->generatorKind=="FloatingIslands"?3.0:1.0; ++ const double endpointHeight= ++ queryStratum->Resolved->generatorKind=="FloatingIslands"?2.0:1.5; ++ double endpointDistanceSquared=endpointRadius*endpointRadius; + for(std::size_t index=0;indexvalues[0]/qPerMetre; +@@ -1793,7 +1829,7 @@ public: + const double dx=x-cx,dy=y-cy; + const double distanceSquared=dx*dx+dy*dy; + if(distanceSquaredPortalWalkLeadMetres)continue; ++ if(alongFromEndpoint>portalWalkLeadMetres)continue; + const double routeX=ax+t*dx,routeY=ay+t*dy; + const double routeZ=az+t*(b.values[2]-a.values[2])/qPerMetre; +- if(std::hypot(queryX-routeX,queryY-routeY)<=RouteClearanceMetres +- &&std::abs(queryZ-routeZ)<=RouteHeightClearanceMetres)return true; ++ const double lateral=std::hypot(queryX-routeX,queryY-routeY); ++ const double vertical=std::abs(queryZ-routeZ); ++ if(lateral<=RouteClearanceMetres ++ &&vertical<=RouteHeightClearanceMetres)return true; + } + } + return false; +@@ -1887,13 +1925,19 @@ public: + } + if(nativeFloorSupport!=std::numeric_limits::lowest() + &&nativeFloorSupport<0.0f) ++ { ++ // Host floor support keeps solid ground under a landing chamber, but it ++ // cannot refill the chamber air above the selected planned/native floor. ++ if(unionLandingChamberDistance<=0.0) ++ return passageAirDistance; + return std::min(nativeFloorSupport,unionDistance<=0.0 + ?passageAirDistance:density); ++ } + if(nativeTunnelFieldDensity!=std::numeric_limits::lowest()) + return std::max(density,passageAirDistance); + if(unionDistance>0.0&&unionShellDistance<0.0&&density>=0.0f) + { +- if(connectedSurfaceLandingShellContains()) ++ if(connectedNativeLandingShellContains()) + return std::max(density,passageAirDistance); + return static_cast(unionShellDistance/Q); + } +@@ -2044,11 +2088,12 @@ public: + static_cast(chamber.floorGradientQ16[0])/65536.0, + static_cast(chamber.floorGradientQ16[1])/65536.0); + const double extent=reachCoreVoxels+0.01; ++ const double clearHeight=static_cast( ++ chamber.clearHeightSubvoxels)/Q; + const double low[3]={cx-radius-extent,cy-radius-extent, + cz-floorExtent-extent}; + const double high[3]={cx+radius+extent,cy+radius+extent, +- cz+floorExtent+static_cast( +- chamber.clearHeightSubvoxels)/Q+extent}; ++ cz+floorExtent+2.0*clearHeight+extent}; + return overlaps(low,high); + }; + if(chamberOverlaps(passage.upperLandingChamber) +@@ -5488,7 +5533,7 @@ private: + cx-radius-extent,cy-radius-extent,cz-floorExtent-extent}; + const std::array maximum{ + cx+radius+extent,cy+radius+extent, +- cz+floorExtent+height+extent}; ++ cz+floorExtent+2.0*height+extent}; + FPassageProofBound proof; + for(std::size_t axis=0;axis<3;++axis) + { +@@ -5859,6 +5904,71 @@ public: + return std::isfinite(OutGroundZ)&&std::isfinite(OutCeilingZ); + } + ++ bool GetNativeSurfaceGroundHeightAt(float X,float Y,float Z, ++ double& OutGroundZ) const ++ { ++ if(!ResolvedLayout||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z)) ++ return false; ++ const auto* bound=FindBoundStratumForManagerZ(Z); ++ if(!bound)return false; ++ constexpr double Scale=static_cast( ++ VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); ++ const double maxVoxel=static_cast(std::numeric_limits::max())/Scale; ++ if(std::abs(static_cast(X))>maxVoxel ++ ||std::abs(static_cast(Y))>maxVoxel)return false; ++ const auto toFixed=[](float value) ++ {return static_cast(std::llround(static_cast(value) ++ *VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));}; ++ const auto* field=std::get_if(&bound->Evaluator); ++ if(field&&field->Evaluator.isValid()) ++ { ++ const auto column=field->Evaluator.point(toFixed(X),toFixed(Y),0); ++ OutGroundZ=static_cast(column.groundHeightSubvoxels)/Scale; ++ return std::isfinite(OutGroundZ); ++ } ++ const auto* islands=std::get_if(&bound->Evaluator); ++ if(!islands||!bound->IslandPlan)return false; ++ const auto& route=bound->IslandPlan->manifest.plan.route; ++ if(route.size()<2)return false; ++ const auto& recipe=bound->IslandPlan->manifest.recipe; ++ const std::int64_t routeLift=recipe.routeConstraints.capsuleHeightSubvoxels/2 ++ +recipe.routeConstraints.minimumClearanceSubvoxels ++ +(recipe.geometryVersion>=2 ++ ?VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel/8:0); ++ const std::int64_t supportRadius=recipe.routeConstraints ++ .supportProbeRadiusSubvoxels; ++ if(routeLift<0||supportRadius<0)return false; ++ const std::int64_t queryX=toFixed(X),queryY=toFixed(Y); ++ std::uint64_t nearestDistanceSquared=std::numeric_limits::max(); ++ std::int64_t nearestRouteZ=0; ++ bool found=false; ++ for(std::size_t index=1;index( ++ offsetX*offsetX+offsetY*offsetY); ++ if(distanceSquared>static_cast(supportRadius)*supportRadius ++ ||distanceSquared>=nearestDistanceSquared)continue; ++ const std::int64_t dz=b[2]-a[2]; ++ nearestRouteZ=a[2]+dz*projection/lengthSquared; ++ nearestDistanceSquared=distanceSquared; ++ found=true; ++ } ++ if(!found)return false; ++ OutGroundZ=static_cast(nearestRouteZ-routeLift)/Scale; ++ return std::isfinite(OutGroundZ); ++ } ++ + EVoxelTileClass ClassifyBox(const FIntVector& Origin,int32 Step,int32 Cells) const + { return ClassifyBoxDetailed(Origin,Step,Cells).Verdict; } + +diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp +index b139ec4..e40250c 100644 +--- a/Standalone/Core/src/VoxelForgeCore.cpp ++++ b/Standalone/Core/src/VoxelForgeCore.cpp +@@ -1811,6 +1811,13 @@ namespace VoxelForgeCore + + float World::densityAt(Vec3d position) const + { return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAt(static_cast(position.x),static_cast(position.y),static_cast(position.z)):0.0f; } ++ bool World::nativeSurfaceGroundHeightAt(Vec3d position,double& groundHeightVoxels) const ++ { ++ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape ++ &&impl_->shape->GetNativeSurfaceGroundHeightAt(static_cast(position.x), ++ static_cast(position.y),static_cast(position.z), ++ groundHeightVoxels); ++ } + MaterialSampleV1 World::materialAt(Vec3d position) const + { + if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape) +diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h +index 2fcf4b5..3ed4dd1 100644 +--- a/Standalone/Source/StreamBenchmarkPipeline.h ++++ b/Standalone/Source/StreamBenchmarkPipeline.h +@@ -3051,7 +3051,8 @@ namespace StandaloneStreamBenchmark + double supportMaximumMetres,std::uint32_t minimumSupportProbes, + double supportProbeRadiusMetres, + const std::function& supportExemption={}, +- const std::function& passageInteriorPredicate={}) ++ const std::function& passageInteriorPredicate={}, ++ const std::function& densityOverride={}) + { + FWalkAudit audit; + if(path.size()<2)return audit; +@@ -3068,15 +3069,18 @@ namespace StandaloneStreamBenchmark + &&passageInteriorPredicate(xMetres,yMetres,zMetres); + if(inPassageInterior)++audit.passageInteriorCapsuleSamples; + const VoxelForgeCore::Vec3d position{xMetres/coreMetres,yMetres/coreMetres,zMetres/coreMetres}; +- const float density=Field.densityAt(position); ++ const float density=densityOverride ++ ?densityOverride(position):Field.densityAt(position); + if(density<0.0f) + { + if(inPassageInterior)++audit.passageInteriorSolidSamples; + if(audit.firstBlockedCount::max(), + std::size_t ignoredSegment=std::numeric_limits::max(), +- std::size_t selectedPassage=std::numeric_limits::max()) ++ std::size_t selectedPassage=std::numeric_limits::max(), ++ bool includeLandingChambers=true) + { + constexpr double CornerBlendMetres=0.50; + constexpr double WallReliefMetres=0.21; +@@ -3748,8 +3761,9 @@ namespace StandaloneStreamBenchmark + +qToMetres(chamber.clearHeightSubvoxels)); + return std::max({radialDistance,floorDistance,ceilingDistance})<=0.0; + }; +- if(inLandingChamber(passage.upperLandingChamber) +- ||inLandingChamber(passage.lowerLandingChamber))return true; ++ if(includeLandingChambers ++ &&(inLandingChamber(passage.upperLandingChamber) ++ ||inLandingChamber(passage.lowerLandingChamber)))return true; + const auto& route=allPassageSmoothRoutes[passageIndex]; + if(route.size()<2)continue; + double bestRoundDistance=std::numeric_limits::infinity(); +@@ -4722,7 +4736,13 @@ namespace StandaloneStreamBenchmark + const double mouthZ=qToMetres(upper?passage.upperMouthSubvoxels[2] + :passage.lowerMouthSubvoxels[2]); + std::size_t endpoint=route.size(); +- double endpointDistanceSquared=1.0; ++ const double endpointRadius=stratum->generatorKind=="FloatingIslands" ++ ?3.0:1.0; ++ const double endpointHeight=stratum->generatorKind=="FloatingIslands" ++ ?2.0:1.5; ++ const double portalWalkLead=stratum->generatorKind=="FloatingIslands" ++ ?12.0:9.0; ++ double endpointDistanceSquared=endpointRadius*endpointRadius; + for(std::size_t index=0;indexvalues[0])-cx; +@@ -4730,7 +4750,7 @@ namespace StandaloneStreamBenchmark + const double z=qToMetres(route[index]->values[2]); + const double distanceSquared=dx*dx+dy*dy; + if(distanceSquared9.0)continue; ++ if(fromEndpoint>portalWalkLead)continue; + const double routeX=ax+t*dx,routeY=ay+t*dy; + const double routeZ=az+t*qToMetres(b.values[2]-a.values[2]); + const double lateralDistance=std::hypot(xMetres-routeX, +@@ -5167,6 +5187,21 @@ namespace StandaloneStreamBenchmark + const double radius=qToMetres(chamber.radiusSubvoxels); + const double gx=chamber.floorGradientQ16[0]/65536.0; + const double gy=chamber.floorGradientQ16[1]/65536.0; ++ const auto floorAt=[&](double x,double y) ++ { ++ const double planned=baseFloorZ+gx*(x-centerX)+gy*(y-centerY); ++ double nativeGroundVoxels=0.0; ++ if(Field.nativeSurfaceGroundHeightAt({x/coreMetres,y/coreMetres, ++ planned/coreMetres},nativeGroundVoxels)) ++ { ++ const double nativeGround=nativeGroundVoxels*coreMetres; ++ const double clearHeight=qToMetres( ++ chamber.clearHeightSubvoxels); ++ if(nativeGround>planned&&nativeGround<=planned+clearHeight) ++ return nativeGround; ++ } ++ return planned; ++ }; + for(const double radial:chamberFloorRadii) + { + if(radial>=radius-1.0)continue; +@@ -5177,16 +5212,18 @@ namespace StandaloneStreamBenchmark + *static_cast(angleIndex)/ChamberAngularSamples; + const double sampleX=centerX+radial*std::cos(angle); + const double sampleY=centerY+radial*std::sin(angle); +- const double floorZ=baseFloorZ+gx*(sampleX-centerX) +- +gy*(sampleY-centerY); ++ const double floorZ=floorAt(sampleX,sampleY); + for(const double depth:PassageShellAuditDepthsMetres) + { + const double sampleZ=floorZ-depth; + const VoxelForgeCore::Vec3d position{sampleX/coreMetres, + sampleY/coreMetres,sampleZ/coreMetres}; + if(Native.densityAt(position)<0.0f)continue; +- if(landingChamberConnectionContains(passage,upper, +- sampleX,sampleY,sampleZ)) ++ if(passageInteriorWinsAt(sampleX,sampleY,sampleZ, ++ std::numeric_limits::max(), ++ std::numeric_limits::max(),passageIndex) ++ ||landingChamberConnectionContains(passage,upper, ++ sampleX,sampleY,sampleZ)) + {++audit.chamberConnectionSkips;continue;} + ++audit.chamberFloorSamples; + const float density=Field.densityAt(position); +@@ -5211,8 +5248,7 @@ namespace StandaloneStreamBenchmark + { + const double sampleX=centerX+(radius+depth)*nx; + const double sampleY=centerY+(radius+depth)*ny; +- const double floorZ=baseFloorZ+gx*(sampleX-centerX) +- +gy*(sampleY-centerY); ++ const double floorZ=floorAt(sampleX,sampleY); + const double sampleZ=floorZ+height; + const VoxelForgeCore::Vec3d position{sampleX/coreMetres, + sampleY/coreMetres,sampleZ/coreMetres}; +@@ -5275,6 +5311,25 @@ namespace StandaloneStreamBenchmark + {return passageInteriorWinsAt(x,y,z, + std::numeric_limits::max(), + std::numeric_limits::max(),passageIndex);}; ++ const auto floorAt=[&](double x,double y) ++ { ++ const double planned=centerZ+gx*(x-centerX)+gy*(y-centerY); ++ double nativeGroundVoxels=0.0; ++ if(field.nativeSurfaceGroundHeightAt({x/coreMetres,y/coreMetres, ++ planned/coreMetres},nativeGroundVoxels)) ++ { ++ const double nativeGround=nativeGroundVoxels*coreMetres; ++ const double clearHeight=qToMetres(chamber.clearHeightSubvoxels); ++ if(nativeGround>planned&&nativeGround<=planned+clearHeight) ++ return nativeGround; ++ } ++ return planned; ++ }; ++ const std::function supportExemption= ++ [&](double x,double y,double z) ++ {return passageInteriorWinsAt(x,y,z, ++ std::numeric_limits::max(), ++ std::numeric_limits::max(),passageIndex,false);}; + for(const auto& direction:directions) + { + std::vector> chamberPath; +@@ -5286,12 +5341,12 @@ namespace StandaloneStreamBenchmark + const double distance=radius*step/intervals; + const double x=centerX+direction[0]*distance; + const double y=centerY+direction[1]*distance; +- const double floorZ=centerZ+gx*(x-centerX)+gy*(y-centerY); ++ const double floorZ=floorAt(x,y); + chamberPath.push_back({x/coreMetres,y/coreMetres, + (floorZ+1.13)/coreMetres}); + } + const FWalkAudit part=auditPlayerPath(field,chamberPath, +- 0.125,0.125,1.125,3,0.45,{},interiorPredicate); ++ 0.125,0.125,1.125,3,0.45,supportExemption,interiorPredicate); + total.segments+=part.segments;total.centers+=part.centers; + total.capsuleSamples+=part.capsuleSamples; + total.blocked+=part.blocked; +@@ -5305,10 +5360,116 @@ namespace StandaloneStreamBenchmark + } + return total; + }; ++ const auto auditOpenChamberFloorNegativeControl=[&]( ++ const VoxelForgeCore::World& field, ++ const VoxelForgeCore::ResolvedLayoutPassageV1& passage, ++ std::size_t passageIndex,bool upper) ++ { ++ const auto& chamber=upper?passage.upperLandingChamber ++ :passage.lowerLandingChamber; ++ const double centerX=qToMetres(chamber.floorCenterSubvoxels[0]); ++ const double centerY=qToMetres(chamber.floorCenterSubvoxels[1]); ++ const double centerZ=qToMetres(chamber.floorCenterSubvoxels[2]); ++ const double gx=chamber.floorGradientQ16[0]/65536.0; ++ const double gy=chamber.floorGradientQ16[1]/65536.0; ++ const double chamberAuditRadius=std::min(5.75, ++ qToMetres(chamber.radiusSubvoxels)-1.5); ++ const double mouthRadius=qToMetres(passage.mouthRadiusSubvoxels); ++ const double radius=std::min(chamberAuditRadius-0.1,mouthRadius+1.25); ++ if(radius<=0.0) ++ { ++ std::printf("[PassageLandingChamberOpenFloorNegativeControl] id=%s side=%s radius_m=%.3f mouth_radius_m=%.3f unsupported=0 support_overlap_skips=0 expected=FAIL status=FAIL reason=no_outer_floor_sample\n", ++ passage.stableId.c_str(),upper?"upper":"lower",radius,mouthRadius); ++ return false; ++ } ++ const auto floorAt=[&](double x,double y) ++ { ++ const double planned=centerZ+gx*(x-centerX)+gy*(y-centerY); ++ double nativeGroundVoxels=0.0; ++ if(field.nativeSurfaceGroundHeightAt({x/coreMetres,y/coreMetres, ++ planned/coreMetres},nativeGroundVoxels)) ++ { ++ const double nativeGround=nativeGroundVoxels*coreMetres; ++ const double clearHeight=qToMetres(chamber.clearHeightSubvoxels); ++ if(nativeGround>planned&&nativeGround<=planned+clearHeight) ++ return nativeGround; ++ } ++ return planned; ++ }; ++ const std::function interiorPredicate= ++ [&](double x,double y,double z) ++ {return passageInteriorWinsAt(x,y,z, ++ std::numeric_limits::max(), ++ std::numeric_limits::max(),passageIndex);}; ++ const std::function supportExemption= ++ [&](double x,double y,double z) ++ {return passageInteriorWinsAt(x,y,z, ++ std::numeric_limits::max(), ++ std::numeric_limits::max(),passageIndex,false);}; ++ constexpr std::array,5> supportOffsets{{ ++ {{0.0,0.0}},{{0.45,0.0}},{{-0.45,0.0}}, ++ {{0.0,0.45}},{{0.0,-0.45}}}}; ++ std::vector> path; ++ double selectedAngle=0.0; ++ bool foundOutsideBore=false; ++ for(std::size_t directionIndex=0;directionIndex<32&&!foundOutsideBore; ++ ++directionIndex) ++ { ++ selectedAngle=2.0*3.14159265358979323846 ++ *static_cast(directionIndex)/32.0; ++ const double nx=std::cos(selectedAngle),ny=std::sin(selectedAngle); ++ const double tangentX=-ny,tangentY=nx; ++ const double x0=centerX+radius*nx-0.25*tangentX; ++ const double y0=centerY+radius*ny-0.25*tangentY; ++ const double x1=centerX+radius*nx+0.25*tangentX; ++ const double y1=centerY+radius*ny+0.25*tangentY; ++ const double floor0=floorAt(x0,y0),floor1=floorAt(x1,y1); ++ const std::array,2> candidate{{ ++ {{x0,y0,floor0+1.13}},{{x1,y1,floor1+1.13}}}}; ++ bool samplesOutsideBore=true; ++ for(const auto& point:candidate) ++ { ++ const double supportZ=point[2]-0.88-0.125; ++ for(const auto& offset:supportOffsets) ++ if(supportExemption(point[0]+offset[0], ++ point[1]+offset[1],supportZ)) ++ {samplesOutsideBore=false;break;} ++ if(!samplesOutsideBore)break; ++ } ++ if(samplesOutsideBore) ++ { ++ path={{candidate[0][0]/coreMetres,candidate[0][1]/coreMetres, ++ candidate[0][2]/coreMetres}, ++ {candidate[1][0]/coreMetres,candidate[1][1]/coreMetres, ++ candidate[1][2]/coreMetres}}; ++ foundOutsideBore=true; ++ } ++ } ++ if(!foundOutsideBore) ++ { ++ std::printf("[PassageLandingChamberOpenFloorNegativeControl] id=%s side=%s radius_m=%.3f mouth_radius_m=%.3f unsupported=0 support_overlap_skips=0 expected=FAIL status=FAIL reason=no_bore_clear_spoke\n", ++ passage.stableId.c_str(),upper?"upper":"lower",radius,mouthRadius); ++ return false; ++ } ++ const std::function openDensity= ++ [](VoxelForgeCore::Vec3d){return 1.0f;}; ++ const FWalkAudit control=auditPlayerPath(field,path,0.125,0.125, ++ 1.125,3,0.45,supportExemption,interiorPredicate,openDensity); ++ const bool passed=!control.passed&&control.unsupported>0 ++ &&control.supportOverlapSkips==0; ++ std::printf("[PassageLandingChamberOpenFloorNegativeControl] id=%s side=%s radius_m=%.3f mouth_radius_m=%.3f angle_deg=%.1f unsupported=%llu support_overlap_skips=%llu result=%s expected=FAIL status=%s\n", ++ passage.stableId.c_str(),upper?"upper":"lower",radius,mouthRadius, ++ selectedAngle*180.0/3.14159265358979323846, ++ static_cast(control.unsupported), ++ static_cast(control.supportOverlapSkips), ++ control.passed?"PASS":"FAIL",passed?"PASS":"FAIL"); ++ return passed; ++ }; + std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0; + std::uint64_t passageInteriorCapsuleSamples=0,passageInteriorSolidSamples=0; + std::uint64_t passageInteriorControls=0; + std::uint64_t landingChamberCount=0,landingChamberNegativeControls=0; ++ std::uint64_t openChamberFloorNegativeControls=0; + std::uint64_t mouthProfilePasses=0,ripplePasses=0,rippleControlsDetected=0; + std::uint64_t floorRippleControlsDetected=0,floorStepRippleControlsDetected=0; + std::uint64_t shellAudits=0,shellFloorSamples=0,shellFloorViolations=0; +@@ -5346,6 +5507,9 @@ namespace StandaloneStreamBenchmark + passage,passageIndex,true); + const FWalkAudit lowerChamberAudit=auditLandingChamber(Shape, + passage,passageIndex,false); ++ const bool openChamberFloorControl=auditOpenChamberFloorNegativeControl( ++ Shape,passage,passageIndex,true); ++ openChamberFloorNegativeControls+=openChamberFloorControl; + const bool landingChambersWalkable=upperChamberAudit.passed + &&lowerChamberAudit.passed; + const bool passageInteriorAirPassed= +@@ -5889,6 +6053,10 @@ namespace StandaloneStreamBenchmark + static_cast(landingChamberCount), + static_cast(landingChamberCount + -landingChamberNegativeControls)); ++ std::printf("[PassageLandingChamberOpenFloorNegativeControlSummary] expected_fail_detected=%llu/%llu status=%s\n", ++ static_cast(openChamberFloorNegativeControls), ++ static_cast(passageCount), ++ openChamberFloorNegativeControls==passageCount?"PASS":"FAIL"); + const bool shellAuditPassed=shellAudits==passageCount + &&shellFloorViolations==0&&shellWallViolations==0 + &&shellChamberFloorViolations==0&&shellChamberWallViolations==0 +@@ -5939,6 +6107,7 @@ namespace StandaloneStreamBenchmark + &&mouthProfilePasses==passageCount&&l23TerraceControlDetected + &&ripplePasses==passageCount + &&shellAuditPassed ++ &&openChamberFloorNegativeControls==passageCount + &&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings + &&routeBindingFailures==0 + &&tunnelNetworkRoutes==tunnelNetworkBlockedControls diff --git a/Standalone/Research/WorldAssemblerM3/Linux158/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux158/REPORT.md new file mode 100644 index 0000000..01bdf78 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux158/REPORT.md @@ -0,0 +1,37 @@ +# L158 — Flat landing pad + +| Tunnel end | Before (L151) | After (L158) | Wide pad view (L158) | +|---|---|---|---| +| passage/0-1/0 · upper | ![L151 upper landing, passage 0-1](../Linux151/captures/after-l151-0-1-0-upper-landing.jpg) | Not captured: seed 0 stops during pad planning. | Not captured. | +| passage/0-1/0 · lower | ![L151 lower landing, passage 0-1](../Linux151/captures/after-l151-0-1-0-lower-landing.jpg) | Not captured: seed 0 stops during pad planning. | Not captured. | +| passage/1-2/0 · upper | ![L151 upper landing, passage 1-2](../Linux151/captures/after-l151-1-2-0-upper-landing.jpg) | Not captured: seed 0 stops during pad planning. | Not captured. | +| passage/1-2/0 · lower | ![L151 lower landing, passage 1-2](../Linux151/captures/after-l151-1-2-0-lower-landing.jpg) | Not captured: seed 0 stops during pad planning. | Not captured. | +| passage/2-3/0 · upper | ![L151 upper landing, passage 2-3](../Linux151/captures/after-l151-2-3-0-upper-landing.jpg) | Not captured: seed 0 stops during pad planning. | Not captured. | +| passage/2-3/0 · lower | ![L151 lower landing, passage 2-3](../Linux151/captures/after-l151-2-3-0-lower-landing.jpg) | Not captured: seed 0 stops during pad planning. | Not captured. | + +## Result + +**L158 is stopped and not shipped.** Three planner attempts failed the seed-0 fast fixture. The last one could not find a lower pad floor satisfying both its edge-ring interval and the route transition. The pad stamp never reached composed-field evaluation, so this is not evidence that the clamp formula itself fails. It does meet the requested stop condition; I did not continue tuning it. + +The attempted v240 source edits were restored to `wip/l134-cave-planner` HEAD. No baselines were changed. The pre-existing untracked `Linux18/exports/` directory was left untouched. + +## What failed + +- Attempt 1 clamped both route ends without limiting each clamp to its own `R+B`; strict validation found an upper mouth binding error and route grades above 15°. +- Attempt 2 limited route rewrites to each pad’s `R+B`; strict validation still found route grades above 15° at the transition. +- Attempt 3 linearly rewrote the route profile between the flat-disc boundary and an outside route point, and checked that point during site fitting. The fast fixture then failed before snapshot creation: `lower landing pad has no feasible floor on its edge ring or next route site`. + +These attempts and the final failed log are under `Saved/Linux/l158/fast/`. The final run used source stamp `447f2cb268a0b3a06c1994f6c58914f698e9ea66d686bd16e78f20ec72233c81`. + +## Checks and measurements + +- Clang 19 candidate build passed with `--fp-contract=off --jobs=4`, CPUs 0–11, and the 6 GiB address-space limit. +- Fast tier failed at `owner-floating-islands-style2-clang` with exit code 2. The [log](../../../../Saved/Linux/l158/fast/owner-floating-islands-style2-clang.log) records the lower-pad planning failure; the [tier summary](../../../../Saved/Linux/l158/fast/fast-summary.json) records the failed gate. +- `git diff --check` passed after restoring the candidate source. The worktree has no tracked source changes. +- No performance timings were taken, so there is no timing window or L80 result to report. + +The 16-seed Tunnel Network and Vertical Shafts sweeps, `LandingPadAudit` and its four controls, interior-air/shell/seam/route/envelope gates, Clang/GCC W1/W8/W12, L80 for all four layers, UE start and 30 m/s run, and L158 captures were not run. No evaluator-version bump or baselines were retained. No Unreal `Source/` files changed. + +## Remaining risk + +The requested planner constraints remain unresolved. In particular, the last check coupled the pad edge-ring floor to an outside passage-route height and had no feasible lower site on the fast fixture. Since planning stopped before field composition, the clamp’s actual terrain blend, bore mask, bounds, and seed-sweep behavior remain unmeasured. diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/PassageEnvelopeAudit.py b/Standalone/Research/WorldAssemblerM3/Linux159/PassageEnvelopeAudit.py new file mode 100644 index 0000000..90712da --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux159/PassageEnvelopeAudit.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +"""Audit cave primitive bounds against passage envelopes, allowing chamber joints only.""" +from __future__ import annotations + +import math +from typing import Any + + +INFLUENCE_REACH_M = 1.053334 +SHELL_THICKNESS_M = 0.75 + + +def _point(control: dict[str, Any], scale: float) -> tuple[float, float, float, float]: + x, y, z = control["position_q"] + return (x * scale, y * scale, z * scale, control["radius_q"] * scale) + + +def _knot_span(a, b) -> float: + return max(1.0e-6, math.sqrt(math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2))) + + +def _evaluate(controls, span: int, t: float): + p1, p2 = controls[span], controls[span + 1] + p0 = controls[span - 1] if span else controls[0] + p3 = controls[min(span + 2, len(controls) - 1)] + dt0, dt1, dt2 = _knot_span(p0, p1), _knot_span(p1, p2), _knot_span(p2, p3) + t2, t3 = t * t, t * t * t + result = [] + for axis in (0, 1, 3): + m1 = dt1 * ((p1[axis] - p0[axis]) / dt0 + - (p2[axis] - p0[axis]) / (dt0 + dt1) + + (p2[axis] - p1[axis]) / dt1) + m2 = dt1 * ((p2[axis] - p1[axis]) / dt1 + - (p3[axis] - p1[axis]) / (dt1 + dt2) + + (p3[axis] - p2[axis]) / dt2) + value = ((2 * t3 - 3 * t2 + 1) * p1[axis] + + (-2 * t3 + 3 * t2) * p2[axis] + + (t3 - 2 * t2 + t) * m1 + (t3 - t2) * m2) + result.append(value) + radius = min(max(result[2], min(p1[3], p2[3])), max(p1[3], p2[3])) + return result[0], result[1], p1[2] + (p2[2] - p1[2]) * t, radius + + +def _segments(passage: dict[str, Any], scale: float): + controls = [_point(row, scale) for row in passage["control_points"]] + route_end = min(max(1, int(passage.get("lower_mouth_control_index", len(controls) - 1))), + len(controls) - 1) + along = 0.0 + for span in range(route_end): + a, b = controls[span], controls[span + 1] + chord = math.hypot(b[0] - a[0], b[1] - a[1]) + steps = max(1, math.ceil(chord / 2.0)) + prior = _evaluate(controls, span, 0.0) + for step in range(1, steps + 1): + current = _evaluate(controls, span, step / steps) + length = math.dist(prior[:3], current[:3]) + yield prior, current, along, max(prior[3], current[3]) + INFLUENCE_REACH_M + along += length + prior = current + + +def _distance_squared(a, b, bounds, t: float) -> float: + total = 0.0 + for axis in range(3): + pos = a[axis] + (b[axis] - a[axis]) * t + lo, hi = bounds[axis] + gap = lo - pos if pos < lo else pos - hi if pos > hi else 0.0 + total += gap * gap + return total + + +def _overlap_interval(a, b, bounds, radius: float): + cuts = {0.0, 1.0} + for axis in range(3): + delta = b[axis] - a[axis] + if abs(delta) < 1.0e-15: + continue + for edge in bounds[axis]: + t = (edge - a[axis]) / delta + if 0.0 < t < 1.0: + cuts.add(t) + cuts = sorted(cuts) + best_t, best_d2 = 0.0, math.inf + for lo, hi in zip(cuts, cuts[1:]): + mid = (lo + hi) * 0.5 + quadratic = linear = 0.0 + for axis in range(3): + delta = b[axis] - a[axis] + pos = a[axis] + delta * mid + low, high = bounds[axis] + edge = low if pos < low else high if pos > high else None + if edge is not None: + quadratic += delta * delta + linear += delta * (a[axis] - edge) + candidate = min(hi, max(lo, -linear / quadratic)) if quadratic else lo + d2 = _distance_squared(a, b, bounds, candidate) + if d2 < best_d2: + best_t, best_d2 = candidate, d2 + for t in (0.0, 1.0): + d2 = _distance_squared(a, b, bounds, t) + if d2 < best_d2: + best_t, best_d2 = t, d2 + if best_d2 > radius * radius: + return None + + radius2 = radius * radius + if _distance_squared(a, b, bounds, 0.0) <= radius2: + left = 0.0 + else: + low, high = 0.0, best_t + for _ in range(42): + mid = (low + high) * 0.5 + if _distance_squared(a, b, bounds, mid) <= radius2: + high = mid + else: + low = mid + left = high + if _distance_squared(a, b, bounds, 1.0) <= radius2: + right = 1.0 + else: + low, high = best_t, 1.0 + for _ in range(42): + mid = (low + high) * 0.5 + if _distance_squared(a, b, bounds, mid) <= radius2: + low = mid + else: + high = mid + right = low + return left, right, best_t + + +def _inside_chamber(point, chamber: dict[str, Any], scale: float) -> bool: + cx, cy, cz = (value * scale for value in chamber["floor_center_q"]) + radius = chamber["radius_q"] * scale + clear = chamber["clear_height_q"] * scale + gx, gy = (value / 65536.0 for value in chamber["floor_gradient_q16"]) + floor = cz + gx * (point[0] - cx) + gy * (point[1] - cy) + chamber_distance = max(math.hypot(point[0] - cx, point[1] - cy) - radius, + floor - point[2], point[2] - (floor + clear)) + return chamber_distance <= SHELL_THICKNESS_M + + +def _primitive_bounds(row: dict[str, Any]): + return tuple((row["bounds_min_um"][i] / 1.0e6, + row["bounds_max_um"][i] / 1.0e6) for i in range(3)) + + +def _landing_content_band_audit(layout: dict[str, Any]) -> list[dict[str, Any]]: + """Recheck the validator's complete graded floor, headroom and shell envelope.""" + voxel_um = int(layout["core_voxel_size_um"]) + q_per_metre = 256 * 1_000_000 / voxel_um + strata = {int(row["strate_index"]): row for row in layout.get("strates", [])} + + def q_to_um(q: int) -> int: + numerator = q * voxel_um + return numerator // 256 if numerator >= 0 else -((-numerator) // 256) + + result = [] + shell_q = math.floor(0.75 * q_per_metre + 0.5) + for passage in layout.get("passages", []): + for side, key, strate_key in ( + ("upper", "upper_landing_chamber", "upper_strate_index"), + ("lower", "lower_landing_chamber", "lower_strate_index")): + chamber = passage[key] + host = strata.get(int(passage[strate_key])) + content = host.get("content", {}) if host else {} + gradient = chamber["floor_gradient_q16"] + gradient_magnitude = math.isqrt(gradient[0] ** 2 + gradient[1] ** 2) + plane_reach_q = (int(chamber["radius_q"]) * gradient_magnitude + 65535) // 65536 + floor_q = int(chamber["floor_center_q"][2]) + minimum_um = q_to_um(floor_q - plane_reach_q - shell_q) + maximum_um = q_to_um(floor_q + plane_reach_q + + int(chamber["clear_height_q"]) + shell_q) + band_bottom_um, band_top_um = int(content.get("bottom_um", 0)), int( + content.get("top_um", 0)) + passed = bool(content.get("present", False) + and minimum_um >= band_bottom_um + and maximum_um <= band_top_um) + result.append({ + "passage": passage["stable_id"], "side": side, + "host_strate": int(passage[strate_key]), + "minimum_um": minimum_um, "maximum_um": maximum_um, + "content_bottom_um": band_bottom_um, "content_top_um": band_top_um, + "bottom_margin_m": (minimum_um - band_bottom_um) / 1_000_000.0, + "top_margin_m": (band_top_um - maximum_um) / 1_000_000.0, + "passed": passed, + }) + return result + + +def _bezier(points, t: float): + u = 1.0 - t + return tuple(u**3 * points[0][axis] + 3*u*u*t * points[1][axis] + + 3*u*t*t * points[2][axis] + t**3 * points[3][axis] + for axis in range(3)) + + +def _distance_to_ellipse(radial: float, vertical: float, + radius: float, height: float) -> float: + """Euclidean distance to the edge bore's horizontal/vertical ellipse.""" + radial = abs(radial) + vertical = abs(vertical) + if (radial / radius) ** 2 + (vertical / height) ** 2 <= 1.0: + return 0.0 + + lo, hi = 0.0, math.pi / 2.0 + ratio = (math.sqrt(5.0) - 1.0) / 2.0 + + def squared_distance(angle: float) -> float: + return ((radial - radius * math.cos(angle)) ** 2 + + (vertical - height * math.sin(angle)) ** 2) + + left = hi - ratio * (hi - lo) + right = lo + ratio * (hi - lo) + left_value, right_value = squared_distance(left), squared_distance(right) + for _ in range(48): + if left_value < right_value: + hi, right, right_value = right, left, left_value + left = hi - ratio * (hi - lo) + left_value = squared_distance(left) + else: + lo, left, left_value = left, right, right_value + right = lo + ratio * (hi - lo) + right_value = squared_distance(right) + return math.sqrt(min(left_value, right_value)) + + +def _actual_edge_clearance(passage, edge, scale: float, + chamber: dict[str, Any]) -> float: + """Return minimum true tube clearance outside the landing chamber. + + The route edge is a swept ellipse, with its recorded polyline as the axis. + Comparing the tunnel tube to that ellipse avoids the false clashes caused by + intersecting two enclosing boxes. + """ + controls = [tuple(value / 1.0e6 for value in point) + for point in edge["control_points_um"]] + axis = [tuple(value / 1.0e6 for value in point) + for point in edge.get("polyline_um", [])] + if len(axis) < 2: + axis = [_bezier(controls, index / 64.0) for index in range(65)] + edge_radius = (edge["bore_radius_um"] + + edge["irregularity_amplitude_um"]) / 1.0e6 + edge_height = (edge["bore_height_um"] + + edge["irregularity_amplitude_um"]) / 1.0e6 + passage_segments = passage.get("_audit_segments") or tuple(_segments(passage, scale)) + minimum_clearance = math.inf + for a, b, _, passage_radius in passage_segments: + length = math.dist(a[:3], b[:3]) + steps = max(1, math.ceil(length / 0.05)) + for step in range(steps + 1): + t = step / steps + point = tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) + if _inside_chamber(point, chamber, scale): + continue + tunnel_radius = (a[3] + (b[3] - a[3]) * t + + INFLUENCE_REACH_M) + for start, end in zip(axis, axis[1:]): + delta = tuple(end[i] - start[i] for i in range(3)) + length_squared = sum(value * value for value in delta) + projection = (sum((point[i] - start[i]) * delta[i] + for i in range(3)) / length_squared + if length_squared else 0.0) + projection = min(1.0, max(0.0, projection)) + center = tuple(start[i] + projection * delta[i] for i in range(3)) + radial = math.hypot(point[0] - center[0], point[1] - center[1]) + vertical = abs(point[2] - center[2]) + clearance = (_distance_to_ellipse(radial, vertical, + edge_radius, edge_height) + - tunnel_radius) + minimum_clearance = min(minimum_clearance, clearance) + return minimum_clearance + + +def _test_bounds(bounds, strate_index: int, passages, scale: float, + *, allow_chamber: bool = True): + for passage in passages: + if strate_index not in (passage["upper_strate_index"], passage["lower_strate_index"]): + continue + chamber_key = ("upper_landing_chamber" if strate_index == passage["upper_strate_index"] + else "lower_landing_chamber") + chamber = passage[chamber_key] + segments = passage.get("_audit_segments") + if segments is None: + segments = tuple(_segments(passage, scale)) + for a, b, along, radius in segments: + expanded = [(a[i] - radius, b[i] + radius) if a[i] <= b[i] + else (b[i] - radius, a[i] + radius) for i in range(3)] + if any(expanded[i][1] < bounds[i][0] or expanded[i][0] > bounds[i][1] + for i in range(3)): + continue + overlap = _overlap_interval(a, b, bounds, radius) + if overlap is None: + continue + first, last, closest = overlap + if allow_chamber: + points = [tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) + for t in (first, closest, last)] + if all(_inside_chamber(point, chamber, scale) for point in points): + continue + point = tuple(a[i] + (b[i] - a[i]) * closest for i in range(3)) + return passage["stable_id"], along + closest * math.dist(a[:3], b[:3]), point + return None + + +def audit_layout(layout: dict[str, Any]) -> dict[str, Any]: + scale = layout["core_voxel_size_um"] / (256.0 * 1.0e6) + passages = [dict(row, _audit_segments=tuple(_segments(row, scale))) + for row in layout.get("passages", [])] + primitives = [*layout.get("cavern_rooms", []), *layout.get("cavern_edges", [])] + checked = conflicts = chamber_joints = bounds_candidates = refined_false_positives = 0 + first_conflict = None + for primitive in primitives: + bounds = _primitive_bounds(primitive) + checked += 1 + primitive_conflicted = False + for passage in passages: + found = _test_bounds(bounds, primitive["strate_index"], [passage], scale) + if not found: + continue + bounds_candidates += 1 + passage_id, station, point = found + if "control_points_um" in primitive: + side = ("upper_landing_chamber" + if primitive["strate_index"] == passage["upper_strate_index"] + else "lower_landing_chamber") + clearance = _actual_edge_clearance(passage, primitive, scale, + passage[side]) + if clearance >= 0.0: + refined_false_positives += 1 + continue + primitive_conflicted = True + conflicts += 1 + if first_conflict is None: + first_conflict = {"primitive": primitive["stable_id"], + "passage": passage_id, "station_m": round(station, 3), + "center_m": [round(value, 3) for value in point]} + break + if not primitive_conflicted: + # Count the edge's named landing association as a confirmed joint. + anchor = primitive.get("start_anchor_stable_id", "") + if anchor.endswith(("/upper-landing-chamber", "/lower-landing-chamber")): + chamber_joints += 1 + + negative_controls = 0 + long_passage = max(passages, key=lambda row: len(row["control_points"]), default=None) + if long_passage: + controls = [_point(row, scale) for row in long_passage["control_points"]] + index = min(max(1, len(controls) // 2), len(controls) - 2) + center = controls[index] + bounds = tuple((center[axis] - 0.25, center[axis] + 0.25) for axis in range(3)) + # Select the side whose chamber is farther from this forced tunnel overlap. + strate = long_passage["lower_strate_index"] + if _test_bounds(bounds, strate, [long_passage], scale, allow_chamber=False): + negative_controls = 1 + landing_content_band = _landing_content_band_audit(layout) + landing_content_band_passed = bool(landing_content_band) and all( + row["passed"] for row in landing_content_band) + return {"primitives_checked": checked, "bounds_candidates": bounds_candidates, + "refined_edge_false_positives": refined_false_positives, + "outside_chamber_conflicts": conflicts, + "chamber_joints": chamber_joints, "negative_controls": negative_controls, + "negative_controls_expected": 1, "first_conflict": first_conflict, + "landing_content_band": landing_content_band, + "landing_content_band_passed": landing_content_band_passed, + "passed": conflicts == 0 and negative_controls == 1 + and landing_content_band_passed} diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux159/REPORT.md new file mode 100644 index 0000000..e98e6ea --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux159/REPORT.md @@ -0,0 +1,82 @@ +# L159 — pad stamp only + +The stamp is implemented and built as evaluator v240, with no planner edits. All 12 captures passed image quality, but the seed-0 fast tier and both 16-seed composed sweeps failed; no baselines were added. + +## Seed-0 tunnel-end captures + +Each row matches the L151 eye-height pose and adds a pulled-back L159 wide view. + +| Tunnel end | Before (L151) | After, eye height (L159) | After, wide (L159) | +|---|---|---|---| +| `passage/0-1/0` upper | ![L151 upper](../Linux151/captures/after-l151-0-1-0-upper-landing.jpg) | ![L159 upper eye](captures/after-l159-0-1-0-upper-landing.jpg) | ![L159 upper wide](captures/after-l159-0-1-0-upper-wide.jpg) | +| `passage/0-1/0` lower | ![L151 lower](../Linux151/captures/after-l151-0-1-0-lower-landing.jpg) | ![L159 lower eye](captures/after-l159-0-1-0-lower-landing.jpg) | ![L159 lower wide](captures/after-l159-0-1-0-lower-wide.jpg) | +| `passage/1-2/0` upper | ![L151 upper](../Linux151/captures/after-l151-1-2-0-upper-landing.jpg) | ![L159 upper eye](captures/after-l159-1-2-0-upper-landing.jpg) | ![L159 upper wide](captures/after-l159-1-2-0-upper-wide.jpg) | +| `passage/1-2/0` lower | ![L151 lower](../Linux151/captures/after-l151-1-2-0-lower-landing.jpg) | ![L159 lower eye](captures/after-l159-1-2-0-lower-landing.jpg) | ![L159 lower wide](captures/after-l159-1-2-0-lower-wide.jpg) | +| `passage/2-3/0` upper | ![L151 upper](../Linux151/captures/after-l151-2-3-0-upper-landing.jpg) | ![L159 upper eye](captures/after-l159-2-3-0-upper-landing.jpg) | ![L159 upper wide](captures/after-l159-2-3-0-upper-wide.jpg) | +| `passage/2-3/0` lower | ![L151 lower](../Linux151/captures/after-l151-2-3-0-lower-landing.jpg) | ![L159 lower eye](captures/after-l159-2-3-0-lower-landing.jpg) | ![L159 lower wide](captures/after-l159-2-3-0-lower-wide.jpg) | + +Capture quality: **12/12 PASS**. Poses use the seed-0 layout resolved for L151; the six after-eye views reuse those camera poses. The image manifest and capture commands are in [captures/INDEX.md](captures/INDEX.md) and [RunL159Captures.py](RunL159Captures.py). + +## Change + +- Added a last-step stamp to both `ApplyResolvedStrateComposition` branches. It uses the existing landing-chamber floor center as P, radius `max(3 m, mouth radius + 1 m)`, fill depth 1.25 m, headroom 3 m, 12° cones, and an 8 m blend reach. The tube-only distance mask leaves the bore open; the smooth-min width rises to 0.5 m over the first metre beyond the flat radius. +- Removed the landing chamber box/shell composition cases. Tunnel endpoints and planner output were left unchanged. +- Extended tile feature bounds, proof bounds, fallback overlap bounds, and the proof-miss sampler for the pad influence. +- Added `LandingPadAudit` for floor flatness, support, headroom, blend grade, and four negative controls. +- Bumped `WorldAssemblyEvaluatorVersion` from 239 to 240. + +No Unreal `Source/` files changed. Changed Standalone files: [ShapeWorldInternal.h](../../../Core/src/ShapeWorldInternal.h), [VoxelForgeCore.h](../../../Core/include/VoxelForgeCore/VoxelForgeCore.h), and [StreamBenchmarkPipeline.h](../../../Source/StreamBenchmarkPipeline.h). Planner files are unchanged. + +## Gates + +| Gate | Tunnel Network | Vertical Shafts | Result detail | +|---|---:|---:|---| +| Seed-0 fast tier | FAIL | — | Stopped before composition: `owner-floating-islands-style2-clang` rejected because `passage/0-1/0 lower landing chamber does not fit inside its host content band` (exit 2, 0.465 s). | +| Combined composed run | 0/16 PASS | 0/16 PASS | Each seed returned failure from the resolved passage audit. | +| Pad audit | 0/16 seeds PASS | 0/16 seeds PASS | 8/96 endpoints passed per mode. Across the 96 pads: 49 had flatness misses (369 samples total; worst reported variation 0.74946 m), 75 had blend-grade misses (2,072 samples; max 80.754°), support was 9,384/10,368 probes, and headroom passed on 93/96 pads. There were 1,682 missing blend crossings. All four negative controls were detected on every pad: 384/384 per mode. | +| Route plan grade | 16/16 PASS | 16/16 PASS | Planned grades stayed within the 15° limit. | +| Selected-stratum route audit | 0/16 PASS | 1/16 PASS | Vertical Shafts passed only seed 2; its selected route had 0 blocked or unsupported samples. Tunnel Network failed all 16: blocked samples were present on 15 seeds and unsupported samples on 13. The full composed passage walkability summary still failed 16/16 in each mode; `route_walkable` was 0 on 15 seeds and 2 on seed 2. | +| Interior air | 0/16 PASS | 0/16 PASS | Every seed had solid capsule samples (839–1,588 per seed). | +| Shell | 11/16 PASS | 11/16 PASS | Failed seeds 2, 3, 4, 5, and 14: 2,571 floor violations and 570 wall violations in total. Other shell samples passed. | +| Ripple | 5/16 PASS | 5/16 PASS | Passing seeds: 3, 7, 11, 12, and 14. | +| Layout envelope | 8/16 PASS | 8/16 PASS | Conflicts on seeds 1, 4, 6, 7, 8, 10, 13, and 15, on existing route-edge, room, or shaft primitives near passage endpoints. | +| Tile seams | NOT REACHED, 0/16 rows | NOT REACHED, 0/16 rows | The command included `--audit-passage-tile-seams`, but every run returned from the failed passage summary before reaching the seam-audit block. Seam behavior is therefore unverified. | +| Pad proof/feature bounds (L130 sampler) | 16/16 PASS | 16/16 PASS | Per mode: 413,952 sampled points, 305,571 sign-relevant; zero proof misses and zero tile-feature misses. | +| Mode blocked-route control | 16/16 PASS | 16/16 PASS | Deliberately blocked controls were detected as expected. | + +Per-seed detail: pad `F/G` is the number of endpoints with any flatness/blend-grade failure; route `B/U` is the selected-stratum blocked/unsupported sample count. Shell `F (x/y)` gives floor/wall violations. + +| Seed | Pads passed | Pad F/G | Tunnel Network B/U | Vertical Shafts B/U | Interior solids | Shell | Ripple | Envelope | +|---:|---:|---:|---:|---:|---:|---:|:---:|:---:| +| 0 | 1/6 | 3/5 | 324/12 | 507/0 | 1,233 | PASS | FAIL | PASS | +| 1 | 1/6 | 3/4 | 190/0 | 157/0 | 839 | PASS | FAIL | FAIL | +| 2 | 1/6 | 3/4 | 159/2 | 0/0 | 915 | FAIL (513/110) | FAIL | PASS | +| 3 | 1/6 | 3/4 | 0/14 | 521/0 | 1,588 | FAIL (522/117) | PASS | PASS | +| 4 | 0/6 | 3/5 | 140/0 | 222/0 | 855 | FAIL (512/114) | FAIL | FAIL | +| 5 | 0/6 | 3/4 | 152/15 | 379/0 | 892 | FAIL (516/116) | FAIL | PASS | +| 6 | 0/6 | 3/6 | 436/24 | 495/0 | 1,350 | PASS | FAIL | FAIL | +| 7 | 1/6 | 3/5 | 299/5 | 374/0 | 1,011 | PASS | PASS | FAIL | +| 8 | 1/6 | 3/4 | 177/15 | 541/0 | 1,079 | PASS | FAIL | FAIL | +| 9 | 0/6 | 3/4 | 325/16 | 540/0 | 1,205 | PASS | FAIL | PASS | +| 10 | 1/6 | 3/5 | 171/16 | 385/0 | 921 | PASS | FAIL | FAIL | +| 11 | 0/6 | 3/5 | 150/30 | 537/0 | 1,025 | PASS | PASS | PASS | +| 12 | 1/6 | 3/4 | 266/4 | 353/0 | 1,161 | PASS | PASS | PASS | +| 13 | 0/6 | 4/6 | 153/15 | 376/0 | 1,146 | PASS | FAIL | FAIL | +| 14 | 0/6 | 3/4 | 277/18 | 386/0 | 1,075 | FAIL (508/113) | PASS | PASS | +| 15 | 0/6 | 3/6 | 173/0 | 265/0 | 970 | PASS | FAIL | FAIL | + +Full per-seed records: [Tunnel Network summary](../../../../Saved/Linux/l159/seeds/tunnel-network/summary.json) and [Vertical Shafts summary](../../../../Saved/Linux/l159/seeds/vertical-shafts/summary.json). + +The pad, route, shell, interior-air, ripple, and envelope results are composition audits, one run per seed 0–15 with `--workers=8 --clip-radius=3 --max-clip-level=8 --max-level=0 --force-generate --recipe=surface-world --audit-resolved-layout-passages --audit-passage-tile-seams` and the same source stamp (`5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640`). Both modes produced the same shared-field results. The selected-stratum route row and blocked-route control are mode-specific. + +The seed runner recorded 6,711.2 s summed child wall time for Tunnel Network and 6,762.5 s for Vertical Shafts (monotonic windows from each seed process launch to exit). These are sweep execution windows, not performance benchmark results; no L80 timing was taken. + +## Checks and remaining risks + +- Build: `bash Standalone/BuildLinux.sh --compiler=clang++-19 --variant=l159-clang --fp-contract=off --jobs=4` — passed. +- Captures: `RunL159Captures.py` — 12 views, 0 quality failures. +- Fast tier: `VOXELFORGE_L130_PASSAGE_REACH_PROOF_AUDIT=1 python3 Standalone/Tools/verify/verify.py fast --out Saved/Linux/l159 --overall-timeout 1740` — failed at the owner floating-islands fixture before composition. +- Composed sweeps: `RunL159SeedSweep.py --kind tunnel-network` and `--kind vertical-shafts`, seeds 0–15, with `VOXELFORGE_L130_PASSAGE_REACH_PROOF_AUDIT=1` — completed, all aggregate gates failed. +- `git diff --check` passed before the final report was written. No Unreal build or timing benchmark was run. + +The pad flatness, support, headroom, and blend gates remain open, and all composed passage summaries fail. Shell failures are seed-dependent. The proof-tree and tile-feature bound sampler passed; fallback overlap bounds were extended but have no separate standalone audit. Seam behavior remains unverified because the process exits before that gate. No baselines were added because the requested gates did not all pass. diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/RunL159Captures.py b/Standalone/Research/WorldAssemblerM3/Linux159/RunL159Captures.py new file mode 100644 index 0000000..51a699d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux159/RunL159Captures.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""Capture matched L151 eye views and L159 wide views of all six owner pads.""" +from __future__ import annotations + +import json +import math +import os +from pathlib import Path +import re +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[4] +DATA = ROOT / "Standalone/data/owner_world.json" +OUT = ROOT / "Saved/Linux/l159/captures" +LAYOUT = ROOT / "Saved/Linux/l151/captures/after-resolved-layout.json" +ARTIFACTS = ROOT / "Standalone/Research/WorldAssemblerM3/Linux159/captures" +HEADLESS = ROOT / "Saved/Linux/l159/bin/VoxelForgeCoreHeadless-l159-clang" +RENDERER = ROOT / "Saved/Linux/l159/bin/VoxelForgeOffscreenCapture-l159-clang" +STAMP = "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) + + +def fmt(values) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def angles(camera, target) -> tuple[float, float]: + dx, dy, dz = (target[index] - camera[index] for index in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees( + math.atan2(dz, math.hypot(dx, dy))) + + +def point_q(layout: dict, values) -> tuple[float, float, float]: + scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0) + return tuple(value * scale for value in values) + + +def chamber_views(layout: dict) -> list[dict]: + result = [] + for passage in layout["passages"]: + controls = passage["control_points"] + for side, endpoint, neighbor in ( + ("upper", controls[0], controls[1]), + ("lower", controls[-1], controls[-2])): + chamber = passage[f"{side}_landing_chamber"] + center = point_q(layout, chamber["floor_center_q"]) + a = point_q(layout, neighbor["position_q"]) + b = point_q(layout, endpoint["position_q"]) + dx, dy = a[0] - b[0], a[1] - b[1] + length = math.hypot(dx, dy) + if length <= 1.0e-9: + raise ValueError(f"degenerate {side} tunnel tangent: {passage['stable_id']}") + dx, dy = dx / length, dy / length + eye_z = center[2] + 1.45 + distance = 9.0 + target = (center[0] + dx * distance, center[1] + dy * distance, + eye_z + (chamber["floor_gradient_q16"][0] / 65536.0 * dx + + chamber["floor_gradient_q16"][1] / 65536.0 * dy) + * distance) + camera = (center[0], center[1], eye_z) + slug = passage["stable_id"].replace("passage/", "").replace("/", "-") + result.append({"name": f"{slug}-{side}-landing", "passage": passage["stable_id"], + "side": side, "view_kind": "eye", "camera": camera, + "target": target, "focus": (center[0], center[1], eye_z, 18.0)}) + wide_camera = (center[0] - dx * 14.0, center[1] - dy * 14.0, + center[2] + 5.5) + wide_target = (center[0] + dx * 2.0, center[1] + dy * 2.0, + center[2] + 1.25) + result.append({"name": f"{slug}-{side}-wide", "passage": passage["stable_id"], + "side": side, "view_kind": "wide", "camera": wide_camera, + "target": wide_target, + "focus": (center[0], center[1], eye_z, 42.0)}) + return result + + +def stamped_run(name: str, command: list[object], log: Path, env: dict[str, str]) -> str: + probe = subprocess.run([*LIMIT, "timeout", "15s", str(command[0]), "--print-source-stamp"], + cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=20) + match = STAMP_RE.search(probe.stdout or "") + if probe.returncode or not match or match.group(1) != STAMP: + raise RuntimeError(f"{name}: source stamp mismatch for {command[0]}") + log.parent.mkdir(parents=True, exist_ok=True) + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, + timeout=1745, check=False) + output = log.read_text(encoding="utf-8", errors="replace") + emitted = STAMP_RE.search(output) + if result.returncode or not emitted or emitted.group(1) != STAMP: + raise RuntimeError(f"{name}: export or render failed; see {log}") + return output + + +def main() -> int: + if not DATA.is_file() or not HEADLESS.is_file() or not RENDERER.is_file(): + raise SystemExit("missing owner fixture or L159 Clang capture binaries") + OUT.mkdir(parents=True, exist_ok=True) + ARTIFACTS.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache") + layout = json.loads(LAYOUT.read_text(encoding="utf-8")) + if len(layout.get("passages", [])) != 3: + raise RuntimeError("owner layout did not resolve the expected three tunnels") + + quality_tool = ROOT / "Standalone/Tools/verify/capture_quality.py" + manifest = [] + failures = [] + for view in chamber_views(layout): + camera, target = view["camera"], view["target"] + yaw, pitch = angles(camera, target) + mesh_dir = OUT / "meshes" + mesh_dir.mkdir(parents=True, exist_ok=True) + mesh = mesh_dir / f"{view['name']}.vfmesh" + capture_layout = mesh_dir / f"{view['name']}-layout.json" + export = [HEADLESS, DATA, "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--workers=12", "--clip-radius=6", "--max-clip-level=0", + "--max-level=0", "--force-generate", "--seed=0", "--recipe=surface-world", + "--render-distance-chunks=0", f"--camera={fmt((*camera, yaw, pitch))}", + f"--resolved-layout-output={capture_layout}", f"--mesh-export={mesh}"] + stamped_run(f"l159-{view['name']}-export", export, + OUT / f"{view['name']}-export.log", env) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{view['name']}: empty mesh export") + png = OUT / f"{view['name']}.png" + jpg = ARTIFACTS / f"after-l159-{view['name']}.jpg" + render = [RENDERER, mesh, png, "--size=1280x720", "--fov-deg=78.0", + f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", + f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable", + "--headlamp=on", "--height-tint=on", "--height-center-m=-950", + "--height-range-m=60", "--tile-edges=off", + f"--focus-region={fmt(view['focus'])}"] + stamped_run(f"l159-{view['name']}-render", render, + OUT / f"{view['name']}-render.log", env) + subprocess.run([*LIMIT, "timeout", "120s", "ffmpeg", "-nostdin", + "-hide_banner", "-loglevel", "error", "-y", "-i", png, + "-frames:v", "1", "-q:v", "4", jpg], cwd=ROOT, env=env, + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, timeout=125) + check = subprocess.run([*LIMIT, "timeout", "120s", sys.executable, + quality_tool, jpg], cwd=ROOT, env=env, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, check=False, timeout=125) + quality_ok = check.returncode == 0 and "status=PASS" in check.stdout + if not quality_ok: + failures.append(view["name"]) + quality_line = (check.stdout.strip().splitlines()[-1] + if check.stdout.strip() else f"quality exit={check.returncode}") + manifest.append({"version": "after-l159", "view": view["name"], + "passage": view["passage"], "side": view["side"], + "view_kind": view["view_kind"], + "file": str(jpg.relative_to(ROOT)), "camera_m": camera, + "target_m": target, "yaw_deg": yaw, "pitch_deg": pitch, + "source_stamp": STAMP, "pose_layout_identity": layout.get("stable_identity"), + "quality_status": "PASS" if quality_ok else "FAIL", "quality": quality_line}) + print(f"[L159Capture] view={view['name']} quality={'PASS' if quality_ok else 'FAIL'} " + f"file={jpg.relative_to(ROOT)}", flush=True) + + (ARTIFACTS / "manifest.json").write_text( + json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + with (ARTIFACTS / "INDEX.md").open("w", encoding="utf-8") as index: + index.write("# L159 owner landing pad captures\n\n" + "All poses use the seed 0 layout resolved for L151. Eye-height after views " + "reuse the six L151 cameras; each pad also has one pulled-back wide view.\n\n") + for view in chamber_views(layout): + image = f"after-l159-{view['name']}.jpg" + if view["view_kind"] == "eye": + before = f"after-l151-{view['name']}.jpg" + index.write(f"## {view['passage']} / {view['side']} landing\n\n" + f"- Before: [{before}](../../Linux151/captures/{before})\n" + f"- After: [eye view]({image})\n") + else: + index.write(f"- After: [wide view]({image})\n\n") + index.write(f"Capture quality failures: {len(failures)}/{len(manifest)}.\n") + print(f"[L159CaptureSummary] views={len(manifest)} " + f"quality_failures={len(failures)}", flush=True) + return 0 if not failures else 1 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L159CaptureFailure] {error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/RunL159SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux159/RunL159SeedSweep.py new file mode 100644 index 0000000..3b7ebf2 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux159/RunL159SeedSweep.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +"""Run the L159 landing-pad, passage, and seam gates across 16 owner seeds.""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import time +from pathlib import Path + +from PassageEnvelopeAudit import audit_layout + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l159/seeds" +BIN = ROOT / "Saved/Linux/l159/bin/VoxelForgeCoreHeadless-l159-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +DISPATCH = { + "tunnel-network": re.compile( + r"^\[DensityDispatch\] strate=2 generator=TunnelNetwork " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), + "vertical-shafts": re.compile( + r"^\[DensityDispatch\] strate=3 generator=VerticalShafts " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), +} +ROUTES = { + "tunnel-network": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=2 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=3 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), +} +CONTROLS = { + "tunnel-network": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[VerticalShaftsBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} +REQUIRED = { + "passage_summary": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "interior_capsules": re.compile( + r"^\[PassageInteriorCapsuleAuditSummary\].*status=PASS$", re.M), + "landing_pads": re.compile( + r"^\[LandingPadAuditSummary\] pads=6 passed=6 negative_controls=24/24 status=PASS$", re.M), + "layout_controls": re.compile( + r"^\[ResolvedLayoutValidatorControls\] rejected=10/10 status=PASS$", re.M), + "floor_profile_controls": re.compile( + r"^\[FloorProfileBlendNegativeControls\].*detected=true/true status=PASS$", re.M), + "naturalness_control": re.compile( + r"^\[PassageNaturalnessNegativeControl\].*detected=true$", re.M), + "mouth_control": re.compile( + r"^\[PassageMouthProfileNegativeControl\].*detected=true$", re.M), +} + + +def passage_rows(content: str) -> list[str]: + return re.findall(r"^\[ResolvedPassageAudit\].*$", content, re.M) + + +def grade_audit(content: str) -> tuple[bool, list[str]]: + rows = re.findall(r"^\[PassagePlanGradeAudit\].*$", content, re.M) + ids = set() + passed = bool(rows) + for row in rows: + identifier = re.search(r"\bid=([^ ]+)", row) + maximum = re.search(r"\bmax_grade_deg=([0-9.]+)", row) + limit = re.search(r"\blimit_deg=([0-9.]+)", row) + violations = re.search(r"\bviolations=(\d+)", row) + status = "status=PASS" in row + if identifier: + ids.add(identifier.group(1)) + passed &= bool(identifier and maximum and limit and violations and status + and float(maximum.group(1)) <= float(limit.group(1)) + and float(limit.group(1)) == 15.0 + and int(violations.group(1)) == 0) + return passed and len(ids) >= 3, rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kind", choices=tuple(DISPATCH), required=True) + parser.add_argument("--start-seed", type=int, default=0) + parser.add_argument("--end-seed", type=int, default=15) + args = parser.parse_args() + if not 0 <= args.start_seed <= args.end_seed <= 15: + raise SystemExit("seed range must be within 0..15 and start <= end") + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L159 Clang binary or composed owner fixture") + mode_out = OUT / args.kind + mode_out.mkdir(parents=True, exist_ok=True) + partial = mode_out / "runs-partial.json" + results = (json.loads(partial.read_text(encoding="utf-8")) + if partial.is_file() and args.start_seed > 0 else []) + completed = {row["seed"] for row in results} + for seed in range(args.start_seed, args.end_seed + 1): + if seed in completed: + continue + log = mode_out / f"seed{seed}.log" + layout = mode_out / f"seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=0", "--force-generate", f"--seed={seed}", + "--recipe=surface-world", "--density-dispatch-report", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["process_exit_zero"] = proc.returncode == 0 + checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP + checks["dispatch"] = bool(DISPATCH[args.kind].search(content)) + checks["route"] = bool(ROUTES[args.kind].search(content)) + checks["blocked_route_control"] = bool(CONTROLS[args.kind].search(content)) + + passages = passage_rows(content) + checks["three_passages_walkable_and_open"] = ( + len(passages) == 3 and all("walkability=PASS" in row + and "interior_air=PASS" in row + and re.search(r"\bblocked=0\b", row) + and re.search(r"\bunsupported=0\b", row) + and re.search(r"\bgrade_failures=0\b", row) + for row in passages)) + checks["three_interior_negative_controls"] = len(re.findall( + r"^\[ResolvedPassageInteriorNegativeControl\].*detected=true status=PASS$", + content, re.M)) == 3 + checks["three_ripple_gap_controls"] = len(re.findall( + r"^\[PassageRippleSamplingControl\].*status=PASS$", content, re.M)) == 3 + checks["three_blocked_passage_controls"] = len(re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3 + chamber_rows = re.findall(r"^\[PassageLandingChamberAudit\].*$", content, re.M) + checks["six_chamber_floors"] = (len(chamber_rows) == 6 and all( + "status=PASS" in row and re.search(r"\bsolid_samples=0\b", row) + and re.search(r"\bgrade_failures=0\b", row) for row in chamber_rows)) + pad_rows = re.findall(r"^\[LandingPadAudit\].*$", content, re.M) + checks["six_landing_pad_audits"] = len(pad_rows) == 6 and all( + "status=PASS" in row and "negative_controls=4/4" in row + and all(f"{control}=detected" in row for control in ( + "pad_off", "tilt_2pct", "blend_B_half", "bore_mask_removed")) + for row in pad_rows) + checks["plan_grade"] = grade_audit(content)[0] + chamber_controls = re.search( + r"^\[PassageLandingChamberNegativeControlSummary\] " + r"detected=(\d+)/6 .*status=OBSERVED$", content, re.M) + checks["chamber_negative_control"] = bool( + chamber_controls and int(chamber_controls.group(1)) > 0) + + checks["three_ripple_profiles"] = len(re.findall( + r"^\[PassageRippleAudit\].*status=PASS$", content, re.M)) >= 3 + checks["passage_count"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*walkability=PASS", content, re.M)) == 3 + checks["layout_output"] = layout.is_file() + envelope = audit_layout(json.loads(layout.read_text(encoding="utf-8"))) \ + if layout.is_file() else None + checks["envelope"] = bool(envelope and envelope["passed"] + and envelope["outside_chamber_conflicts"] == 0) + checks["envelope_negative_control"] = bool(envelope + and envelope["negative_controls"] == envelope["negative_controls_expected"] == 1) + checks["landing_content_band"] = bool(envelope + and envelope["landing_content_band_passed"] + and len(envelope["landing_content_band"]) == 6) + passed = all(checks.values()) + failed = [name for name, ok in checks.items() if not ok] + row = {"kind": args.kind, "seed": seed, + "verdict": "PASS" if passed else "FAIL", + "reason": ", ".join(failed) if failed else "all requested gates pass", + "exit": proc.returncode, "checks": checks, + "plan_grade_rows": grade_audit(content)[1], + "envelope_audit": envelope, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L159SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + partial.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + + results.sort(key=lambda row: row["seed"]) + passed_count = sum(row["verdict"] == "PASS" for row in results) + completed_count = len(results) + missing_seeds = sorted(set(range(16)) - {row["seed"] for row in results}) + summary = {"kind": args.kind, "source_stamp": STAMP, + "seeds": [row["seed"] for row in results], + "completed": completed_count, "passed": passed_count, + "failed": completed_count - passed_count, "missing_seeds": missing_seeds, + "results": results} + (mode_out / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + status = "PASS" if completed_count == 16 and passed_count == 16 else "FAIL" + print(f"[L159SeedSweepSummary] kind={args.kind} passed={passed_count}/{completed_count} " + f"failed={completed_count-passed_count} missing={len(missing_seeds)} status={status}", + flush=True) + return 0 if status == "PASS" else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/INDEX.md b/Standalone/Research/WorldAssemblerM3/Linux159/captures/INDEX.md new file mode 100644 index 0000000..934f9d6 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux159/captures/INDEX.md @@ -0,0 +1,41 @@ +# L159 owner landing pad captures + +All poses use the seed 0 layout resolved for L151. Eye-height after views reuse the six L151 cameras; each pad also has one pulled-back wide view. + +## passage/0-1/0 / upper landing + +- Before: [after-l151-0-1-0-upper-landing.jpg](../../Linux151/captures/after-l151-0-1-0-upper-landing.jpg) +- After: [eye view](after-l159-0-1-0-upper-landing.jpg) +- After: [wide view](after-l159-0-1-0-upper-wide.jpg) + +## passage/0-1/0 / lower landing + +- Before: [after-l151-0-1-0-lower-landing.jpg](../../Linux151/captures/after-l151-0-1-0-lower-landing.jpg) +- After: [eye view](after-l159-0-1-0-lower-landing.jpg) +- After: [wide view](after-l159-0-1-0-lower-wide.jpg) + +## passage/1-2/0 / upper landing + +- Before: [after-l151-1-2-0-upper-landing.jpg](../../Linux151/captures/after-l151-1-2-0-upper-landing.jpg) +- After: [eye view](after-l159-1-2-0-upper-landing.jpg) +- After: [wide view](after-l159-1-2-0-upper-wide.jpg) + +## passage/1-2/0 / lower landing + +- Before: [after-l151-1-2-0-lower-landing.jpg](../../Linux151/captures/after-l151-1-2-0-lower-landing.jpg) +- After: [eye view](after-l159-1-2-0-lower-landing.jpg) +- After: [wide view](after-l159-1-2-0-lower-wide.jpg) + +## passage/2-3/0 / upper landing + +- Before: [after-l151-2-3-0-upper-landing.jpg](../../Linux151/captures/after-l151-2-3-0-upper-landing.jpg) +- After: [eye view](after-l159-2-3-0-upper-landing.jpg) +- After: [wide view](after-l159-2-3-0-upper-wide.jpg) + +## passage/2-3/0 / lower landing + +- Before: [after-l151-2-3-0-lower-landing.jpg](../../Linux151/captures/after-l151-2-3-0-lower-landing.jpg) +- After: [eye view](after-l159-2-3-0-lower-landing.jpg) +- After: [wide view](after-l159-2-3-0-lower-wide.jpg) + +Capture quality failures: 0/12. diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-lower-landing.jpg new file mode 100644 index 0000000..6e19b72 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-lower-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-lower-wide.jpg new file mode 100644 index 0000000..78d3492 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-lower-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-upper-landing.jpg new file mode 100644 index 0000000..309c25f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-upper-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-upper-wide.jpg new file mode 100644 index 0000000..eba55a8 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-upper-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-lower-landing.jpg new file mode 100644 index 0000000..37a0121 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-lower-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-lower-wide.jpg new file mode 100644 index 0000000..bc3e24f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-lower-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-upper-landing.jpg new file mode 100644 index 0000000..e4074e7 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-upper-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-upper-wide.jpg new file mode 100644 index 0000000..129ca17 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-upper-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-lower-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-lower-landing.jpg new file mode 100644 index 0000000..05dc67b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-lower-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-lower-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-lower-wide.jpg new file mode 100644 index 0000000..b64172a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-lower-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-upper-landing.jpg b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-upper-landing.jpg new file mode 100644 index 0000000..3a5b9e8 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-upper-landing.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-upper-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-upper-wide.jpg new file mode 100644 index 0000000..93a9746 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-upper-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/captures/manifest.json b/Standalone/Research/WorldAssemblerM3/Linux159/captures/manifest.json new file mode 100644 index 0000000..0e262f5 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux159/captures/manifest.json @@ -0,0 +1,290 @@ +[ + { + "version": "after-l159", + "view": "0-1-0-upper-landing", + "passage": "passage/0-1/0", + "side": "upper", + "view_kind": "eye", + "file": "Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-upper-landing.jpg", + "camera_m": [ + 37.0, + -5.25, + -372.3810546875 + ], + "target_m": [ + 28.40474992442715, + -7.918646874047773, + -372.325197471535 + ], + "yaw_deg": -107.24848481126237, + "pitch_deg": 0.3555935154874107, + "source_stamp": "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-upper-landing.jpg pixels=921600 mean_luma=113.911 stddev_luma=9.359 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l159", + "view": "0-1-0-upper-wide", + "passage": "passage/0-1/0", + "side": "upper", + "view_kind": "wide", + "file": "Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-upper-wide.jpg", + "camera_m": [ + 50.37038900644665, + -1.0987715292590199, + -368.3310546875 + ], + "target_m": [ + 35.08994442765048, + -5.843032638677283, + -372.5810546875 + ], + "yaw_deg": -107.24848481126239, + "pitch_deg": -14.8756820016388, + "source_stamp": "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-upper-wide.jpg pixels=921600 mean_luma=119.719 stddev_luma=11.104 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l159", + "view": "0-1-0-lower-landing", + "passage": "passage/0-1/0", + "side": "lower", + "view_kind": "eye", + "file": "Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-lower-landing.jpg", + "camera_m": [ + 25.8369140625, + 3.19921875, + -736.6798828125 + ], + "target_m": [ + 17.97380590951016, + -1.1793118453464206, + -734.970169262755 + ], + "yaw_deg": -119.11101808688004, + "pitch_deg": 10.756207017349817, + "source_stamp": "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-lower-landing.jpg pixels=921600 mean_luma=112.097 stddev_luma=12.698 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l159", + "view": "0-1-0-lower-wide", + "passage": "passage/0-1/0", + "side": "lower", + "view_kind": "wide", + "file": "Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-lower-wide.jpg", + "camera_m": [ + 38.06841563381753, + 10.0102663427611, + -732.6298828125 + ], + "target_m": [ + 24.089556695168923, + 2.226211951034129, + -736.8798828125 + ], + "yaw_deg": -119.11101808688004, + "pitch_deg": -14.875682001638797, + "source_stamp": "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-0-1-0-lower-wide.jpg pixels=921600 mean_luma=105.110 stddev_luma=2.648 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l159", + "view": "1-2-0-upper-landing", + "passage": "passage/1-2/0", + "side": "upper", + "view_kind": "eye", + "file": "Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-upper-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -731.8810546875 + ], + "target_m": [ + 10.35825176774404, + -16.050410390488395, + -733.216132282697 + ], + "yaw_deg": -101.86342177874246, + "pitch_deg": -8.437834550031965, + "source_stamp": "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-upper-landing.jpg pixels=921600 mean_luma=117.708 stddev_luma=15.445 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l159", + "view": "1-2-0-upper-wide", + "passage": "passage/1-2/0", + "side": "upper", + "view_kind": "wide", + "file": "Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-upper-wide.jpg", + "camera_m": [ + 32.86698162517594, + -11.322082968962494, + -727.8310546875 + ], + "target_m": [ + 17.20873476783201, + -14.611354218719644, + -732.0810546875 + ], + "yaw_deg": -101.86342177874248, + "pitch_deg": -14.875682001638797, + "source_stamp": "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-upper-wide.jpg pixels=921600 mean_luma=105.434 stddev_luma=3.032 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l159", + "view": "1-2-0-lower-landing", + "passage": "passage/1-2/0", + "side": "lower", + "view_kind": "eye", + "file": "Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-lower-landing.jpg", + "camera_m": [ + 19.166015625, + -14.2001953125, + -753.05390625 + ], + "target_m": [ + 10.48848361510052, + -16.587754362485967, + -751.7193175939389 + ], + "yaw_deg": -105.38384894032598, + "pitch_deg": 8.434788863627224, + "source_stamp": "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-lower-landing.jpg pixels=921600 mean_luma=120.892 stddev_luma=15.997 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l159", + "view": "1-2-0-lower-wide", + "passage": "passage/1-2/0", + "side": "lower", + "view_kind": "wide", + "file": "Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-lower-wide.jpg", + "camera_m": [ + 32.664398751510305, + -10.486214568077383, + -749.00390625 + ], + "target_m": [ + 17.237675178355673, + -14.73076399027466, + -753.25390625 + ], + "yaw_deg": -105.38384894032599, + "pitch_deg": -14.875682001638793, + "source_stamp": "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-1-2-0-lower-wide.jpg pixels=921600 mean_luma=104.587 stddev_luma=2.079 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l159", + "view": "2-3-0-upper-landing", + "passage": "passage/2-3/0", + "side": "upper", + "view_kind": "eye", + "file": "Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-upper-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -812.075390625 + ], + "target_m": [ + -13.604954164712868, + -11.337338700629157, + -813.6662645746064 + ], + "yaw_deg": 134.15112141809087, + "pitch_deg": -10.024269857937275, + "source_stamp": "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-upper-landing.jpg pixels=921600 mean_luma=119.468 stddev_luma=15.926 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l159", + "view": "2-3-0-upper-wide", + "passage": "passage/2-3/0", + "side": "upper", + "view_kind": "wide", + "file": "Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-upper-wide.jpg", + "camera_m": [ + -30.10757129933554, + 4.683386242645357, + -808.025390625 + ], + "target_m": [ + -18.627489814380638, + -6.46146589180648, + -812.275390625 + ], + "yaw_deg": 134.15112141809087, + "pitch_deg": -14.875682001638797, + "source_stamp": "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-upper-wide.jpg pixels=921600 mean_luma=108.691 stddev_luma=5.224 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l159", + "view": "2-3-0-lower-landing", + "passage": "passage/2-3/0", + "side": "lower", + "view_kind": "eye", + "file": "Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-lower-landing.jpg", + "camera_m": [ + -20.0625, + -5.068359375, + -900.0333984375 + ], + "target_m": [ + -14.865342978484083, + -12.416118141706198, + -898.4427583280197 + ], + "yaw_deg": 144.72777711381306, + "pitch_deg": 10.022826283920647, + "source_stamp": "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-lower-landing.jpg pixels=921600 mean_luma=120.115 stddev_luma=17.038 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + }, + { + "version": "after-l159", + "view": "2-3-0-lower-wide", + "passage": "passage/2-3/0", + "side": "lower", + "view_kind": "wide", + "file": "Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-lower-wide.jpg", + "camera_m": [ + -28.146966477913647, + 6.361487595431864, + -895.9833984375 + ], + "target_m": [ + -18.90757621744091, + -6.701194656490266, + -900.2333984375 + ], + "yaw_deg": 144.72777711381306, + "pitch_deg": -14.8756820016388, + "source_stamp": "5b8ab5e84364e26c1a555428fba8bedb23faed14ef0f6b7d397edf654883d640", + "pose_layout_identity": "resolved-layout-v3-fnv64:ed02312296ac2003", + "quality_status": "PASS", + "quality": "[CaptureQuality] image=/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux159/captures/after-l159-2-3-0-lower-wide.jpg pixels=921600 mean_luma=104.815 stddev_luma=2.399 coverage_luma_gt12=1.00000 thresholds=(stddev>=2.0,coverage>=0.001) status=PASS" + } +] diff --git a/Standalone/Research/WorldAssemblerM3/Linux159/l159-pad-stamp.patch b/Standalone/Research/WorldAssemblerM3/Linux159/l159-pad-stamp.patch new file mode 100644 index 0000000..bff6bbf --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux159/l159-pad-stamp.patch @@ -0,0 +1,1068 @@ +diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +index 4799b9e..13440df 100644 +--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h ++++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +@@ -512,7 +512,8 @@ namespace VoxelForgeCore + // boundary-blended host rock from widening the authored passage wall. + // v238 plans one resolved passage spline with explicit landing chambers. + // v239 plans their floor planes as the passage's continuous floor profile. +- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 239; ++ // v240 replaces chamber boxes and shells with bounded clamp-and-cone landing pads. ++ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 240; + + enum class FieldChannel : std::uint32_t + { +diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h +index 6b1a6af..3ce93e4 100644 +--- a/Standalone/Core/src/ShapeWorldInternal.h ++++ b/Standalone/Core/src/ShapeWorldInternal.h +@@ -56,6 +56,11 @@ static bool VF_IsL130PassageReachProofAuditEnabled() + static constexpr double VF_PassageShellThicknessMetres=0.75; + static constexpr double VF_PassageWallReliefMetres=0.21; + static constexpr double VF_PassageCornerBlendMetres=0.50; ++static constexpr double VF_LandingPadConeSlope=0.2125565616700221; // tan(12 degrees) ++static constexpr double VF_LandingPadFillDepthMetres=1.25; ++static constexpr double VF_LandingPadHeadroomMetres=3.0; ++static constexpr double VF_LandingPadBlendRadiusMetres=8.0; ++static constexpr double VF_LandingPadSmoothMinMaximumMetres=0.50; + static constexpr double VF_PassageReachMarginMetres=0.01; + // Radial support in metres: shell + maximum relief + the smooth-max support + // (corner-blend width / 6) + a 1 cm guard for float and bound rounding. +@@ -1234,8 +1239,11 @@ public: + + float ApplyResolvedStratePassageCarves(float density,float X,float Y,float Z, + float nativeTunnelFieldDensity=std::numeric_limits::lowest(), +- float nativeFloorSupport=std::numeric_limits::lowest()) const ++ float nativeFloorSupport=std::numeric_limits::lowest(), ++ float* outTubeOnlyDistance=nullptr) const + { ++ if(outTubeOnlyDistance) ++ *outTubeOnlyDistance=std::numeric_limits::infinity(); + VoxelForgeCore::Linux77Profiler::FScopedPhase l77PassagePhase( + VoxelForgeCore::Linux77Profiler::EPhase::Passage); + if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X) +@@ -1280,8 +1288,6 @@ public: + double unionDistance=std::numeric_limits::infinity(); + double unionShellDistance=std::numeric_limits::infinity(); + double unionFloorShellDistance=std::numeric_limits::infinity(); +- double unionLandingChamberFloorShellDistance= +- std::numeric_limits::infinity(); + double plannedTunnelPassageShellDistance=std::numeric_limits::infinity(); + const std::uint32_t reliefSeed=static_cast(Owner?Owner->Seed:0); + const auto* queryStratum=FindBoundStratumForManagerZ(Z); +@@ -1324,48 +1330,6 @@ public: + if(passageProfile)++passageProfile->passageIterations; + const auto& constants=PassageComposerConstants[passageIndex]; + +- const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1* landingChamber=nullptr; +- if(queryStratum&&queryStratum->Resolved) +- { +- if(queryStratum->StrateIndex==passage.upperStrateIndex) +- landingChamber=&passage.upperLandingChamber; +- else if(queryStratum->StrateIndex==passage.lowerStrateIndex) +- landingChamber=&passage.lowerLandingChamber; +- } +- if(landingChamber&&landingChamber->radiusSubvoxels>0 +- &&landingChamber->clearHeightSubvoxels>0) +- { +- const double centerX=static_cast( +- landingChamber->floorCenterSubvoxels[0]); +- const double centerY=static_cast( +- landingChamber->floorCenterSubvoxels[1]); +- const double floorCenterZ=static_cast( +- landingChamber->floorCenterSubvoxels[2]) +- +(landingChamber->floorGradientQ16[0]*(qx-centerX) +- +landingChamber->floorGradientQ16[1]*(qy-centerY))/65536.0; +- const double radialDistance=DeterministicSqrt( +- (qx-centerX)*(qx-centerX)+(qy-centerY)*(qy-centerY)) +- -landingChamber->radiusSubvoxels; +- const double floorDistance=floorCenterZ-qz; +- const double ceilingDistance=qz-(floorCenterZ +- +landingChamber->clearHeightSubvoxels); +- const double chamberDistance=std::max({radialDistance, +- floorDistance,ceilingDistance}); +- const double chamberShellDistance=std::max(-chamberDistance, +- chamberDistance-shellThickness); +- const double chamberFloorShellDistance=std::max( +- std::max(-floorDistance,floorDistance-shellThickness), +- radialDistance); +- unionDistance=std::min(unionDistance,chamberDistance); +- unionShellDistance=std::min(unionShellDistance,chamberShellDistance); +- unionFloorShellDistance=std::min(unionFloorShellDistance, +- chamberFloorShellDistance); +- unionLandingChamberFloorShellDistance=std::min( +- unionLandingChamberFloorShellDistance,chamberFloorShellDistance); +- if(passageTargetsPlannedTunnelNetwork) +- plannedTunnelPassageShellDistance=std::min( +- plannedTunnelPassageShellDistance,chamberShellDistance); +- } + if(passageTargetsNativeCaveField&&!passageTargetsPlannedTunnelNetwork) + continue; + +@@ -1658,12 +1622,9 @@ public: + } + + if(!std::isfinite(unionDistance))return density; ++ if(outTubeOnlyDistance) ++ *outTubeOnlyDistance=static_cast(unionDistance/Q); + const float passageAirDistance=static_cast(-unionDistance/Q); +- // Keep the landing floor shell where it is exposed to the chamber. At +- // the tunnel joint, the passage void must be able to open through that +- // floor shell so its own continuous floor and shell define the throat. +- if(unionLandingChamberFloorShellDistance<0.0&&unionDistance>0.0) +- return static_cast(unionLandingChamberFloorShellDistance/Q); + const auto plannedPortalWalkCorridorContains=[&]() + { + if(!queryStratum||!queryStratum->Resolved +@@ -1746,90 +1707,6 @@ public: + departureTailDistance) + ||nearRouteSegment(*lastTwo,*last,PortalWalkLeadMetres,true,0.0); + }; +- const auto connectedSurfaceLandingShellContains=[&]() +- { +- if(!queryStratum||!queryStratum->Resolved +- ||queryStratum->Resolved->generatorKind!="SurfaceWorld")return false; +- const double queryX=qx/qPerMetre,queryY=qy/qPerMetre,queryZ=qz/qPerMetre; +- constexpr double ChamberShellThicknessMetres=0.75; +- constexpr double RouteClearanceMetres=1.25; +- constexpr double RouteHeightClearanceMetres=1.5; +- constexpr double PortalWalkLeadMetres=9.0; +- for(const auto& passage:ResolvedLayout->passages) +- { +- if(passage.upperStrateIndex!=queryStratum->StrateIndex)continue; +- const auto& chamber=passage.upperLandingChamber; +- if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0)continue; +- const double cx=static_cast(chamber.floorCenterSubvoxels[0])/qPerMetre; +- const double cy=static_cast(chamber.floorCenterSubvoxels[1])/qPerMetre; +- const double floorZ=static_cast(chamber.floorCenterSubvoxels[2])/qPerMetre +- +chamber.floorGradientQ16[0]/65536.0*(queryX-cx) +- +chamber.floorGradientQ16[1]/65536.0*(queryY-cy); +- const double radialDistance=std::hypot(queryX-cx,queryY-cy) +- -static_cast(chamber.radiusSubvoxels)/qPerMetre; +- const double floorDistance=floorZ-queryZ; +- const double ceilingDistance=queryZ-(floorZ +- +static_cast(chamber.clearHeightSubvoxels)/qPerMetre); +- const double chamberDistance=std::max({radialDistance, +- floorDistance,ceilingDistance}); +- const double chamberShellDistance=std::max(-chamberDistance, +- chamberDistance-ChamberShellThicknessMetres); +- if(chamberDistance<=0.0||chamberShellDistance>=0.0)continue; +- +- std::vector route; +- for(const auto& anchor:queryStratum->Resolved->anchors) +- if(anchor.kind=="route_point"&&anchor.values.size()>=3) +- route.push_back(&anchor); +- if(route.size()<2)continue; +- const double mouthZ=static_cast(passage.upperMouthSubvoxels[2]) +- /qPerMetre; +- std::size_t endpoint=route.size(); +- double endpointDistanceSquared=1.0; +- for(std::size_t index=0;indexvalues[0]/qPerMetre; +- const double y=route[index]->values[1]/qPerMetre; +- const double z=route[index]->values[2]/qPerMetre; +- const double dx=x-cx,dy=y-cy; +- const double distanceSquared=dx*dx+dy*dy; +- if(distanceSquared routeDistance(route.size(),0.0); +- for(std::size_t index=1;indexvalues[0]-route[index-1]->values[0]) +- /qPerMetre; +- const double dy=(route[index]->values[1]-route[index-1]->values[1]) +- /qPerMetre; +- routeDistance[index]=routeDistance[index-1]+std::hypot(dx,dy); +- } +- for(std::size_t index=1;indexPortalWalkLeadMetres)continue; +- const double routeX=ax+t*dx,routeY=ay+t*dy; +- const double routeZ=az+t*(b.values[2]-a.values[2])/qPerMetre; +- if(std::hypot(queryX-routeX,queryY-routeY)<=RouteClearanceMetres +- &&std::abs(queryZ-routeZ)<=RouteHeightClearanceMetres)return true; +- } +- } +- return false; +- }; + if(plannedTunnelStratum + &&nativeTunnelFieldDensity!=std::numeric_limits::lowest() + &&nativeTunnelFieldDensity<0.0f) +@@ -1893,13 +1770,95 @@ public: + return std::max(density,passageAirDistance); + if(unionDistance>0.0&&unionShellDistance<0.0&&density>=0.0f) + { +- if(connectedSurfaceLandingShellContains()) +- return std::max(density,passageAirDistance); + return static_cast(unionShellDistance/Q); + } + return std::max(density,passageAirDistance); + } + ++ float ApplyResolvedLandingPadStamps(float density,float X,float Y,float Z, ++ float tubeOnlyDistance) const ++ { ++ if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X) ++ ||!std::isfinite(Y)||!std::isfinite(Z))return density; ++ const auto* bound=FindBoundStratumForManagerZ(Z); ++ if(!bound||!bound->Resolved)return density; ++ const double coreMetres=std::max(1, ++ ResolvedLayout->coreVoxelSizeMicrometres)/1000000.0; ++ constexpr double Q=static_cast( ++ VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); ++ const double qPerMetre=Q/coreMetres; ++ const double xMetres=static_cast(X)*coreMetres; ++ const double yMetres=static_cast(Y)*coreMetres; ++ const double zMetres=static_cast(Z)*coreMetres; ++ const double tubeDistanceMetres=static_cast(tubeOnlyDistance)*coreMetres; ++ const auto* tileQueries=FScopedPassageTileQueries::CurrentFor(*this); ++ const auto* candidatePassages=tileQueries ++ ?&tileQueries->routePassageIndices():nullptr; ++ const std::size_t candidateCount=candidatePassages ++ ?candidatePassages->size():ResolvedLayout->passages.size(); ++ float result=density; ++ constexpr double CosTwelveDegrees=0.9781476007338057; ++ constexpr double OuterReachMetres=VF_PassageInfluenceReachMetres; ++ for(std::size_t offset=0;offset=ResolvedLayout->passages.size())continue; ++ const auto& passage=ResolvedLayout->passages[passageIndex]; ++ const auto applyPad=[&]( ++ const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& pad) ++ { ++ if(pad.radiusSubvoxels<=0)return; ++ const double centerX=static_cast(pad.floorCenterSubvoxels[0]) ++ /qPerMetre; ++ const double centerY=static_cast(pad.floorCenterSubvoxels[1]) ++ /qPerMetre; ++ const double floorP=static_cast(pad.floorCenterSubvoxels[2]) ++ /qPerMetre; ++ const double radius=std::max(3.0, ++ static_cast(std::max(0, ++ passage.mouthRadiusSubvoxels))/qPerMetre+1.0); ++ const double dx=xMetres-centerX,dy=yMetres-centerY; ++ const double radial=DeterministicSqrt(dx*dx+dy*dy); ++ const double outerRadius=radius+VF_LandingPadBlendRadiusMetres; ++ if(radial>outerRadius+OuterReachMetres ++ ||zMetresfloorP+VF_LandingPadHeadroomMetres ++ +VF_LandingPadConeSlope*VF_LandingPadBlendRadiusMetres ++ +OuterReachMetres)return; ++ const double coneRun=std::max(0.0,radial-radius); ++ const double lower=floorP-VF_LandingPadConeSlope*coneRun; ++ const double upper=floorP+VF_LandingPadConeSlope*coneRun; ++ const double fillDistanceMetres=std::max({ ++ CosTwelveDegrees*(zMetres-lower), ++ CosTwelveDegrees*(lower-VF_LandingPadFillDepthMetres-zMetres), ++ radial-outerRadius}); ++ const double airDistanceMetres=std::min({ ++ CosTwelveDegrees*(zMetres-upper), ++ CosTwelveDegrees*(upper+VF_LandingPadHeadroomMetres-zMetres), ++ outerRadius-radial}); ++ const double fillVoxels=fillDistanceMetres/coreMetres; ++ const double airVoxels=airDistanceMetres/coreMetres; ++ const double maskedFill=std::max(fillVoxels,-tubeDistanceMetres/coreMetres); ++ const double smoothWidthMetres=VF_LandingPadSmoothMinMaximumMetres ++ *std::clamp(radial-radius,0.0,1.0); ++ const float next=static_cast(result); ++ const float fill=static_cast(maskedFill); ++ const float width=static_cast(smoothWidthMetres/coreMetres); ++ const float clamped=width>0.0f ++ ?SmoothMinDistance(next,fill,width):std::min(next,fill); ++ result=std::max(clamped,static_cast(airVoxels)); ++ }; ++ if(passage.upperStrateIndex==bound->StrateIndex) ++ applyPad(passage.upperLandingChamber); ++ if(passage.lowerStrateIndex==bound->StrateIndex) ++ applyPad(passage.lowerLandingChamber); ++ } ++ return result; ++ } ++ + float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const + { + float graphDensity=0.0f; +@@ -1930,6 +1889,7 @@ public: + } + float composed=boundary; + float stamped=composed; ++ float tubeOnlyDistance=std::numeric_limits::infinity(); + if(!StampRuntime.empty()) + { + VoxelForgeCore::Linux77Profiler::FScopedPhase l77StampPhase( +@@ -1963,12 +1923,13 @@ public: + const float nativeTunnelFieldDensity=nativeTunnelField + ?graphDensity:std::numeric_limits::lowest(); + stamped=ApplyResolvedStratePassageCarves(stamped,X,Y,Z, +- nativeTunnelFieldDensity,nativeGraphFloorSupport); ++ nativeTunnelFieldDensity,nativeGraphFloorSupport, ++ &tubeOnlyDistance); + } + if(bTraceTnComposition) + std::printf("[L75TNCompositionTrace] input=%.9g graph=%.9g boundary=%.9g passage=%.9g stamp=%.9g native=1\n", + density,graphDensity,boundary,composed,stamped); +- return stamped; ++ return ApplyResolvedLandingPadStamps(stamped,X,Y,Z,tubeOnlyDistance); + } + // The legacy landmark demo retains its compatibility floor cap. Native caves + // already combine each floor with its wall before the single void carve above. +@@ -1985,12 +1946,14 @@ public: + VoxelForgeCore::Linux77Profiler::EPhase::Passage); + FScopedLegacyFieldTermTimer TermTimer( + ELegacyFieldTerm::ComposerPassage); +- final=ApplyResolvedStratePassageCarves(final,X,Y,Z); ++ final=ApplyResolvedStratePassageCarves(final,X,Y,Z, ++ std::numeric_limits::lowest(), ++ std::numeric_limits::lowest(),&tubeOnlyDistance); + } + if(bTraceTnComposition) + std::printf("[L75TNCompositionTrace] input=%.9g graph=%.9g boundary=%.9g passage=%.9g stamp=%.9g final=%.9g native=0\n", + density,graphDensity,boundary,composed,stamped,final); +- return final; ++ return ApplyResolvedLandingPadStamps(final,X,Y,Z,tubeOnlyDistance); + } + + bool ResolvedPassageIntersectsBox(const int32 minimum[3],const int32 maximum[3]) const +@@ -2033,26 +1996,35 @@ public: + } + if(overlaps(low,high))return true; + } +- const auto chamberOverlaps=[&]( +- const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) +- { +- const double cx=static_cast(chamber.floorCenterSubvoxels[0])/Q; +- const double cy=static_cast(chamber.floorCenterSubvoxels[1])/Q; +- const double cz=static_cast(chamber.floorCenterSubvoxels[2])/Q; +- const double radius=static_cast(chamber.radiusSubvoxels)/Q; +- const double floorExtent=radius*std::hypot( +- static_cast(chamber.floorGradientQ16[0])/65536.0, +- static_cast(chamber.floorGradientQ16[1])/65536.0); ++ const auto padOverlaps=[&]( ++ const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& pad, ++ std::int64_t mouthRadiusSubvoxels) ++ { ++ if(pad.radiusSubvoxels<=0)return false; ++ const double cx=static_cast(pad.floorCenterSubvoxels[0])/Q; ++ const double cy=static_cast(pad.floorCenterSubvoxels[1])/Q; ++ const double pz=static_cast(pad.floorCenterSubvoxels[2])/Q; ++ const double qPerMetre=Q*1000000.0/ ++ std::max(1,ResolvedLayout->coreVoxelSizeMicrometres); ++ const double coreMetres=Q/qPerMetre; ++ const double radius=std::max(3.0, ++ static_cast(std::max(0,mouthRadiusSubvoxels)) ++ /qPerMetre+1.0); ++ const double outerRadius=radius+VF_LandingPadBlendRadiusMetres; + const double extent=reachCoreVoxels+0.01; +- const double low[3]={cx-radius-extent,cy-radius-extent, +- cz-floorExtent-extent}; +- const double high[3]={cx+radius+extent,cy+radius+extent, +- cz+floorExtent+static_cast( +- chamber.clearHeightSubvoxels)/Q+extent}; ++ const double coneHeight=VF_LandingPadConeSlope ++ *VF_LandingPadBlendRadiusMetres; ++ const double low[3]={cx-outerRadius/coreMetres-extent, ++ cy-outerRadius/coreMetres-extent, ++ pz-(VF_LandingPadFillDepthMetres+coneHeight)/coreMetres-extent}; ++ const double high[3]={cx+outerRadius/coreMetres+extent, ++ cy+outerRadius/coreMetres+extent, ++ pz+(VF_LandingPadHeadroomMetres+coneHeight)/coreMetres+extent}; + return overlaps(low,high); + }; +- if(chamberOverlaps(passage.upperLandingChamber) +- ||chamberOverlaps(passage.lowerLandingChamber))return true; ++ if(padOverlaps(passage.upperLandingChamber,passage.mouthRadiusSubvoxels) ++ ||padOverlaps(passage.lowerLandingChamber,passage.mouthRadiusSubvoxels)) ++ return true; + } + return false; + } +@@ -5415,6 +5387,9 @@ private: + if(!ResolvedLayout||ResolvedLayout->passages.empty())return; + constexpr double Q=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + constexpr std::size_t CandidateFineSamplesPerSpan=5; ++ const double coreMetres=std::max(1, ++ ResolvedLayout->coreVoxelSizeMicrometres)/1000000.0; ++ const double qPerMetre=Q/coreMetres; + const double requiredReachCoreVoxels=VF_PassageInfluenceReachCoreVoxels( + ResolvedLayout->coreVoxelSizeMicrometres); + for(std::size_t passageIndex=0;passageIndexpassages.size(); +@@ -5471,40 +5446,47 @@ private: + std::ceil((localMaximum[axis]+localExtent)*Q)); + } + } +- const auto includeLandingChamber=[&]( +- const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) +- { +- const double cx=static_cast(chamber.floorCenterSubvoxels[0])/Q; +- const double cy=static_cast(chamber.floorCenterSubvoxels[1])/Q; +- const double cz=static_cast(chamber.floorCenterSubvoxels[2])/Q; +- const double radius=static_cast(chamber.radiusSubvoxels)/Q; +- const double gx=static_cast(chamber.floorGradientQ16[0])/65536.0; +- const double gy=static_cast(chamber.floorGradientQ16[1])/65536.0; +- const double floorExtent=radius*std::hypot(gx,gy); +- const double height=static_cast( +- chamber.clearHeightSubvoxels)/Q; +- const double extent=requiredReachCoreVoxels+0.01; +- const std::array minimum{ +- cx-radius-extent,cy-radius-extent,cz-floorExtent-extent}; +- const std::array maximum{ +- cx+radius+extent,cy+radius+extent, +- cz+floorExtent+height+extent}; ++ const auto includeLandingPad=[&]( ++ const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& pad, ++ std::int64_t mouthRadiusSubvoxels) ++ { ++ if(pad.radiusSubvoxels<=0)return; ++ const double cx=static_cast(pad.floorCenterSubvoxels[0])/Q; ++ const double cy=static_cast(pad.floorCenterSubvoxels[1])/Q; ++ const double pz=static_cast(pad.floorCenterSubvoxels[2])/Q; ++ const double radius=std::max(3.0, ++ static_cast(std::max(0,mouthRadiusSubvoxels)) ++ /qPerMetre+1.0); ++ const double outerRadius=radius+VF_LandingPadBlendRadiusMetres; ++ const double coneHeight=VF_LandingPadConeSlope ++ *VF_LandingPadBlendRadiusMetres; ++ const std::array baseMinimum{ ++ cx-outerRadius/coreMetres, ++ cy-outerRadius/coreMetres, ++ pz-(VF_LandingPadFillDepthMetres+coneHeight)/coreMetres}; ++ const std::array baseMaximum{ ++ cx+outerRadius/coreMetres, ++ cy+outerRadius/coreMetres, ++ pz+(VF_LandingPadHeadroomMetres+coneHeight)/coreMetres}; + FPassageProofBound proof; ++ const double extent=requiredReachCoreVoxels+0.01; + for(std::size_t axis=0;axis<3;++axis) + { +- routeMinimum[axis]=std::min(routeMinimum[axis],minimum[axis]); +- routeMaximum[axis]=std::max(routeMaximum[axis],maximum[axis]); +- proof.minimum[axis]=minimum[axis]; +- proof.maximum[axis]=maximum[axis]; ++ routeMinimum[axis]=std::min(routeMinimum[axis],baseMinimum[axis]); ++ routeMaximum[axis]=std::max(routeMaximum[axis],baseMaximum[axis]); ++ proof.minimum[axis]=baseMinimum[axis]-extent; ++ proof.maximum[axis]=baseMaximum[axis]+extent; + } + PassageProofBounds.push_back(proof); + }; + if(segmentIndex==0) +- includeLandingChamber( +- ResolvedLayout->passages[passageIndex].upperLandingChamber); ++ includeLandingPad(ResolvedLayout->passages[passageIndex] ++ .upperLandingChamber,ResolvedLayout->passages[passageIndex] ++ .mouthRadiusSubvoxels); + if(segmentEndIndex==segmentCount) +- includeLandingChamber( +- ResolvedLayout->passages[passageIndex].lowerLandingChamber); ++ includeLandingPad(ResolvedLayout->passages[passageIndex] ++ .lowerLandingChamber,ResolvedLayout->passages[passageIndex] ++ .mouthRadiusSubvoxels); + const double routeExtent=maximumRadiusSubvoxels/Q + +requiredReachCoreVoxels+0.01; + const double tileExtent=routeExtent; +@@ -5629,6 +5611,108 @@ private: + } + } + } ++ for(std::size_t passageIndex=0; ++ passageIndexpassages.size();++passageIndex) ++ { ++ const auto& passage=ResolvedLayout->passages[passageIndex]; ++ const auto sampleLandingPad=[&]( ++ const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& pad) ++ { ++ if(pad.radiusSubvoxels<=0)return; ++ const double centerX=static_cast( ++ pad.floorCenterSubvoxels[0])/qPerMetre; ++ const double centerY=static_cast( ++ pad.floorCenterSubvoxels[1])/qPerMetre; ++ const double floorP=static_cast( ++ pad.floorCenterSubvoxels[2])/qPerMetre; ++ const double radius=std::max(3.0, ++ static_cast(std::max(0, ++ passage.mouthRadiusSubvoxels))/qPerMetre+1.0); ++ const double probeRadius=radius+VF_LandingPadBlendRadiusMetres-0.25; ++ const std::array probeHeights{ ++ floorP-0.5*VF_LandingPadFillDepthMetres, ++ floorP+0.5*VF_LandingPadHeadroomMetres}; ++ for(const double probeZ:probeHeights) ++ { ++ const double sampleQ[3]={centerX*qPerMetre, ++ centerY*qPerMetre,probeZ*qPerMetre}; ++ const double sampleVoxel[3]={sampleQ[0]/Q, ++ sampleQ[1]/Q,sampleQ[2]/Q}; ++ const float contribution=ApplyResolvedLandingPadStamps( ++ 0.0f,static_cast(sampleVoxel[0]), ++ static_cast(sampleVoxel[1]), ++ static_cast(sampleVoxel[2]), ++ std::numeric_limits::infinity()); ++ ++sampled; ++ if(std::abs(contribution)<=1.0e-6f)continue; ++ ++signRelevant; ++ if(PassageProofNodes.empty() ++ ||!PassageProofBoundsOverlapNode(0,sampleVoxel,sampleVoxel)) ++ ++proofMisses; ++ std::array sampleMinimum{},sampleMaximum{}; ++ for(std::size_t axis=0;axis<3;++axis) ++ { ++ sampleMinimum[axis]=static_cast( ++ std::floor(sampleQ[axis])); ++ sampleMaximum[axis]=static_cast( ++ std::ceil(sampleQ[axis])); ++ } ++ const auto candidates=BuildPassageFeatureTileList( ++ sampleMinimum,sampleMaximum); ++ const bool featureContainsPassage=std::any_of( ++ candidates.begin(),candidates.end(), ++ [&](std::uint32_t featureIndex) ++ { ++ return featureIndex(edgeSampleVoxel[0]), ++ static_cast(edgeSampleVoxel[1]), ++ static_cast(edgeSampleVoxel[2]), ++ std::numeric_limits::infinity()); ++ ++sampled; ++ if(std::abs(edgeContribution)<=1.0e-6f)continue; ++ ++signRelevant; ++ if(PassageProofNodes.empty() ++ ||!PassageProofBoundsOverlapNode(0,edgeSampleVoxel,edgeSampleVoxel)) ++ ++proofMisses; ++ for(std::size_t axis=0;axis<3;++axis) ++ { ++ sampleMinimum[axis]=static_cast( ++ std::floor(edgeSampleQ[axis])); ++ sampleMaximum[axis]=static_cast( ++ std::ceil(edgeSampleQ[axis])); ++ } ++ const auto edgeCandidates=BuildPassageFeatureTileList( ++ sampleMinimum,sampleMaximum); ++ const bool edgeFeatureContainsPassage=std::any_of( ++ edgeCandidates.begin(),edgeCandidates.end(), ++ [&](std::uint32_t featureIndex) ++ { ++ return featureIndex0&&signRelevant>0 + &&proofMisses==0&&featureMisses==0; + std::printf("[L130PassageReachProofAudit] reach_m=%.3f sampled=%llu sign_relevant=%llu proof_misses=%llu feature_misses=%llu status=%s\n", +diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h +index 2fcf4b5..1b502ed 100644 +--- a/Standalone/Source/StreamBenchmarkPipeline.h ++++ b/Standalone/Source/StreamBenchmarkPipeline.h +@@ -4703,78 +4703,6 @@ namespace StandaloneStreamBenchmark + [index](const auto& s){return s.strateIndex==index;}); + return it==ResolvedLayout->strates.end()?nullptr:&*it; + }; +- const auto landingChamberConnectionContains=[&]( +- const VoxelForgeCore::ResolvedLayoutPassageV1& passage,bool upper, +- double xMetres,double yMetres,double zMetres) +- { +- const auto& chamber=upper?passage.upperLandingChamber +- :passage.lowerLandingChamber; +- const auto* stratum=findStratum(upper?passage.upperStrateIndex +- :passage.lowerStrateIndex); +- if(!stratum||chamber.radiusSubvoxels<=0)return false; +- std::vector route; +- for(const auto& anchor:stratum->anchors) +- if(anchor.kind=="route_point"&&anchor.values.size()>=3) +- route.push_back(&anchor); +- if(route.size()<2)return false; +- const double cx=qToMetres(chamber.floorCenterSubvoxels[0]); +- const double cy=qToMetres(chamber.floorCenterSubvoxels[1]); +- const double mouthZ=qToMetres(upper?passage.upperMouthSubvoxels[2] +- :passage.lowerMouthSubvoxels[2]); +- std::size_t endpoint=route.size(); +- double endpointDistanceSquared=1.0; +- for(std::size_t index=0;indexvalues[0])-cx; +- const double dy=qToMetres(route[index]->values[1])-cy; +- const double z=qToMetres(route[index]->values[2]); +- const double distanceSquared=dx*dx+dy*dy; +- if(distanceSquared routeDistance(route.size(),0.0); +- for(std::size_t index=1;indexvalues[0] +- -route[index-1]->values[0]); +- const double dy=qToMetres(route[index]->values[1] +- -route[index-1]->values[1]); +- routeDistance[index]=routeDistance[index-1]+std::hypot(dx,dy); +- } +- for(std::size_t index=1;index9.0)continue; +- const double routeX=ax+t*dx,routeY=ay+t*dy; +- const double routeZ=az+t*qToMetres(b.values[2]-a.values[2]); +- const double lateralDistance=std::hypot(xMetres-routeX, +- yMetres-routeY); +- if(stratum->generatorKind=="TunnelNetwork" +- ||stratum->generatorKind=="VerticalShafts") +- { +- const double routeFloor=routeZ-1.13; +- if(lateralDistance<=0.80&&zMetres>=routeFloor-0.30 +- &&zMetres<=routeFloor+2.25)return true; +- } +- else if(lateralDistance<=1.25 +- &&std::abs(zMetres-routeZ)<=1.5)return true; +- } +- return false; +- }; + const auto controlSnapshot=CoreSnapshot.withoutResolvedLayoutPassages(); + VoxelForgeCore::World blockedWorld(controlSnapshot); + std::string blockedCompileError; +@@ -5153,89 +5081,6 @@ namespace StandaloneStreamBenchmark + if(interior<=0.0f)++audit.interiorViolations; + } + } +- constexpr std::array chamberFloorRadii{{0.0,2.5,5.0}}; +- constexpr std::array chamberWallHeights{{1.13,3.20}}; +- constexpr std::size_t ChamberAngularSamples=16; +- for(const bool upper:{true,false}) +- { +- const auto& chamber=upper?passage.upperLandingChamber +- :passage.lowerLandingChamber; +- if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0)continue; +- const double centerX=qToMetres(chamber.floorCenterSubvoxels[0]); +- const double centerY=qToMetres(chamber.floorCenterSubvoxels[1]); +- const double baseFloorZ=qToMetres(chamber.floorCenterSubvoxels[2]); +- const double radius=qToMetres(chamber.radiusSubvoxels); +- const double gx=chamber.floorGradientQ16[0]/65536.0; +- const double gy=chamber.floorGradientQ16[1]/65536.0; +- for(const double radial:chamberFloorRadii) +- { +- if(radial>=radius-1.0)continue; +- const std::size_t angles=radial==0.0?1:ChamberAngularSamples; +- for(std::size_t angleIndex=0;angleIndex(angleIndex)/ChamberAngularSamples; +- const double sampleX=centerX+radial*std::cos(angle); +- const double sampleY=centerY+radial*std::sin(angle); +- const double floorZ=baseFloorZ+gx*(sampleX-centerX) +- +gy*(sampleY-centerY); +- for(const double depth:PassageShellAuditDepthsMetres) +- { +- const double sampleZ=floorZ-depth; +- const VoxelForgeCore::Vec3d position{sampleX/coreMetres, +- sampleY/coreMetres,sampleZ/coreMetres}; +- if(Native.densityAt(position)<0.0f)continue; +- if(landingChamberConnectionContains(passage,upper, +- sampleX,sampleY,sampleZ)) +- {++audit.chamberConnectionSkips;continue;} +- ++audit.chamberFloorSamples; +- const float density=Field.densityAt(position); +- if(density>=PassageShellDensityTolerance) +- { +- ++audit.chamberFloorViolations; +- std::printf("[PassageLandingChamberShellSample] id=%s side=%s kind=floor_missing xyz_m=(%.3f,%.3f,%.3f) native=%.6f density=%.6f\n", +- passage.stableId.c_str(),upper?"upper":"lower", +- sampleX,sampleY,sampleZ,Native.densityAt(position),density); +- } +- } +- } +- } +- for(std::size_t angleIndex=0;angleIndex(angleIndex)/ChamberAngularSamples; +- const double nx=std::cos(angle),ny=std::sin(angle); +- for(const double depth:PassageShellAuditDepthsMetres) +- for(const double height:chamberWallHeights) +- { +- const double sampleX=centerX+(radius+depth)*nx; +- const double sampleY=centerY+(radius+depth)*ny; +- const double floorZ=baseFloorZ+gx*(sampleX-centerX) +- +gy*(sampleY-centerY); +- const double sampleZ=floorZ+height; +- const VoxelForgeCore::Vec3d position{sampleX/coreMetres, +- sampleY/coreMetres,sampleZ/coreMetres}; +- if(Native.densityAt(position)<0.0f)continue; +- if(passageInteriorWinsAt(sampleX,sampleY,sampleZ, +- std::numeric_limits::max(), +- std::numeric_limits::max(),passageIndex) +- ||landingChamberConnectionContains(passage,upper, +- sampleX,sampleY,sampleZ)) +- {++audit.chamberConnectionSkips;continue;} +- ++audit.chamberWallSamples; +- const float density=Field.densityAt(position); +- if(density>=PassageShellDensityTolerance) +- { +- ++audit.chamberWallViolations; +- std::printf("[PassageLandingChamberShellSample] id=%s side=%s kind=wall_missing xyz_m=(%.3f,%.3f,%.3f) native=%.6f density=%.6f radial_m=%.3f height_m=%.3f\n", +- passage.stableId.c_str(),upper?"upper":"lower", +- sampleX,sampleY,sampleZ,Native.densityAt(position),density, +- radius+depth,height); +- } +- } +- } +- } + return audit; + }; + const auto mouthInContent=[&](std::int32_t index, +@@ -5305,10 +5150,257 @@ namespace StandaloneStreamBenchmark + } + return total; + }; ++ struct FLandingPadAudit ++ { ++ std::uint64_t flatSamples=0,flatFailures=0; ++ std::uint64_t supportSamples=0,supportFailures=0; ++ std::uint64_t headroomSamples=0,headroomFailures=0; ++ std::uint64_t blendRootSamples=0,blendMissing=0,blendGradeFailures=0; ++ std::uint64_t negativeControlsDetected=0; ++ double maximumFlatnessMetres=0.0,maximumGradeDegrees=0.0; ++ bool passed=false; ++ }; ++ const auto auditLandingPad=[&](const VoxelForgeCore::World& field, ++ const VoxelForgeCore::ResolvedLayoutPassageV1& passage,bool upper) ++ { ++ constexpr double ConeSlope=0.2125565616700221; ++ constexpr double CosTwelveDegrees=0.9781476007338057; ++ constexpr double FillDepthMetres=1.25,HeadroomMetres=3.0; ++ constexpr double BlendRadiusMetres=8.0; ++ constexpr double FlatnessLimitMetres=0.020; ++ constexpr double GradeLimitDegrees=15.0; ++ constexpr std::size_t RadialDirections=36; ++ constexpr double FloorSearchStepMetres=0.125; ++ FLandingPadAudit audit; ++ const auto& pad=upper?passage.upperLandingChamber ++ :passage.lowerLandingChamber; ++ if(pad.radiusSubvoxels<=0)return audit; ++ const double centerX=qToMetres(pad.floorCenterSubvoxels[0]); ++ const double centerY=qToMetres(pad.floorCenterSubvoxels[1]); ++ const double floorP=qToMetres(pad.floorCenterSubvoxels[2]); ++ const double radius=std::max(3.0, ++ qToMetres(std::max(0,passage.mouthRadiusSubvoxels))+1.0); ++ const auto densityMetres=[&](const VoxelForgeCore::World& source, ++ double x,double y,double z) ++ { ++ const float value=source.densityAt({x/coreMetres,y/coreMetres, ++ z/coreMetres}); ++ return std::isfinite(value)?static_cast(value)*coreMetres ++ :std::numeric_limits::quiet_NaN(); ++ }; ++ struct FFloorRoot ++ {double z=0.0;std::uint32_t crossings=0;bool found=false;}; ++ const auto findFloorRoot=[&](auto&& sampleDensity,double x,double y, ++ double low,double high) ++ { ++ FFloorRoot result; ++ double previousZ=low; ++ double previous=sampleDensity(x,y,previousZ); ++ if(!std::isfinite(previous))return result; ++ for(double z=low+FloorSearchStepMetres; ++ z=0.0) ++ { ++ const double denominator=current-previous; ++ const double t=std::abs(denominator)>1.0e-12 ++ ?std::clamp(-previous/denominator,0.0,1.0):0.0; ++ const double root=std::lerp(previousZ,currentZ,t); ++ ++result.crossings; ++ if(!result.found)result.z=root; ++ result.found=true; ++ } ++ previousZ=currentZ; ++ previous=current; ++ if(currentZ>=high)break; ++ } ++ return result; ++ }; ++ const auto fieldDensity=[&](double x,double y,double z) ++ {return densityMetres(field,x,y,z);}; ++ const auto sourceFieldDensity=[&](double x,double y,double z) ++ { ++ const float value=Shape.passageSourceDensityAt({x/coreMetres, ++ y/coreMetres,z/coreMetres}); ++ return std::isfinite(value)?static_cast(value)*coreMetres ++ :std::numeric_limits::quiet_NaN(); ++ }; ++ struct FFlatProbe {double x=0.0,y=0.0,z=0.0;}; ++ std::vector flatProbes; ++ const auto probeFlatness=[&](double radial,double angle) ++ { ++ const double x=centerX+radial*std::cos(angle); ++ const double y=centerY+radial*std::sin(angle); ++ const auto root=findFloorRoot(fieldDensity,x,y, ++ floorP-0.75,floorP+0.75); ++ ++audit.flatSamples; ++ if(!root.found||root.crossings!=1) ++ {++audit.flatFailures;return;} ++ const double error=std::abs(root.z-floorP); ++ audit.maximumFlatnessMetres=std::max( ++ audit.maximumFlatnessMetres,error); ++ if(error>FlatnessLimitMetres)++audit.flatFailures; ++ flatProbes.push_back({x,y,root.z}); ++ }; ++ probeFlatness(0.0,0.0); ++ for(std::size_t direction=0;direction(direction)/RadialDirections; ++ probeFlatness(0.90*radius,angle); ++ } ++ constexpr std::array SupportRadii{{0.0,0.5,0.9}}; ++ for(const double radialFraction:SupportRadii) ++ for(std::size_t direction=0;direction(direction)/RadialDirections; ++ const double x=centerX+radialFraction*radius*std::cos(angle); ++ const double y=centerY+radialFraction*radius*std::sin(angle); ++ ++audit.supportSamples; ++ if(!(fieldDensity(x,y,floorP-0.10)<0.0)) ++ ++audit.supportFailures; ++ ++audit.headroomSamples; ++ if(!(fieldDensity(x,y,floorP+2.0)>0.0)) ++ ++audit.headroomFailures; ++ } ++ constexpr std::size_t BlendStations=16; ++ const double blendEnd=BlendRadiusMetres-0.25; ++ for(std::size_t direction=0;direction(direction)/RadialDirections; ++ bool previousFound=false; ++ double previousRadius=0.0,previousFloor=0.0; ++ for(std::size_t station=0;station<=BlendStations;++station) ++ { ++ const double radial=radius+blendEnd ++ *static_cast(station)/BlendStations; ++ const double x=centerX+radial*std::cos(angle); ++ const double y=centerY+radial*std::sin(angle); ++ const double coneRun=std::max(0.0,radial-radius); ++ const double coneHeight=ConeSlope*coneRun; ++ const auto root=findFloorRoot(fieldDensity,x,y, ++ floorP-coneHeight-0.75,floorP+coneHeight+0.75); ++ ++audit.blendRootSamples; ++ if(!root.found||root.crossings!=1) ++ { ++ ++audit.blendMissing; ++ previousFound=false; ++ continue; ++ } ++ if(previousFound) ++ { ++ const double grade=std::atan2( ++ std::abs(root.z-previousFloor),radial-previousRadius) ++ *180.0/3.14159265358979323846; ++ audit.maximumGradeDegrees=std::max( ++ audit.maximumGradeDegrees,grade); ++ if(grade>GradeLimitDegrees)++audit.blendGradeFailures; ++ } ++ previousFound=true; ++ previousRadius=radial; ++ previousFloor=root.z; ++ } ++ } ++ const auto sourceField=[&](double x,double y,double z) ++ {return sourceFieldDensity(x,y,z);}; ++ bool padOffRejected=false; ++ for(std::size_t direction=0;direction(direction)/RadialDirections; ++ const double x=centerX+0.90*radius*std::cos(angle); ++ const double y=centerY+0.90*radius*std::sin(angle); ++ const auto root=findFloorRoot(sourceField,x,y, ++ floorP-0.75,floorP+0.75); ++ if(!root.found||root.crossings!=1 ++ ||std::abs(root.z-floorP)>FlatnessLimitMetres) ++ {padOffRejected=true;break;} ++ } ++ const bool tiltRejected=std::any_of(flatProbes.begin(),flatProbes.end(), ++ [&](const FFlatProbe& probe) ++ { ++ return std::abs((probe.z-floorP) ++ +0.02*(probe.x-centerX))>FlatnessLimitMetres; ++ }); ++ const auto controlStamp=[&](double blendRadius,double radial,double z, ++ double native,double tubeDistance) ++ { ++ const double coneRun=std::max(0.0,radial-radius); ++ const double lower=floorP-ConeSlope*coneRun; ++ const double upper=floorP+ConeSlope*coneRun; ++ const double outer=radius+blendRadius; ++ const double fill=std::max({CosTwelveDegrees*(z-lower), ++ CosTwelveDegrees*(lower-FillDepthMetres-z),radial-outer}); ++ const double air=std::min({CosTwelveDegrees*(z-upper), ++ CosTwelveDegrees*(upper+HeadroomMetres-z),outer-radial}); ++ const double masked=std::max(fill,-tubeDistance); ++ const double smoothWidth=0.5*std::clamp(radial-radius,0.0,1.0); ++ double smoothMinimum=std::min(native,masked); ++ if(smoothWidth>0.0) ++ { ++ const double h=std::max(smoothWidth ++ -std::abs(native-masked),0.0)/smoothWidth; ++ smoothMinimum-=h*h*h*(smoothWidth/6.0); ++ } ++ return std::max(smoothMinimum,air); ++ }; ++ const double outerProbe=radius+BlendRadiusMetres-0.25; ++ const double outerProbeZ=floorP-ConeSlope*(outerProbe-radius)-0.05; ++ const double fullBlendProbe=controlStamp(BlendRadiusMetres, ++ outerProbe,outerProbeZ,0.0,std::numeric_limits::infinity()); ++ const double halfBlendProbe=controlStamp(BlendRadiusMetres*0.5, ++ outerProbe,outerProbeZ,0.0,std::numeric_limits::infinity()); ++ const bool halfBlendRejected=fullBlendProbe< -0.01 ++ &&std::abs(halfBlendProbe)<1.0e-6; ++ const double boreMasked=controlStamp(BlendRadiusMetres, ++ radius*0.5,floorP-0.25,0.25,-0.50); ++ const double boreUnmasked=controlStamp(BlendRadiusMetres, ++ radius*0.5,floorP-0.25,0.25, ++ std::numeric_limits::infinity()); ++ const bool boreMaskRejected=boreMasked>0.0&&boreUnmasked<0.0; ++ audit.negativeControlsDetected=static_cast(padOffRejected) ++ +static_cast(tiltRejected) ++ +static_cast(halfBlendRejected) ++ +static_cast(boreMaskRejected); ++ audit.passed=audit.flatSamples>0&&audit.flatFailures==0 ++ &&audit.supportSamples>0&&audit.supportFailures==0 ++ &&audit.headroomSamples>0&&audit.headroomFailures==0 ++ &&audit.blendRootSamples>0&&audit.blendMissing==0 ++ &&audit.blendGradeFailures==0 ++ &&audit.negativeControlsDetected==4; ++ std::printf("[LandingPadAudit] id=%s side=%s radius_m=%.3f blend_m=%.3f flatness_samples=%llu flatness_max_m=%.5f flatness_failures=%llu support=%llu/%llu headroom=%llu/%llu blend_transects=%zu blend_roots=%llu blend_missing=%llu blend_grade_max_deg=%.3f blend_grade_failures=%llu negative_controls=%llu/4 pad_off=%s tilt_2pct=%s blend_B_half=%s bore_mask_removed=%s status=%s\n", ++ passage.stableId.c_str(),upper?"upper":"lower",radius, ++ BlendRadiusMetres,static_cast(audit.flatSamples), ++ audit.maximumFlatnessMetres, ++ static_cast(audit.flatFailures), ++ static_cast(audit.supportSamples ++ -audit.supportFailures), ++ static_cast(audit.supportSamples), ++ static_cast(audit.headroomSamples ++ -audit.headroomFailures), ++ static_cast(audit.headroomSamples), ++ RadialDirections, ++ static_cast(audit.blendRootSamples), ++ static_cast(audit.blendMissing), ++ audit.maximumGradeDegrees, ++ static_cast(audit.blendGradeFailures), ++ static_cast(audit.negativeControlsDetected), ++ padOffRejected?"detected":"MISSED",tiltRejected?"detected":"MISSED", ++ halfBlendRejected?"detected":"MISSED", ++ boreMaskRejected?"detected":"MISSED",audit.passed?"PASS":"FAIL"); ++ return audit; ++ }; + std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0; + std::uint64_t passageInteriorCapsuleSamples=0,passageInteriorSolidSamples=0; + std::uint64_t passageInteriorControls=0; + std::uint64_t landingChamberCount=0,landingChamberNegativeControls=0; ++ std::uint64_t landingPadCount=0,landingPadPasses=0,landingPadControls=0; + std::uint64_t mouthProfilePasses=0,ripplePasses=0,rippleControlsDetected=0; + std::uint64_t floorRippleControlsDetected=0,floorStepRippleControlsDetected=0; + std::uint64_t shellAudits=0,shellFloorSamples=0,shellFloorViolations=0; +@@ -5346,6 +5438,13 @@ namespace StandaloneStreamBenchmark + passage,passageIndex,true); + const FWalkAudit lowerChamberAudit=auditLandingChamber(Shape, + passage,passageIndex,false); ++ const FLandingPadAudit upperPadAudit=auditLandingPad(Shape,passage,true); ++ const FLandingPadAudit lowerPadAudit=auditLandingPad(Shape,passage,false); ++ landingPadCount+=2; ++ landingPadPasses+=static_cast(upperPadAudit.passed) ++ +static_cast(lowerPadAudit.passed); ++ landingPadControls+=upperPadAudit.negativeControlsDetected ++ +lowerPadAudit.negativeControlsDetected; + const bool landingChambersWalkable=upperChamberAudit.passed + &&lowerChamberAudit.passed; + const bool passageInteriorAirPassed= +@@ -5426,6 +5525,7 @@ namespace StandaloneStreamBenchmark + floorRippleControlsDetected+=rippleAudit.floorControlDetected; + floorStepRippleControlsDetected+=rippleAudit.floorStepControlDetected; + passagePasses+=passageAudit.passed&&landingChambersWalkable ++ &&upperPadAudit.passed&&lowerPadAudit.passed + &&passageInteriorAirPassed + &&mouthsInRadius&&mouthsInContent + &&naturalAudit.passed&&mouthProfileAudit.passed&&rippleAudit.passed; +@@ -5889,6 +5989,13 @@ namespace StandaloneStreamBenchmark + static_cast(landingChamberCount), + static_cast(landingChamberCount + -landingChamberNegativeControls)); ++ std::printf("[LandingPadAuditSummary] pads=%llu passed=%llu negative_controls=%llu/%llu status=%s\n", ++ static_cast(landingPadCount), ++ static_cast(landingPadPasses), ++ static_cast(landingPadControls), ++ static_cast(landingPadCount*4), ++ landingPadCount>0&&landingPadPasses==landingPadCount ++ &&landingPadControls==landingPadCount*4?"PASS":"FAIL"); + const bool shellAuditPassed=shellAudits==passageCount + &&shellFloorViolations==0&&shellWallViolations==0 + &&shellChamberFloorViolations==0&&shellChamberWallViolations==0 +@@ -5935,6 +6042,8 @@ namespace StandaloneStreamBenchmark + ripplePasses==passageCount?"PASS":"FAIL"); + const bool auditPassed=passageCount>0&&passagePasses==passageCount + &&passageInteriorAuditPassed ++ &&landingPadCount>0&&landingPadPasses==landingPadCount ++ &&landingPadControls==landingPadCount*4 + &&naturalPasses==passageCount&&l23SmoothWallDetected + &&mouthProfilePasses==passageCount&&l23TerraceControlDetected + &&ripplePasses==passageCount diff --git a/Standalone/Research/WorldAssemblerM3/Linux172/__pycache__/RunL172Movement.cpython-313.pyc b/Standalone/Research/WorldAssemblerM3/Linux172/__pycache__/RunL172Movement.cpython-313.pyc new file mode 100644 index 0000000..126a645 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux172/__pycache__/RunL172Movement.cpython-313.pyc differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux173/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux173/REPORT.md new file mode 100644 index 0000000..9509d21 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux173/REPORT.md @@ -0,0 +1,38 @@ +# L173 — combined branch verification and cave planner merge + +`wip/combined` builds and the Linux UE smoke run passes, but the branch is not ready to merge to `assembler/world`: fast is red and both composed cave sweeps finished at 0/16. The cave route planners and their blocked-route controls passed on all Vertical Shafts seeds; the remaining failures are in host passage composition, seams, and shell fit. + +## Merge and fixes + +Resolved all five textual conflicts in the worktree: + +- `CURRENT-STATE.md`: combined the cave planner history with the merged liminal, living-plane, speed, and early-admission state. +- `Standalone/Core/src/ResolvedLayoutInternal.h`: retained the liminal lower-arrival tangent and the cave planner’s native route tangent. +- `Standalone/Source/StreamBenchmarkHeadless.cpp`: retained liminal field detection and cave recipe selection. +- `Standalone/Source/StreamBenchmarkPipeline.h`: retained the liminal road audit and cave chamber, tunnel, and shaft audits. +- `Standalone/Source/WorldAssemblyPrototype.h`: retained the liminal `planPassages` guard and dispatched cave recipes to the Core planner. + +Applied only the band-fit portion of `Standalone/Research/WorldAssemblerM3/Linux156/l156-headroom-landing.patch`. No L153–L159 landing-pad experiment patch or stamp was applied. The merged fast check still fails on the Floating Islands upper arrival, with 40 unsupported chamber samples (lower chamber: 0), 10 chamber-floor shell violations, and 465 blocked strate-0 route samples. The route summary is walkable 1/3. The streaming mid-pass refill check passes. + +Three targeted edits for the same Floating Islands integration failure did not clear fast; they were reverted. I stopped further edits after the third failed attempt. + +The cave branch already carries `WorldAssemblyEvaluatorVersion=239` (the `wip/combined` base was 227). Canonical owner outputs changed, so added v239 baselines for owner4 SurfaceWorld, owner2 SurfaceWorld, and owner2 Floating Islands, and updated `verify.py` to use them. The matrix is 18/18, identical across Clang/GCC and W1/W8/W12. Owner4’s checked-in resolved-layout snapshot fails the stable-identity import check; the matrix regenerated the Core plan with `--seed=0`. This limits that result to the regenerated-plan path. + +## Builds and checks + +- Clang 19 and GCC 14 headless builds pass at source stamp `d62251e728397859bf8c258fbcee819909e65888b5fb5d14b6c0f3e00eee8695` (`Saved/Linux/l173/headless-clang-final.log`, `headless-gcc.log`). +- Five liminal fixtures pass; the Living Plane standalone runner builds (`liminal-fixtures-build.log`, `living-plane-build.log`). +- Fast rerun against the final-source binary fails after 65.2 seconds on `owner-floating-islands-style2-clang`; the refill check passes. A first rerun to a new output directory stopped because that directory had no compiled executable; the recorded result uses the existing final-source binary. +- Linux UE full build passes with all three plugin links pointing to this worktree. `VoxelWorld.cpp` compiled, `VoxelForgeLivingPlane` linked, and the DWARF gate passed at 600 bytes for each of its three measured objects. Owner-world default-on smoke passed: 47 moving samples and 106 dwell samples over 52.789 seconds; frame-resolution arrival was 1.611817 seconds, early-admission wait was 0, tick p99 was 7.0801 ms, and worst tick was 12.272 ms. The harness restored all three VFHost links to the main checkout afterward. See `Saved/Linux/l173/ue/result.json` and `ue/restored-main-links.json`. +- Tunnel Network composed sweep: **0/16**. Passage and seam gates failed on all 16; shell and passage-count failed on 10; the Tunnel Network route gate failed on one seed; blocked-route controls passed 16/16. +- Vertical Shafts composed sweep: **0/16**. Passage and seam gates failed on all 16; shell and passage-count failed on 10. The Vertical Shafts route gate and blocked-route controls passed 16/16. Seed 2 specifically passes the shaft route and negative control; a strate-0 host route has five unsupported samples, leaving route walkability at 2/3. Its shell audit passes. + +The sweep summaries were regraded from captured logs with the current-source grader. Both are under `Saved/Linux/l173/cave-sweeps/seeds/`. + +## Not run and remaining risk + +- No Windows UE build was run. The owner should compile the changed Unreal source file `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`. +- No performance timing matrix was run; the W1/W8/W12 matrix above checks canonical output identity. +- Fast remains red, and neither composed sweep has an all-gates pass. The host content-band, route, seam, and chamber-floor failures need a root-cause fix before merging into `assembler/world`. + +No commit, push, or merge to `assembler/world` was made. Changes remain in the `wip/combined` worktree for review. diff --git a/Standalone/Research/WorldAssemblerM3/Linux173/RunL173CanonicalHashes.py b/Standalone/Research/WorldAssemblerM3/Linux173/RunL173CanonicalHashes.py new file mode 100644 index 0000000..f431853 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux173/RunL173CanonicalHashes.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +"""Compare owner canonical hashes across Clang/GCC and W1/W8/W12.""" +from __future__ import annotations + +import json +import re +import subprocess +import sys +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l173/canonical-hashes" +DATASETS = { + "owner4": ROOT / "Standalone/data/owner_world.json", + "owner2": ROOT / "Standalone/data/regression/owner_world_2strate_l61.json", +} +RECIPES = {"owner4": ("surface-world",), + "owner2": ("surface-world", "floating-islands")} +BINARIES = { + "clang": ROOT / "Saved/Linux/l173/fast/bin/VoxelForgeCoreHeadless-fast-clang", + "gcc": ROOT / "Saved/Linux/l173/bin/VoxelForgeCoreHeadless-l173-gcc", +} +BASELINES = { + ("owner2", "surface-world"): ( + "owner-hash-surface-world-style1-v227.baseline", + "owner-hash-surface-world-style1-v237.baseline", + "owner-hash-surface-world-style1-v239.baseline"), + ("owner2", "floating-islands"): ( + "owner-hash-floating-islands-style2-v227.baseline", + "owner-hash-floating-islands-style2-v237.baseline", + "owner-hash-floating-islands-style2-v239.baseline"), + ("owner4", "surface-world"): ( + "owner-hash-owner4-surface-world-style1-v227.baseline", + "owner-hash-owner4-surface-world-style1-v239.baseline"), +} +BASELINE_ROOT = ROOT / "Standalone/Tools/verify/baselines/owner-hash/canonical" +HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) + + +def main() -> int: + OUT.mkdir(parents=True, exist_ok=True) + stamp = subprocess.check_output( + [sys.executable, str(ROOT / "Standalone/Tools/verify/source_stamp.py"), + "stamp", "--root", str(ROOT)], text=True).strip() + baseline_hashes = {} + for key, filenames in BASELINES.items(): + baseline_hashes[key] = {} + for filename in filenames: + path = BASELINE_ROOT / filename + if path.is_file(): + baseline_hashes[key][filename] = HASH_RE.findall( + path.read_text(encoding="utf-8", errors="replace")) + + rows = [] + groups = {} + partial = OUT / "runs-partial.json" + for dataset, recipes in RECIPES.items(): + for recipe in recipes: + signatures = [] + for workers in (1, 8, 12): + for compiler in ("clang", "gcc"): + binary = BINARIES[compiler] + label = f"{dataset}-{recipe}-{compiler}-w{workers}" + log = OUT / f"{label}.log" + layout = OUT / f"{label}.layout.json" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATASETS[dataset]), + "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--force-generate", f"--workers={workers}", + "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--recipe={recipe}", "--stream-hash", + f"--resolved-layout-output={layout}", + ] + # Rebuild owner4's Core plan from the same exported seed. Its checked-in + # Unreal snapshot currently fails canonical import after the planner merge. + if dataset == "owner4": + command.append("--seed=0") + started = time.monotonic() + code: int | str + try: + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + code = proc.returncode + except subprocess.TimeoutExpired: + code = "OUTER_TIMEOUT" + output = log.read_text(encoding="utf-8", errors="replace") \ + if log.is_file() else "" + hashes = HASH_RE.findall(output) + stamp_match = STAMP_RE.search(output) + run_ok = (code == 0 and len(hashes) == 12 and stamp_match is not None + and stamp_match.group(1) == stamp) + row = { + "dataset": dataset, "recipe": recipe, "compiler": compiler, + "workers": workers, "seed_override": 0 if dataset == "owner4" else None, + "exit_code": code, "run_passed": run_ok, + "hashes": hashes, + "source_stamp": stamp_match.group(1) if stamp_match else None, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), + } + rows.append(row) + signatures.append(row) + print("[L173CanonicalHashRun] " + json.dumps(row, sort_keys=True), + flush=True) + partial.write_text(json.dumps(rows, indent=2) + "\n", encoding="utf-8") + + expected = baseline_hashes[(dataset, recipe)] + first = signatures[0]["hashes"] if signatures else [] + deterministic = (len(signatures) == 6 + and all(row["run_passed"] and row["hashes"] == first + for row in signatures)) + baseline_matches = { + name: bool(first) and first == values + for name, values in expected.items() + } + groups[f"{dataset}/{recipe}"] = { + "all_compilers_workers_match": deterministic, + "hash_rows": len(first), + "baseline_matches": baseline_matches, + "source_stamp": stamp, + } + print("[L173CanonicalHashGroup] " + json.dumps( + {"group": f"{dataset}/{recipe}", **groups[f"{dataset}/{recipe}"]}, + sort_keys=True), flush=True) + + passed = (len(rows) == 18 + and all(row["run_passed"] for row in rows) + and all(group["all_compilers_workers_match"] for group in groups.values())) + summary = {"status": "PASS" if passed else "FAIL", "source_stamp": stamp, + "runs": rows, "groups": groups} + (OUT / "summary.json").write_text( + json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L173CanonicalHashSummary] status={summary['status']} runs={len(rows)} " + f"groups={len(groups)}", flush=True) + return 0 if passed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux173/RunL173CaveSeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux173/RunL173CaveSeedSweep.py new file mode 100644 index 0000000..ab68142 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux173/RunL173CaveSeedSweep.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""Run the composed Tunnel Network or Vertical Shafts gates for seeds 0..15.""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l173/cave-sweeps/seeds" +BIN = ROOT / "Saved/Linux/l173/fast/bin/VoxelForgeCoreHeadless-fast-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP_TOOL = ROOT / "Standalone/Tools/verify/source_stamp.py" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +DISPATCH = { + "tunnel-network": re.compile( + r"^\[DensityDispatch\] strate=2 generator=TunnelNetwork " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), + "vertical-shafts": re.compile( + r"^\[DensityDispatch\] strate=3 generator=VerticalShafts " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), +} +ROUTES = { + "tunnel-network": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=2 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=3 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), +} +CONTROLS = { + "tunnel-network": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[VerticalShaftsBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} +REQUIRED = { + "passages": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "passage_interior": re.compile( + r"^\[PassageInteriorCapsuleAuditSummary\].*status=PASS$", re.M), +} + + +def evaluate_seed(kind: str, content: str, stamp: str, layout: Path) -> tuple[dict, str | None]: + stamp_match = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["source_stamp"] = stamp_match is not None and stamp_match.group(1) == stamp + checks["dispatch"] = bool(DISPATCH[kind].search(content)) + checks["route"] = bool(ROUTES[kind].search(content)) + checks["blocked_route_control"] = bool(CONTROLS[kind].search(content)) + checks["three_passage_interiors"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*interior_air=PASS", content, re.M)) == 3 + checks["three_interior_negative_controls"] = len(re.findall( + r"^\[ResolvedPassageInteriorNegativeControl\].*detected=true status=PASS$", + content, re.M)) == 3 + checks["three_ripple_gap_controls"] = len(re.findall( + r"^\[PassageRippleSamplingControl\].*status=PASS$", content, re.M)) == 3 + checks["three_blocked_passage_controls"] = len(re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3 + checks["passage_count"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*walkability=PASS$", content, re.M)) == 3 + layout_identity = None + try: + checks["layout_output"] = layout.is_file() + if layout.is_file(): + layout_identity = json.loads(layout.read_text(encoding="utf-8")).get( + "stable_identity") + except (OSError, json.JSONDecodeError): + checks["layout_output"] = False + return checks, layout_identity + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kind", choices=tuple(DISPATCH), required=True) + parser.add_argument("--grade-only", action="store_true", + help="regrade completed logs without rerunning the headless executable") + args = parser.parse_args() + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing current Clang binary or composed owner fixture") + stamp = subprocess.check_output( + [sys.executable, str(STAMP_TOOL), "stamp", "--root", str(ROOT)], text=True).strip() + mode_out = OUT / args.kind + mode_out.mkdir(parents=True, exist_ok=True) + if args.grade_only: + partial = mode_out / "runs-partial.json" + previous = json.loads(partial.read_text(encoding="utf-8")) if partial.is_file() else [] + by_seed = {int(row["seed"]): row for row in previous} + results = [] + for seed in range(16): + log = mode_out / f"seed{seed}.log" + layout = mode_out / f"seed{seed}.json" + content = log.read_text(encoding="utf-8", errors="replace") \ + if log.is_file() else "" + checks, identity = evaluate_seed(args.kind, content, stamp, layout) + code = by_seed.get(seed, {}).get("exit") + passed = code == 0 and all(checks.values()) + failed_checks = [name for name, ok in checks.items() if not ok] + row = {"kind": args.kind, "seed": seed, + "verdict": "PASS" if passed else "FAIL", + "reason": ", ".join(failed_checks) if failed_checks else + "all composed gates pass" if code == 0 else f"process exit {code}", + "exit": code, "checks": checks, "source_stamp": stamp, + "layout_identity": identity, + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + passed_count = sum(row["verdict"] == "PASS" for row in results) + summary = {"kind": args.kind, "source_stamp": stamp, + "grading_mode": "existing-captured-logs", "seeds": list(range(16)), + "passed": passed_count, "failed": 16 - passed_count, "results": results} + (mode_out / "runs-regraded.json").write_text( + json.dumps(results, indent=2) + "\n", encoding="utf-8") + (mode_out / "summary.json").write_text( + json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L173CaveSeedSweepSummary] kind={args.kind} " + f"passed={passed_count}/16 failed={16-passed_count} " + f"status={'PASS' if passed_count == 16 else 'FAIL'} grading=existing-logs", + flush=True) + return 0 if passed_count == 16 else 1 + results = [] + partial = mode_out / "runs-partial.json" + for seed in range(16): + log = mode_out / f"seed{seed}.log" + layout = mode_out / f"seed{seed}.json" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=0", "--force-generate", f"--seed={seed}", + "--recipe=surface-world", "--density-dispatch-report", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}", + ] + started = time.monotonic() + try: + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + code = proc.returncode + except subprocess.TimeoutExpired: + code = "OUTER_TIMEOUT" + content = log.read_text(encoding="utf-8", errors="replace") if log.exists() else "" + checks, layout_identity = evaluate_seed(args.kind, content, stamp, layout) + passed = code == 0 and all(checks.values()) + failed_checks = [name for name, ok in checks.items() if not ok] + row = { + "kind": args.kind, "seed": seed, "verdict": "PASS" if passed else "FAIL", + "reason": ", ".join(failed_checks) if failed_checks else "all composed gates pass", + "exit": code, "checks": checks, + "source_stamp": stamp, + "layout_identity": layout_identity, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT)), + } + results.append(row) + print("[L173CaveSeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + partial.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + passed_count = sum(row["verdict"] == "PASS" for row in results) + summary = {"kind": args.kind, "source_stamp": stamp, "seeds": list(range(16)), + "passed": passed_count, "failed": 16 - passed_count, "results": results} + (mode_out / "summary.json").write_text( + json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L173CaveSeedSweepSummary] kind={args.kind} passed={passed_count}/16 " + f"failed={16-passed_count} status={'PASS' if passed_count == 16 else 'FAIL'}", flush=True) + return 0 if passed_count == 16 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux173/RunL173UE.py b/Standalone/Research/WorldAssemblerM3/Linux173/RunL173UE.py new file mode 100644 index 0000000..f519a35 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux173/RunL173UE.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Build the combined tree in VFHost, verify DWARF/module output, and smoke the owner world.""" +from __future__ import annotations + +import importlib.util +import argparse +import json +import os +from pathlib import Path +import re +import sys + + +ROOT = Path(__file__).resolve().parents[4] +SAVED = ROOT / "Saved/Linux/l173" +RUNNER_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux172/RunL172Movement.py" + + +def load_l172_runner(): + spec = importlib.util.spec_from_file_location("l172_runner", RUNNER_PATH) + if spec is None or spec.loader is None: + raise RuntimeError(f"could not load L172 harness: {RUNNER_PATH}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + module.ROOT = ROOT + module.OUT = SAVED + module.UE_OUT = SAVED / "ue" + module.BUILD_OUT = SAVED / "ue/builds" + module.BIN_OUT = SAVED / "ue/bin" + return module + + +def write_json(path: Path, value) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_suffix(path.suffix + ".tmp") + temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", + encoding="utf-8") + temporary.replace(path) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--reuse-prior-build", action="store_true", + help="verify the successful full build already logged in Saved/Linux/l173") + options = parser.parse_args() + SAVED.mkdir(parents=True, exist_ok=True) + runner = load_l172_runner() + result = {"plugin_root": str(ROOT.resolve())} + try: + build_log = SAVED / "ue/builds/ubt-l173-combined.log" + if options.reuse_prior_build: + if not build_log.is_file(): + raise RuntimeError(f"prior full build log is missing: {build_log}") + restored = json.loads((SAVED / "ue/restored-main-links.json").read_text( + encoding="utf-8")) + archive_roots = set() + for item in restored.get("intermediates_moved", []): + moved = Path(item["to"]) + if moved.name == "VoxelForge" and "External" in moved.parts: + external_index = moved.parts.index("External") + archive_roots.add(Path(*moved.parts[:external_index])) + archive_roots = [path for path in archive_roots + if (path / "External/Build/Linux/x64/UnrealEditor/Development/" + "VoxelForge/VoxelWorld.cpp.o").is_file()] + if len(archive_roots) != 1: + raise RuntimeError(f"expected one archived combined-tree intermediate set: " + f"{archive_roots}") + runner.INTERMEDIATE = archive_roots[0] + dwarf = runner.dwarf_pre_run_gate() + runner.INTERMEDIATE = runner.HOST / "Intermediate" + build = { + **runner.root_info(ROOT), + "build_log": str(build_log), + "compiled_voxel_world_cpp": True, + "dwarf_pre_run_gate": dwarf, + "binary": str(SAVED / "ue/bin/l173-combined/" + "libUnrealEditor-VoxelForge.so"), + "binary_sha256": runner.sha256_file( + SAVED / "ue/bin/l173-combined/libUnrealEditor-VoxelForge.so"), + "reused_prior_full_build": True, + } + else: + build = runner.build_tree(ROOT, "l173-combined") + output = build_log.read_text(encoding="utf-8", errors="replace") + if ("Result: Succeeded" not in output + or re.search(r"\[\d+/\d+\] Compile VoxelWorld\.cpp", output) is None): + raise RuntimeError(f"UE build log lacks a successful VoxelWorld.cpp compile: {build_log}") + actor_compiled = re.search(r"Compile .*VoxelForgeLivingPlaneActor\.cpp", output) is not None + module_binary = (runner.HOST / "Binaries/Linux/" + "libUnrealEditor-VoxelForgeLivingPlane.so") + if not actor_compiled or not module_binary.is_file(): + raise RuntimeError( + "UE build did not compile/link the Living Plane module: " + f"actor_compiled={actor_compiled}, module_binary={module_binary.is_file()}; " + f"log={build_log}") + build["living_plane_actor_compiled"] = actor_compiled + build["living_plane_module_binary"] = str(module_binary.resolve()) + result["build"] = build + write_json(SAVED / "ue/build.json", result) + + shader_link = runner.HOST_PLUGIN_LINK / "Shaders" + if shader_link.exists() or shader_link.is_symlink(): + if shader_link.resolve(strict=True) != (ROOT / "Shaders").resolve(strict=True): + raise RuntimeError(f"unexpected existing shader link target: {shader_link}") + created_shader_link = False + else: + temporary = shader_link.with_name(f"Shaders.l173-{os.getpid()}.tmp") + temporary.symlink_to(ROOT / "Shaders") + os.replace(temporary, shader_link) + created_shader_link = True + + smoke = runner.run_trial( + "owner-world-smoke", "owner-world-smoke", "default-on", ROOT, + Path(build["binary"]), runner.WAIT_ON, runner.APPLY_ON_MS, + include_switch_overrides=False) + write_json(SAVED / "ue/owner-world-smoke.json", smoke) + result["smoke"] = smoke + write_json(SAVED / "ue/result.json", result) + print("[L173UE] BUILD PASS VoxelWorld.cpp=compiled " + "VoxelForgeLivingPlane=linked DWARF=PASS", flush=True) + print("[L173UE] OWNER_WORLD_SMOKE PASS " + + json.dumps({key: smoke[key] for key in ( + "frame_resolution_arrival_s", "tick_p99_ms", "tick_worst_ms", + "early_admission_wait_for_critical", "early_admission_apply_budget_ms")}, + sort_keys=True), flush=True) + return 0 + finally: + restored = runner.restore_main_checkout() + shader_link = runner.HOST_PLUGIN_LINK / "Shaders" + if locals().get("created_shader_link", False): + shader_link.unlink(missing_ok=True) + write_json(SAVED / "ue/restored-main-links.json", { + "main_root": str(runner.MAIN_CHECKOUT.resolve()), + "links": restored["links"], + "intermediates_moved": restored["intermediates_moved"], + }) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index c5aaf56..64a402a 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -74,6 +74,7 @@ namespace bool DensityDispatchReport=false; bool CavesRecipe=false; bool LatticeRecipe=false; + bool LegacyTunnelNetwork=false; int LatticeStrateIndex=-1; bool AuditHeightfieldProfile=false; bool LandmarkChamberDemo=false; @@ -1436,6 +1437,7 @@ namespace " --core-snapshot=FILE (benchmark the exact resolved Core snapshot from Unreal)\n" " --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n" " --seed=N --recipe=surface-world|floating-islands|caves|lattice\n" + " --legacy-tunnel-network (keep TN on its compiled-analytic comparison path)\n" " --native-owner-fi (build the existing owner FI plan; leave other strates intact)\n" " --density-dispatch-report (print bound path per strate and tile hashes)\n" " --lattice-strate=N (opt one Maze or VerticalShafts strate into Core.Lattice)\n" @@ -1505,6 +1507,7 @@ namespace else if(arg=="--native-owner-fi")options.NativeOwnerFloatingIslandsRecipe=true; else if(arg=="--density-dispatch-report") {options.DensityDispatchReport=true;options.StreamHash=true;options.StreamHashTiles=true;} + else if(arg=="--legacy-tunnel-network")options.LegacyTunnelNetwork=true; else if(arg=="--audit-landmark-reachability")options.AuditLandmarkReachability=true; else if(arg=="--audit-native-caves")options.AuditNativeCaves=true; else if(arg=="--audit-native-lattice")options.AuditNativeLattice=true; @@ -1745,7 +1748,8 @@ int main(int argc,char** argv) options.DataPath.string().c_str(),options.WorkerCount,options.ClipRadius,options.MaxClipLevel, options.MaxLevel,options.StreamHash?"on":"off", options.StreamHashAllowUniformSkips?"on":"off",options.ProfileColumnOverlap?"on":"off", - options.ForceGenerate?"on":"off",options.CavesRecipe?"caves" + options.ForceGenerate?"on":"off",options.LegacyTunnelNetwork?"legacy-tunnel-network" + :options.CavesRecipe?"caves" :options.LatticeRecipe?"lattice" :options.FloatingIslandsRecipe?"floating-islands":"surface-world", options.LatticeStrateIndex, @@ -1850,7 +1854,9 @@ int main(int argc,char** argv) const VFJson::Value& coreLiminalConfig=ownerJsonRoot.Get("core_liminal_field"); const bool hasCoreLiminalField=!std::holds_alternative( coreLiminalConfig.Data); - const std::string variant=options.LatticeRecipe + const std::string variant=options.LegacyTunnelNetwork + ?"legacy-tunnel-network" + :options.LatticeRecipe ?"lattice/"+std::to_string(options.LatticeStrateIndex) :options.CavesRecipe?"caves" :(options.FloatingIslandsRecipe||options.NativeOwnerFloatingIslandsRecipe) @@ -1864,6 +1870,11 @@ int main(int argc,char** argv) !hasCoreLiminalField); if(!layoutResolved) {std::fprintf(stderr,"Resolved-layout snapshot failed: %s\n",error.c_str());return 2;} + if(useExportedCorePlan&&!options.NativeOwnerFloatingIslandsRecipe + &&!options.LegacyTunnelNetwork&&!options.CavesRecipe + &&!StandaloneWorldAssemblyPrototype::PlanOwnerTunnelNetworkCaves( + *ownerWorld,resolvedLayout,error)) + {std::fprintf(stderr,"Imported owner Tunnel Network cave planning failed: %s\n",error.c_str());return 2;} if(useExportedCorePlan&&!options.NativeOwnerFloatingIslandsRecipe &&!StandaloneWorldAssemblyPrototype::ValidateExportedCorePlanMatchesOwner( *ownerWorld,worldAssembly,resolvedLayout,error)) @@ -2218,8 +2229,11 @@ int main(int argc,char** argv) { std::vector rooms; std::vector edges; - if(!MakeNativeCavesSpine(resolvedLayout,rooms,edges,error) - ||!VoxelForgeCore::ResolveResolvedCavernGraphV1( + if(!MakeNativeCavesSpine(resolvedLayout,rooms,edges,error)) + {std::fprintf(stderr,"Native caves spine resolution failed: %s\n",error.c_str());return 2;} + rooms.insert(rooms.end(),resolvedLayout.cavernRooms.begin(),resolvedLayout.cavernRooms.end()); + edges.insert(edges.end(),resolvedLayout.cavernEdges.begin(),resolvedLayout.cavernEdges.end()); + if(!VoxelForgeCore::ResolveResolvedCavernGraphV1( resolvedLayout,seed,rooms,edges,error)) {std::fprintf(stderr,"Native caves spine resolution failed: %s\n",error.c_str());return 2;} } @@ -2230,8 +2244,11 @@ int main(int argc,char** argv) std::vector rooms; std::vector edges; if(!MakeNativeLatticeSpine(resolvedLayout,seed,options.LatticeStrateIndex, - options.LatticeOrphanControl,rooms,edges,error) - ||!VoxelForgeCore::ResolveResolvedCavernGraphV1( + options.LatticeOrphanControl,rooms,edges,error)) + {std::fprintf(stderr,"Native lattice graph resolution failed: %s\n",error.c_str());return 2;} + rooms.insert(rooms.end(),resolvedLayout.cavernRooms.begin(),resolvedLayout.cavernRooms.end()); + edges.insert(edges.end(),resolvedLayout.cavernEdges.begin(),resolvedLayout.cavernEdges.end()); + if(!VoxelForgeCore::ResolveResolvedCavernGraphV1( resolvedLayout,seed,rooms,edges,error)) {std::fprintf(stderr,"Native lattice graph resolution failed: %s\n",error.c_str());return 2;} } @@ -2352,12 +2369,16 @@ int main(int argc,char** argv) const bool hasNativeLatticeRecipe=std::any_of(resolvedLayout.strates.begin(),resolvedLayout.strates.end(), [](const auto& stratum){return VoxelForgeCore::IsNativeLatticeFieldStratum(stratum) &&stratum.passageCapabilities.hasNativeLatticeField;}); - const auto actualRecipe=hasNativeLatticeRecipe?std::string_view("lattice") - :hasNativeCavesRecipe?std::string_view("caves") + const auto actualRecipe=requestedRecipe=="lattice"&&hasNativeLatticeRecipe + ?std::string_view("lattice") + :requestedRecipe=="caves"&&hasNativeCavesRecipe + ?std::string_view("caves") :requestedRecipe=="surface-world"&&hasSurfaceWorldPlan ?std::string_view("surface-world") :requestedRecipe=="floating-islands"&&hasFloatingIslands ?std::string_view("floating-islands") + :hasNativeLatticeRecipe?std::string_view("lattice") + :hasNativeCavesRecipe?std::string_view("caves") :hasFloatingIslands?std::string_view("floating-islands") :hasSurfaceWorldPlan?std::string_view("surface-world") :std::string_view("missing"); diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index d861a64..bdb65d0 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -1596,6 +1596,208 @@ namespace StandaloneStreamBenchmark return passed; } + inline bool AuditNativeTunnelNetworkField(const VoxelForgeCore::World& shape, + const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout, + std::uint32_t voxelMicrometres) + { + using namespace VoxelForgeCore; + constexpr double Pi=3.14159265358979323846; + const double vm=std::max(1,voxelMicrometres)/1.0e6; + const auto density=[&](double x,double y,double z) + {return static_cast(shape.densityAt({x/vm,y/vm,z/vm}))*vm;}; + const auto cave=std::find_if(layout.strates.begin(),layout.strates.end(), + [](const auto& s){return s.generatorKind=="TunnelNetwork" + &&IsNativeCaveFieldStratum(s)&&s.passageCapabilities.hasNativeCaveField;}); + const bool supportedGraphVocabulary= + layout.cavernGraphVocabularyVersion==ResolvedLayoutCavernGraphVocabularyVersionV1 + ||layout.cavernGraphVocabularyVersion==ResolvedLayoutCavernGraphVocabularyVersionV2; + if(cave==layout.strates.end()||!supportedGraphVocabulary) + {std::printf("[TunnelNetworkCavernAudit] recipe=%s graph=%u status=FAIL\n", + cave==layout.strates.end()?"missing":"present", + layout.cavernGraphVocabularyVersion);return false;} + + const ResolvedLayoutPassageV1* arrival=nullptr; + const ResolvedLayoutPassageV1* departure=nullptr; + for(const auto& passage:layout.passages) + { + if(passage.lowerStrateIndex==cave->strateIndex + &&passage.upperStrateIndexstrateIndex)arrival=&passage; + if(passage.upperStrateIndex==cave->strateIndex + &&passage.lowerStrateIndex>cave->strateIndex)departure=&passage; + } + std::vector rooms; + std::vector routeEdges; + for(const auto& room:layout.cavernRooms) + if(room.strateIndex==cave->strateIndex)rooms.push_back(&room); + for(const auto& edge:layout.cavernEdges) + if(edge.strateIndex==cave->strateIndex + &&edge.stableId.find("/route-edge-")!=std::string::npos) + routeEdges.push_back(&edge); + const auto routeIndex=[](const ResolvedCavernEdgeV1* edge) + { + const auto marker=edge->stableId.rfind("/route-edge-"); + return marker==std::string::npos?std::uint64_t{0} + :static_cast(std::stoull(edge->stableId.substr(marker+12))); + }; + std::sort(routeEdges.begin(),routeEdges.end(),[&](const auto* a,const auto* b) + {return routeIndex(a) compoundCounts; + std::unordered_map compoundRadii; + double largestRoomRadius=0.0,largestOtherRoomRadius=0.0; + std::string largestRoomGroup; + for(const auto* room:rooms) + { + const auto marker=room->stableId.rfind("/primitive-"); + const std::string group=marker==std::string::npos + ?room->stableId:room->stableId.substr(0,marker); + ++compoundCounts[group]; + const double radius=std::max(room->halfExtentsMicrometres[0], + room->halfExtentsMicrometres[1])/1.0e6; + compoundRadii[group]=std::max(compoundRadii[group],radius); + if(radius>largestRoomRadius) + {largestRoomRadius=radius;largestRoomGroup=group;} + } + for(const auto& [group,radius]:compoundRadii) + if(group!=largestRoomGroup) + largestOtherRoomRadius=std::max(largestOtherRoomRadius,radius); + const bool compoundRooms=!compoundCounts.empty() + &&std::all_of(compoundCounts.begin(),compoundCounts.end(), + [](const auto& row){return row.second>=2&&row.second<=4;}); + const bool destination=largestOtherRoomRadius>0.0 + &&largestRoomRadius>largestOtherRoomRadius*1.10; + + std::vector routeAnchors; + for(const auto& anchor:cave->anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + routeAnchors.push_back(&anchor); + double maximumGrade=0.0; + bool gradeOk=routeAnchors.size()>=2; + for(std::size_t i=1;ivalues; + const auto& b=routeAnchors[i]->values; + const double run=std::hypot(static_cast(b[0]-a[0]), + static_cast(b[1]-a[1])); + const double rise=std::abs(static_cast(b[2]-a[2])); + if(run<=0.0){gradeOk=false;continue;} + const double grade=std::atan2(rise,run)*180.0/Pi; + maximumGrade=std::max(maximumGrade,grade); + gradeOk=gradeOk&&grade<=15.0+1.0e-6; + } + bool mouthBinding=arrival&&departure&&routeAnchors.size()>=2; + if(mouthBinding) + { + const auto qOffset=static_cast(std::llround( + 1000000.0*WorldAssemblySubvoxelsPerVoxel/layout.coreVoxelSizeMicrometres)); + const auto& start=routeAnchors.front()->values; + const auto& end=routeAnchors.back()->values; + mouthBinding=start[0]==arrival->lowerMouthSubvoxels[0] + &&start[1]==arrival->lowerMouthSubvoxels[1] + &&start[2]==arrival->lowerMouthSubvoxels[2]+qOffset + &&end[0]==departure->upperMouthSubvoxels[0] + &&end[1]==departure->upperMouthSubvoxels[1] + &&end[2]==departure->upperMouthSubvoxels[2]+qOffset; + } + + std::uint64_t airSamples=0,floorSamples=0,shellSamples=0; + bool centerAir=true,floorSolid=true,shellSolid=true; + for(const auto* edge:routeEdges) + { + const auto& p=edge->controlPointsMicrometres; + const double x=(p[0][0]+3.0*p[1][0]+3.0*p[2][0]+p[3][0])/8.0e6; + const double y=(p[0][1]+3.0*p[1][1]+3.0*p[2][1]+p[3][1])/8.0e6; + const double floor=(p[0][2]+3.0*p[1][2]+3.0*p[2][2]+p[3][2])/8.0e6; + const double bore=edge->boreRadiusMicrometres/1.0e6; + if(density(x,y,floor+1.0)<=0.0)centerAir=false; + ++airSamples; + if(density(x,y,floor-0.10)>=0.0)floorSolid=false; + ++floorSamples; + const double dx=(p[3][0]-p[0][0])/1.0e6; + const double dy=(p[3][1]-p[0][1])/1.0e6; + const double length=std::hypot(dx,dy); + if(length<=1.0e-6){shellSolid=false;continue;} + const double nx=-dy/length,ny=dx/length; + if(density(x+nx*(bore+0.90),y+ny*(bore+0.90),floor+2.0)>=0.0 + ||density(x-nx*(bore+0.90),y-ny*(bore+0.90),floor+2.0)>=0.0) + shellSolid=false; + shellSamples+=2; + } + + const ResolvedCavernEdgeV1* rippleEdge=nullptr; + double longest=0.0; + for(const auto* edge:routeEdges) + { + const auto& p=edge->controlPointsMicrometres; + const double length=std::hypot(static_cast(p[3][0]-p[0][0]), + static_cast(p[3][1]-p[0][1])); + if(length>longest){longest=length;rippleEdge=edge;} + } + std::vector rippleRadii; + if(rippleEdge) + { + const auto& p=rippleEdge->controlPointsMicrometres; + const double radius=rippleEdge->boreRadiusMicrometres/1.0e6; + constexpr std::array stations{{0.18,0.29,0.40,0.51,0.62,0.73,0.84}}; + for(double t:stations) + { + const auto bezier=[&](double u,std::size_t axis) + { + const double one=1.0-u; + return (one*one*one*p[0][axis]+3.0*one*one*u*p[1][axis] + +3.0*one*u*u*p[2][axis]+u*u*u*p[3][axis])/1.0e6; + }; + const double x=bezier(t,0),y=bezier(t,1),floor=bezier(t,2); + const double tx=bezier(std::min(1.0,t+0.01),0) + -bezier(std::max(0.0,t-0.01),0); + const double ty=bezier(std::min(1.0,t+0.01),1) + -bezier(std::max(0.0,t-0.01),1); + const double tangent=std::hypot(tx,ty); + if(tangent<=1.0e-6)continue; + for(const double side:{-1.0,1.0}) + { + double prior=0.20; + double priorDensity=density(x,y,floor+2.0); + bool found=false; + for(double radial=0.25;radial<=radius+1.8;radial+=0.05) + { + const double sampleX=x-side*ty/tangent*radial; + const double sampleY=y+side*tx/tangent*radial; + const double value=density(sampleX,sampleY,floor+2.0); + if(priorDensity>=0.0&&value<0.0) + { + const double fraction=priorDensity/(priorDensity-value); + rippleRadii.push_back(prior+(radial-prior)*fraction); + found=true;break; + } + prior=radial;priorDensity=value; + } + if(!found)continue; + } + } + } + double ripplePeakToPeak=0.0; + if(!rippleRadii.empty()) + { + const auto limits=std::minmax_element(rippleRadii.begin(),rippleRadii.end()); + ripplePeakToPeak=*limits.second-*limits.first; + } + const bool ripple=rippleRadii.size()>=8&&ripplePeakToPeak>=0.02 + &&ripplePeakToPeak<=1.25; + std::printf("[TunnelNetworkCavernAudit] strate=%d recipe=%s rooms=%zu compound_groups=%zu destination_radius_m=%.3f route_edges=%zu route_points=%zu mouth_binding=%s max_grade_deg=%.3f grade_ok=%s center_air=%llu/%llu floor_solid=%llu/%llu shell_solid=%llu/%llu ripple_samples=%zu ripple_peak_to_peak_m=%.4f status=%s\n", + cave->strateIndex,cave->recipeId.c_str(),rooms.size(),compoundCounts.size(), + largestRoomRadius,routeEdges.size(),routeAnchors.size(),mouthBinding?"PASS":"FAIL", + maximumGrade,gradeOk?"PASS":"FAIL", + static_cast(airSamples),static_cast(airSamples), + static_cast(floorSamples),static_cast(floorSamples), + static_cast(shellSamples),static_cast(shellSamples), + rippleRadii.size(),ripplePeakToPeak, + arrival&&departure&&routeEdges.size()>=2&&!rooms.empty()&&compoundRooms + &&destination&&mouthBinding&&gradeOk&¢erAir&&floorSolid&&shellSolid + &&ripple?"PASS":"FAIL"); + return arrival&&departure&&routeEdges.size()>=2&&!rooms.empty()&&compoundRooms + &&destination&&mouthBinding&&gradeOk&¢erAir&&floorSolid&&shellSolid&&ripple; + } + inline bool AuditNativeCavesField(const VoxelForgeCore::World& shape, const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,std::uint32_t voxelMicrometres) { @@ -1606,9 +1808,13 @@ namespace StandaloneStreamBenchmark const auto density=[&](double x,double y,double z) {return static_cast(shape.densityAt({x/vm,y/vm,z/vm}))*vm;}; const auto cave=std::find_if(layout.strates.begin(),layout.strates.end(), - [](const auto& s){return s.generatorKind=="Caves" - &&s.recipeId=="Core.Caves.PrimitiveField"&&s.recipeVersion==1 + [](const auto& s){return IsNativeCaveFieldStratum(s) &&s.passageCapabilities.hasNativeCaveField;}); + if(cave!=layout.strates.end()&&cave->generatorKind=="TunnelNetwork") + return AuditNativeTunnelNetworkField(shape,layout,voxelMicrometres); + if(cave==layout.strates.end()||cave->generatorKind!="Caves") + {std::printf("[CavesAudit] recipe=missing rooms=%zu edges=%zu status=FAIL\n", + layout.cavernRooms.size(),layout.cavernEdges.size());return false;} if(cave==layout.strates.end()||layout.cavernRooms.size()!=1||layout.cavernEdges.size()!=2) {std::printf("[CavesAudit] recipe=%s rooms=%zu edges=%zu status=FAIL\n", cave==layout.strates.end()?"missing":"present",layout.cavernRooms.size(),layout.cavernEdges.size());return false;} @@ -1926,7 +2132,10 @@ namespace StandaloneStreamBenchmark const auto lattice=std::find_if(layout.strates.begin(),layout.strates.end(), [](const auto& s){return IsNativeLatticeFieldStratum(s) &&s.passageCapabilities.hasNativeLatticeField;}); - if(lattice==layout.strates.end()||layout.cavernGraphVocabularyVersion!=1) + const bool supportedGraphVocabulary= + layout.cavernGraphVocabularyVersion==ResolvedLayoutCavernGraphVocabularyVersionV1 + ||layout.cavernGraphVocabularyVersion==ResolvedLayoutCavernGraphVocabularyVersionV2; + if(lattice==layout.strates.end()||!supportedGraphVocabulary) {std::printf("[LatticeAudit] recipe=%s graph=%u status=FAIL\n", lattice==layout.strates.end()?"missing":"present", layout.cavernGraphVocabularyVersion);return false;} @@ -2886,7 +3095,16 @@ namespace StandaloneStreamBenchmark const double horizontal=std::hypot(dx,dy)*coreMetres; const double vertical=std::abs(dz)*coreMetres; if(horizontal<=1.0e-6||vertical>horizontal*0.2679491924311227) + { + if(audit.gradeFailures<8) + std::printf("[ResolvedWalkGradeFailure] segment=%zu a_xyz_m=(%.3f,%.3f,%.3f) b_xyz_m=(%.3f,%.3f,%.3f) horizontal_m=%.6f vertical_m=%.6f grade_deg=%.3f\n", + segment-1,a[0]*coreMetres,a[1]*coreMetres, + a[2]*coreMetres,b[0]*coreMetres,b[1]*coreMetres, + b[2]*coreMetres,horizontal,vertical, + std::atan2(vertical,horizontal) + *180.0/3.14159265358979323846); ++audit.gradeFailures; + } const double length=std::sqrt(dx*dx+dy*dy+dz*dz)*coreMetres; const std::uint64_t intervals=std::max(1, static_cast(std::ceil(length/0.5))); @@ -3322,21 +3540,27 @@ namespace StandaloneStreamBenchmark const auto passageUpperMouthFloorDepth=[&]( const VoxelForgeCore::ResolvedLayoutPassageV1& passage) { - const double controlZ=qToMetres( - passage.controlPoints.front().positionSubvoxels[2]); - const double authoredMouthDepth=std::max(0.0,controlZ - -qToMetres(passage.upperMouthSubvoxels[2])+1.13); const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[&](const auto& stratum) {return stratum.strateIndex==passage.upperStrateIndex;}); + const double mouthCenterHeight=upperStratum!=ResolvedLayout->strates.end() + &&upperStratum->generatorKind=="SurfaceWorld"?0.98:1.13; + const double controlZ=qToMetres( + passage.controlPoints.front().positionSubvoxels[2]); + const double authoredMouthDepth=std::max(0.0,controlZ + -qToMetres(passage.upperMouthSubvoxels[2])+mouthCenterHeight); if(upperStratum==ResolvedLayout->strates.end() - ||upperStratum->generatorKind!="FloatingIslands") + ||(upperStratum->generatorKind!="FloatingIslands" + &&upperStratum->generatorKind!="TunnelNetwork" + &&upperStratum->generatorKind!="SurfaceWorld")) return authoredMouthDepth; std::vector> route; for(const auto& anchor:upperStratum->anchors) if(anchor.kind=="route_point"&&anchor.values.size()>=3) route.push_back({qToMetres(anchor.values[0]), qToMetres(anchor.values[1]),qToMetres(anchor.values[2])}); + if(upperStratum->generatorKind=="TunnelNetwork"&&route.size()>2) + route.erase(route.begin(),route.end()-2); double routeHeight=0.0,distanceSquared=0.0; const double mouthX=qToMetres(passage.controlPoints.front().positionSubvoxels[0]); const double mouthY=qToMetres(passage.controlPoints.front().positionSubvoxels[1]); @@ -3345,7 +3569,8 @@ namespace StandaloneStreamBenchmark ||distanceSquared>std::pow( qToMetres(passage.controlPoints.front().radiusSubvoxels)+2.0,2.0)) return authoredMouthDepth; - const double routeAlignedDepth=std::max(0.0,controlZ-routeHeight+1.13); + const double routeAlignedDepth=std::max(0.0, + controlZ-routeHeight+mouthCenterHeight); return std::max(authoredMouthDepth,routeAlignedDepth); }; const auto passageSplineControls=[&]( @@ -3371,6 +3596,61 @@ namespace StandaloneStreamBenchmark for(const auto& point:passage.controlPoints) profile.push_back(qToMetres(point.positionSubvoxels[2] -point.radiusSubvoxels)); + const auto controls=passageSplineControls(passage); + const auto routeForStrate=[&](std::int32_t strateIndex) + { + std::vector> route; + const auto stratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&](const auto& candidate) + {return candidate.strateIndex==strateIndex;}); + if(stratum!=ResolvedLayout->strates.end() + &&(stratum->generatorKind=="TunnelNetwork" + ||stratum->generatorKind=="FloatingIslands" + ||stratum->generatorKind=="SurfaceWorld")) + for(const auto& anchor:stratum->anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + route.push_back({qToMetres(anchor.values[0]), + qToMetres(anchor.values[1]),qToMetres(anchor.values[2])}); + return route; + }; + const auto upper=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&](const auto& stratum) + {return stratum.strateIndex==passage.upperStrateIndex;}); + if(!passage.floorProfileIncludesLandingChambers + &&upper!=ResolvedLayout->strates.end()&&!controls.empty() + &&(upper->generatorKind=="TunnelNetwork" + ||upper->generatorKind=="SurfaceWorld")) + { + auto route=routeForStrate(passage.upperStrateIndex); + if(upper->generatorKind=="TunnelNetwork"&&route.size()>2) + route.erase(route.begin(),route.end()-2); + double routeHeight=0.0,distanceSquared=0.0; + if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route, + controls.front()[0],controls.front()[1],routeHeight,distanceSquared) + &&distanceSquared<=std::pow(controls.front()[3]+2.0,2.0)) + VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute( + controls,profile,0,route,1.0, + upper->generatorKind=="SurfaceWorld"?0.98:1.13); + } + const auto lower=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&](const auto& stratum) + {return stratum.strateIndex==passage.lowerStrateIndex;}); + const std::size_t routeEnd=std::clamp( + passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1); + if(!passage.floorProfileIncludesLandingChambers + &&lower!=ResolvedLayout->strates.end() + &&lower->generatorKind=="TunnelNetwork"&&routeEnd2)route.resize(2); + double routeHeight=0.0,distanceSquared=0.0; + if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route, + controls[routeEnd][0],controls[routeEnd][1], + routeHeight,distanceSquared) + &&distanceSquared<=std::pow(controls[routeEnd][3]+2.0,2.0)) + VoxelForgeCore::PassageSpline::BlendEndpointFloorToRoute( + controls,profile,routeEnd,route,1.0); + } return profile; }; const auto passageBodyFloorDepth=[&]( @@ -3410,6 +3690,45 @@ namespace StandaloneStreamBenchmark double x=0.0,y=0.0,z=0.0,floorZ=0.0,radius=0.0,distance=0.0; std::size_t segmentIndex=0; }; + const auto blendLandingChamberFloor=[&]( + const VoxelForgeCore::ResolvedLayoutPassageV1& passage, + double alongMetres,double totalLengthMetres,double x,double y, + double plannedFloor) + { + if(passage.floorProfileIncludesLandingChambers)return plannedFloor; + constexpr double LandingFloorBlendMetres=12.0; + const auto weight=[&](double distanceMetres, + const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) + { + const double radius=qToMetres(chamber.radiusSubvoxels); + if(!(radius>0.0))return 0.0; + if(distanceMetres<=radius)return 1.0; + if(distanceMetres>=radius+LandingFloorBlendMetres)return 0.0; + const double t=(distanceMetres-radius)/LandingFloorBlendMetres; + return 1.0-t*t*(3.0-2.0*t); + }; + const double upperWeight=weight(std::max(0.0,alongMetres), + passage.upperLandingChamber); + const double lowerWeight=weight(std::max(0.0, + totalLengthMetres-alongMetres),passage.lowerLandingChamber); + if(upperWeight<=0.0&&lowerWeight<=0.0)return plannedFloor; + const auto floorAt=[&]( + const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) + { + const double cx=qToMetres(chamber.floorCenterSubvoxels[0]); + const double cy=qToMetres(chamber.floorCenterSubvoxels[1]); + const double cz=qToMetres(chamber.floorCenterSubvoxels[2]); + return cz+chamber.floorGradientQ16[0]/65536.0*(x-cx) + +chamber.floorGradientQ16[1]/65536.0*(y-cy); + }; + const double upperFloor=floorAt(passage.upperLandingChamber); + const double lowerFloor=floorAt(passage.lowerLandingChamber); + const double totalWeight=upperWeight+lowerWeight; + if(totalWeight<=1.0) + return plannedFloor*(1.0-totalWeight) + +upperFloor*upperWeight+lowerFloor*lowerWeight; + return (upperFloor*upperWeight+lowerFloor*lowerWeight)/totalWeight; + }; const auto passageSmoothRoute=[&]( const VoxelForgeCore::ResolvedLayoutPassageV1& passage,double bodyDepth) { @@ -3439,65 +3758,176 @@ namespace StandaloneStreamBenchmark segment.alongMetres+segment.lengthMetres, segment.controlSpan+1}); } + if(route.size()>=2) + { + const double totalLength=route.back().distance; + for(auto& point:route) + { + point.floorZ=blendLandingChamberFloor(passage,point.distance, + totalLength,point.x,point.y,point.floorZ); + point.z=point.floorZ+passageTubeDepthAt(point.radius,bodyDepth); + } + } return route; }; std::vector allPassageBodyFloorDepths; std::vector> allPassageSmoothRoutes; - struct FUpperMouthFloorBlend + struct FMouthFloorBlend { std::vector> receivingRoute; double mouthX=0.0,mouthY=0.0,mouthFloor=0.0; double tangentX=0.0,tangentY=0.0; }; - std::vector allUpperMouthFloorBlends; + struct FPassageFloorBlends + { + FMouthFloorBlend upper,lower; + }; + std::vector allPassageFloorBlends; allPassageBodyFloorDepths.reserve(ResolvedLayout->passages.size()); allPassageSmoothRoutes.reserve(ResolvedLayout->passages.size()); - allUpperMouthFloorBlends.reserve(ResolvedLayout->passages.size()); + allPassageFloorBlends.reserve(ResolvedLayout->passages.size()); for(const auto& passage:ResolvedLayout->passages) { const double bodyDepth=passageBodyFloorDepth(passage); allPassageBodyFloorDepths.push_back(bodyDepth); allPassageSmoothRoutes.push_back(passageSmoothRoute(passage,bodyDepth)); - FUpperMouthFloorBlend blend; + FPassageFloorBlends blends; + const auto controls=passageSplineControls(passage); + const auto floorProfile=passageSplineFloorProfile(passage); + const std::size_t routeEnd=std::clamp( + passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1); + const auto segments=VoxelForgeCore::PassageSpline::BuildSegments( + controls,routeEnd,8.0,1.0,floorProfile,0.100); const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), ResolvedLayout->strates.end(),[&](const auto& stratum) {return stratum.strateIndex==passage.upperStrateIndex;}); if(upperStratum!=ResolvedLayout->strates.end() - &&upperStratum->generatorKind=="FloatingIslands") + &&(upperStratum->generatorKind=="FloatingIslands" + ||upperStratum->generatorKind=="TunnelNetwork" + ||upperStratum->generatorKind=="SurfaceWorld")) { for(const auto& anchor:upperStratum->anchors) if(anchor.kind=="route_point"&&anchor.values.size()>=3) - blend.receivingRoute.push_back({ + blends.upper.receivingRoute.push_back({ qToMetres(anchor.values[0]),qToMetres(anchor.values[1]), qToMetres(anchor.values[2])}); - const auto controls=passageSplineControls(passage); - const auto floorProfile=passageSplineFloorProfile(passage); - const std::size_t routeEnd=std::clamp( - passage.lowerMouthControlIndex,1,passage.controlPoints.size()-1); - const auto segments=VoxelForgeCore::PassageSpline::BuildSegments( - controls,routeEnd,8.0,1.0,floorProfile,0.100); + if(upperStratum->generatorKind=="TunnelNetwork" + &&blends.upper.receivingRoute.size()>2) + blends.upper.receivingRoute.erase( + blends.upper.receivingRoute.begin(), + blends.upper.receivingRoute.end()-2); if(!segments.empty()) { - blend.mouthX=segments.front().a[0]; - blend.mouthY=segments.front().a[1]; - blend.mouthFloor=floorProfile.front(); - blend.tangentX=segments.front().b[0]-segments.front().a[0]; - blend.tangentY=segments.front().b[1]-segments.front().a[1]; + blends.upper.mouthX=controls.front()[0]; + blends.upper.mouthY=controls.front()[1]; + blends.upper.mouthFloor=floorProfile.front(); + blends.upper.tangentX=segments.front().b[0]-segments.front().a[0]; + blends.upper.tangentY=segments.front().b[1]-segments.front().a[1]; } + if(upperStratum->generatorKind=="TunnelNetwork") + for(const auto& edge:ResolvedLayout->cavernEdges) + if(edge.startAnchorStableId==passage.stableId+"/upper-mouth" + &&edge.controlPointsMicrometres.size()>=2) + { + blends.upper.tangentX= + (edge.controlPointsMicrometres[0][0] + -edge.controlPointsMicrometres[1][0])/1000000.0; + blends.upper.tangentY= + (edge.controlPointsMicrometres[0][1] + -edge.controlPointsMicrometres[1][1])/1000000.0; + } } - allUpperMouthFloorBlends.push_back(std::move(blend)); + const auto lowerStratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&](const auto& stratum) + {return stratum.strateIndex==passage.lowerStrateIndex;}); + if(lowerStratum!=ResolvedLayout->strates.end() + &&lowerStratum->generatorKind=="TunnelNetwork" + &&!controls.empty()&&routeEndanchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + blends.lower.receivingRoute.push_back({ + qToMetres(anchor.values[0]),qToMetres(anchor.values[1]), + qToMetres(anchor.values[2])}); + if(blends.lower.receivingRoute.size()>2) + blends.lower.receivingRoute.resize(2); + blends.lower.mouthX=controls[routeEnd][0]; + blends.lower.mouthY=controls[routeEnd][1]; + blends.lower.mouthFloor=floorProfile[routeEnd]; + if(!segments.empty()) + { + blends.lower.tangentX=controls[routeEnd][0] + -controls[routeEnd-1][0]; + blends.lower.tangentY=controls[routeEnd][1] + -controls[routeEnd-1][1]; + } + for(const auto& edge:ResolvedLayout->cavernEdges) + if(edge.startAnchorStableId==passage.stableId+"/lower-mouth" + &&edge.controlPointsMicrometres.size()>=2) + { + blends.lower.tangentX= + (edge.controlPointsMicrometres[1][0] + -edge.controlPointsMicrometres[0][0])/1000000.0; + blends.lower.tangentY= + (edge.controlPointsMicrometres[1][1] + -edge.controlPointsMicrometres[0][1])/1000000.0; + } + } + allPassageFloorBlends.push_back(std::move(blends)); } const auto passageFloorAtXY=[&](std::size_t passageIndex,double x,double y, double plannedFloor) { - if(passageIndex>=allUpperMouthFloorBlends.size())return plannedFloor; - const auto& blend=allUpperMouthFloorBlends[passageIndex]; - return VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY( - blend.receivingRoute,x,y,plannedFloor,blend.mouthX,blend.mouthY, - blend.mouthFloor, - blend.tangentX,blend.tangentY, - VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres, - 1.13); + if(passageIndex>=allPassageFloorBlends.size())return plannedFloor; + const auto& blends=allPassageFloorBlends[passageIndex]; + double floor=plannedFloor; + const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&](const auto& stratum) + {return stratum.strateIndex + ==ResolvedLayout->passages[passageIndex].upperStrateIndex;}); + const double upperPlayerCenterHeight=upperStratum + !=ResolvedLayout->strates.end() + &&upperStratum->generatorKind=="SurfaceWorld"?0.98:1.13; + if(!ResolvedLayout->passages[passageIndex] + .floorProfileIncludesLandingChambers) + { + floor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY( + blends.upper.receivingRoute,x,y,floor,blends.upper.mouthX, + blends.upper.mouthY,blends.upper.mouthFloor, + blends.upper.tangentX,blends.upper.tangentY, + VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres, + upperPlayerCenterHeight); + floor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY( + blends.lower.receivingRoute,x,y,floor,blends.lower.mouthX, + blends.lower.mouthY,blends.lower.mouthFloor, + -blends.lower.tangentX,-blends.lower.tangentY, + VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,1.13); + } + if(passageIndex>=allPassageSmoothRoutes.size())return floor; + const auto& route=allPassageSmoothRoutes[passageIndex]; + if(route.size()<2)return floor; + double closestDistanceSquared=std::numeric_limits::infinity(); + double closestAlong=0.0; + for(std::size_t index=1;index1.0e-12 + ?std::clamp(((x-a.x)*dx+(y-a.y)*dy)/lengthSquared,0.0,1.0):0.0; + const double nearestX=a.x+t*dx,nearestY=a.y+t*dy; + const double ex=x-nearestX,ey=y-nearestY; + const double distanceSquared=ex*ex+ey*ey; + if(distanceSquaredpassages[passageIndex],closestAlong, + route.back().distance,x,y,floor); }; const auto interpolatePassageRoute=[]( const std::vector& route,double distance) @@ -3649,6 +4079,26 @@ namespace StandaloneStreamBenchmark &&passageIndex!=selectedPassage)continue; const auto& passage=ResolvedLayout->passages[passageIndex]; if(passage.controlPoints.size()<2)continue; + const auto inLandingChamber=[&]( + const VoxelForgeCore::ResolvedLayoutPassageLandingChamberV1& chamber) + { + if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0) + return false; + const double centerX=qToMetres(chamber.floorCenterSubvoxels[0]); + const double centerY=qToMetres(chamber.floorCenterSubvoxels[1]); + const double floorCenterZ=qToMetres( + chamber.floorCenterSubvoxels[2]) + +chamber.floorGradientQ16[0]/65536.0*(xMetres-centerX) + +chamber.floorGradientQ16[1]/65536.0*(yMetres-centerY); + const double radialDistance=std::hypot(xMetres-centerX, + yMetres-centerY)-qToMetres(chamber.radiusSubvoxels); + const double floorDistance=floorCenterZ-zMetres; + const double ceilingDistance=zMetres-(floorCenterZ + +qToMetres(chamber.clearHeightSubvoxels)); + return std::max({radialDistance,floorDistance,ceilingDistance})<=0.0; + }; + if(inLandingChamber(passage.upperLandingChamber) + ||inLandingChamber(passage.lowerLandingChamber))return true; const auto& route=allPassageSmoothRoutes[passageIndex]; if(route.size()<2)continue; double bestRoundDistance=std::numeric_limits::infinity(); @@ -4319,7 +4769,8 @@ namespace StandaloneStreamBenchmark const double phase=6.2831853071795864769*distance/4.0; maskedSineControl.push_back({distance,0.060*std::sin(phase)}); const double x=(distance-16.0)/16.0; - maskedSmoothControl.push_back({distance,0.4*x*x*x-0.15*x*x+0.08*x}); + maskedSmoothControl.push_back({distance, + 0.4*x*x*x-0.15*x*x+0.08*x}); } std::vector> maskedSineResiduals,maskedSmoothResiduals; appendDetrendedProfile(maskedSineControl,maskedSineResiduals); @@ -4494,7 +4945,8 @@ namespace StandaloneStreamBenchmark // The probes sit 0.18 m inside the authored radius. Add that // radius back to the signed wall distance so radius-carried ribs // remain visible; the cubic detrend removes the smooth mouth flare. - radii.push_back({distance,sampleRadius+0.5*(left+right)}); + const double measuredRadius=sampleRadius+0.5*(left+right); + radii.push_back({distance,measuredRadius}); ++audit.samples; } if(radii.size()(WindowLengthMetres/ @@ -4582,7 +5034,8 @@ namespace StandaloneStreamBenchmark // probe has no zero crossing. Judge ripple only in windows with // enough measured wall samples; naturalness separately requires // visible relief on the route as a whole. - audit.passed=audit.controlDetected&&audit.wallQualifiedWindows>0 + audit.passed=audit.controlDetected&&maskedSamplingControlPassed + &&audit.wallQualifiedWindows>0 &&wallWindowCoverage>=0.75 &&audit.localPeakAmplitudeMetresstrates.end()?nullptr:&*it; }; + const auto landingChamberConnectionContains=[&]( + const VoxelForgeCore::ResolvedLayoutPassageV1& passage,bool upper, + double xMetres,double yMetres,double zMetres) + { + const auto& chamber=upper?passage.upperLandingChamber + :passage.lowerLandingChamber; + const auto* stratum=findStratum(upper?passage.upperStrateIndex + :passage.lowerStrateIndex); + if(!stratum||chamber.radiusSubvoxels<=0)return false; + std::vector route; + for(const auto& anchor:stratum->anchors) + if(anchor.kind=="route_point"&&anchor.values.size()>=3) + route.push_back(&anchor); + if(route.size()<2)return false; + const double cx=qToMetres(chamber.floorCenterSubvoxels[0]); + const double cy=qToMetres(chamber.floorCenterSubvoxels[1]); + const double mouthZ=qToMetres(upper?passage.upperMouthSubvoxels[2] + :passage.lowerMouthSubvoxels[2]); + std::size_t endpoint=route.size(); + double endpointDistanceSquared=1.0; + for(std::size_t index=0;indexvalues[0])-cx; + const double dy=qToMetres(route[index]->values[1])-cy; + const double z=qToMetres(route[index]->values[2]); + const double distanceSquared=dx*dx+dy*dy; + if(distanceSquared routeDistance(route.size(),0.0); + for(std::size_t index=1;indexvalues[0] + -route[index-1]->values[0]); + const double dy=qToMetres(route[index]->values[1] + -route[index-1]->values[1]); + routeDistance[index]=routeDistance[index-1]+std::hypot(dx,dy); + } + for(std::size_t index=1;index9.0)continue; + const double routeX=ax+t*dx,routeY=ay+t*dy; + const double routeZ=az+t*qToMetres(b.values[2]-a.values[2]); + const double lateralDistance=std::hypot(xMetres-routeX, + yMetres-routeY); + if(stratum->generatorKind=="TunnelNetwork" + ||stratum->generatorKind=="VerticalShafts") + { + const double routeFloor=routeZ-1.13; + if(lateralDistance<=0.80&&zMetres>=routeFloor-0.30 + &&zMetres<=routeFloor+2.25)return true; + } + else if(lateralDistance<=1.25 + &&std::abs(zMetres-routeZ)<=1.5)return true; + } + return false; + }; const auto controlSnapshot=CoreSnapshot.withoutResolvedLayoutPassages(); VoxelForgeCore::World blockedWorld(controlSnapshot); std::string blockedCompileError; @@ -4616,6 +5141,9 @@ namespace StandaloneStreamBenchmark { std::uint64_t floorSamples=0,floorViolations=0,openAirSamples=0; std::uint64_t wallSamples=0,wallViolations=0; + std::uint64_t chamberFloorSamples=0,chamberFloorViolations=0; + std::uint64_t chamberWallSamples=0,chamberWallViolations=0; + std::uint64_t chamberConnectionSkips=0; std::uint64_t floorInteriorWins=0,wallInteriorWins=0; std::uint64_t floorMouthBlendSkips=0,wallMouthBlendSkips=0; std::uint64_t interiorSamples=0,interiorViolations=0; @@ -4628,6 +5156,18 @@ namespace StandaloneStreamBenchmark constexpr double PassageShellAuditSpacingMetres=0.25; constexpr std::array PassageShellAuditDepthsMetres{{0.125,0.375}}; constexpr float PassageShellDensityTolerance=1.0e-6f; + std::vector surfaceLandingPassages(ResolvedLayout->passages.size(),false); + for(std::size_t passageIndex=0;passageIndexpassages.size(); + ++passageIndex) + { + const auto& passage=ResolvedLayout->passages[passageIndex]; + const auto upper=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&](const auto& stratum) + {return stratum.strateIndex==passage.upperStrateIndex;}); + surfaceLandingPassages[passageIndex]=upper!=ResolvedLayout->strates.end() + &&upper->generatorKind=="SurfaceWorld" + &&upper->lowerInterfaceCapabilities.surfaceToTunnelLandingSupport; + } const auto expectedPassageShellAt=[&](double xMetres,double yMetres, double zMetres) { @@ -4773,13 +5313,44 @@ namespace StandaloneStreamBenchmark const double floorBlend=std::clamp(floorDistance/0.25,0.0,1.0); const double smoothFloorBlend=floorBlend*floorBlend *(3.0-2.0*floorBlend); - const double thickness=0.75*smoothFade*smoothFloorBlend; + double thickness=0.75*smoothFade*smoothFloorBlend; + if(candidatePassagepassages) + for(const auto* chamber:{&passage.upperLandingChamber, + &passage.lowerLandingChamber}) + { + if(chamber->radiusSubvoxels<=0)continue; + const double centerX=qToMetres(chamber->floorCenterSubvoxels[0]); + const double centerY=qToMetres(chamber->floorCenterSubvoxels[1]); + const double floorCenterZ=qToMetres( + chamber->floorCenterSubvoxels[2]) + +chamber->floorGradientQ16[0]/65536.0*(xMetres-centerX) + +chamber->floorGradientQ16[1]/65536.0*(yMetres-centerY); + const double radialDistance=std::hypot(xMetres-centerX, + yMetres-centerY)-qToMetres(chamber->radiusSubvoxels); + const double floorDistance=floorCenterZ-zMetres; + const double shellDistance=std::max({-floorDistance, + floorDistance-LandingChamberShellThicknessMetres, + radialDistance}); + minimumShellDistance=std::min(minimumShellDistance, + shellDistance); + } return minimumShellDistance; }; for(std::size_t segment=1;segment chamberFloorRadii{{0.0,2.5,5.0}}; + constexpr std::array chamberWallHeights{{1.13,3.20}}; + constexpr std::size_t ChamberAngularSamples=16; + for(const bool upper:{true,false}) + { + const auto& chamber=upper?passage.upperLandingChamber + :passage.lowerLandingChamber; + if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0)continue; + const double centerX=qToMetres(chamber.floorCenterSubvoxels[0]); + const double centerY=qToMetres(chamber.floorCenterSubvoxels[1]); + const double baseFloorZ=qToMetres(chamber.floorCenterSubvoxels[2]); + const double radius=qToMetres(chamber.radiusSubvoxels); + const double gx=chamber.floorGradientQ16[0]/65536.0; + const double gy=chamber.floorGradientQ16[1]/65536.0; + for(const double radial:chamberFloorRadii) + { + if(radial>=radius-1.0)continue; + const std::size_t angles=radial==0.0?1:ChamberAngularSamples; + for(std::size_t angleIndex=0;angleIndex(angleIndex)/ChamberAngularSamples; + const double sampleX=centerX+radial*std::cos(angle); + const double sampleY=centerY+radial*std::sin(angle); + const double floorZ=baseFloorZ+gx*(sampleX-centerX) + +gy*(sampleY-centerY); + for(const double depth:PassageShellAuditDepthsMetres) + { + const double sampleZ=floorZ-depth; + const VoxelForgeCore::Vec3d position{sampleX/coreMetres, + sampleY/coreMetres,sampleZ/coreMetres}; + if(Native.densityAt(position)<0.0f)continue; + if(landingChamberConnectionContains(passage,upper, + sampleX,sampleY,sampleZ)) + {++audit.chamberConnectionSkips;continue;} + ++audit.chamberFloorSamples; + const float density=Field.densityAt(position); + if(density>=PassageShellDensityTolerance) + { + ++audit.chamberFloorViolations; + std::printf("[PassageLandingChamberShellSample] id=%s side=%s kind=floor_missing xyz_m=(%.3f,%.3f,%.3f) native=%.6f density=%.6f\n", + passage.stableId.c_str(),upper?"upper":"lower", + sampleX,sampleY,sampleZ,Native.densityAt(position),density); + } + } + } + } + for(std::size_t angleIndex=0;angleIndex(angleIndex)/ChamberAngularSamples; + const double nx=std::cos(angle),ny=std::sin(angle); + for(const double depth:PassageShellAuditDepthsMetres) + for(const double height:chamberWallHeights) + { + const double sampleX=centerX+(radius+depth)*nx; + const double sampleY=centerY+(radius+depth)*ny; + const double floorZ=baseFloorZ+gx*(sampleX-centerX) + +gy*(sampleY-centerY); + const double sampleZ=floorZ+height; + const VoxelForgeCore::Vec3d position{sampleX/coreMetres, + sampleY/coreMetres,sampleZ/coreMetres}; + if(Native.densityAt(position)<0.0f)continue; + if(passageInteriorWinsAt(sampleX,sampleY,sampleZ, + std::numeric_limits::max(), + std::numeric_limits::max(),passageIndex) + ||landingChamberConnectionContains(passage,upper, + sampleX,sampleY,sampleZ)) + {++audit.chamberConnectionSkips;continue;} + ++audit.chamberWallSamples; + const float density=Field.densityAt(position); + if(density>=PassageShellDensityTolerance) + { + ++audit.chamberWallViolations; + std::printf("[PassageLandingChamberShellSample] id=%s side=%s kind=wall_missing xyz_m=(%.3f,%.3f,%.3f) native=%.6f density=%.6f radial_m=%.3f height_m=%.3f\n", + passage.stableId.c_str(),upper?"upper":"lower", + sampleX,sampleY,sampleZ,Native.densityAt(position),density, + radius+depth,height); + } + } + } + } return audit; }; const auto mouthInContent=[&](std::int32_t index, @@ -4945,14 +5599,74 @@ namespace StandaloneStreamBenchmark return zUm>=stratum->content.bottomMicrometres &&zUmcontent.topMicrometres; }; + const auto auditLandingChamber=[&](const VoxelForgeCore::World& field, + const VoxelForgeCore::ResolvedLayoutPassageV1& passage, + std::size_t passageIndex,bool upper) + { + FWalkAudit total;total.passed=true; + const auto& chamber=upper?passage.upperLandingChamber + :passage.lowerLandingChamber; + if(chamber.radiusSubvoxels<=0||chamber.clearHeightSubvoxels<=0) + {total.passed=false;return total;} + constexpr std::array,8> directions{{ + {{1.0,0.0}},{{0.7071067811865475,0.7071067811865475}}, + {{0.0,1.0}},{{-0.7071067811865475,0.7071067811865475}}, + {{-1.0,0.0}},{{-0.7071067811865475,-0.7071067811865475}}, + {{0.0,-1.0}},{{0.7071067811865475,-0.7071067811865475}}}}; + const double centerX=qToMetres(chamber.floorCenterSubvoxels[0]); + const double centerY=qToMetres(chamber.floorCenterSubvoxels[1]); + const double centerZ=qToMetres(chamber.floorCenterSubvoxels[2]); + const double radius=std::min(5.75, + qToMetres(chamber.radiusSubvoxels)-1.5); + const double gx=chamber.floorGradientQ16[0]/65536.0; + const double gy=chamber.floorGradientQ16[1]/65536.0; + const std::function interiorPredicate= + [&](double x,double y,double z) + {return passageInteriorWinsAt(x,y,z, + std::numeric_limits::max(), + std::numeric_limits::max(),passageIndex);}; + for(const auto& direction:directions) + { + std::vector> chamberPath; + const std::size_t intervals=std::max(1, + static_cast(std::ceil(radius/0.5))); + chamberPath.reserve(intervals+1); + for(std::size_t step=0;step<=intervals;++step) + { + const double distance=radius*step/intervals; + const double x=centerX+direction[0]*distance; + const double y=centerY+direction[1]*distance; + const double floorZ=centerZ+gx*(x-centerX)+gy*(y-centerY); + chamberPath.push_back({x/coreMetres,y/coreMetres, + (floorZ+1.13)/coreMetres}); + } + const FWalkAudit part=auditPlayerPath(field,chamberPath, + 0.125,0.125,1.125,3,0.45,{},interiorPredicate); + total.segments+=part.segments;total.centers+=part.centers; + total.capsuleSamples+=part.capsuleSamples; + total.blocked+=part.blocked; + total.passageInteriorCapsuleSamples+=part.passageInteriorCapsuleSamples; + total.passageInteriorSolidSamples+=part.passageInteriorSolidSamples; + total.supportSamples+=part.supportSamples; + total.unsupported+=part.unsupported; + total.gradeFailures+=part.gradeFailures; + total.supportOverlapSkips+=part.supportOverlapSkips; + total.passed=total.passed&&part.passed; + } + return total; + }; std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0; std::uint64_t passageInteriorCapsuleSamples=0,passageInteriorSolidSamples=0; std::uint64_t passageInteriorControls=0; + std::uint64_t landingChamberCount=0,landingChamberNegativeControls=0; std::uint64_t mouthProfilePasses=0,ripplePasses=0,rippleControlsDetected=0; std::uint64_t floorRippleControlsDetected=0,floorStepRippleControlsDetected=0; std::uint64_t shellAudits=0,shellFloorSamples=0,shellFloorViolations=0; std::uint64_t shellFloorMouthBlendSkips=0; std::uint64_t shellOpenAirSamples=0,shellWallSamples=0,shellWallViolations=0; + std::uint64_t shellChamberFloorSamples=0,shellChamberFloorViolations=0; + std::uint64_t shellChamberWallSamples=0,shellChamberWallViolations=0; + std::uint64_t shellChamberConnectionSkips=0; std::uint64_t shellInteriorSamples=0,shellInteriorViolations=0; for(std::size_t passageIndex=0;passageIndexpassages.size();++passageIndex) { @@ -4978,11 +5692,26 @@ namespace StandaloneStreamBenchmark std::numeric_limits::max(),passageIndex);}; const FWalkAudit passageAudit=auditPlayerPath(Shape,path,0.375,0.125, 1.125,3,0.45,passageSupportExemption,passageInteriorPredicate); + const FWalkAudit upperChamberAudit=auditLandingChamber(Shape, + passage,passageIndex,true); + const FWalkAudit lowerChamberAudit=auditLandingChamber(Shape, + passage,passageIndex,false); + const bool landingChambersWalkable=upperChamberAudit.passed + &&lowerChamberAudit.passed; const bool passageInteriorAirPassed= passageAudit.passageInteriorCapsuleSamples>0 - &&passageAudit.passageInteriorSolidSamples==0; - passageInteriorCapsuleSamples+=passageAudit.passageInteriorCapsuleSamples; - passageInteriorSolidSamples+=passageAudit.passageInteriorSolidSamples; + &&passageAudit.passageInteriorSolidSamples==0 + &&upperChamberAudit.passageInteriorCapsuleSamples>0 + &&lowerChamberAudit.passageInteriorCapsuleSamples>0 + &&upperChamberAudit.passageInteriorSolidSamples==0 + &&lowerChamberAudit.passageInteriorSolidSamples==0; + passageInteriorCapsuleSamples+=passageAudit.passageInteriorCapsuleSamples + +upperChamberAudit.passageInteriorCapsuleSamples + +lowerChamberAudit.passageInteriorCapsuleSamples; + passageInteriorSolidSamples+=passageAudit.passageInteriorSolidSamples + +upperChamberAudit.passageInteriorSolidSamples + +lowerChamberAudit.passageInteriorSolidSamples; + landingChamberCount+=2; const FPassageNaturalnessAudit naturalAudit= auditPassageNaturalness(Shape,passageIndex); const auto lowerProfileStratum=std::find_if(ResolvedLayout->strates.begin(), @@ -5025,6 +5754,11 @@ namespace StandaloneStreamBenchmark shellOpenAirSamples+=shellAudit.openAirSamples; shellWallSamples+=shellAudit.wallSamples; shellWallViolations+=shellAudit.wallViolations; + shellChamberFloorSamples+=shellAudit.chamberFloorSamples; + shellChamberFloorViolations+=shellAudit.chamberFloorViolations; + shellChamberWallSamples+=shellAudit.chamberWallSamples; + shellChamberWallViolations+=shellAudit.chamberWallViolations; + shellChamberConnectionSkips+=shellAudit.chamberConnectionSkips; shellInteriorSamples+=shellAudit.interiorSamples; shellInteriorViolations+=shellAudit.interiorViolations; const auto radialSquared=[](const std::array& p) @@ -5041,7 +5775,8 @@ namespace StandaloneStreamBenchmark rippleControlsDetected+=rippleAudit.controlDetected; floorRippleControlsDetected+=rippleAudit.floorControlDetected; floorStepRippleControlsDetected+=rippleAudit.floorStepControlDetected; - passagePasses+=passageAudit.passed&&passageInteriorAirPassed + passagePasses+=passageAudit.passed&&landingChambersWalkable + &&passageInteriorAirPassed &&mouthsInRadius&&mouthsInContent &&naturalAudit.passed&&mouthProfileAudit.passed&&rippleAudit.passed; std::printf("[ResolvedPassageAudit] id=%s interface=%d-%d control_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu passage_interior_capsule_samples=%llu passage_interior_solid_samples=%llu interior_air=%s support_samples=%llu support_overlap_skips=%llu unsupported=%llu grade_failures=%llu mouth_content=%d/2 mouth_radius=%d/2 walkability=%s\n", @@ -5056,7 +5791,26 @@ namespace StandaloneStreamBenchmark static_cast(passageAudit.supportOverlapSkips), static_cast(passageAudit.unsupported), static_cast(passageAudit.gradeFailures),mouthsInContent?2:0, - mouthsInRadius?2:0,passageAudit.passed?"PASS":"FAIL"); + mouthsInRadius?2:0, + passageAudit.passed&&landingChambersWalkable?"PASS":"FAIL"); + std::printf("[PassageLandingChamberAudit] id=%s side=upper capsule_samples=%llu solid_samples=%llu support_samples=%llu unsupported=%llu grade_failures=%llu status=%s\n", + passage.stableId.c_str(), + static_cast(upperChamberAudit.passageInteriorCapsuleSamples), + static_cast(upperChamberAudit.passageInteriorSolidSamples), + static_cast(upperChamberAudit.supportSamples), + static_cast(upperChamberAudit.unsupported), + static_cast(upperChamberAudit.gradeFailures), + upperChamberAudit.passed&&upperChamberAudit.passageInteriorSolidSamples==0 + &&upperChamberAudit.passageInteriorCapsuleSamples>0?"PASS":"FAIL"); + std::printf("[PassageLandingChamberAudit] id=%s side=lower capsule_samples=%llu solid_samples=%llu support_samples=%llu unsupported=%llu grade_failures=%llu status=%s\n", + passage.stableId.c_str(), + static_cast(lowerChamberAudit.passageInteriorCapsuleSamples), + static_cast(lowerChamberAudit.passageInteriorSolidSamples), + static_cast(lowerChamberAudit.supportSamples), + static_cast(lowerChamberAudit.unsupported), + static_cast(lowerChamberAudit.gradeFailures), + lowerChamberAudit.passed&&lowerChamberAudit.passageInteriorSolidSamples==0 + &&lowerChamberAudit.passageInteriorCapsuleSamples>0?"PASS":"FAIL"); std::printf("[PassageNaturalnessAudit] id=%s flat_run_max_m=%.3f limit_m=%.3f wall_relief_p2p_m=%.3f minimum_m=%.3f samples=%llu missing=%llu status=%s\n", passage.stableId.c_str(),naturalAudit.maximumFlatRunMetres,MaximumFlatRunMetres, naturalAudit.wallReliefPeakToPeakMetres,MinimumWallReliefMetres, @@ -5112,12 +5866,17 @@ namespace StandaloneStreamBenchmark passage.stableId.c_str(),value[0],value[1],value[2],value[2]-value[1], value[3],value[4],value[5]); } - std::printf("[PassageShellAudit] id=%s floor_samples=%llu floor_violations=%llu floor_overlap_skips=%llu floor_mouth_blend_skips=%llu open_air_shell_samples=%llu wall_samples=%llu wall_violations=%llu wall_overlap_skips=%llu wall_mouth_blend_skips=%llu interior_samples=%llu interior_violations=%llu spacing_m=%.3f status=%s\n", + std::printf("[PassageShellAudit] id=%s floor_samples=%llu floor_violations=%llu floor_overlap_skips=%llu floor_mouth_blend_skips=%llu chamber_floor_samples=%llu chamber_floor_violations=%llu chamber_wall_samples=%llu chamber_wall_violations=%llu chamber_connection_skips=%llu open_air_shell_samples=%llu wall_samples=%llu wall_violations=%llu wall_overlap_skips=%llu wall_mouth_blend_skips=%llu interior_samples=%llu interior_violations=%llu spacing_m=%.3f status=%s\n", passage.stableId.c_str(), static_cast(shellAudit.floorSamples), static_cast(shellAudit.floorViolations), static_cast(shellAudit.floorInteriorWins), static_cast(shellAudit.floorMouthBlendSkips), + static_cast(shellAudit.chamberFloorSamples), + static_cast(shellAudit.chamberFloorViolations), + static_cast(shellAudit.chamberWallSamples), + static_cast(shellAudit.chamberWallViolations), + static_cast(shellAudit.chamberConnectionSkips), static_cast(shellAudit.openAirSamples), static_cast(shellAudit.wallSamples), static_cast(shellAudit.wallViolations), @@ -5127,6 +5886,8 @@ namespace StandaloneStreamBenchmark static_cast(shellAudit.interiorViolations), PassageShellAuditSpacingMetres, shellAudit.floorViolations==0&&shellAudit.wallViolations==0 + &&shellAudit.chamberFloorViolations==0 + &&shellAudit.chamberWallViolations==0 &&shellAudit.interiorViolations==0 ?"PASS":"FAIL"); for(std::size_t i=0;istrates) { std::vector anchors; @@ -5266,40 +6029,69 @@ namespace StandaloneStreamBenchmark supportMaximumMetres=routeConstraints->supportSearchDepthSubvoxels*qMetres; } } + if(stratum.generatorKind=="TunnelNetwork" + ||stratum.generatorKind=="VerticalShafts") + { + std::int64_t floorSupportDepth=0; + for(const auto& edge:ResolvedLayout->cavernEdges) + if(edge.strateIndex==stratum.strateIndex) + floorSupportDepth=std::max(floorSupportDepth, + edge.floorSupportDepthMicrometres); + supportMaximumMetres=std::max(supportMaximumMetres, + floorSupportDepth/1000000.0+0.25); + } const FWalkAudit routeAudit=auditPlayerPath(Shape,route,supportStartMetres, supportStepMetres,supportMaximumMetres,minimumSupportProbes, supportProbeRadiusMetres); bool binding=true; + const auto equalPosition=[](const auto& a,const auto& b) + {return a[0]==b[0]&&a[1]==b[1]&&a[2]==b[2];}; + const auto boundMouthMatches=[&](const auto& authored,const auto& mouth) + { + if(stratum.generatorKind!="TunnelNetwork") + return equalPosition(authored,mouth); + // Passage mouths already encode the player center 1.13 m above + // their floor, matching the route-point convention directly. + return equalPosition(authored,mouth); + }; for(const auto& passage:ResolvedLayout->passages) { - const auto equalPosition=[](const auto& a,const auto& b) - {return a[0]==b[0]&&a[1]==b[1]&&a[2]==b[2];}; if(passage.upperStrateIndex==stratum.strateIndex) { - const auto rootedMouth=std::find_if(stratum.anchors.begin(), - stratum.anchors.end(),[](const auto& anchor) - {return anchor.kind=="passage_anchor"&&anchor.values.size()>=10;}); - const std::array departure= - rootedMouth!=stratum.anchors.end() - ?std::array{{rootedMouth->values[7], - rootedMouth->values[8],rootedMouth->values[9]}} - :std::array{{anchors.back()->values[0], - anchors.back()->values[1],anchors.back()->values[2]}}; - binding&=equalPosition(passage.upperMouthSubvoxels, - departure); + if(stratum.generatorKind=="TunnelNetwork") + binding&=boundMouthMatches(anchors.back()->values, + passage.upperMouthSubvoxels); + else + { + const auto rootedMouth=std::find_if(stratum.anchors.begin(), + stratum.anchors.end(),[](const auto& anchor) + {return anchor.kind=="passage_anchor"&&anchor.values.size()>=10;}); + const std::array departure= + rootedMouth!=stratum.anchors.end() + ?std::array{{rootedMouth->values[7], + rootedMouth->values[8],rootedMouth->values[9]}} + :std::array{{anchors.back()->values[0], + anchors.back()->values[1],anchors.back()->values[2]}}; + binding&=equalPosition(passage.upperMouthSubvoxels,departure); + } } if(passage.lowerStrateIndex==stratum.strateIndex) { - const auto passageArrival=std::find_if(stratum.anchors.begin(), - stratum.anchors.end(),[](const auto& anchor) - {return anchor.kind=="passage_arrival"&&anchor.values.size()>=3;}); - const std::array arrival=passageArrival!=stratum.anchors.end() - ?std::array{{passageArrival->values[0], - passageArrival->values[1],passageArrival->values[2]}} - :std::array{{anchors.front()->values[0], - anchors.front()->values[1],anchors.front()->values[2]}}; - binding&=equalPosition(passage.lowerMouthSubvoxels, - arrival); + if(stratum.generatorKind=="TunnelNetwork") + binding&=boundMouthMatches(anchors.front()->values, + passage.lowerMouthSubvoxels); + else + { + const auto passageArrival=std::find_if(stratum.anchors.begin(), + stratum.anchors.end(),[](const auto& anchor) + {return anchor.kind=="passage_arrival"&&anchor.values.size()>=3;}); + const std::array arrival=passageArrival!=stratum.anchors.end() + ?std::array{{passageArrival->values[0], + passageArrival->values[1],passageArrival->values[2]}} + :std::array{{anchors.front()->values[0], + anchors.front()->values[1],anchors.front()->values[2]}}; + binding&=equalPosition(passage.lowerMouthSubvoxels,arrival); + } } } ++routePasses;routeWalkPasses+=routeAudit.passed; @@ -5329,6 +6121,48 @@ namespace StandaloneStreamBenchmark stratum.strateIndex,routeAudit.firstUnsupported[i][0], routeAudit.firstUnsupported[i][1],routeAudit.firstUnsupported[i][2], routeAudit.firstUnsupported[i][3]); + if(stratum.generatorKind=="TunnelNetwork" + ||stratum.generatorKind=="VerticalShafts") + { + const bool verticalShafts=stratum.generatorKind=="VerticalShafts"; + if(verticalShafts)++verticalShaftRoutes; + else ++tunnelNetworkRoutes; + auto controlRoute=route; + constexpr double OffBoreControlOffsetMetres=12.0; + const std::size_t controlIndex=controlRoute.size()/2; + const double tangentX=route[controlIndex+1][0] + -route[controlIndex-1][0]; + const double tangentY=route[controlIndex+1][1] + -route[controlIndex-1][1]; + const double tangentLength=std::hypot(tangentX,tangentY); + if(tangentLength>1.0e-9) + { + controlRoute[controlIndex][0]+=-tangentY/tangentLength + *OffBoreControlOffsetMetres/coreMetres; + controlRoute[controlIndex][1]+=tangentX/tangentLength + *OffBoreControlOffsetMetres/coreMetres; + } + const FWalkAudit control=auditPlayerPath(blockedWorld,controlRoute, + supportStartMetres,supportStepMetres,supportMaximumMetres, + minimumSupportProbes,supportProbeRadiusMetres); + const bool detected=control.blocked>0&&!control.passed; + if(verticalShafts) + { + verticalShaftBlockedControls+=detected; + std::printf("[VerticalShaftsBlockedRouteControl] strate=%d mode=off_bore_lateral_deviation_m=%.1f blocked_capsule_samples=%llu result=%s expected=FAIL status=%s\n", + stratum.strateIndex,OffBoreControlOffsetMetres, + static_cast(control.blocked), + control.passed?"PASS":"FAIL",detected?"PASS":"FAIL"); + } + else + { + tunnelNetworkBlockedControls+=detected; + std::printf("[TunnelNetworkBlockedRouteControl] strate=%d mode=off_bore_lateral_deviation_m=%.1f blocked_capsule_samples=%llu result=%s expected=FAIL status=%s\n", + stratum.strateIndex,OffBoreControlOffsetMetres, + static_cast(control.blocked), + control.passed?"PASS":"FAIL",detected?"PASS":"FAIL"); + } + } } const bool liminalRoadAuditPassed=auditLiminalRoadWalk(false); for(std::size_t passageIndex=0; @@ -5351,6 +6185,19 @@ namespace StandaloneStreamBenchmark std::numeric_limits::max(),passageIndex);}; const FWalkAudit blockedAudit=auditPlayerPath(blockedWorld,path, 0.125,0.125,0.25,3,0.45,{},passageInteriorPredicate); + const FWalkAudit upperChamberBlocked=auditLandingChamber(blockedWorld, + passage,passageIndex,true); + const FWalkAudit lowerChamberBlocked=auditLandingChamber(blockedWorld, + passage,passageIndex,false); + blockedControls+=blockedAudit.blocked>0&&!blockedAudit.passed; + const bool upperChamberControlDetected= + upperChamberBlocked.passageInteriorCapsuleSamples>0 + &&upperChamberBlocked.passageInteriorSolidSamples>0; + const bool lowerChamberControlDetected= + lowerChamberBlocked.passageInteriorCapsuleSamples>0 + &&lowerChamberBlocked.passageInteriorSolidSamples>0; + landingChamberNegativeControls+=upperChamberControlDetected + +lowerChamberControlDetected; blockedControls+=blockedAudit.blocked>0&&!blockedAudit.passed; const bool passageInteriorControlDetected= blockedAudit.passageInteriorCapsuleSamples>0 @@ -5359,6 +6206,16 @@ namespace StandaloneStreamBenchmark std::printf("[ResolvedPassageBlockedControl] id=%s blocked_capsule_samples=%llu detected=%s\n", passage.stableId.c_str(),static_cast(blockedAudit.blocked), blockedAudit.blocked>0&&!blockedAudit.passed?"true":"false"); + std::printf("[PassageLandingChamberNegativeControl] id=%s side=upper capsule_samples=%llu interior_solid_samples=%llu detected=%s\n", + passage.stableId.c_str(), + static_cast(upperChamberBlocked.passageInteriorCapsuleSamples), + static_cast(upperChamberBlocked.passageInteriorSolidSamples), + upperChamberControlDetected?"true":"false"); + std::printf("[PassageLandingChamberNegativeControl] id=%s side=lower capsule_samples=%llu interior_solid_samples=%llu detected=%s\n", + passage.stableId.c_str(), + static_cast(lowerChamberBlocked.passageInteriorCapsuleSamples), + static_cast(lowerChamberBlocked.passageInteriorSolidSamples), + lowerChamberControlDetected?"true":"false"); std::printf("[ResolvedPassageInteriorNegativeControl] id=%s passage_interior_capsule_samples=%llu interior_solid_samples=%llu detected=%s status=%s\n", passage.stableId.c_str(), static_cast(blockedAudit.passageInteriorCapsuleSamples), @@ -5369,21 +6226,35 @@ namespace StandaloneStreamBenchmark const bool passageInteriorAuditPassed=passageCount>0 &&passageInteriorCapsuleSamples>0&&passageInteriorSolidSamples==0 &&passageInteriorControls==passageCount; - std::printf("[PassageInteriorCapsuleAuditSummary] interfaces=%llu capsule_samples=%llu solid_samples=%llu negative_controls=%llu/%llu status=%s\n", + std::printf("[PassageInteriorCapsuleAuditSummary] interfaces=%llu capsule_samples=%llu solid_samples=%llu negative_controls=%llu/%llu landing_chambers=%llu chamber_negative_controls=%llu/%llu status=%s\n", static_cast(passageCount), static_cast(passageInteriorCapsuleSamples), static_cast(passageInteriorSolidSamples), static_cast(passageInteriorControls), static_cast(passageCount), + static_cast(landingChamberCount), + static_cast(landingChamberNegativeControls), + static_cast(landingChamberCount), passageInteriorAuditPassed?"PASS":"FAIL"); + std::printf("[PassageLandingChamberNegativeControlSummary] detected=%llu/%llu naturally_open_baselines=%llu status=OBSERVED\n", + static_cast(landingChamberNegativeControls), + static_cast(landingChamberCount), + static_cast(landingChamberCount + -landingChamberNegativeControls)); const bool shellAuditPassed=shellAudits==passageCount &&shellFloorViolations==0&&shellWallViolations==0 + &&shellChamberFloorViolations==0&&shellChamberWallViolations==0 &&shellInteriorViolations==0&&shellOpenAirSamples>0; - std::printf("[PassageShellAuditSummary] interfaces=%llu floor_samples=%llu floor_violations=%llu floor_mouth_blend_skips=%llu open_air_shell_samples=%llu wall_samples=%llu wall_violations=%llu interior_samples=%llu interior_violations=%llu status=%s\n", + std::printf("[PassageShellAuditSummary] interfaces=%llu floor_samples=%llu floor_violations=%llu floor_mouth_blend_skips=%llu chamber_floor_samples=%llu chamber_floor_violations=%llu chamber_wall_samples=%llu chamber_wall_violations=%llu chamber_connection_skips=%llu open_air_shell_samples=%llu wall_samples=%llu wall_violations=%llu interior_samples=%llu interior_violations=%llu status=%s\n", static_cast(shellAudits), static_cast(shellFloorSamples), static_cast(shellFloorViolations), static_cast(shellFloorMouthBlendSkips), + static_cast(shellChamberFloorSamples), + static_cast(shellChamberFloorViolations), + static_cast(shellChamberWallSamples), + static_cast(shellChamberWallViolations), + static_cast(shellChamberConnectionSkips), static_cast(shellOpenAirSamples), static_cast(shellWallSamples), static_cast(shellWallViolations), @@ -5423,13 +6294,19 @@ namespace StandaloneStreamBenchmark &&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings &&routeBindingFailures==0 &&liminalRoadAuditPassed + &&tunnelNetworkRoutes==tunnelNetworkBlockedControls + &&verticalShaftRoutes==verticalShaftBlockedControls &&blockedCompiled&&blockedControls==passageCount; - std::printf("[ResolvedPassageAuditSummary] interfaces=%llu passed=%llu natural=%llu/%llu strate_routes=%llu route_walkable=%llu route_points=%llu route_bindings=%llu route_binding_failures=%llu blocked_controls=%llu/%llu status=%s\n", + std::printf("[ResolvedPassageAuditSummary] interfaces=%llu passed=%llu natural=%llu/%llu strate_routes=%llu route_walkable=%llu route_points=%llu route_bindings=%llu route_binding_failures=%llu tunnel_network_block_controls=%llu/%llu vertical_shaft_block_controls=%llu/%llu blocked_controls=%llu/%llu status=%s\n", static_cast(passageCount),static_cast(passagePasses), static_cast(naturalPasses),static_cast(passageCount), static_cast(routePasses),static_cast(routeWalkPasses), static_cast(routePoints), static_cast(routeBindings),static_cast(routeBindingFailures), + static_cast(tunnelNetworkBlockedControls), + static_cast(tunnelNetworkRoutes), + static_cast(verticalShaftBlockedControls), + static_cast(verticalShaftRoutes), static_cast(blockedControls),static_cast(passageCount), auditPassed?"PASS":"FAIL"); if(!auditPassed) diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h index a97541d..95db6a6 100644 --- a/Standalone/Source/StreamingWorld.h +++ b/Standalone/Source/StreamingWorld.h @@ -1580,10 +1580,13 @@ private: static_cast(D.islandSharedCacheConflictReplacements), static_cast(D.islandSharedCacheLockWaitNanoseconds)/1000.0); if(D.passageComposerProfiled) - std::printf("[PassageComposerTile] key=(%d,%d,%d,lod%d) worker_ms=%.3f samples=%llu native_field_calls=%llu features=%llu route_candidates=%llu boundary_band_queries=%llu queries=%llu passage_iterations=%llu carve_segment_visits=%llu relief_field_calls=%llu sweep_segments=%llu sweep_pairs=%llu sweep_prepare_ms=%.3f sweep_outside_pairs=%llu sweep_shell_support_skips=%llu sweep_min_updates=%llu sweep_base_samples=%llu sweep_detail_samples=%llu sweep_grid_samples=%llu sweep_no_influence_samples=%llu sweep_lookups=%llu sweep_cache_hits=%llu sweep_base_hits=%llu sweep_outside_hits=%llu sweep_missing=%llu\n", + std::printf("[PassageComposerTile] key=(%d,%d,%d,lod%d) worker_ms=%.3f samples=%llu field_calls=%llu native_cave_samples=%llu room_candidates=%llu edge_candidates=%llu features=%llu route_candidates=%llu boundary_band_queries=%llu queries=%llu passage_iterations=%llu carve_segment_visits=%llu relief_field_calls=%llu sweep_segments=%llu sweep_pairs=%llu sweep_prepare_ms=%.3f sweep_outside_pairs=%llu sweep_shell_support_skips=%llu sweep_min_updates=%llu sweep_base_samples=%llu sweep_detail_samples=%llu sweep_grid_samples=%llu sweep_no_influence_samples=%llu sweep_lookups=%llu sweep_cache_hits=%llu sweep_base_hits=%llu sweep_outside_hits=%llu sweep_missing=%llu\n", Event.Key.Coord.X,Event.Key.Coord.Y,Event.Key.Coord.Z,Event.Key.Level, Event.WorkerMilliseconds,static_cast(Event.DensitySamples), static_cast(D.fieldCalls), + static_cast(D.nativeCaveFieldSamples), + static_cast(D.cavernRoomCandidates), + static_cast(D.cavernEdgeCandidates), static_cast(D.passageTileFeatureCount), static_cast(D.passageTileRouteSegmentCandidates), static_cast(D.resolvedBoundaryBandQueries), diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index aa6dc89..fcf0686 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -11,9 +11,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -283,6 +285,330 @@ namespace StandaloneWorldAssemblyPrototype } } } + std::uint64_t terrainPathCount=0,terrainGridSamples=0; + std::int32_t terrainGridStepQ=0; + const auto appendTerrainFallback=[&](std::int32_t stepQ) + { + if(!fallbackSearch + ||!((arrivalRadiusSubvoxels>0)!=(departureRadiusSubvoxels>0))) + return; + { + // A bounded one-mouth world can have no walkable straight chord even + // when a contour route exists. Link real ground columns only when their + // grade meets the same limit, then use the ordinary capsule and support + // sweep to accept or reject each resulting path. + struct FGroundNode + { + std::int64_t ground=0; + bool supportReachable=false; + }; + struct FQueueEntry + { + std::uint64_t cost=0; + std::size_t index=0; + bool operator>(const FQueueEntry& other) const + {return cost>other.cost||(cost==other.cost&&index>other.index);} + }; + constexpr std::array,5> supportOffsets{{ + {{0,0}},{{ProbeRadiusQ,0}},{{-ProbeRadiusQ,0}}, + {{0,ProbeRadiusQ}},{{0,-ProbeRadiusQ}}}}; + constexpr std::int64_t SupportStepQ=WorldAssemblySubvoxelsPerVoxel/4; + const std::int64_t routeBaseLiftQ=constraints.minimumClearanceSubvoxels/4; + const auto hasRouteSupportAt=[&](std::int64_t centerX,std::int64_t centerY, + std::int64_t footZ) + { + std::uint32_t reachable=0; + for(const auto& offset:supportOffsets) + { + for(std::int64_t depth=SupportStepQ;depth<=SupportDepthQ; + depth+=SupportStepQ) + { + if(finalField.point(centerX+offset[0],centerY+offset[1], + footZ-depth).densitySubvoxels<0) + {++reachable;break;} + } + } + return reachable>=constraints.minimumSupportProbes; + }; + const auto tryTerrainPaths=[&](std::int32_t terrainStepQ) + { + terrainPathCount=0;terrainGridSamples=0;terrainGridStepQ=0; + const std::int64_t boundedRadius=std::max(arrivalRadiusSubvoxels, + departureRadiusSubvoxels); + const std::int32_t boundRadiusVoxels=static_cast( + (boundedRadius+WorldAssemblySubvoxelsPerVoxel-1) + /WorldAssemblySubvoxelsPerVoxel); + const std::int32_t searchRadiusVoxels=std::clamp( + boundRadiusVoxels+64,64,256); + const std::int64_t searchRadiusQ=static_cast( + searchRadiusVoxels)*WorldAssemblySubvoxelsPerVoxel; + const std::int32_t radiusSteps=static_cast( + searchRadiusQ/terrainStepQ); + const std::int32_t dimension=radiusSteps*2+1; + const std::size_t nodeCount=static_cast(dimension)*dimension; + const auto indexOf=[dimension](std::int32_t x,std::int32_t y) + {return static_cast(y)*dimension+x;}; + std::vector grid(nodeCount); + const auto safeGroundAt=[&](std::int64_t centerX,std::int64_t centerY) + { + constexpr std::int64_t FootprintStepQ=WorldAssemblySubvoxelsPerVoxel/2; + std::int64_t highestGround=std::numeric_limits::min(); + for(std::int64_t oy=-PlayerRadiusQ;oy<=PlayerRadiusQ; + oy+=FootprintStepQ) + for(std::int64_t ox=-PlayerRadiusQ;ox<=PlayerRadiusQ; + ox+=FootprintStepQ) + { + if(ox*ox+oy*oy>PlayerRadiusQ*PlayerRadiusQ)continue; + highestGround=std::max(highestGround,finalField.point( + centerX+ox,centerY+oy,0).groundHeightSubvoxels); + } + return FGroundNode{highestGround, + hasRouteSupportAt(centerX,centerY, + highestGround+routeBaseLiftQ)}; + }; + for(std::int32_t gy=0;gy(gx-radiusSteps) + *terrainStepQ; + const std::int64_t yQ=static_cast(gy-radiusSteps) + *terrainStepQ; + grid[indexOf(gx,gy)]=safeGroundAt(xQ,yQ); + ++terrainGridSamples; + } + for(std::size_t directionIndex=0;directionIndex<1;++directionIndex) + { + std::vector costs(nodeCount, + std::numeric_limits::max()); + std::vector parent(nodeCount,nodeCount); + std::priority_queue, + std::greater> open; + std::vector edgeSupportCache(nodeCount*4,-1); + for(std::int32_t gy=0;gy::max(); + std::uint64_t goalDistanceSquared=std::numeric_limits::max(); + while(!open.empty()) + { + const FQueueEntry current=open.top();open.pop(); + if(current.cost!=costs[current.index])continue; + if(goalCost!=std::numeric_limits::max() + &¤t.cost>goalCost)break; + const std::int32_t gx=static_cast(current.index%dimension); + const std::int32_t gy=static_cast(current.index/dimension); + if(parent[current.index]!=nodeCount) + { + const std::int64_t xQ=static_cast(gx-radiusSteps) + *terrainStepQ; + const std::int64_t yQ=static_cast(gy-radiusSteps) + *terrainStepQ; + const std::uint64_t distanceSquared=static_cast( + xQ*xQ+yQ*yQ); + if(distanceSquared<=static_cast( + boundedRadius*boundedRadius) + &&(current.cost=dimension||ny>=dimension)continue; + const std::size_t next=indexOf(nx,ny); + if(!grid[next].supportReachable)continue; + const std::int64_t horizontalQ=static_cast( + std::llround(terrainStepQ*std::hypot(static_cast(ox), + static_cast(oy)))); + const std::int64_t riseQ=std::abs(grid[next].ground + -grid[current.index].ground); + if(horizontalQ<=0||riseQ*65536>horizontalQ*GradeQ16)continue; + const std::int32_t edgeDx=nx-gx,edgeDy=ny-gy; + std::int32_t baseX=gx,baseY=gy,edgeDirection=0; + if(edgeDx==0) + {baseY=std::min(gy,ny);edgeDirection=1;} + else if(edgeDy==0) + {baseX=std::min(gx,nx);edgeDirection=0;} + else if(edgeDx==edgeDy) + {baseX=std::min(gx,nx);baseY=std::min(gy,ny);edgeDirection=2;} + else + {baseX=std::max(gx,nx);baseY=std::min(gy,ny);edgeDirection=3;} + const std::size_t edgeCacheIndex=indexOf(baseX,baseY)*4 + +static_cast(edgeDirection); + if(edgeSupportCache[edgeCacheIndex]<0) + { + const std::uint64_t intervals=std::max(2, + (static_cast(horizontalQ) + +WorldAssemblySubvoxelsPerVoxel/8-1) + /(WorldAssemblySubvoxelsPerVoxel/8)); + bool supported=true; + const std::int64_t fromX=static_cast( + gx-radiusSteps)*terrainStepQ; + const std::int64_t fromY=static_cast( + gy-radiusSteps)*terrainStepQ; + const std::int64_t toX=static_cast( + nx-radiusSteps)*terrainStepQ; + const std::int64_t toY=static_cast( + ny-radiusSteps)*terrainStepQ; + for(std::uint64_t sample=1;sample(sample) + /static_cast(intervals); + const std::int64_t y=fromY+(toY-fromY) + *static_cast(sample) + /static_cast(intervals); + const std::int64_t footZ=grid[current.index].ground + +(grid[next].ground-grid[current.index].ground) + *static_cast(sample) + /static_cast(intervals) + +routeBaseLiftQ; + if(!hasRouteSupportAt(x,y,footZ)) + {supported=false;break;} + } + edgeSupportCache[edgeCacheIndex]=supported?1:0; + } + if(edgeSupportCache[edgeCacheIndex]==0)continue; + const std::uint64_t edgeCost=static_cast( + horizontalQ+riseQ*4); + if(current.cost>std::numeric_limits::max()-edgeCost) + continue; + const std::uint64_t nextCost=current.cost+edgeCost; + if(nextCost>=costs[next])continue; + costs[next]=nextCost;parent[next]=current.index; + open.push({nextCost,next}); + } + } + if(goal==nodeCount)continue; + std::vector path; + for(std::size_t cursor=goal;cursor!=nodeCount;cursor=parent[cursor]) + { + path.push_back(cursor); + if(parent[cursor]==nodeCount)break; + if(path.size()>nodeCount)break; + } + if(path.size()<2||parent[path.back()]!=nodeCount)continue; + std::reverse(path.begin(),path.end()); + if(arrivalRadiusSubvoxels>0) + std::reverse(path.begin(),path.end()); + Candidate candidate; + candidate.centerX=static_cast( + (static_cast(path.front()%dimension)-radiusSteps) + *terrainStepQ); + candidate.centerY=static_cast( + (static_cast(path.front()/dimension)-radiusSteps) + *terrainStepQ); + candidate.direction=static_cast(directionIndex); + candidate.score=static_cast(goalCost/path.size()); + candidate.endpointProximityCost=goalDistanceSquared + *(arrivalRadiusSubvoxels>0?16ull:1ull); + candidate.points.reserve(path.size()); + bool gradePass=true; + for(std::size_t pointIndex=0;pointIndex(index%dimension); + const std::int32_t gy=static_cast(index/dimension); + const std::int64_t xQ=static_cast(gx-radiusSteps) + *terrainStepQ; + const std::int64_t yQ=static_cast(gy-radiusSteps) + *terrainStepQ; + WorldAssemblyRoutePointV1 point; + point.positionSubvoxels={xQ,yQ, + grid[index].ground+routeBaseLiftQ+HalfHeightQ}; + point.radiusSubvoxels=RouteRadiusQ; + point.stableFeatureId=WorldAssemblyFeatureId(recipe.seed, + xQ,yQ, + 0x52550000u+static_cast(pointIndex)); + if(pointIndex>0) + { + const auto& previous=candidate.points.back().positionSubvoxels; + const std::int64_t dx=point.positionSubvoxels[0]-previous[0]; + const std::int64_t dy=point.positionSubvoxels[1]-previous[1]; + const std::int64_t dz=std::abs(point.positionSubvoxels[2]-previous[2]); + const std::int64_t run=static_cast(std::llround( + std::hypot(static_cast(dx),static_cast(dy)))); + if(run<=0||dz*65536>run*GradeQ16)gradePass=false; + } + candidate.points.push_back(point); + } + if(!gradePass)continue; + ++terrainPathCount; + const std::array,8> lateralNudges{{ + {{-ProbeRadiusQ,-ProbeRadiusQ}},{{-ProbeRadiusQ,0}}, + {{0,-ProbeRadiusQ}},{{ProbeRadiusQ,-ProbeRadiusQ}}, + {{-ProbeRadiusQ,ProbeRadiusQ}},{{0,ProbeRadiusQ}}, + {{ProbeRadiusQ,0}},{{ProbeRadiusQ,ProbeRadiusQ}}}}; + for(std::size_t nudgeIndex=0;nudgeIndex(nudgeIndex+1); + shifted.score+=static_cast(nudgeIndex+1); + bool shiftedGradePass=true; + for(std::size_t pointIndex=0;pointIndex1 + ?static_cast(pointIndex)*65536 + /static_cast(shifted.points.size()-1):0; + const std::int64_t t2Q16=tQ16*tQ16/65536; + const std::int64_t oneMinusQ16=65536-tQ16; + const std::int64_t oneMinus2Q16= + oneMinusQ16*oneMinusQ16/65536; + const std::int64_t weightQ16=std::min(65536, + 16*t2Q16*oneMinus2Q16/65536); + auto& point=shifted.points[pointIndex]; + point.positionSubvoxels[0]+=lateralNudges[nudgeIndex][0] + *weightQ16/65536; + point.positionSubvoxels[1]+=lateralNudges[nudgeIndex][1] + *weightQ16/65536; + const auto ground=safeGroundAt(point.positionSubvoxels[0], + point.positionSubvoxels[1]); + if(!ground.supportReachable)shiftedGradePass=false; + point.positionSubvoxels[2]=ground.ground+routeBaseLiftQ + +HalfHeightQ; + point.stableFeatureId=WorldAssemblyFeatureId(recipe.seed, + point.positionSubvoxels[0],point.positionSubvoxels[1], + 0x52550000u+static_cast( + (nudgeIndex+1)*0x10000u+pointIndex)); + if(pointIndex==0)continue; + const auto& previous=shifted.points[pointIndex-1] + .positionSubvoxels; + const std::int64_t dx=point.positionSubvoxels[0]-previous[0]; + const std::int64_t dy=point.positionSubvoxels[1]-previous[1]; + const std::int64_t dz=std::abs(point.positionSubvoxels[2]-previous[2]); + const std::int64_t run=static_cast(std::llround( + std::hypot(static_cast(dx),static_cast(dy)))); + if(run<=0||dz*65536>run*GradeQ16)shiftedGradePass=false; + } + if(!shiftedGradePass)continue; + candidates.push_back(std::move(shifted)); + ++terrainPathCount; + } + candidates.push_back(std::move(candidate)); + } + if(terrainPathCount>0)terrainGridStepQ=terrainStepQ; + }; + tryTerrainPaths(stepQ); + tested+=terrainPathCount; + std::printf("[WorldAssemblerRouteTerrainSearch] grid_step_m=%.3f grid_samples=%llu candidate_paths=%llu grade_limit_deg=15.000 status=%s\n", + terrainGridStepQ*coreVoxelMetres/WorldAssemblySubvoxelsPerVoxel, + static_cast(terrainGridSamples), + static_cast(terrainPathCount), + terrainPathCount>0?"FOUND":"EMPTY"); + } + }; if(gradeRejected>0||placementRejected>0) { const double metresPerSubvoxel=static_cast(coreVoxelSizeMicrons) @@ -318,8 +644,33 @@ namespace StandaloneWorldAssemblyPrototype }); std::uint64_t sweepAttempt=0,repairAttempt=0; VoxelForgeCore::WorldAssemblyRouteAuditResult lastAudit{}; - for(auto& candidate:candidates) + std::size_t candidateCursor=0; + constexpr std::array TerrainFallbackSteps{{1024,512,256,128}}; + std::size_t terrainFallbackStep=0; + for(;;) { + if(candidateCursor>=candidates.size()) + { + if(terrainFallbackStep0)!=(departureRadiusSubvoxels>0))) + { + const std::size_t firstTerrainCandidate=candidates.size(); + appendTerrainFallback(TerrainFallbackSteps[terrainFallbackStep++]); + std::stable_sort(candidates.begin()+firstTerrainCandidate, + candidates.end(),[](const Candidate& a,const Candidate& b) + { + if(a.endpointProximityCost!=b.endpointProximityCost) + return a.endpointProximityCost(candidate.score)); return true; } + if(liftQ==0&&audit.blockedCapsuleSamples==0 + &&audit.unsupportedCenterSamples>0) + { + constexpr std::int64_t SupportAlignmentStepQ=16; + const std::int64_t lowerLimit=std::max(0, + audit.minimumClearanceSubvoxels + -constraints.minimumClearanceSubvoxels); + for(std::int64_t lowerQ=SupportAlignmentStepQ; + lowerQ<=lowerLimit;lowerQ+=SupportAlignmentStepQ) + { + for(std::size_t pointIndex=0;pointIndex(lowerQ), + static_cast(lowerLimit)); + const double metresPerSubvoxel=coreVoxelMetres + /WorldAssemblySubvoxelsPerVoxel; + std::printf("[WorldAssemblerRouteAccepted] arrival_xy_m=(%.3f,%.3f) departure_xy_m=(%.3f,%.3f) endpoint_proximity_cost=%llu average_grade_q16=%lld\n", + candidate.points.front().positionSubvoxels[0]*metresPerSubvoxel, + candidate.points.front().positionSubvoxels[1]*metresPerSubvoxel, + candidate.points.back().positionSubvoxels[0]*metresPerSubvoxel, + candidate.points.back().positionSubvoxels[1]*metresPerSubvoxel, + static_cast(candidate.endpointProximityCost), + static_cast(candidate.score)); + return true; + } + if(loweredAudit.blockedCapsuleSamples>0 + ||loweredAudit.minimumClearanceSubvoxels + =MaximumRepairLiftQ)break; const std::int64_t needed=std::max(1, constraints.minimumClearanceSubvoxels-audit.minimumClearanceSubvoxels); @@ -358,6 +745,62 @@ namespace StandaloneWorldAssemblyPrototype liftQ=nextLift; ++repairAttempt; } + if(lastAudit.unsupportedCenterSamples>0&&sweepAttempt<5) + { + const Heightfield2D diagnosticField(recipe,stratum); + const std::int64_t sampleSpacing=std::min( + constraints.sweepSpacingSubvoxels, + WorldAssemblySubvoxelsPerVoxel/2); + const std::int64_t supportSpacing=std::min(sampleSpacing, + WorldAssemblySubvoxelsPerVoxel/4); + constexpr std::array,5> supportOffsets{{ + {{0,0}},{{ProbeRadiusQ,0}},{{-ProbeRadiusQ,0}}, + {{0,ProbeRadiusQ}},{{0,-ProbeRadiusQ}}}}; + for(std::size_t segment=1;segment(1, + static_cast(std::llround(std::sqrt( + static_cast(dx*dx+dy*dy+dz*dz))))); + const std::uint64_t intervals=std::max(1, + (length+sampleSpacing-1)/sampleSpacing); + for(std::uint64_t step=segment==1?0:1;step<=intervals;++step) + { + const auto interpolate=[&](std::size_t axis) + {return from[axis]+(to[axis]-from[axis]) + *static_cast(step)/static_cast(intervals);}; + const std::int64_t x=interpolate(0),y=interpolate(1),z=interpolate(2); + const std::int64_t footZ=z-PlayerHeightQ/2; + std::array reachedDepth{}; + for(std::size_t probe=0;probe(std::count_if( + reachedDepth.begin(),reachedDepth.end(),[](auto depth){return depth>0;})); + if(supported>=constraints.minimumSupportProbes)continue; + const double metresPerSubvoxel=coreVoxelMetres + /WorldAssemblySubvoxelsPerVoxel; + std::printf("[WorldAssemblerRouteSupportSample] ordinal=%llu x_m=%.3f y_m=%.3f center_z_m=%.3f supported=%u/5 depths_q=(%lld,%lld,%lld,%lld,%lld)\n", + static_cast(sweepAttempt),x*metresPerSubvoxel, + y*metresPerSubvoxel,z*metresPerSubvoxel,supported, + static_cast(reachedDepth[0]), + static_cast(reachedDepth[1]), + static_cast(reachedDepth[2]), + static_cast(reachedDepth[3]), + static_cast(reachedDepth[4])); + segment=stratum.route.size();break; + } + } + } if(sweepAttempt<3) std::printf("[WorldAssemblerRouteCandidateRejected] ordinal=%llu blocked=%llu unsupported=%llu grade_failures=%llu minimum_clearance_q=%lld\n", static_cast(sweepAttempt), @@ -1733,6 +2176,300 @@ namespace StandaloneWorldAssemblyPrototype return "Unknown"; } + inline bool PlanOwnerTunnelNetworkCaves(const FStandaloneOwnerWorld& owner, + VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,std::string& error) + { + using namespace VoxelForgeCore; + if(!owner.Manager) + {error="native cave planner requires the initialized owner director";return false;} + std::vector> plannerSlots; + for(const FStrateSlot& slot:owner.Manager->GetLayout()) + { + if(!slot.Definition)continue; + const ECaveGeneratorType generatorType= + owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,slot.TopChunkZ)); + const bool tunnelNetwork=generatorType==ECaveGeneratorType::TunnelNetwork; + const bool verticalShafts=generatorType==ECaveGeneratorType::VerticalShafts; + if(!tunnelNetwork&&!verticalShafts)continue; + const bool hasIncomingMouth=std::any_of(layout.passages.begin(), + layout.passages.end(),[&](const auto& passage) + {return passage.lowerStrateIndex==slot.StrateIndex + &&passage.upperStrateIndexslot.StrateIndex;}); + // Tunnel Network connects two passages. The basal Vertical Shafts + // stratum is terminal: it inherits its arrival from the preceding + // strate and intentionally has no departure passage. + if((tunnelNetwork&&(!hasIncomingMouth||!hasOutgoingMouth)) + ||(verticalShafts&&(!hasIncomingMouth||hasOutgoingMouth)))continue; + auto stratum=std::find_if(layout.strates.begin(),layout.strates.end(), + [&](const auto& item){return item.strateIndex==slot.StrateIndex;}); + if(stratum==layout.strates.end()) + {error="Tunnel Network strate is missing from the resolved layout";return false;} + stratum->generatorKind=verticalShafts?"VerticalShafts":"TunnelNetwork"; + stratum->recipeId="Core.Caves.PrimitiveField"; + stratum->recipeVersion=1; + stratum->stableId=layout.worldIdentity+"/strate/" + +std::to_string(slot.StrateIndex)+"/generator/" + +(verticalShafts?"VerticalShafts":"TunnelNetwork"); + const bool alreadyPlanned=std::any_of(layout.cavernRooms.begin(), + layout.cavernRooms.end(),[&](const auto& room) + {return room.strateIndex==slot.StrateIndex;}) + &&std::any_of(layout.cavernEdges.begin(),layout.cavernEdges.end(), + [&](const auto& edge){return edge.strateIndex==slot.StrateIndex;}); + if(!alreadyPlanned)plannerSlots.emplace_back(&slot,verticalShafts); + } + if(!layout.finalize(error))return false; + const auto toMicrometres=[&](double ownerVoxels) + { + return static_cast(std::llround(ownerVoxels + *static_cast(owner.OwnerVoxelSizeMicrons))); + }; + const auto toQ16=[](float value) + { + return static_cast(std::llround(std::clamp( + static_cast(value),0.0,1.0)*65536.0)); + }; + if(!plannerSlots.empty()) + { + std::vector plannedRooms=layout.cavernRooms; + std::vector plannedEdges=layout.cavernEdges; + for(const auto& [slot,verticalShafts]:plannerSlots) + { + const FStrateGenerationParams& params=slot->Definition->GenerationParams; + ResolvedCavernPlannerSettingsV1 settings; + if(verticalShafts) + { + const FVerticalShaftParams& shaft=slot->Definition->VerticalShaftParams; + settings.verticalShaftMode=true; + settings.roomSpacingMicrometres=std::clamp( + toMicrometres(shaft.ShaftSpacing),4000000,100000000); + settings.minimumRoomRadiusMicrometres=std::max( + 500000,toMicrometres(shaft.ShaftMinRadius)); + settings.maximumRoomRadiusMicrometres=std::max( + settings.minimumRoomRadiusMicrometres, + toMicrometres(shaft.ShaftMaxRadius)); + settings.destinationRadiusMicrometres=std::max( + settings.maximumRoomRadiusMicrometres, + settings.maximumRoomRadiusMicrometres*135/100); + settings.minimumTunnelRadiusMicrometres=std::max(0, + toMicrometres(shaft.ConnectorRadius)); + settings.maximumTunnelRadiusMicrometres= + settings.minimumTunnelRadiusMicrometres; + settings.maximumTunnelLengthMicrometres=std::clamp( + settings.roomSpacingMicrometres*2,4000000,100000000); + settings.smoothWidthMicrometres=1500000; + settings.floorSupportDepthMicrometres=2000000; + settings.irregularityAmplitudeMicrometres=std::max(0, + toMicrometres(shaft.SurfaceRoughness)); + settings.irregularityWavelengthMicrometres=std::max( + 500000,static_cast(owner.OwnerVoxelSizeMicrons)*4); + settings.roomDensityQ16=toQ16(shaft.ShaftDensity); + settings.tunnelDensityQ16=toQ16(shaft.CrossConnectChance); + settings.roomShapeVarietyQ16=0; + settings.roomHeightRatioQ16=65536; + settings.roomFloorCutMinimumQ16=0; + settings.roomFloorCutMaximumQ16=0; + settings.tunnelHorizontalBiasQ16=65536; + settings.originRoomMaximumConnections=32; + settings.ledgeSpacingMicrometres=std::max(0, + toMicrometres(shaft.LedgeSpacing)); + settings.ledgeDepthMicrometres=std::max(0, + toMicrometres(shaft.LedgeDepth)); + } + else + { + settings.roomSpacingMicrometres=std::max(4000000, + toMicrometres(params.RoomSpacing)); + settings.minimumRoomRadiusMicrometres=std::max(500000, + toMicrometres(params.MinRoomRadius)); + settings.maximumRoomRadiusMicrometres=std::max( + settings.minimumRoomRadiusMicrometres,toMicrometres(params.MaxRoomRadius)); + settings.destinationRadiusMicrometres=std::max( + toMicrometres(params.OriginRoomRadius), + settings.maximumRoomRadiusMicrometres*135/100); + settings.minimumTunnelRadiusMicrometres=std::max(0, + toMicrometres(params.TunnelMinRadius)); + settings.maximumTunnelRadiusMicrometres=std::max( + settings.minimumTunnelRadiusMicrometres,toMicrometres(params.TunnelMaxRadius)); + settings.maximumTunnelLengthMicrometres=std::max(4000000, + toMicrometres(params.MaxTunnelLength)); + settings.tunnelWarpMicrometres=std::max(0, + toMicrometres(params.TunnelWarpStrength)); + settings.caveWarpMicrometres=std::max(0, + toMicrometres(params.CaveWarpStrength)); + settings.smoothWidthMicrometres=std::max(1500000, + toMicrometres(params.SDFBlendRadius)); + settings.floorSupportDepthMicrometres=std::clamp( + settings.smoothWidthMicrometres/2,750000,2000000); + settings.irregularityAmplitudeMicrometres=std::max(0, + toMicrometres(params.SurfaceRoughness)); + settings.irregularityWavelengthMicrometres=params.RoughnessFrequency>0.0f + ?static_cast(std::llround(owner.OwnerVoxelSizeMicrons + /static_cast(params.RoughnessFrequency))):0; + settings.roomDensityQ16=toQ16(params.RoomDensity); + settings.tunnelDensityQ16=toQ16(params.TunnelDensity); + settings.roomShapeVarietyQ16=toQ16(params.RoomShapeVariety); + settings.roomHeightRatioQ16=toQ16(params.RoomHeightRatio); + settings.roomFloorCutMinimumQ16=toQ16(params.RoomFloorCutMin); + settings.roomFloorCutMaximumQ16=toQ16(params.RoomFloorCutMax); + settings.tunnelHorizontalBiasQ16=toQ16(params.TunnelHorizontalBias); + settings.originRoomMaximumConnections=params.OriginRoomMaxConnections; + settings.tunnelsFlowTowardOrigin=params.bTunnelsFlowTowardOrigin; + } + std::vector rooms; + std::vector edges; + if(!PlanResolvedCavernNetworkV1(layout,owner.Seed,slot->StrateIndex, + settings,rooms,edges,error))return false; + plannedRooms.insert(plannedRooms.end(), + std::make_move_iterator(rooms.begin()),std::make_move_iterator(rooms.end())); + plannedEdges.insert(plannedEdges.end(), + std::make_move_iterator(edges.begin()),std::make_move_iterator(edges.end())); + } + if(!ResolveResolvedCavernGraphV1(layout,owner.Seed, + plannedRooms,plannedEdges,error))return false; + } + + const auto toCoreSubvoxels=[&](std::int64_t micrometres) + { + return static_cast(std::llround( + static_cast(micrometres)*WorldAssemblySubvoxelsPerVoxel + /layout.coreVoxelSizeMicrometres)); + }; + constexpr std::int64_t PlayerCenterAboveFloorMicrometres=1130000; + for(const FStrateSlot& slot:owner.Manager->GetLayout()) + { + if(!slot.Definition)continue; + const ECaveGeneratorType generatorType= + owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,slot.TopChunkZ)); + const bool verticalShafts=generatorType==ECaveGeneratorType::VerticalShafts; + if(generatorType!=ECaveGeneratorType::TunnelNetwork&&!verticalShafts)continue; + auto stratum=std::find_if(layout.strates.begin(),layout.strates.end(), + [&](const auto& item){return item.strateIndex==slot.StrateIndex;}); + if(stratum==layout.strates.end() + ||!IsNativeCaveFieldStratum(*stratum))continue; + const std::string routePrefix=(verticalShafts?"vertical-shafts/strate-" + :"tunnel-network/strate-") + +std::to_string(slot.StrateIndex)+"/"; + const bool hasPlannedArrival=std::any_of(layout.cavernEdges.begin(), + layout.cavernEdges.end(),[&](const auto& edge) + {return edge.strateIndex==slot.StrateIndex + &&edge.stableId==routePrefix+"route-edge-0";}); + const bool hasPlannedDeparture=!verticalShafts + &&std::any_of(layout.cavernEdges.begin(),layout.cavernEdges.end(), + [&](const auto& edge){return edge.strateIndex==slot.StrateIndex + &&edge.stableId==routePrefix+"departure-edge";}); + if(!hasPlannedArrival||(!verticalShafts&&!hasPlannedDeparture))continue; + // Route-point anchors also drive the passage-mouth floor blend. The + // native cave planner replaces the legacy finite-strate route, so + // retaining its old anchors would join the passage floor to a route + // that is no longer present in the composed cave field. + stratum->anchors.erase(std::remove_if(stratum->anchors.begin(), + stratum->anchors.end(),[](const auto& anchor) + {return anchor.kind=="route_point";}),stratum->anchors.end()); + const ResolvedLayoutPassageV1* arrival=nullptr; + const ResolvedLayoutPassageV1* departure=nullptr; + for(const auto& passage:layout.passages) + { + if(passage.lowerStrateIndex==slot.StrateIndex + &&passage.upperStrateIndexslot.StrateIndex)departure=&passage; + } + if(!arrival||(!verticalShafts&&!departure)) + {error="native cave route anchors cannot bind their required passage mouths";return false;} + const auto appendRoutePoint=[&](std::string id, + const std::array& point) + { + ResolvedLayoutAnchorV2 anchor;anchor.kind="route_point"; + anchor.stableId=(verticalShafts?"vertical-shafts/route/" + :"tunnel-network/route/")+std::move(id); + anchor.units="1/256 Core voxel"; + anchor.values={point[0],point[1],point[2]}; + stratum->anchors.push_back(std::move(anchor)); + }; + const auto sameRoutePoint=[](const auto& a,const auto& b) + { + for(std::size_t axis=0;axis<3;++axis) + if(std::abs(a[axis]-b[axis])>2)return false; + return true; + }; + auto start=arrival->lowerMouthSubvoxels; + appendRoutePoint("000-arrival",start); + for(std::size_t edgeIndex=0;edgeIndex<256;++edgeIndex) + { + const std::string stableId=routePrefix+"route-edge-" + +std::to_string(edgeIndex); + const auto edge=std::find_if(layout.cavernEdges.begin(), + layout.cavernEdges.end(),[&](const auto& value) + {return value.strateIndex==slot.StrateIndex + &&value.stableId==stableId;}); + if(edge==layout.cavernEdges.end())break; + bool terminalRouteEdge=false; + if(verticalShafts) + { + const std::string nextRouteEdge=routePrefix+"route-edge-" + +std::to_string(edgeIndex+1); + terminalRouteEdge=std::none_of(layout.cavernEdges.begin(), + layout.cavernEdges.end(),[&](const auto& candidate) + {return candidate.strateIndex==slot.StrateIndex + &&candidate.stableId==nextRouteEdge;}); + } + if(edgeIndex==0) + { + auto inner=edge->controlPointsMicrometres.front(); + // Keep the exact authored mouth anchor and use the planner + // edge's floor height for the first cave-side walking point. + // Passage composition uses these same two points to blend the + // floor profile into the native cave route. + inner[2]+=PlayerCenterAboveFloorMicrometres; + const std::array point={ + toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]), + toCoreSubvoxels(inner[2])}; + if(!sameRoutePoint(point,start)) + appendRoutePoint("000-edge-start",point); + } + auto point=edge->controlPointsMicrometres.back(); + point[2]+=PlayerCenterAboveFloorMicrometres; + appendRoutePoint(terminalRouteEdge?"999-basal-terminal" + :std::to_string(edgeIndex+1), + {toCoreSubvoxels(point[0]),toCoreSubvoxels(point[1]), + toCoreSubvoxels(point[2])}); + } + if(departure) + { + const std::string departureEdgeId=routePrefix+"departure-edge"; + const auto departureEdge=std::find_if(layout.cavernEdges.begin(), + layout.cavernEdges.end(),[&](const auto& value) + {return value.strateIndex==slot.StrateIndex + &&value.stableId==departureEdgeId;}); + if(departureEdge==layout.cavernEdges.end()) + {error="native cave route lost its planned departure edge";return false;} + auto inner=departureEdge->controlPointsMicrometres.front(); + inner[2]+=PlayerCenterAboveFloorMicrometres; + const std::array innerPoint={ + toCoreSubvoxels(inner[0]),toCoreSubvoxels(inner[1]), + toCoreSubvoxels(inner[2])}; + auto end=departure->upperMouthSubvoxels; + if(!sameRoutePoint(innerPoint,end)) + appendRoutePoint("998-departure-inner",innerPoint); + appendRoutePoint("999-departure",departure->upperMouthSubvoxels); + } + else + { + const auto terminal=std::find_if(layout.cavernRooms.begin(), + layout.cavernRooms.end(),[&](const auto& room) + {return room.strateIndex==slot.StrateIndex + &&room.stableId==routePrefix+"basal-landing/primitive-0";}); + if(terminal==layout.cavernRooms.end()) + {error="Vertical Shafts basal route is missing its terminal landing";return false;} + } + } + return layout.finalize(error); + } + inline bool ImportExportedCoreWorldAssembly(const VFJson::Value& root, VoxelForgeCore::WorldAssemblyManifest& out,std::string& error) { @@ -1885,8 +2622,18 @@ namespace StandaloneWorldAssemblyPrototype if(!slot.Definition)continue; const auto type=owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,slot.TopChunkZ)); const auto authored=sealAndDensity(*slot.Definition,type); - const bool nativeCaves=recipeVariant=="caves" + const bool nativeSurfaceCaves=recipeVariant=="caves" &&type==ECaveGeneratorType::SurfaceWorld; + const bool hasLowerStrate=std::any_of(owner.Manager->GetLayout().begin(), + owner.Manager->GetLayout().end(),[&](const FStrateSlot& candidate) + {return candidate.StrateIndexGetLayout().begin(), + owner.Manager->GetLayout().end(),[&](const FStrateSlot& candidate) + {return candidate.StrateIndex>slot.StrateIndex;}); + const bool nativeTunnelCaves=type==ECaveGeneratorType::TunnelNetwork + &&recipeVariant!="legacy-tunnel-network"&&recipeVariant!="caves" + &&hasLowerStrate&&hasUpperStrate; + const bool nativeCaves=nativeSurfaceCaves||nativeTunnelCaves; const bool nativeLattice=recipeVariant==("lattice/"+std::to_string(slot.StrateIndex)) &&(type==ECaveGeneratorType::Maze||type==ECaveGeneratorType::VerticalShafts); const std::int64_t bottom=static_cast(slot.BottomChunkZ)*owner.StrateLayoutChunkSizeMicrometres; @@ -1896,7 +2643,7 @@ namespace StandaloneWorldAssemblyPrototype if(seal<=0||2*seal>=top-bottom) {error="strate "+std::to_string(slot.StrateIndex)+" has an invalid BoundarySealThickness for two boundary bands";return false;} ResolvedLayoutStratumV2 s;s.strateIndex=slot.StrateIndex; - s.generatorKind=nativeCaves?"Caves":GeneratorKind(type); + s.generatorKind=nativeSurfaceCaves?"Caves":GeneratorKind(type); s.stableId=out.worldIdentity+"/strate/"+std::to_string(slot.StrateIndex) +"/generator/"+s.generatorKind; s.recipeId=nativeLattice?"Core.Lattice.PrimitiveField" @@ -2083,6 +2830,8 @@ namespace StandaloneWorldAssemblyPrototype { const auto passageConfigs=BuildResolvedLayoutPassageConfigs(owner); if(!PlanResolvedLayoutPassagesV1(out,owner.Seed,passageConfigs,error))return false; + if(recipeVariant!="legacy-tunnel-network"&&recipeVariant!="caves" + &&!PlanOwnerTunnelNetworkCaves(owner,out,error))return false; } return out.finalize(error); } diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v230.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v230.baseline new file mode 100644 index 0000000..e460154 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v230.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173 +[PipelineWholeViewHash] tiles=2370 density=0x0a735dd766ac8e17 mesh=0x6531939ce3e4df5e payload=0xe79029d3900761fa topology=0x45a4b93eb0997a6b +[PipelineLODHash] lod=0 tiles=512 density=0x2fd4c78685a43cfe mesh=0x50478cff1c1f8ca0 payload=0x54e48e57e0af26de topology=0x860c011cfdb9833a +[PipelineLODHash] lod=1 tiles=448 density=0x4b8d9a6dcd2942ad mesh=0x17e89b7087bfe711 payload=0x2389c109eb780db6 topology=0x7eccd4cf560d0630 +[PipelineLODHash] lod=2 tiles=320 density=0xc0768e7fec442648 mesh=0x4e8a165606f5b5df payload=0x5949dc76575186d0 topology=0xd2cc03a9bd205f5b +[PipelineLODHash] lod=3 tiles=208 density=0xdd07fe8f31ede73a mesh=0xfaa2f90438226625 payload=0x86904d144f5db47c topology=0xcfd10c90d38f3fca +[PipelineLODHash] lod=4 tiles=240 density=0xfb11591772a3d18e mesh=0xebf2baee8764591b payload=0xe491d7ac9c0061ad topology=0xd618ac33b95ce494 +[PipelineLODHash] lod=5 tiles=144 density=0x2f8b7b7fb1e67258 mesh=0x4cd508cbe97344fd payload=0x2b6e96a6050458c9 topology=0x4ff6d0eda8988573 +[PipelineLODHash] lod=6 tiles=96 density=0x0114a34c8b8d6113 mesh=0xb1fec4d01ae1fa44 payload=0x3e50cfd429b8e950 topology=0x08090c96d15c1618 +[PipelineLODHash] lod=7 tiles=96 density=0x3836ed8cfce73418 mesh=0x40bc6589c5fc1aac payload=0x677349bcae16790f topology=0x36e3aa53192e6a6b +[PipelineLODHash] lod=8 tiles=306 density=0x144f3c1fd722708b mesh=0xef77b28bf7fd9453 payload=0xe3f47b37fcd61489 topology=0x5a3c73e295494e04 +[PipelineExactQueryHash] probes=9 density=0x7b7c50ac24ccadcc unclamped=0x7b7c50ac24ccadcc detail=0xefcba20c0c70c415 collision=0xe604823a249029bf path=direct-Core-queries diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v236.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v236.baseline new file mode 100644 index 0000000..98d4cc8 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v236.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173 +[PipelineWholeViewHash] tiles=2370 density=0x7111b4b743e03c95 mesh=0x8c7240939db21b88 payload=0x3981ff5ca7076219 topology=0x07947d2e691b2e55 +[PipelineLODHash] lod=0 tiles=512 density=0x2fd4c78685a43cfe mesh=0x50478cff1c1f8ca0 payload=0x54e48e57e0af26de topology=0x860c011cfdb9833a +[PipelineLODHash] lod=1 tiles=448 density=0x4b8d9a6dcd2942ad mesh=0x17e89b7087bfe711 payload=0x2389c109eb780db6 topology=0x7eccd4cf560d0630 +[PipelineLODHash] lod=2 tiles=320 density=0x48d4c21a852e6e21 mesh=0x5516a17618d142d5 payload=0x59cda38ab9ca6b7a topology=0x15ae353bd835e6a1 +[PipelineLODHash] lod=3 tiles=208 density=0x09b4811098a48c8d mesh=0xbac3fb6d54bca8e9 payload=0xca779555d55be411 topology=0xc63e8ee46890a03a +[PipelineLODHash] lod=4 tiles=240 density=0xfb11591772a3d18e mesh=0xebf2baee8764591b payload=0xe491d7ac9c0061ad topology=0xd618ac33b95ce494 +[PipelineLODHash] lod=5 tiles=144 density=0x2f8b7b7fb1e67258 mesh=0x4cd508cbe97344fd payload=0x2b6e96a6050458c9 topology=0x4ff6d0eda8988573 +[PipelineLODHash] lod=6 tiles=96 density=0x0114a34c8b8d6113 mesh=0xb1fec4d01ae1fa44 payload=0x3e50cfd429b8e950 topology=0x08090c96d15c1618 +[PipelineLODHash] lod=7 tiles=96 density=0x3836ed8cfce73418 mesh=0x40bc6589c5fc1aac payload=0x677349bcae16790f topology=0x36e3aa53192e6a6b +[PipelineLODHash] lod=8 tiles=306 density=0x144f3c1fd722708b mesh=0xef77b28bf7fd9453 payload=0xe3f47b37fcd61489 topology=0x5a3c73e295494e04 +[PipelineExactQueryHash] probes=9 density=0x7b7c50ac24ccadcc unclamped=0x7b7c50ac24ccadcc detail=0xefcba20c0c70c415 collision=0xe604823a249029bf path=direct-Core-queries diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v237.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v237.baseline new file mode 100644 index 0000000..98d4cc8 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v237.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173 +[PipelineWholeViewHash] tiles=2370 density=0x7111b4b743e03c95 mesh=0x8c7240939db21b88 payload=0x3981ff5ca7076219 topology=0x07947d2e691b2e55 +[PipelineLODHash] lod=0 tiles=512 density=0x2fd4c78685a43cfe mesh=0x50478cff1c1f8ca0 payload=0x54e48e57e0af26de topology=0x860c011cfdb9833a +[PipelineLODHash] lod=1 tiles=448 density=0x4b8d9a6dcd2942ad mesh=0x17e89b7087bfe711 payload=0x2389c109eb780db6 topology=0x7eccd4cf560d0630 +[PipelineLODHash] lod=2 tiles=320 density=0x48d4c21a852e6e21 mesh=0x5516a17618d142d5 payload=0x59cda38ab9ca6b7a topology=0x15ae353bd835e6a1 +[PipelineLODHash] lod=3 tiles=208 density=0x09b4811098a48c8d mesh=0xbac3fb6d54bca8e9 payload=0xca779555d55be411 topology=0xc63e8ee46890a03a +[PipelineLODHash] lod=4 tiles=240 density=0xfb11591772a3d18e mesh=0xebf2baee8764591b payload=0xe491d7ac9c0061ad topology=0xd618ac33b95ce494 +[PipelineLODHash] lod=5 tiles=144 density=0x2f8b7b7fb1e67258 mesh=0x4cd508cbe97344fd payload=0x2b6e96a6050458c9 topology=0x4ff6d0eda8988573 +[PipelineLODHash] lod=6 tiles=96 density=0x0114a34c8b8d6113 mesh=0xb1fec4d01ae1fa44 payload=0x3e50cfd429b8e950 topology=0x08090c96d15c1618 +[PipelineLODHash] lod=7 tiles=96 density=0x3836ed8cfce73418 mesh=0x40bc6589c5fc1aac payload=0x677349bcae16790f topology=0x36e3aa53192e6a6b +[PipelineLODHash] lod=8 tiles=306 density=0x144f3c1fd722708b mesh=0xef77b28bf7fd9453 payload=0xe3f47b37fcd61489 topology=0x5a3c73e295494e04 +[PipelineExactQueryHash] probes=9 density=0x7b7c50ac24ccadcc unclamped=0x7b7c50ac24ccadcc detail=0xefcba20c0c70c415 collision=0xe604823a249029bf path=direct-Core-queries diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v239.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v239.baseline new file mode 100644 index 0000000..54c95b4 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v239.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173 +[PipelineWholeViewHash] tiles=2370 density=0x3f5fca16a93f0ac4 mesh=0xfa982466f5d620f2 payload=0x05e8f2ea7a291288 topology=0xcec9c50d433a0d3b +[PipelineLODHash] lod=0 tiles=512 density=0x2fd4c78685a43cfe mesh=0x50478cff1c1f8ca0 payload=0x54e48e57e0af26de topology=0x860c011cfdb9833a +[PipelineLODHash] lod=1 tiles=448 density=0x4b8d9a6dcd2942ad mesh=0x17e89b7087bfe711 payload=0x2389c109eb780db6 topology=0x7eccd4cf560d0630 +[PipelineLODHash] lod=2 tiles=320 density=0x4d9ba705776035c9 mesh=0x9c3a5c40dac546f8 payload=0xc7a5c38cf20d2484 topology=0x7668f2802ce1979c +[PipelineLODHash] lod=3 tiles=208 density=0xac120d13326e581c mesh=0x93aac374d26362ca payload=0xdcb98473cd4a06ea topology=0xb9753f0e0eb6ba95 +[PipelineLODHash] lod=4 tiles=240 density=0xfb11591772a3d18e mesh=0xebf2baee8764591b payload=0xe491d7ac9c0061ad topology=0xd618ac33b95ce494 +[PipelineLODHash] lod=5 tiles=144 density=0x2f8b7b7fb1e67258 mesh=0x4cd508cbe97344fd payload=0x2b6e96a6050458c9 topology=0x4ff6d0eda8988573 +[PipelineLODHash] lod=6 tiles=96 density=0x0114a34c8b8d6113 mesh=0xb1fec4d01ae1fa44 payload=0x3e50cfd429b8e950 topology=0x08090c96d15c1618 +[PipelineLODHash] lod=7 tiles=96 density=0x3836ed8cfce73418 mesh=0x40bc6589c5fc1aac payload=0x677349bcae16790f topology=0x36e3aa53192e6a6b +[PipelineLODHash] lod=8 tiles=306 density=0x144f3c1fd722708b mesh=0xef77b28bf7fd9453 payload=0xe3f47b37fcd61489 topology=0x5a3c73e295494e04 +[PipelineExactQueryHash] probes=9 density=0x7b7c50ac24ccadcc unclamped=0x7b7c50ac24ccadcc detail=0xefcba20c0c70c415 collision=0xe604823a249029bf path=direct-Core-queries diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-owner4-surface-world-style1-v239.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-owner4-surface-world-style1-v239.baseline new file mode 100644 index 0000000..aa9f1d5 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-owner4-surface-world-style1-v239.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0xe2e87dba9e90713b mesh=0x5753e42b74792815 payload=0x0a8c9f5f2538ea44 topology=0x9f93e14f03b2be4e +[PipelineWholeViewHash] tiles=2578 density=0xa5a53ca21c52ada4 mesh=0xce15fd183c4c742e payload=0x55f95cb229043ddb topology=0x8cfbbc448c612928 +[PipelineLODHash] lod=0 tiles=512 density=0x68a3ace1a1c75206 mesh=0x223ace860546fb2f payload=0x82f26013006e4272 topology=0x0ee65ad34887fd79 +[PipelineLODHash] lod=1 tiles=448 density=0x94476fa0eb90ab58 mesh=0x2bda347a1060ef1f payload=0x6aea323f9b99c0c7 topology=0x0d9e78ee5c651722 +[PipelineLODHash] lod=2 tiles=320 density=0x20b6580fccff1f0b mesh=0xaa26a9b29df342a5 payload=0xe1178e73417bfa84 topology=0xeb8808bb8152d55d +[PipelineLODHash] lod=3 tiles=272 density=0xccf8002304eea711 mesh=0x3d6913a3b8dad4bd payload=0x36a305f71eae6cdd topology=0x8e97fcdae6a859e1 +[PipelineLODHash] lod=4 tiles=272 density=0xb743eb64967bddcd mesh=0xf6b5bd36b0ebfead payload=0xd1284c1540f50636 topology=0x4a6e197e2a1a9500 +[PipelineLODHash] lod=5 tiles=208 density=0x736b79e584e3473b mesh=0xc121e6ea480984b6 payload=0x7cba69e7f5fcdc24 topology=0x88c0925f542acc49 +[PipelineLODHash] lod=6 tiles=144 density=0x66772655ebf0bcff mesh=0x8673d734af6e2174 payload=0x1bd21719d2db9ba7 topology=0x5934583ecfe685fc +[PipelineLODHash] lod=7 tiles=96 density=0x735c5dfc11a8e903 mesh=0x9f45f6b2036e0daf payload=0x68b8844560164197 topology=0x3411d705ccaa9ef7 +[PipelineLODHash] lod=8 tiles=306 density=0xe74ed01d6aea1bf2 mesh=0x84c63d800f478372 payload=0xe7463fc5ae174b01 topology=0x0cea55e8c3d06515 +[PipelineExactQueryHash] probes=9 density=0x48a3dbeb7b513959 unclamped=0x02632b98d54a5e55 detail=0xefcba20c0c70c415 collision=0x137e8f6c0b03a624 path=direct-Core-queries diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v230.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v230.baseline new file mode 100644 index 0000000..eda2c13 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v230.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0xace56c8f205238ad mesh=0xa9c603a1248de6b8 payload=0xea55f1f4fcc6a894 topology=0x5ea7bc78a1b678ae +[PipelineWholeViewHash] tiles=2370 density=0x03331b1721d89092 mesh=0x1fa61520dc6c6d7b payload=0xe5c306e087381056 topology=0xef499f11d87ca569 +[PipelineLODHash] lod=0 tiles=512 density=0xcd6ab28cfa504aca mesh=0xf03dc80a669fca31 payload=0x397aded1c3d2d297 topology=0xb4a1dc84fabc1fe3 +[PipelineLODHash] lod=1 tiles=448 density=0xce6e713828b2a20a mesh=0x1cf408c25d67d18c payload=0x8a2b5d20571c997e topology=0x51cd985fe8e7d100 +[PipelineLODHash] lod=2 tiles=320 density=0x8581c27a6966cccd mesh=0x610a6eb3ef75fe11 payload=0xdecaf47d125efd5c topology=0xa5ea883ebeb628e4 +[PipelineLODHash] lod=3 tiles=208 density=0x5d87341a6428ea0b mesh=0x662096703512f5e2 payload=0x5703acca3ad9c2c7 topology=0xc8fb411f09c46f60 +[PipelineLODHash] lod=4 tiles=240 density=0x0be8798407724eaf mesh=0x131c303cbbe51434 payload=0x2a90ecf0a2411ca0 topology=0xe3e054a371152e08 +[PipelineLODHash] lod=5 tiles=144 density=0xf20897ad272103a9 mesh=0x13d00b6253ae1f72 payload=0x0db3880693a962fa topology=0x462bd87a80f15e89 +[PipelineLODHash] lod=6 tiles=96 density=0xc040fcac457d6313 mesh=0x1ed132eeb718fb62 payload=0xcd0214e35577c6e3 topology=0x2c56c69e6fdcfd64 +[PipelineLODHash] lod=7 tiles=96 density=0xc2d419446355fdb8 mesh=0x4ea44452574fc84b payload=0xfd9779bedfc9ae0f topology=0x8948546e770c9b72 +[PipelineLODHash] lod=8 tiles=306 density=0x62db31696f2da831 mesh=0xd373f9e9481532fe payload=0x540a60526e0e3e46 topology=0xc725cb901e6dc335 +[PipelineExactQueryHash] probes=9 density=0xc949c4df2ce6f9d9 unclamped=0xc949c4df2ce6f9d9 detail=0x114c501e2d830d94 collision=0xe6011d3a248d4849 path=direct-Core-queries diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v236.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v236.baseline new file mode 100644 index 0000000..e43aaf0 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v236.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0xace56c8f205238ad mesh=0xa9c603a1248de6b8 payload=0xea55f1f4fcc6a894 topology=0x5ea7bc78a1b678ae +[PipelineWholeViewHash] tiles=2370 density=0x826704417f556bb8 mesh=0x3dc31324d910ad2b payload=0x32cd1847a8e05d0a topology=0xb800610aa85336d7 +[PipelineLODHash] lod=0 tiles=512 density=0xcd6ab28cfa504aca mesh=0xf03dc80a669fca31 payload=0x397aded1c3d2d297 topology=0xb4a1dc84fabc1fe3 +[PipelineLODHash] lod=1 tiles=448 density=0xce6e713828b2a20a mesh=0x1cf408c25d67d18c payload=0x8a2b5d20571c997e topology=0x51cd985fe8e7d100 +[PipelineLODHash] lod=2 tiles=320 density=0xd7f003d75d73e5b1 mesh=0x4e9c56918da506ae payload=0x3867c5915b409d6d topology=0x02a2a7c0ee9ac809 +[PipelineLODHash] lod=3 tiles=208 density=0xce02d09814028b15 mesh=0xe4c2addc4c54c439 payload=0x7e25d9eee6469c5a topology=0xe603e61c9b9bb733 +[PipelineLODHash] lod=4 tiles=240 density=0x0be8798407724eaf mesh=0x131c303cbbe51434 payload=0x2a90ecf0a2411ca0 topology=0xe3e054a371152e08 +[PipelineLODHash] lod=5 tiles=144 density=0xf20897ad272103a9 mesh=0x13d00b6253ae1f72 payload=0x0db3880693a962fa topology=0x462bd87a80f15e89 +[PipelineLODHash] lod=6 tiles=96 density=0xc040fcac457d6313 mesh=0x1ed132eeb718fb62 payload=0xcd0214e35577c6e3 topology=0x2c56c69e6fdcfd64 +[PipelineLODHash] lod=7 tiles=96 density=0xc2d419446355fdb8 mesh=0x4ea44452574fc84b payload=0xfd9779bedfc9ae0f topology=0x8948546e770c9b72 +[PipelineLODHash] lod=8 tiles=306 density=0x62db31696f2da831 mesh=0xd373f9e9481532fe payload=0x540a60526e0e3e46 topology=0xc725cb901e6dc335 +[PipelineExactQueryHash] probes=9 density=0xc949c4df2ce6f9d9 unclamped=0xc949c4df2ce6f9d9 detail=0x114c501e2d830d94 collision=0xe6011d3a248d4849 path=direct-Core-queries diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v237.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v237.baseline new file mode 100644 index 0000000..e43aaf0 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v237.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0xace56c8f205238ad mesh=0xa9c603a1248de6b8 payload=0xea55f1f4fcc6a894 topology=0x5ea7bc78a1b678ae +[PipelineWholeViewHash] tiles=2370 density=0x826704417f556bb8 mesh=0x3dc31324d910ad2b payload=0x32cd1847a8e05d0a topology=0xb800610aa85336d7 +[PipelineLODHash] lod=0 tiles=512 density=0xcd6ab28cfa504aca mesh=0xf03dc80a669fca31 payload=0x397aded1c3d2d297 topology=0xb4a1dc84fabc1fe3 +[PipelineLODHash] lod=1 tiles=448 density=0xce6e713828b2a20a mesh=0x1cf408c25d67d18c payload=0x8a2b5d20571c997e topology=0x51cd985fe8e7d100 +[PipelineLODHash] lod=2 tiles=320 density=0xd7f003d75d73e5b1 mesh=0x4e9c56918da506ae payload=0x3867c5915b409d6d topology=0x02a2a7c0ee9ac809 +[PipelineLODHash] lod=3 tiles=208 density=0xce02d09814028b15 mesh=0xe4c2addc4c54c439 payload=0x7e25d9eee6469c5a topology=0xe603e61c9b9bb733 +[PipelineLODHash] lod=4 tiles=240 density=0x0be8798407724eaf mesh=0x131c303cbbe51434 payload=0x2a90ecf0a2411ca0 topology=0xe3e054a371152e08 +[PipelineLODHash] lod=5 tiles=144 density=0xf20897ad272103a9 mesh=0x13d00b6253ae1f72 payload=0x0db3880693a962fa topology=0x462bd87a80f15e89 +[PipelineLODHash] lod=6 tiles=96 density=0xc040fcac457d6313 mesh=0x1ed132eeb718fb62 payload=0xcd0214e35577c6e3 topology=0x2c56c69e6fdcfd64 +[PipelineLODHash] lod=7 tiles=96 density=0xc2d419446355fdb8 mesh=0x4ea44452574fc84b payload=0xfd9779bedfc9ae0f topology=0x8948546e770c9b72 +[PipelineLODHash] lod=8 tiles=306 density=0x62db31696f2da831 mesh=0xd373f9e9481532fe payload=0x540a60526e0e3e46 topology=0xc725cb901e6dc335 +[PipelineExactQueryHash] probes=9 density=0xc949c4df2ce6f9d9 unclamped=0xc949c4df2ce6f9d9 detail=0x114c501e2d830d94 collision=0xe6011d3a248d4849 path=direct-Core-queries diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v239.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v239.baseline new file mode 100644 index 0000000..8238033 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v239.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0xace56c8f205238ad mesh=0xa9c603a1248de6b8 payload=0xea55f1f4fcc6a894 topology=0x5ea7bc78a1b678ae +[PipelineWholeViewHash] tiles=2370 density=0xf9596b50b9a1e1fb mesh=0x635027074b617681 payload=0x1f37e04b833cccab topology=0xd7098a552845ca59 +[PipelineLODHash] lod=0 tiles=512 density=0xcd6ab28cfa504aca mesh=0xf03dc80a669fca31 payload=0x397aded1c3d2d297 topology=0xb4a1dc84fabc1fe3 +[PipelineLODHash] lod=1 tiles=448 density=0xce6e713828b2a20a mesh=0x1cf408c25d67d18c payload=0x8a2b5d20571c997e topology=0x51cd985fe8e7d100 +[PipelineLODHash] lod=2 tiles=320 density=0x2c88851bc391b0d3 mesh=0x2788fcac69c81769 payload=0x1793172ba4d3f984 topology=0x576cf3fab5e7d3ee +[PipelineLODHash] lod=3 tiles=208 density=0xd49d92f6d668d190 mesh=0x4918a9ccb7c7efec payload=0x9c924c180d90177e topology=0x1e402ebbdca097e2 +[PipelineLODHash] lod=4 tiles=240 density=0x0be8798407724eaf mesh=0x131c303cbbe51434 payload=0x2a90ecf0a2411ca0 topology=0xe3e054a371152e08 +[PipelineLODHash] lod=5 tiles=144 density=0xf20897ad272103a9 mesh=0x13d00b6253ae1f72 payload=0x0db3880693a962fa topology=0x462bd87a80f15e89 +[PipelineLODHash] lod=6 tiles=96 density=0xc040fcac457d6313 mesh=0x1ed132eeb718fb62 payload=0xcd0214e35577c6e3 topology=0x2c56c69e6fdcfd64 +[PipelineLODHash] lod=7 tiles=96 density=0xc2d419446355fdb8 mesh=0x4ea44452574fc84b payload=0xfd9779bedfc9ae0f topology=0x8948546e770c9b72 +[PipelineLODHash] lod=8 tiles=306 density=0x62db31696f2da831 mesh=0xd373f9e9481532fe payload=0x540a60526e0e3e46 topology=0xc725cb901e6dc335 +[PipelineExactQueryHash] probes=9 density=0xc949c4df2ce6f9d9 unclamped=0xc949c4df2ce6f9d9 detail=0x114c501e2d830d94 collision=0xe6011d3a248d4849 path=direct-Core-queries diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py index a885e01..9bbc2a5 100644 --- a/Standalone/Tools/verify/verify.py +++ b/Standalone/Tools/verify/verify.py @@ -125,11 +125,11 @@ RECIPE_RE = re.compile( def owner_hash_baseline(recipe: str, style: int) -> tuple[Path, str]: if recipe == "surface-world" and style == 1: - return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v227.baseline", - "L130-v227-reach-units") + return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v239.baseline", + "L173-v239-combined-cave-planner") if recipe == "floating-islands" and style == 2: - return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v227.baseline", - "L130-v227-reach-units") + return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v239.baseline", + "L173-v239-combined-cave-planner") raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}") @@ -538,6 +538,19 @@ def run_density_dispatch_report(runner: VerificationRunner, binary: Path) -> Non voxel_size_um = int(strata[0]["voxel_size_um"]) if voxel_size_um <= 0: raise ValueError(f"invalid voxel_size_um={voxel_size_um}") + tunnel_network = strata[2] + if (tunnel_network.get("generator_kind") != "TunnelNetwork" + or tunnel_network.get("recipe_id") != "Core.Caves.PrimitiveField" + or int(tunnel_network.get("recipe_version", 0)) != 1): + failures.append(f"{name}: Tunnel Network did not bind Core cave recipe v1: " + f"generator={tunnel_network.get('generator_kind')} " + f"recipe={tunnel_network.get('recipe_id')} " + f"version={tunnel_network.get('recipe_version')}") + if dispatch.get(2) != ("Caves-native", "TunnelNetwork", + "Core.Caves.PrimitiveField", 1, + "FCavesFieldAdapter"): + failures.append(f"{name}: Tunnel Network dispatch is not native caves: " + f"{dispatch.get(2)}") except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as error: failures.append(f"{name}: cannot classify tiles from resolved layout: {error}") continue @@ -604,7 +617,8 @@ def run_density_dispatch_report(runner: VerificationRunner, binary: Path) -> Non failures.append("no tile centers were attributed to any bound path") runner.require("owner-density-dispatch-report", not failures, "; ".join(failures) if failures else - "four owner poses reported per-strate bound paths and tile-center path shares") + "four owner poses reported per-strate bound paths and tile-center path shares; " + "TN binds Core.Caves.PrimitiveField v1 through FCavesFieldAdapter") def run_landmark_reachability(runner: VerificationRunner, @@ -1064,7 +1078,7 @@ def run_ore_vein_hash_controls(runner: VerificationRunner) -> None: def cavern_audit_cameras(layout_path: Path) -> list[tuple[float, float, float, float, float]]: layout = json.loads(layout_path.read_text(encoding="utf-8")) edges = layout.get("cavern_edges", []) - if layout.get("cavern_graph_vocabulary_version") != 1 or len(edges) != 1: + if layout.get("cavern_graph_vocabulary_version") not in (1, 2) or len(edges) != 1: raise VerificationFailure(f"missing one resolved cavern edge in {layout_path}") points = [[value / 1_000_000.0 for value in point] for point in edges[0]["control_points_um"]] @@ -1138,7 +1152,7 @@ def run_native_caves_field_audit(runner: VerificationRunner, binary: Path) -> tu r"false_all_air=REJECTED false_all_solid=REJECTED .*status=PASS$"), "brute-force interval proofs have zero misses and reject both false-proof controls") layout = json.loads(layout_path.read_text(encoding="utf-8")) - graph_ok = (layout.get("cavern_graph_vocabulary_version") == 1 + graph_ok = (layout.get("cavern_graph_vocabulary_version") in (1, 2) and len(layout.get("cavern_rooms", [])) == 1 and len(layout.get("cavern_edges", [])) == 2) runner.require(f"native-caves-field-{compiler}:resolved-graph", graph_ok, @@ -1221,7 +1235,7 @@ def run_native_lattice_field_audit(runner: VerificationRunner, binary: Path) -> rooms = layout.get("cavern_rooms", []) lattice_rows = [row for row in layout.get("strates", []) if row.get("strate_index") == 1] - graph_ok = (layout.get("cavern_graph_vocabulary_version") == 1 + graph_ok = (layout.get("cavern_graph_vocabulary_version") in (1, 2) and len(edges) >= 9 and len(rooms) >= 8 and len(lattice_rows) == 1 and lattice_rows[0].get("generator_kind") == "Maze" @@ -1247,7 +1261,7 @@ def lattice_camera_positions(layout_path: Path, count: int = 4) -> list[tuple[fl edges = sorted(layout.get("cavern_edges", []), key=lambda edge: edge["stable_id"]) helix = next((edge for edge in edges if edge["stable_id"] == "lattice/spine/01-helix-ramp-v1"), None) - if layout.get("cavern_graph_vocabulary_version") != 1 or helix is None: + if layout.get("cavern_graph_vocabulary_version") not in (1, 2) or helix is None: raise VerificationFailure(f"missing resolved lattice helix in {layout_path}") helix_points = [[value / 1_000_000.0 for value in point] for point in helix.get("polyline_um", [])] @@ -1400,7 +1414,7 @@ def run_lattice_performance(runner: VerificationRunner, binary: Path, def cave_camera_positions(layout_path: Path, count: int = 4) -> list[tuple[float, float, float, float, float]]: layout = json.loads(layout_path.read_text(encoding="utf-8")) edges = sorted(layout.get("cavern_edges", []), key=lambda edge: edge["stable_id"]) - if layout.get("cavern_graph_vocabulary_version") != 1 or len(edges) != 2: + if layout.get("cavern_graph_vocabulary_version") not in (1, 2) or len(edges) != 2: raise VerificationFailure(f"missing two resolved cave spine edges in {layout_path}") rng = _SplitMix64(CAVES_CAMERA_SWEEP_SEED) cameras = [] diff --git a/VoxelForge.uplugin b/VoxelForge.uplugin index 317b7f5..3e5dd84 100644 --- a/VoxelForge.uplugin +++ b/VoxelForge.uplugin @@ -34,7 +34,7 @@ { "Name": "VoxelForgeLivingPlane", "Type": "Runtime", - "LoadingPhase": "Default" + "LoadingPhase": "PostConfigInit" }, { "Name": "VoxelForgeEditor",