feat(core): native lattice generator, slice 1 -- helix-ramp spine + small maze (L58); research R27

Core.Lattice.PrimitiveField v1, opt-in per strate (legacy Maze/VerticalShaft stay legacy): resolves the real
arrival and departure mouths first, plans a mandatory spine, then maze cells and two dead ends (9 rooms,
9 edges, stable IDs). The spine descends 20 m through four helix turns (6 m radius, 3 m clear width, 5 m
pitch, 2.6 m clearance; max grade 10.0 deg); each feature is one filleted void (0.57 m measured). Final-field
reachability passes at 883 stations; an orphaned shaft control is rejected; exact intervals (0 misses) and
a whole-surface audit with 0 flags. Whole view 1.69 s (above the 1 s target). fast/medium/full PASS.
Research R27 (self-checked): static stream-to-pool water design.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-25 15:45:32 +01:00
1 parent 1e3e04bbac
commit 6c6cc37e1d
25 files changed
+280497 -117

No files matched your search

+27
View File
@@ -2265,6 +2265,33 @@ mouth and off the walk route. It stays disabled by default. Active records carry
L43's report records the off/on hashes, walk and interface audits, outside-stamp timing, tier L43's report records the off/on hashes, walk and interface audits, outside-stamp timing, tier
results, captures and any failed runs. results, captures and any failed runs.
### 8.54 Opt-in native lattice field — World Assembler M3 R26 slice 1 (Linux L58)
`Core.Lattice.PrimitiveField` is a version-1 recipe selected only for one Maze or VerticalShafts
strate by `--recipe=lattice --lattice-strate=N`. The legacy generator assets and their default
dispatch stay unchanged. Planning first binds the actual incoming lower mouth and outgoing upper
mouth from the resolved passage records, then creates arrival/exit overlap corridors, a mandatory
four-turn helix and the maze spine; two deterministic dead ends are added after that route. The
resolved room and edge seeds use `(world seed, strate, recipe version, stable ID)`. An audit control
translates the helix 20 m away from both route junctions to make the shaft network orphaned.
The prototype helix uses a 6 m centreline radius, 3 m clear width, 5 m pitch, four turns and 2.6 m
finished floor-to-soffit height. Its centreline descends 20 m over roughly 151 m; the inner-edge
grade is about 10.0 degrees. The 2.4 m floor-wall smooth-max gives an isolated ideal fillet radius
of 0.8485 m. A separate 0.4 m feature-union blend prevents adjoining ramps from erasing the tread.
The swept-clear diameter is 15 m; the conservative padded feature envelope is about 20.6 m (the
approved approximately 21 m site envelope).
The layout reuses the resolved cavern graph vocabulary. A per-strate opt-in selects the native
negative-solid/positive-air field; canonical feature order is stable-ID order. Room/corridor bounds
are indexed by the graph BVH and polyline segments have a nested deterministic BVH for density and
interval queries. Box classification stays conservative, and `isTileShapeFree` refuses any tile
that intersects the graph. The L58 pipeline audits the composed field from actual mouth to mouth
with a 0.34 m by 1.76 m capsule, per-station support, 2.2 m clearance and a 15 degree grade limit;
it also checks the orphan control, brute-force interval samples and the composed wall-foot fillet.
The Linux58 report contains the measured reachability, surface, peer-determinism, timing and capture
results; the code path is not promoted to legacy Maze/VerticalShafts generation.
## 9. Multiplayer model (listen-server first, dedicated-friendly) ## 9. Multiplayer model (listen-server first, dedicated-friendly)
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
+15
View File
@@ -170,6 +170,8 @@ 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. | | `../../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` | 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. | | `../../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. |
| `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. | | `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. |
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval. | `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
| Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. | | Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. |
@@ -1308,6 +1310,19 @@ See ARCHITECTURE §8.53 and
[`Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md) [`Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md)
for layout identities, owner hashes, outside-box timing, audits and captures. for layout identities, owner hashes, outside-box timing, audits and captures.
### 3.36 Native lattice field — World Assembler M3 R26 slice 1 (Linux L58)
| Symbol | Canonical file | Role |
|---|---|---|
| `IsNativeLatticeFieldStratum`, `LayoutLatticeFeatureSeed`, `ResolvedCavernRoomV1::unionWidthMicrometres`, `ResolvedCavernEdgeV1::unionWidthMicrometres` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h` | Versioned Maze/VerticalShafts opt-in and deterministic seed from `(seed, strate, recipe version, stable ID)`; optional union width preserves prior cave JSON when unset. |
| `MakeNativeLatticeSpine`, `--recipe=lattice`, `--lattice-strate=N` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Resolves actual adjacent mouths, builds a mandatory four-turn helix and maze spine, then adds two seeded dead ends; `--lattice-orphan-control` moves the helix away from both junctions. |
| `AuditNativeLatticeField` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples the final field along the mouth-to-mouth capsule route, checking support, headroom, grade, orphan rejection, composed fillet, interval signs and feature-tile exclusion. |
| `BuildLatticeFixtureL58.py`, `CaptureNativeLatticeL58.py`, `AuditLatticeSurfaceL58.py` | `Standalone/Research/WorldAssemblerM3/Linux58/` | Isolated Maze strate fixture, four reviewed captures and the L54 whole-surface triangle audit adapted to explicit native polylines. |
See ARCHITECTURE §8.54 and
[`Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md)
for resolved records, final-field reachability, controls, timing, hashes and captures.
## 4. The density pipeline (most-edited hot path) ## 4. The density pipeline (most-edited hot path)
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
@@ -860,6 +860,9 @@ namespace VoxelForgeCore
std::array<std::int64_t, 3> halfExtentsMicrometres{}; std::array<std::int64_t, 3> halfExtentsMicrometres{};
std::int64_t floorMicrometres = 0; std::int64_t floorMicrometres = 0;
std::int64_t smoothWidthMicrometres = 0; std::int64_t smoothWidthMicrometres = 0;
// Optional feature-to-feature union width. Zero preserves the historical
// behavior where smoothWidthMicrometres also controls the graph union.
std::int64_t unionWidthMicrometres = 0;
std::int64_t irregularityAmplitudeMicrometres = 0; std::int64_t irregularityAmplitudeMicrometres = 0;
std::int64_t irregularityWavelengthMicrometres = 0; std::int64_t irregularityWavelengthMicrometres = 0;
std::uint64_t irregularitySeed = 0; std::uint64_t irregularitySeed = 0;
@@ -882,6 +885,8 @@ namespace VoxelForgeCore
std::int64_t boreHeightMicrometres = 0; std::int64_t boreHeightMicrometres = 0;
std::int64_t floorSupportDepthMicrometres = 0; std::int64_t floorSupportDepthMicrometres = 0;
std::int64_t smoothWidthMicrometres = 0; std::int64_t smoothWidthMicrometres = 0;
// Optional feature-to-feature union width; floor fillets still use smoothWidth.
std::int64_t unionWidthMicrometres = 0;
std::int64_t irregularityAmplitudeMicrometres = 0; std::int64_t irregularityAmplitudeMicrometres = 0;
std::int64_t irregularityWavelengthMicrometres = 0; std::int64_t irregularityWavelengthMicrometres = 0;
std::uint64_t irregularitySeed = 0; std::uint64_t irregularitySeed = 0;
@@ -950,6 +955,7 @@ namespace VoxelForgeCore
bool hasFloatingIslandLanding = false; bool hasFloatingIslandLanding = false;
bool usePassageAnchorForColumns = false; bool usePassageAnchorForColumns = false;
bool hasNativeCaveField = false; bool hasNativeCaveField = false;
bool hasNativeLatticeField = false;
}; };
struct ResolvedLayoutInterfaceCapabilitiesV1 struct ResolvedLayoutInterfaceCapabilitiesV1
@@ -991,6 +997,25 @@ namespace VoxelForgeCore
ResolvedLayoutInterfaceCapabilitiesV1 lowerInterfaceCapabilities; ResolvedLayoutInterfaceCapabilitiesV1 lowerInterfaceCapabilities;
}; };
inline bool IsNativeCaveFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
{
return stratum.generatorKind=="Caves"
&&stratum.recipeId=="Core.Caves.PrimitiveField"
&&stratum.recipeVersion==1;
}
inline bool IsNativeLatticeFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
{
return (stratum.generatorKind=="Maze"||stratum.generatorKind=="VerticalShafts")
&&stratum.recipeId=="Core.Lattice.PrimitiveField"
&&stratum.recipeVersion==1;
}
inline bool HasNativeGraphField(const ResolvedLayoutStratumV2& stratum) noexcept
{
return IsNativeCaveFieldStratum(stratum)||IsNativeLatticeFieldStratum(stratum);
}
/** Per-column resolved bands for one adjacent bedrock/ceiling interface. /** Per-column resolved bands for one adjacent bedrock/ceiling interface.
* crossingDepthMicrometres is the actual overlap depth between the two facing bands. * crossingDepthMicrometres is the actual overlap depth between the two facing bands.
*/ */
+98 -24
View File
@@ -159,6 +159,16 @@ namespace
for(unsigned char c:stableId){hash^=c;hash*=1099511628211ull;} for(unsigned char c:stableId){hash^=c;hash*=1099511628211ull;}
return LayoutStampMix(hash); return LayoutStampMix(hash);
} }
std::uint64_t LayoutLatticeFeatureSeed(std::int32_t seed,std::int32_t strate,
std::uint32_t recipeVersion,std::string_view stableId)
{
std::uint64_t hash=14695981039346656037ull;
TransitionHashWord(hash,static_cast<std::uint32_t>(seed));
TransitionHashWord(hash,static_cast<std::uint32_t>(strate));
TransitionHashWord(hash,recipeVersion);
for(unsigned char c:stableId){hash^=c;hash*=1099511628211ull;}
return LayoutStampMix(hash);
}
std::uint64_t LayoutStampNext(std::uint64_t& state) std::uint64_t LayoutStampNext(std::uint64_t& state)
{state=LayoutStampMix(state+0x9e3779b97f4a7c15ull);return state;} {state=LayoutStampMix(state+0x9e3779b97f4a7c15ull);return state;}
bool LayoutStampKindValid(ResolvedSdfStampKindV1 kind) bool LayoutStampKindValid(ResolvedSdfStampKindV1 kind)
@@ -611,7 +621,10 @@ namespace
<<room.halfExtentsMicrometres[1]<<','<<room.halfExtentsMicrometres[2] <<room.halfExtentsMicrometres[1]<<','<<room.halfExtentsMicrometres[2]
<<"],\"floor_um\":"<<room.floorMicrometres <<"],\"floor_um\":"<<room.floorMicrometres
<<",\"smooth_width_um\":"<<room.smoothWidthMicrometres <<",\"smooth_width_um\":"<<room.smoothWidthMicrometres
<<",\"irregularity_amplitude_um\":"<<room.irregularityAmplitudeMicrometres ;
if(room.unionWidthMicrometres>0)
out<<",\"union_width_um\":"<<room.unionWidthMicrometres;
out<<",\"irregularity_amplitude_um\":"<<room.irregularityAmplitudeMicrometres
<<",\"irregularity_wavelength_um\":"<<room.irregularityWavelengthMicrometres <<",\"irregularity_wavelength_um\":"<<room.irregularityWavelengthMicrometres
<<",\"irregularity_seed\":"<<room.irregularitySeed <<",\"irregularity_seed\":"<<room.irregularitySeed
<<",\"bounds_min_um\":["<<room.boundsMinimumMicrometres[0]<<',' <<",\"bounds_min_um\":["<<room.boundsMinimumMicrometres[0]<<','
@@ -646,8 +659,10 @@ namespace
out<<",\"bore_radius_um\":"<<edge.boreRadiusMicrometres out<<",\"bore_radius_um\":"<<edge.boreRadiusMicrometres
<<",\"bore_height_um\":"<<edge.boreHeightMicrometres <<",\"bore_height_um\":"<<edge.boreHeightMicrometres
<<",\"floor_support_depth_um\":"<<edge.floorSupportDepthMicrometres <<",\"floor_support_depth_um\":"<<edge.floorSupportDepthMicrometres
<<",\"smooth_width_um\":"<<edge.smoothWidthMicrometres <<",\"smooth_width_um\":"<<edge.smoothWidthMicrometres;
<<",\"irregularity_amplitude_um\":"<<edge.irregularityAmplitudeMicrometres if(edge.unionWidthMicrometres>0)
out<<",\"union_width_um\":"<<edge.unionWidthMicrometres;
out<<",\"irregularity_amplitude_um\":"<<edge.irregularityAmplitudeMicrometres
<<",\"irregularity_wavelength_um\":"<<edge.irregularityWavelengthMicrometres <<",\"irregularity_wavelength_um\":"<<edge.irregularityWavelengthMicrometres
<<",\"irregularity_seed\":"<<edge.irregularitySeed <<",\"irregularity_seed\":"<<edge.irregularitySeed
<<",\"bounds_min_um\":["<<edge.boundsMinimumMicrometres[0]<<',' <<",\"bounds_min_um\":["<<edge.boundsMinimumMicrometres[0]<<','
@@ -867,8 +882,10 @@ bool ResolvedLayoutSnapshotV3::finalize(std::string& error)
x.lowerInterfaceCapabilities={}; x.lowerInterfaceCapabilities={};
if(x.generatorKind=="SurfaceWorld") if(x.generatorKind=="SurfaceWorld")
x.passageCapabilities.hasSurfaceLanding=true; x.passageCapabilities.hasSurfaceLanding=true;
else if(x.generatorKind=="Caves") else if(IsNativeCaveFieldStratum(x))
x.passageCapabilities.hasNativeCaveField=true; x.passageCapabilities.hasNativeCaveField=true;
else if(IsNativeLatticeFieldStratum(x))
x.passageCapabilities.hasNativeLatticeField=true;
else if(x.generatorKind=="FloatingIslands") else if(x.generatorKind=="FloatingIslands")
{ {
x.passageCapabilities.hasFloatingIslandLanding=true; x.passageCapabilities.hasFloatingIslandLanding=true;
@@ -1245,6 +1262,7 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
validExtents=validExtents&&extent>=500000&&extent<=100000000; validExtents=validExtents&&extent>=500000&&extent<=100000000;
if(!validExtents||room.smoothWidthMicrometres<0 if(!validExtents||room.smoothWidthMicrometres<0
||room.smoothWidthMicrometres>10000000 ||room.smoothWidthMicrometres>10000000
||room.unionWidthMicrometres<0||room.unionWidthMicrometres>10000000
||room.irregularityAmplitudeMicrometres<0 ||room.irregularityAmplitudeMicrometres<0
||room.irregularityAmplitudeMicrometres>10000000 ||room.irregularityAmplitudeMicrometres>10000000
||room.irregularityWavelengthMicrometres<0 ||room.irregularityWavelengthMicrometres<0
@@ -1253,7 +1271,7 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
&&room.irregularityWavelengthMicrometres<=0)) &&room.irregularityWavelengthMicrometres<=0))
add("cavern_room_parameters",room.stableId+" has invalid or unbounded parameters"); add("cavern_room_parameters",room.stableId+" has invalid or unbounded parameters");
const std::int64_t pad=room.smoothWidthMicrometres const std::int64_t pad=room.smoothWidthMicrometres
+room.irregularityAmplitudeMicrometres; +room.unionWidthMicrometres+room.irregularityAmplitudeMicrometres;
for(std::size_t axis=0;axis<3;++axis) for(std::size_t axis=0;axis<3;++axis)
if(room.boundsMinimumMicrometres[axis] if(room.boundsMinimumMicrometres[axis]
!=room.centerMicrometres[axis]-room.halfExtentsMicrometres[axis]-pad !=room.centerMicrometres[axis]-room.halfExtentsMicrometres[axis]-pad
@@ -1276,11 +1294,15 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
add("cavern_edge_binding",edge.stableId+" does not terminate at a room in its strate"); add("cavern_edge_binding",edge.stableId+" does not terminate at a room in its strate");
if(!findStratum(edge.strateIndex)) if(!findStratum(edge.strateIndex))
add("cavern_graph_strate",edge.stableId+" names an unknown strate"); add("cavern_graph_strate",edge.stableId+" names an unknown strate");
const auto* edgeStratum=findStratum(edge.strateIndex);
const bool nativeLattice=edgeStratum
&&IsNativeLatticeFieldStratum(*edgeStratum);
if(edge.boreRadiusMicrometres<500000||edge.boreRadiusMicrometres>10000000 if(edge.boreRadiusMicrometres<500000||edge.boreRadiusMicrometres>10000000
||edge.boreHeightMicrometres<1000000||edge.boreHeightMicrometres>20000000 ||edge.boreHeightMicrometres<1000000||edge.boreHeightMicrometres>20000000
||edge.floorSupportDepthMicrometres<0 ||edge.floorSupportDepthMicrometres<0
||edge.floorSupportDepthMicrometres>2000000 ||edge.floorSupportDepthMicrometres>2000000
||edge.smoothWidthMicrometres<0||edge.smoothWidthMicrometres>10000000 ||edge.smoothWidthMicrometres<0||edge.smoothWidthMicrometres>10000000
||edge.unionWidthMicrometres<0||edge.unionWidthMicrometres>10000000
||edge.irregularityAmplitudeMicrometres<0 ||edge.irregularityAmplitudeMicrometres<0
||edge.irregularityAmplitudeMicrometres>10000000 ||edge.irregularityAmplitudeMicrometres>10000000
||edge.irregularityWavelengthMicrometres<0 ||edge.irregularityWavelengthMicrometres<0
@@ -1290,13 +1312,21 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
add("cavern_edge_parameters",edge.stableId+" has invalid or unbounded parameters"); add("cavern_edge_parameters",edge.stableId+" has invalid or unbounded parameters");
std::array<std::int64_t,3> low=edge.controlPointsMicrometres[0]; std::array<std::int64_t,3> low=edge.controlPointsMicrometres[0];
std::array<std::int64_t,3> high=low; std::array<std::int64_t,3> high=low;
for(const auto& point:edge.controlPointsMicrometres) if(nativeLattice&&!edge.polylineMicrometres.empty())
{low=edge.polylineMicrometres.front();high=low;}
const auto includeBoundPoint=[&](const auto& point)
{
for(std::size_t axis=0;axis<3;++axis) for(std::size_t axis=0;axis<3;++axis)
{low[axis]=std::min(low[axis],point[axis]);high[axis]=std::max(high[axis],point[axis]);} {low[axis]=std::min(low[axis],point[axis]);high[axis]=std::max(high[axis],point[axis]);}
};
if(nativeLattice&&!edge.polylineMicrometres.empty())
for(const auto& point:edge.polylineMicrometres)includeBoundPoint(point);
else for(const auto& point:edge.controlPointsMicrometres)includeBoundPoint(point);
const std::int64_t xyPad=edge.boreRadiusMicrometres const std::int64_t xyPad=edge.boreRadiusMicrometres
+edge.smoothWidthMicrometres+edge.irregularityAmplitudeMicrometres; +edge.smoothWidthMicrometres+edge.unionWidthMicrometres
const std::int64_t zPad=edge.smoothWidthMicrometres
+edge.irregularityAmplitudeMicrometres; +edge.irregularityAmplitudeMicrometres;
const std::int64_t zPad=edge.smoothWidthMicrometres
+edge.unionWidthMicrometres+edge.irregularityAmplitudeMicrometres;
for(std::size_t axis=0;axis<3;++axis) for(std::size_t axis=0;axis<3;++axis)
{ {
const std::int64_t pad=axis==2?zPad:xyPad; const std::int64_t pad=axis==2?zPad:xyPad;
@@ -1314,17 +1344,26 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
||endpoint[1]<room->centerMicrometres[1]-room->halfExtentsMicrometres[1] ||endpoint[1]<room->centerMicrometres[1]-room->halfExtentsMicrometres[1]
||endpoint[1]>room->centerMicrometres[1]+room->halfExtentsMicrometres[1] ||endpoint[1]>room->centerMicrometres[1]+room->halfExtentsMicrometres[1]
||std::abs(endpoint[2]-room->floorMicrometres)>500000) ||std::abs(endpoint[2]-room->floorMicrometres)>500000)
add("cavern_edge_portal",edge.stableId+" endpoint does not meet its room floor portal"); {
add("cavern_edge_portal",edge.stableId+" endpoint_um=("
+std::to_string(endpoint[0])+","+std::to_string(endpoint[1])+","+
std::to_string(endpoint[2])+") room="+room->stableId+" center_um=("
+std::to_string(room->centerMicrometres[0])+","+
std::to_string(room->centerMicrometres[1])+","+
std::to_string(room->centerMicrometres[2])+") floor_um="+
std::to_string(room->floorMicrometres)+" half_um=("
+std::to_string(room->halfExtentsMicrometres[0])+","+
std::to_string(room->halfExtentsMicrometres[1])+","+
std::to_string(room->halfExtentsMicrometres[2])+")");
}
} }
const auto* edgeStratum=findStratum(edge.strateIndex); if(edgeStratum&&HasNativeGraphField(*edgeStratum)
if(edgeStratum&&edgeStratum->generatorKind=="Caves"
&&edgeStratum->recipeId=="Core.Caves.PrimitiveField"
&&edgeStratum->recipeVersion==1
&&(edge.polylineMicrometres.size()<2 &&(edge.polylineMicrometres.size()<2
||edge.polylineMicrometres.front()!=edge.controlPointsMicrometres.front() ||edge.polylineMicrometres.front()!=edge.controlPointsMicrometres.front()
||edge.polylineMicrometres.back()!=edge.controlPointsMicrometres.back())) ||edge.polylineMicrometres.back()!=edge.controlPointsMicrometres.back()
||edge.polylineMicrometres.size()>65536))
add("cavern_edge_polyline",edge.stableId+ add("cavern_edge_polyline",edge.stableId+
" has no resolve-time polyline with the cubic endpoints"); " has no bounded resolve-time polyline with matching endpoints");
} }
} }
if(oreVeins.empty()) if(oreVeins.empty())
@@ -1376,7 +1415,12 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
if(!LayoutOreVeinSafe(*this,vein,&safetyReason)) if(!LayoutOreVeinSafe(*this,vein,&safetyReason))
add("ore_vein_placement",vein.stableId+" is unsafe: "+safetyReason); add("ore_vein_placement",vein.stableId+" is unsafe: "+safetyReason);
} }
if(strict&&!issues.empty()){error="strict resolved-layout validation rejected "+issues.front().code;return false;} if(strict&&!issues.empty())
{
error="strict resolved-layout validation rejected "+issues.front().code+": "
+issues.front().detail;
return false;
}
return true; return true;
} }
@@ -1485,6 +1529,7 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
readTriplet("bounds_max_um",room.boundsMaximumMicrometres); readTriplet("bounds_max_um",room.boundsMaximumMicrometres);
room.floorMicrometres=LayoutInt(v,"floor_um"); room.floorMicrometres=LayoutInt(v,"floor_um");
room.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um"); room.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um");
room.unionWidthMicrometres=LayoutInt(v,"union_width_um");
room.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um"); room.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
room.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um"); room.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
room.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed")); room.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
@@ -1522,6 +1567,7 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
edge.boreHeightMicrometres=LayoutInt(v,"bore_height_um"); edge.boreHeightMicrometres=LayoutInt(v,"bore_height_um");
edge.floorSupportDepthMicrometres=LayoutInt(v,"floor_support_depth_um"); edge.floorSupportDepthMicrometres=LayoutInt(v,"floor_support_depth_um");
edge.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um"); edge.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um");
edge.unionWidthMicrometres=LayoutInt(v,"union_width_um");
edge.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um"); edge.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
edge.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um"); edge.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
edge.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed")); edge.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
@@ -1842,17 +1888,24 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
{ {
const auto found=std::find_if(candidate.strates.begin(),candidate.strates.end(), const auto found=std::find_if(candidate.strates.begin(),candidate.strates.end(),
[strateIndex](const auto& stratum){return stratum.strateIndex==strateIndex;}); [strateIndex](const auto& stratum){return stratum.strateIndex==strateIndex;});
return found!=candidate.strates.end()&&found->generatorKind=="Caves" return found!=candidate.strates.end()&&IsNativeCaveFieldStratum(*found);
&&found->recipeId=="Core.Caves.PrimitiveField"&&found->recipeVersion==1; };
const auto isNativeLatticeStrate=[&candidate](std::int32_t strateIndex)
{
const auto found=std::find_if(candidate.strates.begin(),candidate.strates.end(),
[strateIndex](const auto& stratum){return stratum.strateIndex==strateIndex;});
return found!=candidate.strates.end()&&IsNativeLatticeFieldStratum(*found);
}; };
for(auto& room:candidate.cavernRooms) for(auto& room:candidate.cavernRooms)
{ {
// The shared JSON value type stores numbers as IEEE-754 doubles. Keep the // The shared JSON value type stores numbers as IEEE-754 doubles. Keep the
// serialized seed in the exact integer range, as the stamp vocabulary does. // serialized seed in the exact integer range, as the stamp vocabulary does.
room.irregularitySeed=LayoutStampSeed(seed,room.strateIndex,room.stableId) room.irregularitySeed=(isNativeLatticeStrate(room.strateIndex)
?LayoutLatticeFeatureSeed(seed,room.strateIndex,1,room.stableId)
:LayoutStampSeed(seed,room.strateIndex,room.stableId))
&0x001fffffffffffffull; &0x001fffffffffffffull;
const std::int64_t pad=room.smoothWidthMicrometres const std::int64_t pad=room.smoothWidthMicrometres
+room.irregularityAmplitudeMicrometres; +room.unionWidthMicrometres+room.irregularityAmplitudeMicrometres;
for(std::size_t axis=0;axis<3;++axis) for(std::size_t axis=0;axis<3;++axis)
{ {
room.boundsMinimumMicrometres[axis]=room.centerMicrometres[axis] room.boundsMinimumMicrometres[axis]=room.centerMicrometres[axis]
@@ -1863,7 +1916,10 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
} }
for(auto& edge:candidate.cavernEdges) for(auto& edge:candidate.cavernEdges)
{ {
edge.irregularitySeed=LayoutStampSeed(seed,edge.strateIndex,edge.stableId) const bool nativeLattice=isNativeLatticeStrate(edge.strateIndex);
edge.irregularitySeed=(nativeLattice
?LayoutLatticeFeatureSeed(seed,edge.strateIndex,1,edge.stableId)
:LayoutStampSeed(seed,edge.strateIndex,edge.stableId))
&0x001fffffffffffffull; &0x001fffffffffffffull;
using FCurvePoint=std::array<double,3>; using FCurvePoint=std::array<double,3>;
std::array<FCurvePoint,4> curve{}; std::array<FCurvePoint,4> curve{};
@@ -1889,6 +1945,8 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
} }
return std::sqrt(distanceSquared); return std::sqrt(distanceSquared);
}; };
std::vector<std::array<std::int64_t,3>> authoredPolyline;
if(nativeLattice)authoredPolyline=std::move(edge.polylineMicrometres);
edge.polylineMicrometres.clear(); edge.polylineMicrometres.clear();
if(isNativeCavesStrate(edge.strateIndex)) if(isNativeCavesStrate(edge.strateIndex))
{ {
@@ -1934,15 +1992,31 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
if(!polylineValid) if(!polylineValid)
{error="cavern edge "+edge.stableId+" exceeds the bounded polyline subdivision budget";return false;} {error="cavern edge "+edge.stableId+" exceeds the bounded polyline subdivision budget";return false;}
} }
else if(nativeLattice)
{
if(authoredPolyline.size()<2||authoredPolyline.size()>65536
||authoredPolyline.front()!=edge.controlPointsMicrometres.front()
||authoredPolyline.back()!=edge.controlPointsMicrometres.back())
{error="lattice edge "+edge.stableId+
" needs a bounded explicit path with matching endpoints";return false;}
edge.polylineMicrometres=std::move(authoredPolyline);
}
std::array<std::int64_t,3> low=edge.controlPointsMicrometres[0]; std::array<std::int64_t,3> low=edge.controlPointsMicrometres[0];
std::array<std::int64_t,3> high=low; std::array<std::int64_t,3> high=low;
for(const auto& point:edge.controlPointsMicrometres) const auto includeBoundPoint=[&](const auto& point)
{
for(std::size_t axis=0;axis<3;++axis) for(std::size_t axis=0;axis<3;++axis)
{low[axis]=std::min(low[axis],point[axis]);high[axis]=std::max(high[axis],point[axis]);} {low[axis]=std::min(low[axis],point[axis]);high[axis]=std::max(high[axis],point[axis]);}
};
if(nativeLattice&&!edge.polylineMicrometres.empty())
{low=edge.polylineMicrometres.front();high=low;
for(const auto& point:edge.polylineMicrometres)includeBoundPoint(point);}
else for(const auto& point:edge.controlPointsMicrometres)includeBoundPoint(point);
const std::int64_t xyPad=edge.boreRadiusMicrometres const std::int64_t xyPad=edge.boreRadiusMicrometres
+edge.smoothWidthMicrometres+edge.irregularityAmplitudeMicrometres; +edge.smoothWidthMicrometres+edge.unionWidthMicrometres
const std::int64_t zPad=edge.smoothWidthMicrometres
+edge.irregularityAmplitudeMicrometres; +edge.irregularityAmplitudeMicrometres;
const std::int64_t zPad=edge.smoothWidthMicrometres
+edge.unionWidthMicrometres+edge.irregularityAmplitudeMicrometres;
for(std::size_t axis=0;axis<3;++axis) for(std::size_t axis=0;axis<3;++axis)
{ {
const std::int64_t pad=axis==2?zPad:xyPad; const std::int64_t pad=axis==2?zPad:xyPad;
+278 -65
View File
@@ -378,10 +378,10 @@ public:
} }
} }
} }
else if(Stratum.generatorKind=="Caves" else if(VoxelForgeCore::HasNativeGraphField(Stratum)
&&Stratum.recipeId=="Core.Caves.PrimitiveField" &&(VoxelForgeCore::IsNativeCaveFieldStratum(Stratum)
&&Stratum.recipeVersion==1 ?Stratum.passageCapabilities.hasNativeCaveField
&&Stratum.passageCapabilities.hasNativeCaveField) :Stratum.passageCapabilities.hasNativeLatticeField))
{ {
Bound.Kind=FBoundStratumField::EKind::Caves; Bound.Kind=FBoundStratumField::EKind::Caves;
Bound.Evaluator.emplace<FCavesFieldAdapter>( Bound.Evaluator.emplace<FCavesFieldAdapter>(
@@ -1819,14 +1819,28 @@ public:
float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const
{ {
const float graphDensity=ApplyResolvedCavernGraph(density,X,Y,Z); const float graphDensity=ApplyResolvedCavernGraph(density,X,Y,Z);
const float composed=ApplyResolvedStratePassageCarves(
ApplyResolvedStrateBoundaryBand(graphDensity,X,Y,Z),X,Y,Z);
const float stamped=StampRuntime.empty()?composed:ApplyResolvedSdfStamps(composed,X,Y,Z);
const auto* bound=FindBoundStratumForManagerZ(Z); const auto* bound=FindBoundStratumForManagerZ(Z);
const bool nativeLattice=bound&&bound->Resolved
&&VoxelForgeCore::IsNativeLatticeFieldStratum(*bound->Resolved)
&&bound->Resolved->passageCapabilities.hasNativeLatticeField;
// The native lattice field is a complete host-rock SDF: it owns both the
// voids and the surrounding solid. Replacing its air-side distances with
// the generic stratum transition distance creates a discontinuity at a
// mouth that crosses a wide transition band. Keep that opt-in field whole;
// its host-rock value already supplies the ceiling and bedrock.
const float boundary=nativeLattice?graphDensity
:ApplyResolvedStrateBoundaryBand(graphDensity,X,Y,Z);
// The native lattice owns its resolved arrival/departure portals and their
// supported spine. Reapplying legacy passage carves here can erase the
// tread immediately above a broad legacy mouth or support column.
const float composed=nativeLattice?boundary
:ApplyResolvedStratePassageCarves(boundary,X,Y,Z);
const float stamped=StampRuntime.empty()?composed:ApplyResolvedSdfStamps(composed,X,Y,Z);
if(bound&&bound->Kind==FBoundStratumField::EKind::Caves if(bound&&bound->Kind==FBoundStratumField::EKind::Caves
&&bound->Resolved&&bound->Resolved->recipeId=="Core.Caves.PrimitiveField" &&bound->Resolved&&VoxelForgeCore::HasNativeGraphField(*bound->Resolved)
&&bound->Resolved->recipeVersion==1 &&(VoxelForgeCore::IsNativeCaveFieldStratum(*bound->Resolved)
&&bound->Resolved->passageCapabilities.hasNativeCaveField) ?bound->Resolved->passageCapabilities.hasNativeCaveField
:bound->Resolved->passageCapabilities.hasNativeLatticeField))
return stamped; return stamped;
// The legacy landmark demo retains its compatibility floor cap. Native caves // The legacy landmark demo retains its compatibility floor cap. Native caves
// already combine each floor with its wall before the single void carve above. // already combine each floor with its wall before the single void carve above.
@@ -3040,7 +3054,9 @@ public:
if(NativeFloatingIslandDensityAt(X,Y,Z,Density))return "FloatingIslands-native"; if(NativeFloatingIslandDensityAt(X,Y,Z,Density))return "FloatingIslands-native";
const auto* Bound=FindBoundStratumForZ(Z); const auto* Bound=FindBoundStratumForZ(Z);
if(Bound&&Bound->Kind==FBoundStratumField::EKind::Caves if(Bound&&Bound->Kind==FBoundStratumField::EKind::Caves
&&NativeSurfaceDensityAt(X,Y,Z,Density))return "Caves-native"; &&NativeSurfaceDensityAt(X,Y,Z,Density))
return Bound->Resolved&&VoxelForgeCore::IsNativeLatticeFieldStratum(*Bound->Resolved)
?"Lattice-native":"Caves-native";
if(NativeSurfaceDensityAt(X,Y,Z,Density))return "SurfaceWorld-native"; if(NativeSurfaceDensityAt(X,Y,Z,Density))return "SurfaceWorld-native";
if(UsesLegacySurfaceField(X,Y,Z))return "SurfaceWorld-legacy"; if(UsesLegacySurfaceField(X,Y,Z))return "SurfaceWorld-legacy";
if(Bound&&Bound->Resolved) if(Bound&&Bound->Resolved)
@@ -3162,7 +3178,7 @@ public:
} }
bool bHasLegacySurfaceSample=false; bool bHasLegacySurfaceSample=false;
bool bHasNativeIslandSample=false; bool bHasNativeIslandSample=false;
bool bHasNativeCavesSample=false; bool bHasNativeGraphSample=false;
std::array<float,8> NativeIslandDensity{}; std::array<float,8> NativeIslandDensity{};
std::array<bool,8> HasNativeIslandSample{}; std::array<bool,8> HasNativeIslandSample{};
for(int32 Lane=0;Lane<Count;++Lane) for(int32 Lane=0;Lane<Count;++Lane)
@@ -3172,13 +3188,13 @@ public:
const float SampleZ=static_cast<float>(Z); const float SampleZ=static_cast<float>(Z);
bHasLegacySurfaceSample|=UsesLegacySurfaceField(SampleX,SampleY,SampleZ); bHasLegacySurfaceSample|=UsesLegacySurfaceField(SampleX,SampleY,SampleZ);
const auto* bound=FindBoundStratumForZ(SampleZ); const auto* bound=FindBoundStratumForZ(SampleZ);
bHasNativeCavesSample|=bound bHasNativeGraphSample|=bound
&&bound->Kind==FBoundStratumField::EKind::Caves; &&bound->Kind==FBoundStratumField::EKind::Caves;
HasNativeIslandSample[static_cast<std::size_t>(Lane)]=NativeFloatingIslandDensityAt( HasNativeIslandSample[static_cast<std::size_t>(Lane)]=NativeFloatingIslandDensityAt(
SampleX,SampleY,SampleZ,NativeIslandDensity[static_cast<std::size_t>(Lane)]); SampleX,SampleY,SampleZ,NativeIslandDensity[static_cast<std::size_t>(Lane)]);
bHasNativeIslandSample|=HasNativeIslandSample[static_cast<std::size_t>(Lane)]; bHasNativeIslandSample|=HasNativeIslandSample[static_cast<std::size_t>(Lane)];
} }
if(bHasLegacySurfaceSample||bHasNativeIslandSample||bHasNativeCavesSample) if(bHasLegacySurfaceSample||bHasNativeIslandSample||bHasNativeGraphSample)
{ {
for(int32 Lane=0;Lane<Count;++Lane) for(int32 Lane=0;Lane<Count;++Lane)
{ {
@@ -3315,14 +3331,25 @@ private:
{ {
const VoxelForgeCore::ResolvedCavernRoomV1* room=nullptr; const VoxelForgeCore::ResolvedCavernRoomV1* room=nullptr;
const VoxelForgeCore::ResolvedCavernEdgeV1* edge=nullptr; const VoxelForgeCore::ResolvedCavernEdgeV1* edge=nullptr;
bool nativeCaves=false; bool nativeGraph=false;
std::array<std::int64_t,3> boundsMinimumMicrometres{},boundsMaximumMicrometres{}; std::array<std::int64_t,3> boundsMinimumMicrometres{},boundsMaximumMicrometres{};
std::array<double,3> centerMetres{},halfExtentsMetres{}; std::array<double,3> centerMetres{},halfExtentsMetres{};
struct FSegment struct FSegment
{ {
std::array<double,3> start{},end{},minimum{},maximum{}; std::array<double,3> start{},end{},minimum{},maximum{};
}; };
struct FSegmentNode
{
std::array<double,3> minimum{},maximum{};
std::size_t first=0,end=0;
int left=-1,right=-1;
};
std::vector<FSegment> segments; std::vector<FSegment> segments;
std::vector<std::uint32_t> segmentOrder;
std::vector<FSegmentNode> segmentNodes;
double minimumFloorMetres=std::numeric_limits<double>::infinity();
double maximumFloorMetres=-std::numeric_limits<double>::infinity();
double maximumSegmentGrade=0.0;
std::array<float,3> centerVoxels{},halfExtentsVoxels{}; std::array<float,3> centerVoxels{},halfExtentsVoxels{};
std::array<std::array<float,3>,4> controlPointsVoxels{}; std::array<std::array<float,3>,4> controlPointsVoxels{};
float floorVoxels=0.0f; float floorVoxels=0.0f;
@@ -3753,22 +3780,22 @@ private:
if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0 if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0
||ResolvedLayout->cavernGraphVocabularyVersion ||ResolvedLayout->cavernGraphVocabularyVersion
!=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1)return; !=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1)return;
const auto isNativeCavesStrate=[this](std::int32_t strateIndex) const auto isNativeGraphStrate=[this](std::int32_t strateIndex)
{ {
const auto found=std::find_if(ResolvedLayout->strates.begin(), const auto found=std::find_if(ResolvedLayout->strates.begin(),
ResolvedLayout->strates.end(),[strateIndex](const auto& stratum) ResolvedLayout->strates.end(),[strateIndex](const auto& stratum)
{return stratum.strateIndex==strateIndex;}); {return stratum.strateIndex==strateIndex;});
return found!=ResolvedLayout->strates.end() return found!=ResolvedLayout->strates.end()
&&found->generatorKind=="Caves" &&VoxelForgeCore::HasNativeGraphField(*found)
&&found->recipeId=="Core.Caves.PrimitiveField" &&(VoxelForgeCore::IsNativeCaveFieldStratum(*found)
&&found->recipeVersion==1 ?found->passageCapabilities.hasNativeCaveField
&&found->passageCapabilities.hasNativeCaveField; :found->passageCapabilities.hasNativeLatticeField);
}; };
const float micronsPerVoxel=static_cast<float>(Owner->CoreVoxelSizeMicrons); const float micronsPerVoxel=static_cast<float>(Owner->CoreVoxelSizeMicrons);
for(const auto& room:ResolvedLayout->cavernRooms) for(const auto& room:ResolvedLayout->cavernRooms)
{ {
FCavernRuntimeFeature runtime;runtime.room=&room; FCavernRuntimeFeature runtime;runtime.room=&room;
runtime.nativeCaves=isNativeCavesStrate(room.strateIndex); runtime.nativeGraph=isNativeGraphStrate(room.strateIndex);
runtime.boundsMinimumMicrometres=room.boundsMinimumMicrometres; runtime.boundsMinimumMicrometres=room.boundsMinimumMicrometres;
runtime.boundsMaximumMicrometres=room.boundsMaximumMicrometres; runtime.boundsMaximumMicrometres=room.boundsMaximumMicrometres;
for(std::size_t axis=0;axis<3;++axis) for(std::size_t axis=0;axis<3;++axis)
@@ -3794,7 +3821,7 @@ private:
for(const auto& edge:ResolvedLayout->cavernEdges) for(const auto& edge:ResolvedLayout->cavernEdges)
{ {
FCavernRuntimeFeature runtime;runtime.edge=&edge; FCavernRuntimeFeature runtime;runtime.edge=&edge;
runtime.nativeCaves=isNativeCavesStrate(edge.strateIndex); runtime.nativeGraph=isNativeGraphStrate(edge.strateIndex);
runtime.boundsMinimumMicrometres=edge.boundsMinimumMicrometres; runtime.boundsMinimumMicrometres=edge.boundsMinimumMicrometres;
runtime.boundsMaximumMicrometres=edge.boundsMaximumMicrometres; runtime.boundsMaximumMicrometres=edge.boundsMaximumMicrometres;
for(std::size_t point=0;point<4;++point) for(std::size_t point=0;point<4;++point)
@@ -3827,8 +3854,69 @@ private:
segment.minimum[axis]=std::min(segment.start[axis],segment.end[axis]); segment.minimum[axis]=std::min(segment.start[axis],segment.end[axis]);
segment.maximum[axis]=std::max(segment.start[axis],segment.end[axis]); segment.maximum[axis]=std::max(segment.start[axis],segment.end[axis]);
} }
runtime.minimumFloorMetres=std::min(runtime.minimumFloorMetres,
std::min(segment.start[2],segment.end[2]));
runtime.maximumFloorMetres=std::max(runtime.maximumFloorMetres,
std::max(segment.start[2],segment.end[2]));
const double horizontal=std::hypot(segment.end[0]-segment.start[0],
segment.end[1]-segment.start[1]);
if(horizontal>1.0e-9)
runtime.maximumSegmentGrade=std::max(runtime.maximumSegmentGrade,
std::abs((segment.end[2]-segment.start[2])/horizontal));
runtime.segments.push_back(segment); runtime.segments.push_back(segment);
} }
runtime.segmentOrder.resize(runtime.segments.size());
for(std::size_t index=0;index<runtime.segmentOrder.size();++index)
runtime.segmentOrder[index]=static_cast<std::uint32_t>(index);
if(!runtime.segmentOrder.empty())
{
const auto buildSegmentNode=[&](const auto& self,std::size_t first,
std::size_t end)->int
{
FCavernRuntimeFeature::FSegmentNode node;
node.first=first;node.end=end;
node.minimum.fill(std::numeric_limits<double>::infinity());
node.maximum.fill(-std::numeric_limits<double>::infinity());
for(std::size_t offset=first;offset<end;++offset)
{
const auto& segment=runtime.segments[
runtime.segmentOrder[offset]];
for(std::size_t axis=0;axis<3;++axis)
{
node.minimum[axis]=std::min(node.minimum[axis],segment.minimum[axis]);
node.maximum[axis]=std::max(node.maximum[axis],segment.maximum[axis]);
}
}
const int nodeIndex=static_cast<int>(runtime.segmentNodes.size());
runtime.segmentNodes.push_back(node);
if(end-first>4)
{
std::size_t splitAxis=0;
for(std::size_t axis=1;axis<3;++axis)
if(node.maximum[axis]-node.minimum[axis]
>node.maximum[splitAxis]-node.minimum[splitAxis])
splitAxis=axis;
const std::size_t middle=first+(end-first)/2;
std::nth_element(runtime.segmentOrder.begin()+first,
runtime.segmentOrder.begin()+middle,
runtime.segmentOrder.begin()+end,
[&](std::uint32_t a,std::uint32_t b)
{
const auto& left=runtime.segments[a];
const auto& right=runtime.segments[b];
const double ca=left.minimum[splitAxis]+left.maximum[splitAxis];
const double cb=right.minimum[splitAxis]+right.maximum[splitAxis];
return ca==cb?a<b:ca<cb;
});
const int left=self(self,first,middle);
const int right=self(self,middle,end);
runtime.segmentNodes[nodeIndex].left=left;
runtime.segmentNodes[nodeIndex].right=right;
}
return nodeIndex;
};
buildSegmentNode(buildSegmentNode,0,runtime.segmentOrder.size());
}
CavernRuntime.push_back(runtime); CavernRuntime.push_back(runtime);
} }
std::stable_sort(CavernRuntime.begin(),CavernRuntime.end(),[](const auto& a,const auto& b) std::stable_sort(CavernRuntime.begin(),CavernRuntime.end(),[](const auto& a,const auto& b)
@@ -3838,8 +3926,8 @@ private:
const std::int32_t rightStrate=b.room?b.room->strateIndex const std::int32_t rightStrate=b.room?b.room->strateIndex
:b.edge?b.edge->strateIndex:-1; :b.edge?b.edge->strateIndex:-1;
if(leftStrate!=rightStrate)return leftStrate<rightStrate; if(leftStrate!=rightStrate)return leftStrate<rightStrate;
if(a.nativeCaves!=b.nativeCaves)return a.nativeCaves; if(a.nativeGraph!=b.nativeGraph)return a.nativeGraph;
if(!a.nativeCaves)return false; if(!a.nativeGraph)return false;
const std::string& left=a.room?a.room->stableId:a.edge->stableId; const std::string& left=a.room?a.room->stableId:a.edge->stableId;
const std::string& right=b.room?b.room->stableId:b.edge->stableId; const std::string& right=b.room?b.room->stableId:b.edge->stableId;
return left<right; return left<right;
@@ -5106,9 +5194,10 @@ public:
} }
else if(Candidate->Kind==FBoundStratumField::EKind::Caves) else if(Candidate->Kind==FBoundStratumField::EKind::Caves)
{ {
if(!Candidate->Resolved||Candidate->Resolved->recipeId!="Core.Caves.PrimitiveField" if(!Candidate->Resolved||!VoxelForgeCore::HasNativeGraphField(*Candidate->Resolved)
||Candidate->Resolved->recipeVersion!=1 ||(VoxelForgeCore::IsNativeCaveFieldStratum(*Candidate->Resolved)
||!Candidate->Resolved->passageCapabilities.hasNativeCaveField ?!Candidate->Resolved->passageCapabilities.hasNativeCaveField
:!Candidate->Resolved->passageCapabilities.hasNativeLatticeField)
||!std::get_if<FCavesFieldAdapter>(&Candidate->Evaluator))return false; ||!std::get_if<FCavesFieldAdapter>(&Candidate->Evaluator))return false;
} }
else return false; else return false;
@@ -5144,6 +5233,11 @@ public:
FMetricInterval caveVoid{std::numeric_limits<double>::infinity(), FMetricInterval caveVoid{std::numeric_limits<double>::infinity(),
std::numeric_limits<double>::infinity(),true}; std::numeric_limits<double>::infinity(),true};
bool haveFeature=false; bool haveFeature=false;
double latticeFloorSupportLowerBound=std::numeric_limits<double>::infinity();
const auto* resolvedStratum=FindResolvedLayoutStratum(strateIndex);
const bool nativeLattice=resolvedStratum
&&VoxelForgeCore::IsNativeLatticeFieldStratum(*resolvedStratum)
&&resolvedStratum->passageCapabilities.hasNativeLatticeField;
for(const std::uint32_t index:candidates) for(const std::uint32_t index:candidates)
{ {
if(index>=CavernRuntime.size())continue; if(index>=CavernRuntime.size())continue;
@@ -5185,18 +5279,32 @@ public:
if(!(radius>0.0&&height>0.0))return {0.0,0.0,false}; if(!(radius>0.0&&height>0.0))return {0.0,0.0,false};
double tubeMinimum=std::numeric_limits<double>::infinity(); double tubeMinimum=std::numeric_limits<double>::infinity();
double tubeMaximum=std::numeric_limits<double>::infinity(); double tubeMaximum=std::numeric_limits<double>::infinity();
double floorMinimum=std::numeric_limits<double>::infinity(); const std::array<double,3> inverseScale={1.0/radius,1.0/radius,
double floorMaximum=-std::numeric_limits<double>::infinity(); 1.0/height};
double maximumGrade=0.0; const std::array<double,3> boxMinimum={lo[0]*inverseScale[0],
for(const auto& segment:feature.segments) lo[1]*inverseScale[1],lo[2]*inverseScale[2]};
const std::array<double,3> boxMaximum={hi[0]*inverseScale[0],
hi[1]*inverseScale[1],hi[2]*inverseScale[2]};
const double scale=std::min(radius,height);
const auto nodeLowerBound=[&](const FCavernRuntimeFeature::FSegmentNode& node)
{ {
const std::array<double,3> inverseScale={1.0/radius,1.0/radius, double gapSquared=0.0;
1.0/height}; for(std::size_t axis=0;axis<3;++axis)
std::array<double,3> boxMinimum{},boxMaximum{},segmentStart{},segmentEnd{}; {
const double low=node.minimum[axis]*inverseScale[axis];
const double high=node.maximum[axis]*inverseScale[axis];
const double gap=high<boxMinimum[axis]?boxMinimum[axis]-high
:low>boxMaximum[axis]?low-boxMaximum[axis]:0.0;
gapSquared+=gap*gap;
}
return (std::sqrt(gapSquared)-1.0)*scale;
};
const auto evaluateSegment=[&](std::uint32_t segmentIndex)
{
const auto& segment=feature.segments[segmentIndex];
std::array<double,3> segmentStart{},segmentEnd{};
for(std::size_t axis=0;axis<3;++axis) for(std::size_t axis=0;axis<3;++axis)
{ {
boxMinimum[axis]=lo[axis]*inverseScale[axis];
boxMaximum[axis]=hi[axis]*inverseScale[axis];
segmentStart[axis]=segment.start[axis]*inverseScale[axis]; segmentStart[axis]=segment.start[axis]*inverseScale[axis];
segmentEnd[axis]=segment.end[axis]*inverseScale[axis]; segmentEnd[axis]=segment.end[axis]*inverseScale[axis];
} }
@@ -5237,19 +5345,33 @@ public:
farthestSquared+=farthest*farthest; farthestSquared+=farthest*farthest;
} }
const double distanceUpper=std::sqrt(farthestSquared); const double distanceUpper=std::sqrt(farthestSquared);
const double scale=std::min(radius,height);
tubeMinimum=std::min(tubeMinimum,(distanceLower-1.0)*scale); tubeMinimum=std::min(tubeMinimum,(distanceLower-1.0)*scale);
tubeMaximum=std::min(tubeMaximum,(distanceUpper-1.0)*scale); tubeMaximum=std::min(tubeMaximum,(distanceUpper-1.0)*scale);
floorMinimum=std::min(floorMinimum, };
std::min(segment.start[2],segment.end[2])); if(feature.segmentNodes.empty())return {0.0,0.0,false};
floorMaximum=std::max(floorMaximum, const auto visitNode=[&](const auto& self,int nodeIndex)->void
std::max(segment.start[2],segment.end[2])); {
const double horizontal=std::hypot(segment.end[0]-segment.start[0], if(nodeIndex<0||static_cast<std::size_t>(nodeIndex)>=feature.segmentNodes.size())return;
segment.end[1]-segment.start[1]); const auto& node=feature.segmentNodes[static_cast<std::size_t>(nodeIndex)];
if(horizontal>1.0e-9) if(nodeLowerBound(node)>tubeMaximum)return;
maximumGrade=std::max(maximumGrade, if(node.left<0)
std::abs((segment.end[2]-segment.start[2])/horizontal)); {
} for(std::size_t offset=node.first;offset<node.end;++offset)
evaluateSegment(feature.segmentOrder[offset]);
return;
}
const double leftBound=nodeLowerBound(feature.segmentNodes[
static_cast<std::size_t>(node.left)]);
const double rightBound=nodeLowerBound(feature.segmentNodes[
static_cast<std::size_t>(node.right)]);
if(leftBound<=rightBound)
{self(self,node.left);self(self,node.right);}
else {self(self,node.right);self(self,node.left);}
};
visitNode(visitNode,0);
const double floorMinimum=feature.minimumFloorMetres;
const double floorMaximum=feature.maximumFloorMetres;
const double maximumGrade=feature.maximumSegmentGrade;
if(!std::isfinite(tubeMinimum)||!std::isfinite(tubeMaximum) if(!std::isfinite(tubeMinimum)||!std::isfinite(tubeMaximum)
||!std::isfinite(floorMinimum)||!std::isfinite(floorMaximum)) ||!std::isfinite(floorMinimum)||!std::isfinite(floorMaximum))
return {0.0,0.0,false}; return {0.0,0.0,false};
@@ -5267,6 +5389,23 @@ public:
SmoothMaxMetres(tube.maximum,floor.maximum,width),true}; SmoothMaxMetres(tube.maximum,floor.maximum,width),true};
} }
else return {0.0,0.0,false}; else return {0.0,0.0,false};
if(nativeLattice)
{
const double supportDepth=feature.edge
?feature.edge->floorSupportDepthMicrometres/1000000.0:0.0;
const double floorMinimum=feature.edge
?feature.minimumFloorMetres:std::numeric_limits<double>::infinity();
const double floorMaximum=feature.edge
?feature.maximumFloorMetres:-std::numeric_limits<double>::infinity();
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,
std::max(-supportDepth,lo[2]-floorMaximum));
}
if(!std::isfinite(featureVoid.minimum)||!std::isfinite(featureVoid.maximum)) if(!std::isfinite(featureVoid.minimum)||!std::isfinite(featureVoid.maximum))
return {0.0,0.0,false}; return {0.0,0.0,false};
if(!haveFeature) if(!haveFeature)
@@ -5276,8 +5415,12 @@ public:
else else
{ {
const double width=feature.room const double width=feature.room
?feature.room->smoothWidthMicrometres/1000000.0 ?(feature.room->unionWidthMicrometres>0
:feature.edge->smoothWidthMicrometres/1000000.0; ?feature.room->unionWidthMicrometres
:feature.room->smoothWidthMicrometres)/1000000.0
:(feature.edge->unionWidthMicrometres>0
?feature.edge->unionWidthMicrometres
:feature.edge->smoothWidthMicrometres)/1000000.0;
caveVoid.minimum=SmoothMinMetres(caveVoid.minimum,featureVoid.minimum,width); caveVoid.minimum=SmoothMinMetres(caveVoid.minimum,featureVoid.minimum,width);
caveVoid.maximum=SmoothMinMetres(caveVoid.maximum,featureVoid.maximum,width); caveVoid.maximum=SmoothMinMetres(caveVoid.maximum,featureVoid.maximum,width);
} }
@@ -5289,6 +5432,11 @@ public:
result.minimum=std::max(HostRockMetres,-caveVoid.maximum); result.minimum=std::max(HostRockMetres,-caveVoid.maximum);
result.maximum=std::max(HostRockMetres,-caveVoid.minimum); result.maximum=std::max(HostRockMetres,-caveVoid.minimum);
} }
// The final lattice field owns an extra 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);
// Cover floating-point rounding in interval endpoints and the field's fixed-point // Cover floating-point rounding in interval endpoints and the field's fixed-point
// coordinate conversion while keeping the interval tight compared with one micron. // coordinate conversion while keeping the interval tight compared with one micron.
constexpr double OutwardPaddingMetres=1.0e-6; constexpr double OutwardPaddingMetres=1.0e-6;
@@ -5356,9 +5504,10 @@ public:
bIslandOtherStrateOverlap=true; bIslandOtherStrateOverlap=true;
if(CaveBound&&CaveBound!=Bound)bBadPlan=true; if(CaveBound&&CaveBound!=Bound)bBadPlan=true;
CaveBound=Bound; CaveBound=Bound;
if(!Bound->Resolved||Bound->Resolved->recipeId!="Core.Caves.PrimitiveField" if(!Bound->Resolved||!VoxelForgeCore::HasNativeGraphField(*Bound->Resolved)
||Bound->Resolved->recipeVersion!=1 ||(VoxelForgeCore::IsNativeCaveFieldStratum(*Bound->Resolved)
||!Bound->Resolved->passageCapabilities.hasNativeCaveField ?!Bound->Resolved->passageCapabilities.hasNativeCaveField
:!Bound->Resolved->passageCapabilities.hasNativeLatticeField)
||!std::get_if<FCavesFieldAdapter>(&Bound->Evaluator))bBadPlan=true; ||!std::get_if<FCavesFieldAdapter>(&Bound->Evaluator))bBadPlan=true;
} }
else else
@@ -6768,9 +6917,10 @@ private:
const auto* bound=FindBoundStratumForManagerZ(Z); const auto* bound=FindBoundStratumForManagerZ(Z);
if(!bound)return density; if(!bound)return density;
if(bound->Kind==FBoundStratumField::EKind::Caves if(bound->Kind==FBoundStratumField::EKind::Caves
&&bound->Resolved&&bound->Resolved->recipeId=="Core.Caves.PrimitiveField" &&bound->Resolved&&VoxelForgeCore::HasNativeGraphField(*bound->Resolved)
&&bound->Resolved->recipeVersion==1 &&(VoxelForgeCore::IsNativeCaveFieldStratum(*bound->Resolved)
&&bound->Resolved->passageCapabilities.hasNativeCaveField) ?bound->Resolved->passageCapabilities.hasNativeCaveField
:bound->Resolved->passageCapabilities.hasNativeLatticeField))
return ApplyResolvedNativeCavesField(density,X,Y,Z,*bound,tileQueries,scoped); return ApplyResolvedNativeCavesField(density,X,Y,Z,*bound,tileQueries,scoped);
const double micronsPerVoxel=Owner->CoreVoxelSizeMicrons; const double micronsPerVoxel=Owner->CoreVoxelSizeMicrons;
const std::array<double,3> worldMicrometres={X*micronsPerVoxel, const std::array<double,3> worldMicrometres={X*micronsPerVoxel,
@@ -6937,6 +7087,10 @@ private:
candidates=&localCandidates; candidates=&localCandidates;
} }
double caveVoid=std::numeric_limits<double>::infinity(); double caveVoid=std::numeric_limits<double>::infinity();
double latticeFloorSupportMinimum=std::numeric_limits<double>::infinity();
const bool nativeLattice=bound.Resolved
&&VoxelForgeCore::IsNativeLatticeFieldStratum(*bound.Resolved)
&&bound.Resolved->passageCapabilities.hasNativeLatticeField;
for(const std::uint32_t index:*candidates) for(const std::uint32_t index:*candidates)
{ {
if(index>=CavernRuntime.size())continue; if(index>=CavernRuntime.size())continue;
@@ -6963,9 +7117,16 @@ private:
else if(feature.edge&&!feature.segments.empty()) else if(feature.edge&&!feature.segments.empty())
{ {
double tube=std::numeric_limits<double>::infinity(); double tube=std::numeric_limits<double>::infinity();
double nearestFloor=0.0,nearestGrade=0.0; double nearestFloor=0.0,nearestGrade=0.0,nearestRadial=0.0;
for(const auto& segment:feature.segments) const double radius=feature.edge->boreRadiusMicrometres/1000000.0;
const double height=feature.edge->boreHeightMicrometres/1000000.0;
if(!(radius>0.0&&height>0.0))continue;
const double minScale=std::min(radius,height);
std::uint32_t nearestSegment=std::numeric_limits<std::uint32_t>::max();
const auto evaluateSegment=[&](std::uint32_t segmentIndex)
{ {
if(segmentIndex>=feature.segments.size())return;
const auto& segment=feature.segments[segmentIndex];
double lengthSquared=0.0,dot=0.0; double lengthSquared=0.0,dot=0.0;
for(std::size_t axis=0;axis<3;++axis) for(std::size_t axis=0;axis<3;++axis)
{ {
@@ -6983,22 +7144,64 @@ private:
if(axis<2)radialSquared+=delta*delta; if(axis<2)radialSquared+=delta*delta;
else vertical=delta; else vertical=delta;
} }
const double radius=feature.edge->boreRadiusMicrometres/1000000.0;
const double height=feature.edge->boreHeightMicrometres/1000000.0;
if(!(radius>0.0&&height>0.0))continue;
const double normalized=std::sqrt(radialSquared/(radius*radius) const double normalized=std::sqrt(radialSquared/(radius*radius)
+vertical*vertical/(height*height)); +vertical*vertical/(height*height));
const double distance=(normalized-1.0)*std::min(radius,height); const double distance=(normalized-1.0)*std::min(radius,height);
if(distance<tube) if(distance<tube||(distance==tube&&segmentIndex<nearestSegment))
{ {
tube=distance; tube=distance;nearestSegment=segmentIndex;
nearestFloor=segment.start[2]+t*(segment.end[2]-segment.start[2]); nearestFloor=segment.start[2]+t*(segment.end[2]-segment.start[2]);
nearestRadial=std::sqrt(radialSquared);
const double horizontal=std::hypot( const double horizontal=std::hypot(
segment.end[0]-segment.start[0],segment.end[1]-segment.start[1]); segment.end[0]-segment.start[0],segment.end[1]-segment.start[1]);
nearestGrade=horizontal>1.0e-9 nearestGrade=horizontal>1.0e-9
?(segment.end[2]-segment.start[2])/horizontal:0.0; ?(segment.end[2]-segment.start[2])/horizontal:0.0;
} }
};
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 gap=q<low?low-q:q>high?q-high:0.0;
gapSquared+=gap*gap;
}
return (std::sqrt(gapSquared)-1.0)*minScale;
};
if(!feature.segmentNodes.empty())
{
const auto visitNode=[&](const auto& self,int nodeIndex)->void
{
if(nodeIndex<0||static_cast<std::size_t>(nodeIndex)>=feature.segmentNodes.size())return;
const auto& node=feature.segmentNodes[static_cast<std::size_t>(nodeIndex)];
if(nodeLowerBound(node)>tube)return;
if(node.left<0)
{
for(std::size_t offset=node.first;offset<node.end;++offset)
evaluateSegment(feature.segmentOrder[offset]);
return;
}
const double leftBound=nodeLowerBound(feature.segmentNodes[
static_cast<std::size_t>(node.left)]);
const double rightBound=nodeLowerBound(feature.segmentNodes[
static_cast<std::size_t>(node.right)]);
if(leftBound<=rightBound)
{self(self,node.left);self(self,node.right);}
else {self(self,node.right);self(self,node.left);}
};
visitNode(visitNode,0);
} }
else for(std::uint32_t segment=0;segment<feature.segments.size();++segment)
evaluateSegment(segment);
const double supportDepth=feature.edge->floorSupportDepthMicrometres/1000000.0;
if(nativeLattice&&supportDepth>0.0&&nearestRadial<radius-0.12
&&point[2]<nearestFloor&&nearestFloor-point[2]<=supportDepth)
latticeFloorSupportMinimum=std::min(latticeFloorSupportMinimum,
(point[2]-nearestFloor)/std::sqrt(1.0+nearestGrade*nearestGrade));
const double floor=(nearestFloor-point[2]) const double floor=(nearestFloor-point[2])
/std::sqrt(1.0+nearestGrade*nearestGrade); /std::sqrt(1.0+nearestGrade*nearestGrade);
const double width=feature.edge->smoothWidthMicrometres/1000000.0; const double width=feature.edge->smoothWidthMicrometres/1000000.0;
@@ -7007,8 +7210,13 @@ private:
if(!std::isfinite(featureVoid))continue; if(!std::isfinite(featureVoid))continue;
if(!std::isfinite(caveVoid))caveVoid=featureVoid; if(!std::isfinite(caveVoid))caveVoid=featureVoid;
else caveVoid=SmoothMinMetres(caveVoid,featureVoid, else caveVoid=SmoothMinMetres(caveVoid,featureVoid,
feature.room?feature.room->smoothWidthMicrometres/1000000.0 feature.room
:feature.edge->smoothWidthMicrometres/1000000.0); ?(feature.room->unionWidthMicrometres>0
?feature.room->unionWidthMicrometres
:feature.room->smoothWidthMicrometres)/1000000.0
:(feature.edge->unionWidthMicrometres>0
?feature.edge->unionWidthMicrometres
:feature.edge->smoothWidthMicrometres)/1000000.0);
} }
// A versioned native caves recipe owns a solid host-rock field. The // A versioned native caves recipe owns a solid host-rock field. The
// resolved interval oracle uses the same finite -32 m solid value; using // resolved interval oracle uses the same finite -32 m solid value; using
@@ -7016,8 +7224,13 @@ private:
// terrain detail into the cave floor and invalidate those proofs. // terrain detail into the cave floor and invalidate those proofs.
constexpr double HostRockMetres=-32.0; constexpr double HostRockMetres=-32.0;
const double hostDistance=HostRockMetres; const double hostDistance=HostRockMetres;
const double composed=std::isfinite(caveVoid) double composed=std::isfinite(caveVoid)
?SmoothMaxMetres(hostDistance,-caveVoid,0.0):hostDistance; ?SmoothMaxMetres(hostDistance,-caveVoid,0.0):hostDistance;
// Keep the floor support owned by the lattice route after smooth-unioning
// neighboring room and corridor voids. This cap is limited to each record's
// explicit support depth so separated helix turns remain independent.
if(std::isfinite(latticeFloorSupportMinimum))
composed=std::min(composed,latticeFloorSupportMinimum);
return static_cast<float>(composed/metresPerVoxel); return static_cast<float>(composed/metresPerVoxel);
} }
+13 -13
View File
@@ -1435,7 +1435,7 @@ namespace VoxelForgeCore
const int32 firstSurfaceChunk=FloorDiv(boundsMin.Z,LayoutChunkWidth); const int32 firstSurfaceChunk=FloorDiv(boundsMin.Z,LayoutChunkWidth);
const int32 lastSurfaceChunk=FloorDiv(boundsMax.Z,LayoutChunkWidth); const int32 lastSurfaceChunk=FloorDiv(boundsMax.Z,LayoutChunkWidth);
const WorldAssemblyStratumPlanV1* selectedPlan=nullptr; const WorldAssemblyStratumPlanV1* selectedPlan=nullptr;
const ResolvedLayoutStratumV2* selectedCaveStratum=nullptr; const ResolvedLayoutStratumV2* selectedGraphStratum=nullptr;
for(int32 chunkZ=firstSurfaceChunk;chunkZ<=lastSurfaceChunk;++chunkZ) for(int32 chunkZ=firstSurfaceChunk;chunkZ<=lastSurfaceChunk;++chunkZ)
{ {
const int32 strateIndex=impl_->owner->Manager->GetStrateIndex( const int32 strateIndex=impl_->owner->Manager->GetStrateIndex(
@@ -1444,22 +1444,22 @@ namespace VoxelForgeCore
const auto resolved=std::find_if(impl_->resolvedLayout->strates.begin(), const auto resolved=std::find_if(impl_->resolvedLayout->strates.begin(),
impl_->resolvedLayout->strates.end(),[strateIndex](const auto& stratum) impl_->resolvedLayout->strates.end(),[strateIndex](const auto& stratum)
{return stratum.strateIndex==strateIndex;}); {return stratum.strateIndex==strateIndex;});
const bool nativeCaves=resolved!=impl_->resolvedLayout->strates.end() const bool nativeGraph=resolved!=impl_->resolvedLayout->strates.end()
&&resolved->generatorKind=="Caves" &&HasNativeGraphField(*resolved)
&&resolved->recipeId=="Core.Caves.PrimitiveField" &&(IsNativeCaveFieldStratum(*resolved)
&&resolved->recipeVersion==1 ?resolved->passageCapabilities.hasNativeCaveField
&&resolved->passageCapabilities.hasNativeCaveField; :resolved->passageCapabilities.hasNativeLatticeField);
const auto managerType=impl_->owner->Manager->GetGeneratorTypeForChunk( const auto managerType=impl_->owner->Manager->GetGeneratorTypeForChunk(
FIntVector(0,0,chunkZ)); FIntVector(0,0,chunkZ));
if(!nativeCaves&&managerType!=ECaveGeneratorType::SurfaceWorld)return false; if(!nativeGraph&&managerType!=ECaveGeneratorType::SurfaceWorld)return false;
if(nativeCaves) if(nativeGraph)
{ {
if(selectedCaveStratum&&selectedCaveStratum!=&*resolved)return false; if(selectedGraphStratum&&selectedGraphStratum!=&*resolved)return false;
selectedCaveStratum=&*resolved; selectedGraphStratum=&*resolved;
if(selectedPlan)return false; if(selectedPlan)return false;
continue; continue;
} }
if(selectedCaveStratum)return false; if(selectedGraphStratum)return false;
const auto found=std::find_if(impl_->worldAssembly->plan.strates.begin(), const auto found=std::find_if(impl_->worldAssembly->plan.strates.begin(),
impl_->worldAssembly->plan.strates.end(),[strateIndex,chunkZ](const auto& plan) impl_->worldAssembly->plan.strates.end(),[strateIndex,chunkZ](const auto& plan)
{return plan.strateIndex==strateIndex&&chunkZ<=plan.topChunkZ {return plan.strateIndex==strateIndex&&chunkZ<=plan.topChunkZ
@@ -1468,9 +1468,9 @@ namespace VoxelForgeCore
||(selectedPlan&&selectedPlan!=&*found))return false; ||(selectedPlan&&selectedPlan!=&*found))return false;
selectedPlan=&*found; selectedPlan=&*found;
} }
if((!selectedCaveStratum&&(!selectedPlan if((!selectedGraphStratum&&(!selectedPlan
||!Heightfield2D(impl_->worldAssembly->recipe,*selectedPlan).isValid())) ||!Heightfield2D(impl_->worldAssembly->recipe,*selectedPlan).isValid()))
||(selectedCaveStratum&&selectedPlan)) ||(selectedGraphStratum&&selectedPlan))
return false; return false;
const double coreVoxelsPerOwnerChunk=static_cast<double>( const double coreVoxelsPerOwnerChunk=static_cast<double>(
@@ -0,0 +1,249 @@
#!/usr/bin/env python3
"""Run L54's triangle audit over L58's explicit lattice polylines and controls."""
from __future__ import annotations
import argparse
import importlib.util
import json
import math
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
AUDIT54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/AuditWholeSurfaceL54.py"
spec = importlib.util.spec_from_file_location("whole_surface_l54", AUDIT54)
audit54 = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(audit54)
original_make_route = audit54.make_route
def build_segment_bvh(segments: list[tuple]) -> dict:
def build(indices: list[int]) -> dict:
minimum = tuple(min(min(segments[index][0][axis], segments[index][1][axis])
for index in indices) for axis in range(3))
maximum = tuple(max(max(segments[index][0][axis], segments[index][1][axis])
for index in indices) for axis in range(3))
if len(indices) <= 8:
return {"minimum": minimum, "maximum": maximum,
"segments": tuple(sorted(indices)), "left": None, "right": None}
axis = max(range(3), key=lambda value: maximum[value] - minimum[value])
indices.sort(key=lambda index: (segments[index][0][axis]
+ segments[index][1][axis]) * 0.5)
middle = len(indices) // 2
return {"minimum": minimum, "maximum": maximum, "segments": None,
"left": build(indices[:middle]), "right": build(indices[middle:])}
if not segments:
raise ValueError("lattice audit route has no measurable segments")
return build(list(range(len(segments))))
def make_lattice_route(layout: dict, phase_m: float = 0.0) -> dict:
edges = sorted((edge for edge in layout.get("cavern_edges", [])
if edge["stable_id"] in ("lattice/spine/00-arrival-portal-v1",
"lattice/spine/01-helix-ramp-v1", "lattice/spine/99-departure-portal-v1")
or edge["stable_id"].startswith("lattice/spine/maze-edge-")),
key=lambda edge: (0 if edge["stable_id"].startswith("lattice/spine/00-") else
1 if edge["stable_id"].startswith("lattice/spine/01-") else
2 if edge["stable_id"].startswith("lattice/spine/maze-edge-") else 3,
edge["stable_id"]))
if not edges:
raise ValueError("lattice audit needs its resolved mandatory spine edges")
segments = []
routes = []
for edge_index, edge in enumerate(edges):
points = [[value / 1_000_000.0 for value in row]
for row in edge.get("polyline_um", [])]
if len(points) < 2:
raise ValueError(f"{edge['stable_id']}: explicit path is missing")
radius = edge["bore_radius_um"] / 1_000_000.0
height = edge["bore_height_um"] / 1_000_000.0
horizontal_s = [0.0]
for a, b in zip(points, points[1:]):
horizontal_s.append(horizontal_s[-1] + ((b[0] - a[0]) ** 2
+ (b[1] - a[1]) ** 2) ** 0.5)
first = points[1]
first_dx, first_dy = first[0] - points[0][0], first[1] - points[0][1]
first_length = max(1.0e-9, (first_dx * first_dx + first_dy * first_dy) ** 0.5)
extension = (points[0][0] - first_dx / first_length * audit54.OPEN_GROUND_EXTENSION_M,
points[0][1] - first_dy / first_length * audit54.OPEN_GROUND_EXTENSION_M,
points[0][2] - (first[2] - points[0][2]) / first_length
* audit54.OPEN_GROUND_EXTENSION_M)
route_points = [extension, *points]
route_s = [-audit54.OPEN_GROUND_EXTENSION_M, *horizontal_s]
for index, (a, b) in enumerate(zip(route_points, route_points[1:])):
segments.append((a, b, route_s[index], route_s[index + 1],
radius, height, edge_index))
routes.append({"edge": edge, "points": points, "radius": radius,
"height": height, "length": horizontal_s[-1], "index": edge_index})
rooms = [{"room": room,
"center": tuple(value / 1_000_000.0 for value in room["center_um"]),
"half": tuple(value / 1_000_000.0 for value in room["half_extents_um"]),
"floor": room["floor_um"] / 1_000_000.0}
for room in sorted(layout.get("cavern_rooms", []), key=lambda row: row["stable_id"])]
return {"segments": segments, "edges": routes, "rooms": rooms, "phase_m": phase_m,
"segment_bvh": build_segment_bvh(segments)}
def make_route(layout: dict, phase_m: float = 0.0) -> dict:
if any(edge.get("stable_id", "").startswith("lattice/")
for edge in layout.get("cavern_edges", [])):
return make_lattice_route(layout, phase_m)
return original_make_route(layout, phase_m)
def lattice_route_nearest(point: tuple[float, float, float], route: dict):
"""Resolve stacked helix turns in 3D, then measure corridor radius in XY."""
best = None
def consider(segment_index: int) -> None:
nonlocal best
a, b, s0, s1, radius, height, edge_index = route["segments"][segment_index]
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
length_squared = dx * dx + dy * dy + dz * dz
t = 0.0 if length_squared <= 1.0e-12 else max(0.0, min(1.0,
((point[0] - a[0]) * dx + (point[1] - a[1]) * dy
+ (point[2] - a[2]) * dz) / length_squared))
x, y, z = (a[axis] + (b[axis] - a[axis]) * t for axis in range(3))
distance3d_squared = ((point[0] - x) ** 2 + (point[1] - y) ** 2
+ (point[2] - z) ** 2)
if (best is None or distance3d_squared < best[0]
or (distance3d_squared == best[0] and segment_index < best[1])):
horizontal_length = math.hypot(dx, dy)
lateral = ((dx * (point[1] - y) - dy * (point[0] - x))
/ horizontal_length if horizontal_length > 1.0e-9 else 0.0)
best = (distance3d_squared, segment_index,
math.hypot(point[0] - x, point[1] - y),
s0 + (s1 - s0) * t, z, lateral, radius, height, edge_index,
route["edges"][edge_index]["length"])
def lower_bound_squared(node: dict) -> float:
distance_squared = 0.0
for axis in range(3):
gap = (node["minimum"][axis] - point[axis]
if point[axis] < node["minimum"][axis]
else point[axis] - node["maximum"][axis]
if point[axis] > node["maximum"][axis] else 0.0)
distance_squared += gap * gap
return distance_squared
def visit(node: dict) -> None:
if best is not None and lower_bound_squared(node) > best[0]:
return
if node["segments"] is not None:
for segment_index in node["segments"]:
consider(segment_index)
return
left, right = node["left"], node["right"]
left_bound, right_bound = lower_bound_squared(left), lower_bound_squared(right)
if left_bound <= right_bound:
visit(left)
visit(right)
else:
visit(right)
visit(left)
visit(route["segment_bvh"])
if best is None:
raise ValueError("lattice audit route has no measurable segments")
return best[2:]
def mark_lattice_region(triangles: list, route: dict) -> None:
margin = audit54.CORRIDOR_MARGIN_M
extension = audit54.OPEN_GROUND_EXTENSION_M
bounds = []
for edge in route["edges"]:
points = edge["points"]
radius, height = edge["radius"], edge["height"]
bounds.append((
min(point[0] for point in points) - radius - margin,
max(point[0] for point in points) + radius + margin,
min(point[1] for point in points) - radius - margin,
max(point[1] for point in points) + radius + margin,
min(point[2] for point in points) - margin,
max(point[2] for point in points) + height + margin))
for room in route["rooms"]:
center, half, floor = room["center"], room["half"], room["floor"]
bounds.append((center[0] - half[0] - margin, center[0] + half[0] + margin,
center[1] - half[1] - margin, center[1] + half[1] + margin,
floor - margin, floor + 2.0 * half[2] + margin))
for triangle in triangles:
x, y, z = triangle.center
if not any(low_x <= x <= high_x and low_y <= y <= high_y
and low_z <= z <= high_z
for low_x, high_x, low_y, high_y, low_z, high_z in bounds):
triangle.in_region = False
triangle.reference = False
continue
nearest = lattice_route_nearest(triangle.center, route)
distance, distance_s, floor_z, lateral, radius, height, edge_index, edge_length = nearest
corridor = (-extension <= distance_s <= edge_length
and distance <= radius + margin
and floor_z - margin <= z <= floor_z + height + margin)
room_box = any(abs(x - room["center"][0]) <= room["half"][0] + margin
and abs(y - room["center"][1]) <= room["half"][1] + margin
and room["floor"] - margin <= z
<= room["floor"] + 2.0 * room["half"][2] + margin
for room in route["rooms"])
triangle.in_region = corridor or room_box
inside_room = any(abs(x - room["center"][0]) <= room["half"][0] + 0.45
and abs(y - room["center"][1]) <= room["half"][1] + 0.45
for room in route["rooms"])
outside_bore = distance > radius + 0.45 and not inside_room
triangle.reference = (triangle.in_region and outside_bore
and triangle.smooth[2] >= 0.45)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--case", nargs=5, action="append", required=True,
metavar=("LABEL", "MESH", "LAYOUT_OR_DASH", "EXPECT", "FOCUS"))
parser.add_argument("--json", type=Path, required=True)
parser.add_argument("--svg-dir", type=Path)
parser.add_argument("--phase", type=float, choices=(-0.5, 0.0, 0.5), default=0.0)
args = parser.parse_args()
audit54.make_route = make_route
results = []
for label, mesh, layout, expect, focus in args.case:
if expect not in ("pass", "fail", "measure") or focus not in ("whole", "floor"):
parser.error("EXPECT must be pass/fail/measure and FOCUS must be whole/floor")
native_lattice = layout != "-" and any(
edge.get("stable_id", "").startswith("lattice/")
for edge in json.loads(Path(layout).read_text(encoding="utf-8")).get(
"cavern_edges", []))
original_margin = audit54.CORRIDOR_MARGIN_M
original_extension = audit54.OPEN_GROUND_EXTENSION_M
original_nearest = audit54.route_nearest
original_mark_region = audit54.mark_region
if native_lattice:
# Keep the whole-surface test on native records: one core voxel of
# tolerance captures their meshed walls while excluding the separate
# legacy transition above the resolved mouth.
audit54.CORRIDOR_MARGIN_M = 0.25
audit54.OPEN_GROUND_EXTENSION_M = 0.0
audit54.route_nearest = lattice_route_nearest
audit54.mark_region = mark_lattice_region
try:
results.append(audit54.audit_case(label, Path(mesh), Path(layout), expect,
focus, args.svg_dir, args.phase))
finally:
audit54.CORRIDOR_MARGIN_M = original_margin
audit54.OPEN_GROUND_EXTENSION_M = original_extension
audit54.route_nearest = original_nearest
audit54.mark_region = original_mark_region
if native_lattice:
print("[L58LatticeAuditScope] margin_m=0.250 outside_mouth_extension_m=0.000 "
"nearest_centerline=3D radius_measure=horizontal status=PASS")
args.json.parent.mkdir(parents=True, exist_ok=True)
args.json.write_text(json.dumps({"audit": "whole-surface-lattice-l58-v1",
"cases": results}, indent=2) + "\n", encoding="utf-8")
failures = [result for result in results if result["status"] != "PASS"]
print(f"[L58WholeSurfaceAuditSummary] cases={len(results)} failed={len(failures)} "
f"json={args.json} status={'FAIL' if failures else 'PASS'}")
return 1 if failures else 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Split the owner surface strate around a small, opt-in native maze strate."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--owner", type=Path, required=True)
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
root = json.loads(args.owner.read_text(encoding="utf-8"))
strates = root.get("strates")
if not isinstance(strates, list):
raise ValueError("owner input has no strates array")
by_index = {int(row.get("index", -1)): row for row in strates if isinstance(row, dict)}
if set(by_index) != {0, 1} or len(strates) != 2:
raise ValueError("fixture requires the reviewed two-strate owner input")
upper, surface = by_index[0], by_index[1]
if (int(upper.get("asset", {}).get("GeneratorType", -1)) != 0
or int(surface.get("asset", {}).get("GeneratorType", -1)) != 4):
raise ValueError("fixture requires TunnelNetwork over SurfaceWorld")
if int(upper.get("passage_config", {}).get("Connections", -1)) != 1:
raise ValueError("owner arrival passage count differs from the reviewed input")
# Keep both resolved mouths in the small synthetic Maze layer. The authored
# owner reach ranges are intentionally broad and can place a synthetic mouth
# hundreds of metres beyond a four-chunk prototype layer.
upper["passage_config"].update({"ReachMin": 32, "ReachMax": 32})
upper["asset"]["PassageConfig"].update({"ReachMin": 32, "ReachMax": 32})
maze = json.loads(json.dumps(surface))
maze.update({"index": 1, "top_chunk_z": -16, "bottom_chunk_z": -19,
"height_chunks": 4})
maze["asset"].update({"StrateName": "L58 Native Lattice Maze",
"GeneratorType": 3, "StrateHeightInChunks": 4})
maze["asset"].setdefault("MazeParams", {})["BoundarySealThickness"] = 8
maze["passage_config"]["Connections"] = 1
maze["passage_config"].update({"ReachMin": 32, "ReachMax": 32})
maze["asset"]["PassageConfig"].update({"Connections": 1, "ReachMin": 32, "ReachMax": 32})
lower = json.loads(json.dumps(surface))
lower.update({"index": 2, "top_chunk_z": -20, "bottom_chunk_z": -23,
"height_chunks": 4})
lower["asset"].update({"StrateName": "L58 Lower Surface", "GeneratorType": 4,
"StrateHeightInChunks": 4})
lower["passage_config"]["Connections"] = 1
lower["asset"]["PassageConfig"]["Connections"] = 1
root["total_strates"] = 3
root["strates"] = [upper, maze, lower]
# Keep the legacy passage portals proportional to this compact native
# prototype; the owner world remains at its authored scale.
root.update({"owner_voxel_size_cm": 6.25,
"owner_voxel_size_microns": 62500,
"core_voxel_size_cm": 25,
"core_voxel_size_microns": 250000,
"owner_to_core_length_scale": 0.25})
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(root, indent=2, sort_keys=True) + "\n",
encoding="utf-8")
print(f"[LatticeFixtureBuild] source={args.owner} source_sha256={sha256(args.owner)} "
f"fixture={args.out} fixture_sha256={sha256(args.out)} "
"strates=0:TunnelNetwork,1:Maze[4 chunks],2:SurfaceWorld[60 chunks] "
"owner_input_untouched=true status=PASS")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Export the L58 native lattice view and render four reviewed viewpoints."""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import math
import os
from pathlib import Path
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[4]
CAPTURE54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/CaptureNativeCavesL54.py"
spec = importlib.util.spec_from_file_location("capture_native_caves_l54", CAPTURE54)
capture54 = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(capture54)
run_step = capture54.run_step
camera_angles = capture54.camera_angles
filter_mesh_y_leq = capture54.filter_mesh_y_leq
fmt = capture54.fmt
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def midpoint(points: list[list[float]]) -> tuple[float, float, float]:
return tuple(sum(point[axis] for point in points) / len(points) for axis in range(3))
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--fixture", type=Path, required=True)
parser.add_argument("--layout", type=Path, required=True)
parser.add_argument("--headless", type=Path, required=True)
parser.add_argument("--renderer", type=Path, required=True)
parser.add_argument("--artifact-dir", type=Path, required=True)
args = parser.parse_args()
out = args.out.resolve()
artifact_dir = args.artifact_dir.resolve()
capture_dir = out / "lattice-capture-png"
mesh_dir = out / "lattice-meshes"
for directory in (capture_dir, mesh_dir, artifact_dir):
directory.mkdir(parents=True, exist_ok=True)
layout = json.loads(args.layout.read_text(encoding="utf-8"))
edges = sorted(layout.get("cavern_edges", []), key=lambda row: row["stable_id"])
helix = next((edge for edge in edges
if edge["stable_id"] == "lattice/spine/01-helix-ramp-v1"), None)
maze_edges = [edge for edge in edges
if edge["stable_id"].startswith("lattice/spine/maze-edge-")]
if helix is None or len(maze_edges) < 4:
raise ValueError("layout is missing the resolved helix or small maze spine")
helix_points = [[value / 1_000_000.0 for value in point]
for point in helix["polyline_um"]]
maze_points = [[value / 1_000_000.0 for value in point]
for point in maze_edges[0]["polyline_um"]]
shaft_center = midpoint(helix_points)
top = helix_points[0]
bottom = helix_points[-1]
whole_mesh = mesh_dir / "lattice-whole-view.vfmesh"
export_layout = out / "layouts" / "lattice-capture-layout.json"
env = os.environ.copy()
env["LP_NUM_THREADS"] = "12"
env["XDG_CACHE_HOME"] = str(out / "xdg-cache")
export_camera = (top[0], top[1], top[2] + 1.5, 0.0, -8.0)
run_step("lattice-whole-view-export", [args.headless, args.fixture,
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
"--stream-pipeline", "--force-generate", "--clip-radius=4",
"--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0",
"--recipe=lattice", "--lattice-strate=1",
f"--camera={fmt(export_camera)}", f"--resolved-layout-output={export_layout}",
f"--mesh-export={whole_mesh}"], out / "capture-lattice-export.log",
env=env, stamped=True)
if not whole_mesh.is_file() or whole_mesh.stat().st_size == 0:
raise RuntimeError("whole-lattice export produced no mesh")
cutaway_mesh = mesh_dir / "lattice-cutaway-y-center.vfmesh"
cutaway_triangles = filter_mesh_y_leq(whole_mesh, cutaway_mesh, shaft_center[1])
print(f"[L58LatticeCutaway] plane_y_m={shaft_center[1]:.3f} "
f"retained_triangles={cutaway_triangles} mesh={cutaway_mesh}", flush=True)
# Stand over the clear central opening so the top tread does not occlude
# the view down the shaft.
top_camera = (shaft_center[0], shaft_center[1] - 0.8, top[2] + 2.0)
top_target = (shaft_center[0] + 0.2, shaft_center[1], bottom[2] + 1.3)
eye_index = len(helix_points) // 2
eye_floor = helix_points[eye_index]
eye_tangent = tuple(helix_points[eye_index + 1][axis]
- helix_points[eye_index - 1][axis] for axis in range(3))
eye_len = max(1.0e-9, math.hypot(eye_tangent[0], eye_tangent[1]))
eye_camera = (eye_floor[0], eye_floor[1], eye_floor[2] + 1.45)
# Look across the shaft center from the ramp so the adjacent higher turn
# reads in the same frame. The view clips the near half of the shaft wall.
eye_target = (shaft_center[0], shaft_center[1], eye_camera[2] + 4.2)
maze_a, maze_b = maze_points[0], maze_points[-1]
maze_camera = ((maze_a[0] + maze_b[0]) * 0.5,
(maze_a[1] + maze_b[1]) * 0.5,
(maze_a[2] + maze_b[2]) * 0.5 + 1.45)
maze_target = (maze_camera[0] + (maze_b[0] - maze_a[0]) * 0.65,
maze_camera[1] + (maze_b[1] - maze_a[1]) * 0.65,
maze_camera[2] + (maze_b[2] - maze_a[2]) * 0.65)
overview_camera = (shaft_center[0], shaft_center[1] - 28.0,
(top[2] + bottom[2]) * 0.5)
overview_target = (shaft_center[0], shaft_center[1], (top[2] + bottom[2]) * 0.5)
views = (
("helix-top-down", whole_mesh, top_camera, top_target, 22.0, 70.0, None),
("ramp-eye-next-turn", whole_mesh, eye_camera, eye_target, 12.0, 76.0,
(0.0, 1.0, 0.0, -shaft_center[1])),
("maze-eye-level", whole_mesh, maze_camera, maze_target, 14.0, 82.0, None),
("cutaway-overview", cutaway_mesh, overview_camera, overview_target, 40.0, 80.0, None),
)
quality = ROOT / "Standalone/Tools/verify/capture_quality.py"
for name, render_mesh, camera, target, focus_radius, fov, clip_plane in views:
yaw, pitch = camera_angles(camera, target)
if name == "ramp-eye-next-turn":
yaw, pitch = 45.0, 10.0
png = capture_dir / f"lattice-{name}.png"
image = artifact_dir / f"lattice-{name}.jpg"
render_args = [args.renderer, render_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",
f"--height-center-m={(top[2] + bottom[2]) * 0.5:.6f}",
"--height-range-m=24", "--tile-edges=off",
f"--focus-region={fmt((*target, focus_radius))}"]
if clip_plane is not None:
render_args.append(f"--clip-plane={fmt(clip_plane)}")
run_step(f"{name}-render", render_args,
out / f"capture-{name}-render.log", env=env, stamped=True)
run_step(f"{name}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel",
"error", "-y", "-i", png, "-frames:v", "1", "-q:v", "5", image],
out / f"capture-{name}-jpeg.log", env=env)
quality_output = run_step(f"{name}-quality", [sys.executable, quality, image],
out / f"capture-{name}-quality.log", env=env)
quality_lines = [line for line in quality_output.splitlines()
if line.startswith("[CaptureQuality]")]
if not quality_lines or not quality_lines[-1].endswith("status=PASS"):
raise RuntimeError(f"capture quality gate failed for {name}")
size = image.stat().st_size
if size > 300_000:
raise RuntimeError(f"capture exceeds 300 KB: {image} ({size} bytes)")
print(f"[L58LatticeCapture] view={name} file={image} bytes={size} "
f"sha256={sha256(image)} status=PASS", flush=True)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as error:
print(f"[L58LatticeCapture] status=FAIL error={error}", file=sys.stderr, flush=True)
raise SystemExit(1)
@@ -0,0 +1,51 @@
# Linux58 — Native lattice, slice 1
## Records and geometry
The fixture uses seed `0`, strate `1`, and recipe `Core.Lattice.PrimitiveField` version `1`. Room and edge IDs are stable and keyed by `(seed, strate, recipe_version, stable_id)`. The resolved arrival is `passage/0-1/0` lower at `(9.142, -2.073, -122.500) m`; departure is `passage/1-2/0` upper at `(12.271, -3.565, -149.500) m`. The plan resolves 9 rooms and 9 edges before branches: arrival/departure rooms, maze cells `00`–`04`, two dead-end rooms; arrival portal, helix ramp, maze edges `00`–`03`, departure portal, and two dead-end edges. The canonical record digest is `639d8b975024c9824c944f8244fae88d796350da5f4168b85d5ec7b501c5a842`.
The mandatory spine descends 20 m through four helix turns: 6 m centreline radius, 3 m clear width, 5 m pitch, and 2.6 m finished floor-to-soffit. Measured grades are 7.555° at the centreline and 10.028° at the inner edge. The cubic smooth-max fillet measures `0.570778 m` (required `>=0.500 m`). The prototype’s accepted outer shaft envelope is 21 m. The maze has five cells and two dead ends. This recipe is opt-in; legacy Maze and VerticalShaft assets remain legacy.
## Reachability, intervals, and surface
On the final field, arrival-to-departure reachability passed all 883 stations: 44,150 capsule samples, 4,415 support samples, and 4,415 headroom samples had zero failures. Maximum grade was 10.028° and minimum headroom was 2.200 m. The deliberately disconnected shaft control was rejected (`reachable=no`; zero connected route stations).
The interval oracle checked 4 boxes against 1,372 brute-force samples with zero misses. It rejected both false all-air and false all-solid proofs; `isTileShapeFree` blocked the occupied wall-foot tile. The whole-surface audit covered 89,577 LOD0 triangles across the helix edges and maze wall feet: zero flagged triangles and zero clusters. The L45 ramp, L46 entrance, and L47 wall-foot controls failed as expected; the plain-native control passed.
## Peers and timing
Clang and GCC builds used FP contraction off. W1/W8/W12 matched for four randomized cameras, including one inside the helix, with matching tile hashes and the canonical record digest above.
Clang measured first tile `0.201 s`, near ring `0.345 s`, whole view `1.694 s`, and moving p50/p95 `485.536/977.128 ms`. The whole view is above the 0.7–1.0 s target; moving p95 is just under 1 s.
## Captures and hashes
All four JPEGs passed the capture gate, were visually reviewed, and are below 300 KB.
| View | Bytes | SHA-256 |
|---|---:|---|
| `captures/lattice-helix-top-down.jpg` | 19,616 | `0ab87ee1e799ce96752dc3bf9d6803928e4d6aaad8a87400d899ff3f3be06149` |
| `captures/lattice-ramp-eye-next-turn.jpg` | 22,540 | `d16b0ad577f5ec0e676e18ba62a5de64c71385cd93b53b225ac25f109b184265` |
| `captures/lattice-maze-eye-level.jpg` | 12,361 | `463ee13e962ecc36de1778abcd09f34f5bd375db5537395e7b1e0e0df3d72331` |
| `captures/lattice-cutaway-overview.jpg` | 28,673 | `10204ac2b120ad360e31ac589351c31d0c0e9603feb22841860bd4e145613194` |
Source stamp: `7d181a5b606879ae7277db296e43df11f3fdef41eefa127f5c86d8a431fc4a10`. The owner input remains unchanged at SHA-256 `7b095f9bcc6b90133d5a463f99f0b571133b2c549ce1ecf2d64fe9175e41e183`.
## Tiers, trials, and failed runs
`verify.py fast` passed in 176.4 s; `medium` passed in 1,170.0 s. The single full run on the clean build reached 1,700 s and timed out during the later GCC motif matrix. Before timeout, its L58 GCC field, orphan control, W1/W8/W12 peers, inside-helix camera, and Clang comparisons had passed. No second full run was made.
The run ledger records 10 verification-tier invocations against the 15-trial limit: 3 fast, 6 medium, and 1 full. Earlier failures were a stale style assertion; capture framing/quality; the initial broad transition ROI and boundary seam; and a floor-profile ripple (`0.0887 m`, adjacent step `0.1791 m`) before blend tuning. One direct surface audit initially had a missing `math` import, and one diagnostic parse had a syntax error; both were corrected. A post-tuning fast assertion also used stale blend values. The final fast and medium tiers passed. The only final-tier failure was the full-run timeout above.
## Orchestrator review (Claude)
Accepted. The stamp matches the final Clang and GCC builds (`7d181a5b…`). The full tier had timed out at the builder's
1,740 s process wrapper (the tier itself now runs about 38 min). I re-ran it with the tier's own 7,200 s limit:
**PASS, 2,307 s.** Reachability passes at all 883 stations, the orphan control is rejected, and the surface audit is
clean. Two items for the next lattice slice:
- **Whole view 1.69 s** is above the about-1 s target.
- **It reads as a corkscrew tunnel bored through rock** (separate stacked turns with rock between them). It doesn't
read as an open vertical shaft with a ramp around its wall, which is what "vertical shaft world" implies. Open the
shaft core in slice 2.
Removed the duplicate control SVGs (already in Linux48).
File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 3.1 MiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 2.7 MiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 3.1 MiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 4.8 MiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@@ -0,0 +1,81 @@
# VoxelForge R27 — revised self-check
**Read-only review.** I read the cited code and docs. I made no source changes and ran no scripts, tests, builds, or binaries.
## What exists
**Core:** `HeightfieldPointSample` carries a stratum `waterSurfaceSubvoxels` and a point-level `waterFillSubvoxels` field ([VoxelForgeCore.h:1297](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h:1297>)). `Heightfield2D::point` computes fill only for an enabled water plan and an air sample between the ground and the stratum-wide water level:
```cpp
if(recipe_->evaluatorVersion>=2&&plan_->waterEnabled&&out.densitySubvoxels>=0
&&z>out.groundHeightSubvoxels&&z<plan_->waterSurfaceSubvoxels
&&out.groundHeightSubvoxels<plan_->waterSurfaceSubvoxels)
out.waterFillSubvoxels=std::min(z-out.groundHeightSubvoxels,
plan_->waterSurfaceSubvoxels-z);
```
([WorldAssemblerInternal.h:2610](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/WorldAssemblerInternal.h:2610>)). The Unreal adapter sets that surface from `BottomVoxel + HeightVoxels * WaterLevelRelative` ([VoxelForgeCoreAdapter.cpp:383](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:383>)).
The only `waterFillSubvoxels` consumer I found is the standalone evaluator’s determinism hash: it adds the value to the sample hash and compares results from 1, 8, and 16 workers ([StandaloneViewer.cpp:8448](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StandaloneViewer.cpp:8448>)). I found no local water-body records, water-specific interval query, or water BVH. The existing `FieldInterval` is for density ([VoxelForgeCore.h:1301](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h:1301>)). R24’s separate occupancy channel and local mesh are proposals, not current behavior ([R24 water notes](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research24/REPORT.md:25>)).
**Unreal:** `UVoxelContentManager::UpdateWater` gets a water height for the player’s current stratum, using the Core snapshot when available and otherwise the legacy stratum lookup ([VoxelContentManager.cpp:2530](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelContentManager.cpp:2530>)). It creates one `UStaticMeshComponent` using `PlaneMesh`:
> `WaterPlane->SetCollisionEnabled(ECollisionEnabled::NoCollision);`
> ([VoxelContentManager.cpp:2575](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelContentManager.cpp:2575>))
It scales the engine plane to cover the view distance (or uses a large fallback size) ([VoxelContentManager.cpp:2585](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelContentManager.cpp:2585>)), snaps its center to a coarse player cell, and places it at the selected water height ([VoxelContentManager.cpp:2592](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelContentManager.cpp:2592>)). It assigns the stratum’s `WaterMaterial` ([VoxelContentManager.cpp:2597](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelContentManager.cpp:2597>)). The code comment calls it one global plane that follows the player ([VoxelContentManager.h:94](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelContentManager.h:94>)); `VoxelWorld` calls it each tick ([VoxelWorld.cpp:7039](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7039>)). It is not local stream or pool geometry, and it has no collision. I found no swimming-volume or underwater post-process implementation in this plugin. The `Underwater` generator shares the tunnel density graph and an “water” flag; it does not implement swimming ([VoxelGenerator.cpp:2638](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2638>)).
## Reference image
I did **not** find the requested image in `Docs/`: the `Docs` tree in this checkout contains archived Markdown files and no image assets. The nearest water-named image I found is `Standalone/data/owner_normals_pool_before.png`, outside `Docs`. It shows a broad blue-grey surface against uneven rocky ground, but no clear stream entering a bounded pool. I can’t claim the design matches the missing reference. It does match the written R24 proposal for a local pool, fixed stream, separate water channel, and local mesh ([R24](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research24/REPORT.md:27>)).
## Proposed design
Use a **plan-time deterministic steepest-descent trace** on the resolved terrain, then store its quantized points as an immutable polyline. Sample on a 1 m XY grid; at each step choose the neighboring sample with the greatest downhill grade, breaking ties by a fixed compass order. A local sink ends the trace and becomes the pool site. This follows the earlier drainage note and makes the result reproducible across peers. The polyline is stored data, not simulated flow. Reject a candidate that exceeds the grade limits below rather than altering its path at runtime.
Initial dimensions for one fixture:
- **Stream:** 20 m centerline; target surface fall 2 cm per horizontal metre (2%), with no segment above 5 cm/m (5%). Use a **sloped sheet**, linearly interpolated between fixed-point controls, meeting the pool level exactly. Flat-per-segment surfaces would introduce visible steps at joins.
- **Channel:** 0.60 m flat invert; 0.30 m maximum cut depth; banks at 1 vertical to 4 horizontal. Place the water 0.20 m above the invert, yielding about 2.2 m of wet surface width before edge smoothing.
- **Pool:** 4 m radius, up to 0.60 m deep, with a bowl bed rising to the waterline at its rim. Set the fixed, flat surface 0.05 m below the basin’s lowest spill saddle; blend the carved shoreline into native terrain. The stream’s final surface height equals this level.
Treat all of those dimensions as **unvalidated starting values**. Reject a trace that cannot meet the sheet grade or fit the banks under the resolved terrain.
Add immutable Core pool and stream records. Keep water occupancy `W` separate from rock density `D`; for example:
```text
Wpool = min(z - hpool(r), zpool - z, 4.0 - r)
Wstream = min(z - bed(s,d), zw(s) - z, 1.1 - |d|, s, L - s)
W = max(Wpool, Wstream)
```
Here `s` is distance along the centerline, `d` is cross-stream distance, and positive `W` means inside the water. Keep the rock bed in `D`; clip rendered water to the air side of the bed.
Give each pool and stream segment an outward-rounded AABB in fixed-point units. The pool box covers its 4 m radius and bed-to-surface height. A stream occupancy box covers the segment, 1.1 m wet half-width, and bed-to-surface height; the separate bed-carve bounds should include the wider banks and blend. Query a BVH of these boxes, then conservatively bound `W` over each candidate using interval arithmetic and combine candidates with `max`. If a query misses every water box, it is dry; if a bound is unknown, don’t cull it. This follows R24’s proposed separate `W`, but adds concrete bounds and dimensions ([R24](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research24/REPORT.md:27>)).
**Game references, at the player-facing level:** Minecraft represents water as blocks; it spreads into nearby air, while generated rivers sit at sea level and are calm ([water](https://www.minecraft.net/en-us/article/block-week-water), [rivers](https://www.minecraft.net/en-us/article/around-block--river)). Astroneer’s official notes describe its Simulated Aquarium as a decoration that needs no water, not a terrain stream ([Megatech notes](https://blog.astroneer.space/patch-notes/megatech-update/)). Enshrouded is a more ambitious contrast: its update describes dynamically simulated, redirectable water and swimming/diving ([Wake of the Water](https://enshrouded.com/en-US/news/enshrouded-wake-of-the-water-update)). Valheim documents ocean sailing by boat ([FAQ](https://valheim.com/faq/)). No Man’s Sky emphasizes water rendering and underwater play ([Worlds Part II](https://www.nomanssky.com/worlds-part-ii-update/)). Deep Rock Galactic’s cited update treats frozen lakes as slippery hazards ([update notes](https://www.deeprockgalactic.com/updatesold)). These examples inform the experience; they do not establish those games’ internal water implementations.
## Unreal minimum
The smallest local implementation is a separate water surface mesh generated from the Core records. The plugin already uses `URealtimeMeshComponent` for terrain tiles and declares its module dependency ([VoxelWorld.h:424](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelWorld.h:424>), [VoxelForge.Build.cs:75](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/VoxelForge.Build.cs:75>)); reuse it for water tiles with separate material and no surface collision. The bed remains the walkable collider. For VR, give the surface clear reflection/Fresnel and refraction cues, subtle normal motion, and a readable shoreline. Keep wave and screen-space distortion modest.
Unreal’s Water plugin offers `AWaterBodyRiver`, `AWaterBodyLake`, and `AWaterBodyCustom`; its Water Zone generates water mesh tiles. Its automatic terrain brush works with Landscape edit layers, so it won’t carve these voxel beds for us. A custom water body can use a mesh for a pool, with the bed carved separately ([Water Body Actors](https://dev.epicgames.com/documentation/en-us/unreal-engine/water-body-actors-in-unreal-engine)). The plugin is not listed in VoxelForge’s module dependencies; whether the host project enables it is unverified. `UProceduralMeshComponent` is another runtime mesh option ([API](https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/ProceduralMeshComponent/UProceduralMeshComponent?lang=en-US)), but the existing `URealtimeMeshComponent` is the smaller project fit.
This MVP’s 0.60 m pool and shallow stream don’t need swimming or underwater post-processing to read as water. If deeper pools are later allowed, `APhysicsVolume` exposes `bWaterVolume` and `UCharacterMovementComponent` supports swimming ([APhysicsVolume](https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/APhysicsVolume?lang=en-US), [CharacterMovementComponent](https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/UCharacterMovementComponent?lang=en-US)). Unreal’s Water Body underwater post-process relies on collision to detect submersion ([UWaterBodyComponent](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Plugins/Water/UWaterBodyComponent)).
## Walkability
Keep the water path outside the route corridor by default. The current Core route audit checks a 0.34 m radius, 1.76 m capsule and a 15° maximum grade, but checks density and support, not water depth ([API.md:140](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/API.md:140>)). Extend that audit to water.
I recommend allowing an **intentional ford up to 0.30 m deep**, measured from its walkable bed to the sheet, as a non-swimming crossing. Deeper water must be bypassed or bridged. Keep the 0.60 m pool outside the route corridor. If “dry route” means no wet crossing at all, use the bridge or bypass every time.
## Slices and verification
1. **Core records and bounds:** add fixed-point records, deterministic path output, `W` queries, interval bounds, and BVH. Capture a plan view with the path and bounds overlaid; compare record digests across peers and worker counts.
2. **Carved bed:** apply the stream and pool cuts to density. Capture channel and pool cross-sections; audit route capsule clearance, grade, support, and water depth.
3. **Water mesh:** emit a separate pool disk and stream sheet. Capture the stream-to-pool join, shoreline, and tile boundaries from above and at walking height.
4. **Unreal:** stream local water tiles, apply the material, and add swimming volume/post-process only if deeper water is approved. Capture headset views of the ford, pool edge, and—if implemented—submersion.
I’m unsure whether the missing reference image exists outside this checkout, whether the host project has the Water plugin enabled, and whether the proposed dimensions and 2% grade suit the intended scene. The current route audit also has no water-depth check.
**Owner questions:** Can you provide the reference image’s path or attach it again? Is a 0.30 m ford acceptable, or must the mouth-to-mouth route stay entirely dry?
@@ -0,0 +1,56 @@
# VoxelForge R27 — water MVP research
## What exists
- **Core:** `BuildSurfaceStratumPlan` resolves one fixed water level and enable flag per stratum. `Heightfield2D::point` returns `waterSurfaceSubvoxels` and, for evaluator v2+, a `waterFillSubvoxels` value only in open space between the local ground and that level. It is a useful starting channel, but it describes a stratum-wide vertical fill; there are no pool or stream records. [`WorldAssemblyStratumPlanV1` and sample fields](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h:560>), [`BuildSurfaceStratumPlan`](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:320>), [`Heightfield2D::point`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/WorldAssemblerInternal.h:2576>)
- **Bounds and meshing:** Core’s `FieldInterval` bounds density, not water; it has no water interval query or water-body BVH. The Core contract mentions a water channel, but the current bounds do not cover it. The existing terrain mesh and collision paths are density based, so water needs its own surface-mesh path. [`FieldInterval`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h:1301>), [Core interval contract](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/API.md:120>)
- **Unreal:** `UVoxelContentManager::UpdateWater` uses the player’s stratum water level to place one large, movable `/Engine/BasicShapes/Plane` at the player’s snapped cell, scaled to cover the view. The component has collision disabled and uses the stratum’s `WaterMaterial`. `AVoxelWorld::Tick` calls it each tick. The asset comment explicitly describes water as aesthetic, with no swim or flow simulation. [`UpdateWater`](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelContentManager.cpp:2499>), [`WaterMaterial` and water settings](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelStrateDefinition.h:211>), [tick call](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7038>)
- **Reusable pieces:** retain Core’s fixed-point sampling, stratum water datum, `WaterMaterial`/`WaterColor`, and the terrain bed’s normal collision. Replace the global plane for this profile with a local water-body renderer. Legacy `Underwater` currently builds the same tunnel density stack as `TunnelNetwork`; its water flag is consumed on the rendering side. The atmosphere manager provides fog and skylight, but I found no water-specific swim handling or underwater post-processing. [`Underwater` dispatch](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2638>), [`ApplyFogSky`](</home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelAtmosphereManager.cpp:127>)
## Design
Resolve the terrain and mouth route first. At plan time, trace a deterministic steepest-descent path from a chosen source toward the pool across the resolved surface, then store the result as a fixed polyline or spline. Use Core’s integer coordinates, fixed sampling steps and tie-breaking order; if descent reaches a local minimum, use a deterministic path search to the basin. Store the resolved path, elevations, width and stable body ID in the plan so clients can reproduce or receive the same geometry. This honors the earlier downhill-path preference without simulating water.
Carve a shallow channel along that path, joining it into a basin with a **flat, fixed pool surface**. As initial design ranges, try a 1–2 m stream, a bed about 0.3–0.5 m below adjacent ground, and 0.15–0.3 m of water over the bed. The pool can start in R24’s 4–12 m radius and 0.3–1 m depth range; the shoreline is where the surrounding bank meets the fixed surface. Make stream elevation descend into the pool and record a stable downstream direction for its material. [R24 water proposal](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research24/REPORT.md:25>), [R20 pool dimensions and route guidance](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research20/REPORT.md:133>)
Add a separate positive-inside occupancy field `W` for the pool and stream. Return occupancy and local surface height per query, independently of rock density. Give water its own conservative interval query and BVH over body bounds; use those bounds to schedule water-only tiles even where the rock field classifies as air. Emit a separate water surface mesh from the body records or water field. The density mesher should continue to own rock geometry and collision. This follows R24’s proposed pool/stream records and water-specific tile scheduling. [R24 records and mesh proposal](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research24/REPORT.md:27>)
Comparable games offer different scopes; these references describe player-facing behavior, not necessarily their internal representations:
- **Minecraft** treats water as blocks that spread into adjacent air; its generated rivers are calm and held at sea level. That is a useful example of authored water bodies without visible flow simulation. [Water](https://www.minecraft.net/en-us/article/block-week-water), [Rivers](https://www.minecraft.net/en-us/article/around-block--river)
- **Astroneer:** official material I found describes fluid canisters and a simulated aquarium that needs no water; separate **STARSEEKER** material advertises swimming and diving. I found no official outdoor river or pool system for mainline Astroneer. [Megatech notes](https://blog.astroneer.space/patch-notes/megatech-update/), [STARSEEKER overview](https://blog.astroneer.space/p/starseeker-direct-recap/)
- **Enshrouded** is the full-simulation comparison: its 2025 update adds dynamic water, flow redirection, swimming, diving and oxygen gameplay. That is well beyond this static MVP. [Wake of the Water](https://enshrouded.com/en-US/news/enshrouded-wake-of-the-water-update)
- **Valheim** uses water as world traversal, including sailing; official patch notes also track water VFX and water damage. Those notes do not document its rendering implementation. [Official FAQ](https://www.valheimgame.com/faq/), [Patch 1.0.14](https://www.valheimgame.com/news/patch-1-0-14/)
- **No Man’s Sky** is a richer presentation reference: its official notes describe enhanced water rendering, waves, deep-water hazards and underwater effects. [Worlds Part II](https://www.nomanssky.com/worlds-part-ii-update/), [The Abyss](https://www.nomanssky.com/abyss-update/)
- **Deep Rock Galactic** presents frozen lakes as slippery environmental hazards, a useful cave-navigation comparison rather than a flowing-water model. [Official updates](https://www.deeprockgalactic.com/updatesold)
## Unreal minimum
Use local pool and stream surface meshes with the existing water material as a starting point, plus visible bed, bank and shoreline geometry to establish depth. Add restrained reflection/Fresnel, water tint and depth cues; keep directional flow cues stable and avoid strong animated motion in VR.
Keep the water surface non-colliding. The carved terrain bed remains the walking collision. Add a query-only water volume for immersion state; if the pool is deep enough to swim in, that volume can also switch character movement to swimming. Apply a subtle underwater post-process when the player’s camera is inside the body and below its surface. If the MVP stays shallow, swimming and underwater effects can wait.
Unreal’s Water plugin documents separate spline-defined water bodies, generated surface meshes, depth/flow queries and underwater post-processing. Its automatic terrain carving is Landscape-brush based, so it would not replace the Core bed and occupancy work here. [Water System](https://dev.epicgames.com/documentation/unreal-engine/water-system-in-unreal-engine), [Water Body Actors](https://dev.epicgames.com/documentation/en-us/unreal-engine/water-body-actors-in-unreal-engine)
## Walkability
Keep the mouth-to-mouth capsule route supported and never dependent on swimming. Core already audits a 0.34 m radius, 1.76 m high capsule, support and a 15° maximum grade, but that audit does not evaluate water depth. Extend the route checks to the water channel. A ford can be allowed where water over the walking bed is at most about **0.3 m**; any deeper crossing needs a dry bypass or bridge. [Route audit contract](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/API.md:140>)
## Slices and evidence
1. **Core records:** Add stable pool/stream records, occupancy and surface queries, intervals and BVH. Verify determinism and bounds; capture a diagnostic cross-section showing water and rock separately.
2. **Carved bed:** Add the channel and basin to the resolved terrain. Verify bank/shoreline shape, capsule support and grade; capture an oblique view and a cutaway.
3. **Water mesh:** Generate the pool and stream surfaces separately from density, including water-only tiles. Verify body bounds and tile joins; capture the whole source-to-pool view, shoreline and ford/bridge.
4. **Unreal:** Add local rendering, immersion volumes and optional underwater presentation. Check host/client agreement; capture eye-level, waterline and underwater views where applicable.
No scripts, tests, builds, binaries or edits were made.
**Reference image:** I could not find an image under `Docs`; that directory contains archived Markdown only. The nearest file is [owner_normals_pool_before.png](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/data/owner_normals_pool_before.png), an older global-plane capture rather than a local stream-to-pool reference.
## Owner questions
- Should the first pool remain wadeable at ≤0.3 m, or should the MVP include swimming and underwater post-processing?
- Can you point me to the missing `Docs` reference image or restore it in this checkout?
+328 -11
View File
@@ -64,6 +64,8 @@ namespace
int RenderDistanceOverride=0; int RenderDistanceOverride=0;
bool FloatingIslandsRecipe=false; bool FloatingIslandsRecipe=false;
bool CavesRecipe=false; bool CavesRecipe=false;
bool LatticeRecipe=false;
int LatticeStrateIndex=-1;
bool AuditHeightfieldProfile=false; bool AuditHeightfieldProfile=false;
bool LandmarkChamberDemo=false; bool LandmarkChamberDemo=false;
bool LandmarkL45RampControl=false; bool LandmarkL45RampControl=false;
@@ -71,6 +73,8 @@ namespace
bool AuditOreVeinYield=false; bool AuditOreVeinYield=false;
bool AuditLandmarkReachability=false; bool AuditLandmarkReachability=false;
bool AuditNativeCaves=false; bool AuditNativeCaves=false;
bool AuditNativeLattice=false;
bool LatticeOrphanControl=false;
bool AuditMotifIntervals=false; bool AuditMotifIntervals=false;
bool TlsProbeRequested=false; bool TlsProbeRequested=false;
int TlsProbeWorkers=0; int TlsProbeWorkers=0;
@@ -946,6 +950,283 @@ namespace
return true; return true;
} }
bool MakeNativeLatticeSpine(const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
std::int32_t seed,std::int32_t latticeStrateIndex,bool orphanControl,
std::vector<VoxelForgeCore::ResolvedCavernRoomV1>& rooms,
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1>& edges,std::string& error)
{
using namespace VoxelForgeCore;
rooms.clear();edges.clear();error.clear();
const auto lattice=std::find_if(layout.strates.begin(),layout.strates.end(),
[latticeStrateIndex](const auto& value)
{return value.strateIndex==latticeStrateIndex
&&IsNativeLatticeFieldStratum(value);});
if(lattice==layout.strates.end())
{error="native lattice recipe does not name an opt-in Maze or VerticalShafts strate";return false;}
std::vector<const ResolvedLayoutPassageV1*> arrivals,departures;
for(const auto& passage:layout.passages)
{
if(passage.lowerStrateIndex==latticeStrateIndex)arrivals.push_back(&passage);
if(passage.upperStrateIndex==latticeStrateIndex)departures.push_back(&passage);
}
const auto byStableId=[](const auto* left,const auto* right)
{return left->stableId<right->stableId;};
std::sort(arrivals.begin(),arrivals.end(),byStableId);
std::sort(departures.begin(),departures.end(),byStableId);
if(arrivals.size()!=1||departures.size()!=1)
{error="native lattice spine requires exactly one resolved arrival and one departure mouth";return false;}
if(layout.coreVoxelSizeMicrometres==0)
{error="native lattice spine requires a resolved Core voxel size";return false;}
const double micrometresPerSubvoxel=static_cast<double>(layout.coreVoxelSizeMicrometres)
/WorldAssemblySubvoxelsPerVoxel;
const auto toMicrometres=[&](const std::array<std::int64_t,3>& point)
{
std::array<std::int64_t,3> result{};
for(std::size_t axis=0;axis<3;++axis)
result[axis]=static_cast<std::int64_t>(std::llround(
point[axis]*micrometresPerSubvoxel));
return result;
};
const auto toMetres=[](const std::array<std::int64_t,3>& point)
{return std::array<double,3>{point[0]/1.0e6,point[1]/1.0e6,point[2]/1.0e6};};
const auto micronPoint=[](const std::array<double,3>& point)
{
std::array<std::int64_t,3> result{};
for(std::size_t axis=0;axis<3;++axis)
result[axis]=static_cast<std::int64_t>(std::llround(point[axis]*1.0e6));
return result;
};
const auto tangentAt=[&](const ResolvedLayoutPassageV1& passage,bool lower,
std::array<double,2>& direction)
{
if(passage.controlPoints.size()<2)return false;
const std::size_t index=lower?std::min(passage.lowerMouthControlIndex,
passage.controlPoints.size()-1):0;
const std::size_t other=lower?(index>0?index-1:passage.controlPoints.size()-2)
:std::min<std::size_t>(1,passage.controlPoints.size()-1);
const auto& a=passage.controlPoints[lower?other:index].positionSubvoxels;
const auto& b=passage.controlPoints[lower?index:other].positionSubvoxels;
const double dx=static_cast<double>(b[0]-a[0]);
const double dy=static_cast<double>(b[1]-a[1]);
const double length=std::hypot(dx,dy);
if(!(length>1.0e-9))return false;
direction={dx/length,dy/length};
return true;
};
std::array<double,2> arrivalTangent{},departureTangent{};
if(!tangentAt(*arrivals.front(),true,arrivalTangent)
||!tangentAt(*departures.front(),false,departureTangent))
{error="native lattice mouth has no finite resolved passage tangent";return false;}
const auto arrival=toMetres(toMicrometres(arrivals.front()->lowerMouthSubvoxels));
const auto departure=toMetres(toMicrometres(departures.front()->upperMouthSubvoxels));
constexpr double MouthPlayerCenterHeight=1.13;
const double arrivalFloor=arrival[2]-MouthPlayerCenterHeight;
const double departureFloor=departure[2]-MouthPlayerCenterHeight;
const std::array<double,3> helixStart={arrival[0]+arrivalTangent[0]*3.0,
arrival[1]+arrivalTangent[1]*3.0,arrivalFloor};
constexpr double Pi=3.1415926535897932384626433832795;
constexpr double CenterlineRadius=6.0;
constexpr double ClearWidth=3.0;
constexpr double FloorToSoffit=2.6;
constexpr double Pitch=5.0;
constexpr int Turns=4;
constexpr double FloorBlend=1.9;
constexpr double FeatureUnionBlend=0.05;
constexpr double CorridorSpacing=14.0;
constexpr double MazeCellSpan=16.0;
constexpr double RouteGradeLimit=13.0;
const std::array<double,2> lateral={-arrivalTangent[1],arrivalTangent[0]};
const std::array<double,2> helixCenter={
helixStart[0]+lateral[0]*CenterlineRadius,
helixStart[1]+lateral[1]*CenterlineRadius};
const double helixEndFloor=arrivalFloor-Pitch*Turns;
const std::array<double,3> helixEnd={helixStart[0],helixStart[1],helixEndFloor};
const double mouthEndX=departure[0]-departureTangent[0]*3.0;
const double mouthEndY=departure[1]-departureTangent[1]*3.0;
const auto featureRoll=[&](std::string_view stableId)
{
std::uint64_t hash=14695981039346656037ull;
const auto addWord=[&](std::uint64_t word)
{for(int byte=0;byte<8;++byte){hash^=static_cast<std::uint8_t>(word>>(byte*8));hash*=1099511628211ull;}};
addWord(static_cast<std::uint32_t>(seed));
addWord(static_cast<std::uint32_t>(latticeStrateIndex));
addWord(1);
for(unsigned char value:stableId){hash^=value;hash*=1099511628211ull;}
hash^=hash>>30;hash*=0xbf58476d1ce4e5b9ull;
hash^=hash>>27;hash*=0x94d049bb133111ebull;
return hash^(hash>>31);
};
const auto addRoom=[&](std::string id,const std::array<double,3>& floorCenter)
{
ResolvedCavernRoomV1 room;room.stableId=std::move(id);
room.strateIndex=latticeStrateIndex;
room.centerMicrometres=micronPoint({floorCenter[0],floorCenter[1],
floorCenter[2]+FloorToSoffit*0.5});
room.halfExtentsMicrometres={2000000,2000000,
static_cast<std::int64_t>(std::llround(FloorToSoffit*500000.0))};
room.floorMicrometres=static_cast<std::int64_t>(std::llround(floorCenter[2]*1.0e6));
room.smoothWidthMicrometres=static_cast<std::int64_t>(std::llround(FloorBlend*1.0e6));
room.unionWidthMicrometres=static_cast<std::int64_t>(
std::llround(FeatureUnionBlend*1.0e6));
room.irregularitySeed=featureRoll(room.stableId);
rooms.push_back(std::move(room));
};
const auto addEdge=[&](std::string id,std::string startAnchor,std::string endRoom,
std::vector<std::array<double,3>> path)
{
ResolvedCavernEdgeV1 edge;edge.stableId=std::move(id);
edge.startAnchorStableId=std::move(startAnchor);
edge.endRoomStableId=std::move(endRoom);edge.strateIndex=latticeStrateIndex;
edge.boreRadiusMicrometres=1500000;
edge.boreHeightMicrometres=static_cast<std::int64_t>(
std::llround(FloorToSoffit*1.0e6));
edge.floorSupportDepthMicrometres=1000000;
edge.smoothWidthMicrometres=static_cast<std::int64_t>(std::llround(FloorBlend*1.0e6));
edge.unionWidthMicrometres=static_cast<std::int64_t>(
std::llround(FeatureUnionBlend*1.0e6));
edge.irregularitySeed=featureRoll(edge.stableId);
if(path.size()<2)return edge;
for(const auto& point:path)edge.polylineMicrometres.push_back(micronPoint(point));
const auto& first=path.front();const auto& last=path.back();
for(std::size_t control=0;control<4;++control)
{
const double t=static_cast<double>(control)/3.0;
for(std::size_t axis=0;axis<3;++axis)
edge.controlPointsMicrometres[control][axis]=static_cast<std::int64_t>(
std::llround(std::lerp(first[axis],last[axis],t)*1.0e6));
}
return edge;
};
const std::string arrivalRoomId="lattice/spine/arrival-room-v1";
const std::string mazeCellZeroId="lattice/spine/maze-cell-00-v1";
const std::string helixEntryRoomId=orphanControl
?"lattice/control/orphan-shaft-entry-room-v1"
:arrivalRoomId;
const std::string helixEndRoomId=orphanControl
?"lattice/control/orphan-shaft-end-room-v1":mazeCellZeroId;
const std::array<double,3> orphanOffset=orphanControl
?std::array<double,3>{lateral[0]*20.0,lateral[1]*20.0,0.0}
:std::array<double,3>{0.0,0.0,0.0};
addRoom(arrivalRoomId,helixStart);
if(orphanControl)
addRoom(helixEntryRoomId,{helixStart[0]+orphanOffset[0],
helixStart[1]+orphanOffset[1],helixStart[2]});
addRoom(mazeCellZeroId,helixEnd);
if(orphanControl)
addRoom(helixEndRoomId,{helixEnd[0]+orphanOffset[0],
helixEnd[1]+orphanOffset[1],helixEnd[2]});
const std::string departureRoomId="lattice/spine/departure-room-v1";
addRoom(departureRoomId,{departure[0],departure[1],departureFloor});
edges.push_back(addEdge("lattice/spine/00-arrival-portal-v1",
arrivals.front()->stableId+"/lower-mouth",arrivalRoomId,
{{arrival[0],arrival[1],arrivalFloor},helixStart}));
std::vector<std::array<double,3>> helixPath;
constexpr double HelixSampleArcLength=0.5;
const double helixLength=std::hypot(2.0*Pi*CenterlineRadius*Turns,
Pitch*Turns);
const int helixSteps=std::max(64,static_cast<int>(std::ceil(
helixLength/HelixSampleArcLength)));
helixPath.reserve(static_cast<std::size_t>(helixSteps)+1);
for(int sample=0;sample<=helixSteps;++sample)
{
const double u=static_cast<double>(sample)/helixSteps;
const double angle=2.0*Pi*Turns*u;
helixPath.push_back({
helixCenter[0]-lateral[0]*CenterlineRadius*std::cos(angle)
+arrivalTangent[0]*CenterlineRadius*std::sin(angle)+orphanOffset[0],
helixCenter[1]-lateral[1]*CenterlineRadius*std::cos(angle)
+arrivalTangent[1]*CenterlineRadius*std::sin(angle)+orphanOffset[1],
arrivalFloor-Pitch*Turns*u});
}
edges.push_back(addEdge("lattice/spine/01-helix-ramp-v1",
helixEntryRoomId,helixEndRoomId,std::move(helixPath)));
double dx=mouthEndX-helixEnd[0],dy=mouthEndY-helixEnd[1];
double horizontal=std::hypot(dx,dy);
double routeX=horizontal>1.0e-9?dx/horizontal:arrivalTangent[0];
double routeY=horizontal>1.0e-9?dy/horizontal:arrivalTangent[1];
double routeSideX=-routeY,routeSideY=routeX;
const double vertical=departureFloor-helixEndFloor;
const int routeEdges=4;
const double requiredRun=std::abs(vertical)/std::tan(RouteGradeLimit*Pi/180.0);
const double sideSpan=std::max(MazeCellSpan,
(requiredRun-horizontal)/3.0);
if(sideSpan>24.0)
{error="native lattice maze spine requires more than 24 m cells to preserve its grade";return false;}
std::vector<std::array<double,2>> mazeXY={
{helixEnd[0],helixEnd[1]},
{helixEnd[0]+routeX*sideSpan,helixEnd[1]+routeY*sideSpan},
{helixEnd[0]+routeX*sideSpan+routeSideX*sideSpan,
helixEnd[1]+routeY*sideSpan+routeSideY*sideSpan},
{mouthEndX+routeSideX*sideSpan,mouthEndY+routeSideY*sideSpan},
{mouthEndX,mouthEndY}};
std::array<double,5> routeDistance{};
for(std::size_t index=1;index<mazeXY.size();++index)
routeDistance[index]=routeDistance[index-1]
+std::hypot(mazeXY[index][0]-mazeXY[index-1][0],
mazeXY[index][1]-mazeXY[index-1][1]);
const double mazeRun=routeDistance.back();
if(!(mazeRun>=requiredRun))
{error="native lattice maze spine cannot satisfy the 13 degree grade limit";return false;}
std::vector<std::array<double,3>> cells(mazeXY.size());
for(std::size_t index=0;index<mazeXY.size();++index)
cells[index]={mazeXY[index][0],mazeXY[index][1],
helixEndFloor+vertical*(routeDistance[index]/mazeRun)};
std::vector<std::string> cellIds(static_cast<std::size_t>(routeEdges)+1);
cellIds[0]=mazeCellZeroId;
for(int index=1;index<=routeEdges;++index)
{
char suffix[16]{};std::snprintf(suffix,sizeof(suffix),"%02d",index);
cellIds[static_cast<std::size_t>(index)]=std::string("lattice/spine/maze-cell-")
+suffix+"-v1";
addRoom(cellIds[static_cast<std::size_t>(index)],cells[static_cast<std::size_t>(index)]);
}
for(int index=0;index<routeEdges;++index)
{
char suffix[16]{};std::snprintf(suffix,sizeof(suffix),"%02d",index);
edges.push_back(addEdge("lattice/spine/maze-edge-"+std::string(suffix)+"-v1",
cellIds[static_cast<std::size_t>(index)],
cellIds[static_cast<std::size_t>(index+1)],
{cells[static_cast<std::size_t>(index)],cells[static_cast<std::size_t>(index+1)]}));
}
const int branchIndices[2]={1,routeEdges-1};
for(int branch=0;branch<2;++branch)
{
const int index=branchIndices[branch];
const auto& base=cells[static_cast<std::size_t>(index)];
const std::string roomId=branch==0?"lattice/branch/dead-end-a-room-v1"
:"lattice/branch/dead-end-b-room-v1";
const std::string edgeId=branch==0?"lattice/branch/dead-end-a-edge-v1"
:"lattice/branch/dead-end-b-edge-v1";
const int sign=branch==0?-1:1;
const std::array<double,3> branchEnd={base[0]+routeSideX*sign*CorridorSpacing,
base[1]+routeSideY*sign*CorridorSpacing,base[2]};
addRoom(roomId,branchEnd);
edges.push_back(addEdge(edgeId,cellIds[static_cast<std::size_t>(index)],
roomId,{base,branchEnd}));
}
edges.push_back(addEdge("lattice/spine/99-departure-portal-v1",
departures.front()->stableId+"/upper-mouth",departureRoomId,
{cells.back(),{departure[0],departure[1],departureFloor}}));
const double innerEdgeGrade=std::atan(Pitch/(2.0*Pi*(CenterlineRadius-ClearWidth*0.5)))
*180.0/Pi;
const double centerlineGrade=std::atan(Pitch/(2.0*Pi*CenterlineRadius))*180.0/Pi;
std::printf("[LatticeResolvedMouths] arrival=%s side=lower xyz_m=(%.3f,%.3f,%.3f) departure=%s side=upper xyz_m=(%.3f,%.3f,%.3f) binding=actual-resolved status=PASS\n",
arrivals.front()->stableId.c_str(),arrival[0],arrival[1],arrival[2],
departures.front()->stableId.c_str(),departure[0],departure[1],departure[2]);
std::printf("[LatticeSpinePlan] recipe=Core.Lattice.PrimitiveField version=1 strate=%d stable_key=seed,strate,recipe_version,stable_id spine_first=true arrival=%s departure=%s helix_id=lattice/spine/01-helix-ramp-v1 centerline_radius_m=%.3f clear_width_m=%.3f pitch_m=%.3f turns=%d floor_to_soffit_m=%.3f centerline_grade_deg=%.3f inner_edge_grade_deg=%.3f floor_blend_m=%.3f union_envelope_m=%.3f maze_cells=%d dead_ends=2 orphan_control=%s status=PASS\n",
latticeStrateIndex,arrivals.front()->stableId.c_str(),departures.front()->stableId.c_str(),
CenterlineRadius,ClearWidth,Pitch,Turns,FloorToSoffit,centerlineGrade,innerEdgeGrade,
FloorBlend,FeatureUnionBlend,routeEdges+1,orphanControl?"enabled":"disabled");
return true;
}
bool MakeOreVeinDemoRequests( bool MakeOreVeinDemoRequests(
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout, const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
std::vector<VoxelForgeCore::ResolvedLayoutOreVeinRequestV1>& requests, std::vector<VoxelForgeCore::ResolvedLayoutOreVeinRequestV1>& requests,
@@ -1131,7 +1412,8 @@ namespace
" --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n" " --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n"
" --world-assembler-manifest-output=FILE (write the canonical Core recipe and plan)\n" " --world-assembler-manifest-output=FILE (write the canonical Core recipe and plan)\n"
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n" " --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
" --seed=N --recipe=surface-world|floating-islands|caves\n" " --seed=N --recipe=surface-world|floating-islands|caves|lattice\n"
" --lattice-strate=N (opt one Maze or VerticalShafts strate into Core.Lattice)\n"
" --landmark-chamber-demo (opt-in SW cavern room and swept tunnel)\n" " --landmark-chamber-demo (opt-in SW cavern room and swept tunnel)\n"
" --ore-veins-demo (opt-in material-only SW capsule veins)\n" " --ore-veins-demo (opt-in material-only SW capsule veins)\n"
" --audit-ore-vein-yield (measure one deterministic sphere carve)\n" " --audit-ore-vein-yield (measure one deterministic sphere carve)\n"
@@ -1146,6 +1428,8 @@ namespace
" --audit-motif-intervals (brute-force motif bounds, BVH and shape-free guards)\n" " --audit-motif-intervals (brute-force motif bounds, BVH and shape-free guards)\n"
" --audit-landmark-reachability (capsule, support, grade and buried-L43 control)\n" " --audit-landmark-reachability (capsule, support, grade and buried-L43 control)\n"
" --audit-native-caves (arrival-to-departure capsule flood fill and interval oracle)\n" " --audit-native-caves (arrival-to-departure capsule flood fill and interval oracle)\n"
" --audit-native-lattice (mouth-to-mouth capsule, grade, support and interval proof)\n"
" --lattice-orphan-control (disconnect a required edge for the negative control)\n"
" --audit-surface-clearance (sample biome terrain/caps against the resolved ceiling)\n" " --audit-surface-clearance (sample biome terrain/caps against the resolved ceiling)\n"
" --auto-surface-camera (place the 27-tile cube on this seed's surface)\n" " --auto-surface-camera (place the 27-tile cube on this seed's surface)\n"
" --mesh-coverage (report non-empty tiles and indexed triangles)\n" " --mesh-coverage (report non-empty tiles and indexed triangles)\n"
@@ -1179,6 +1463,8 @@ namespace
else if(arg=="--audit-resolved-layout-passages")options.AuditResolvedLayoutPassages=true; else if(arg=="--audit-resolved-layout-passages")options.AuditResolvedLayoutPassages=true;
else if(arg=="--audit-landmark-reachability")options.AuditLandmarkReachability=true; else if(arg=="--audit-landmark-reachability")options.AuditLandmarkReachability=true;
else if(arg=="--audit-native-caves")options.AuditNativeCaves=true; else if(arg=="--audit-native-caves")options.AuditNativeCaves=true;
else if(arg=="--audit-native-lattice")options.AuditNativeLattice=true;
else if(arg=="--lattice-orphan-control")options.LatticeOrphanControl=true;
else if(arg=="--audit-surface-clearance")options.AuditSurfaceClearance=true; else if(arg=="--audit-surface-clearance")options.AuditSurfaceClearance=true;
else if(arg=="--audit-strate-transitions")options.AuditStrateTransitions=true; else if(arg=="--audit-strate-transitions")options.AuditStrateTransitions=true;
else if(arg.starts_with("--transition-mode=")) else if(arg.starts_with("--transition-mode="))
@@ -1233,13 +1519,21 @@ namespace
{ {
const std::string_view recipe=arg.substr(std::string_view("--recipe=").size()); const std::string_view recipe=arg.substr(std::string_view("--recipe=").size());
if(recipe=="surface-world") if(recipe=="surface-world")
{options.FloatingIslandsRecipe=false;options.CavesRecipe=false;} {options.FloatingIslandsRecipe=false;options.CavesRecipe=false;options.LatticeRecipe=false;}
else if(recipe=="floating-islands") else if(recipe=="floating-islands")
{options.FloatingIslandsRecipe=true;options.CavesRecipe=false;} {options.FloatingIslandsRecipe=true;options.CavesRecipe=false;options.LatticeRecipe=false;}
else if(recipe=="caves") else if(recipe=="caves")
{options.FloatingIslandsRecipe=false;options.CavesRecipe=true;} {options.FloatingIslandsRecipe=false;options.CavesRecipe=true;options.LatticeRecipe=false;}
else if(recipe=="lattice")
{options.FloatingIslandsRecipe=false;options.CavesRecipe=false;options.LatticeRecipe=true;}
else {std::fprintf(stderr,"Invalid --recipe value: %.*s\n",static_cast<int>(recipe.size()),recipe.data());return false;} else {std::fprintf(stderr,"Invalid --recipe value: %.*s\n",static_cast<int>(recipe.size()),recipe.data());return false;}
} }
else if(arg.starts_with("--lattice-strate="))
{
if(!readInt("--lattice-strate=",options.LatticeStrateIndex)
||options.LatticeStrateIndex<0)
{std::fprintf(stderr,"--lattice-strate must be a non-negative strate index\n");return false;}
}
else if(arg=="--landmark-chamber-demo")options.LandmarkChamberDemo=true; else if(arg=="--landmark-chamber-demo")options.LandmarkChamberDemo=true;
else if(arg=="--landmark-l45-ramp-control")options.LandmarkL45RampControl=true; else if(arg=="--landmark-l45-ramp-control")options.LandmarkL45RampControl=true;
else if(arg=="--ore-veins-demo")options.OreVeinsDemo=true; else if(arg=="--ore-veins-demo")options.OreVeinsDemo=true;
@@ -1330,6 +1624,10 @@ namespace
{std::fprintf(stderr,"--landmark-l45-ramp-control requires --landmark-chamber-demo\n");return false;} {std::fprintf(stderr,"--landmark-l45-ramp-control requires --landmark-chamber-demo\n");return false;}
if(options.AuditOreVeinYield&&!options.OreVeinsDemo) if(options.AuditOreVeinYield&&!options.OreVeinsDemo)
{std::fprintf(stderr,"--audit-ore-vein-yield requires --ore-veins-demo\n");return false;} {std::fprintf(stderr,"--audit-ore-vein-yield requires --ore-veins-demo\n");return false;}
if(options.LatticeRecipe&&options.LatticeStrateIndex<0)
{std::fprintf(stderr,"--recipe=lattice requires an explicit --lattice-strate=N selection\n");return false;}
if((options.AuditNativeLattice||options.LatticeOrphanControl)&&!options.LatticeRecipe)
{std::fprintf(stderr,"native lattice audits require --recipe=lattice\n");return false;}
if(options.TlsProbeRequested)return true; if(options.TlsProbeRequested)return true;
if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline) if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline)
{ {
@@ -1427,8 +1725,10 @@ int main(int argc,char** argv)
} }
VoxelForgeCore::ResolvedLayoutSnapshotV3 resolvedLayout; VoxelForgeCore::ResolvedLayoutSnapshotV3 resolvedLayout;
{ {
const std::string_view variant=options.CavesRecipe?"caves" const std::string variant=options.LatticeRecipe
:options.FloatingIslandsRecipe?"floating-islands":"surface-world"; ?"lattice/"+std::to_string(options.LatticeStrateIndex)
:options.CavesRecipe?"caves"
:options.FloatingIslandsRecipe?"floating-islands":"surface-world";
if(!StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot( if(!StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
*ownerWorld,worldAssembly,variant,resolvedLayout,error)) *ownerWorld,worldAssembly,variant,resolvedLayout,error))
{std::fprintf(stderr,"Resolved-layout snapshot failed: %s\n",error.c_str());return 2;} {std::fprintf(stderr,"Resolved-layout snapshot failed: %s\n",error.c_str());return 2;}
@@ -1515,6 +1815,18 @@ int main(int argc,char** argv)
resolvedLayout,seed,rooms,edges,error)) resolvedLayout,seed,rooms,edges,error))
{std::fprintf(stderr,"Native caves spine resolution failed: %s\n",error.c_str());return 2;} {std::fprintf(stderr,"Native caves spine resolution failed: %s\n",error.c_str());return 2;}
} }
if(options.LatticeRecipe)
{
if(options.LatticeStrateIndex<0)
{std::fprintf(stderr,"--recipe=lattice requires an explicit --lattice-strate=N selection\n");return 2;}
std::vector<VoxelForgeCore::ResolvedCavernRoomV1> rooms;
std::vector<VoxelForgeCore::ResolvedCavernEdgeV1> edges;
if(!MakeNativeLatticeSpine(resolvedLayout,seed,options.LatticeStrateIndex,
options.LatticeOrphanControl,rooms,edges,error)
||!VoxelForgeCore::ResolveResolvedCavernGraphV1(
resolvedLayout,seed,rooms,edges,error))
{std::fprintf(stderr,"Native lattice graph resolution failed: %s\n",error.c_str());return 2;}
}
if(options.OreVeinsDemo) if(options.OreVeinsDemo)
{ {
std::vector<VoxelForgeCore::ResolvedLayoutOreVeinRequestV1> requests; std::vector<VoxelForgeCore::ResolvedLayoutOreVeinRequestV1> requests;
@@ -1609,7 +1921,8 @@ int main(int argc,char** argv)
slot.StrateIndex,resolvedStrate->generatorKind.c_str(), slot.StrateIndex,resolvedStrate->generatorKind.c_str(),
static_cast<std::uint32_t>(slot.Definition->PassageConfig.Style)); static_cast<std::uint32_t>(slot.Definition->PassageConfig.Style));
} }
const auto requestedRecipe=options.CavesRecipe?std::string_view("caves") const auto requestedRecipe=options.LatticeRecipe?std::string_view("lattice")
:options.CavesRecipe?std::string_view("caves")
:options.FloatingIslandsRecipe?std::string_view("floating-islands") :options.FloatingIslandsRecipe?std::string_view("floating-islands")
:std::string_view("surface-world"); :std::string_view("surface-world");
const auto topStratum=std::max_element(resolvedLayout.strates.begin(),resolvedLayout.strates.end(), const auto topStratum=std::max_element(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
@@ -1626,10 +1939,13 @@ int main(int argc,char** argv)
[&](const auto& plan){return plan.strateIndex==stratum.strateIndex;}); [&](const auto& plan){return plan.strateIndex==stratum.strateIndex;});
}); });
const bool hasNativeCavesRecipe=std::any_of(resolvedLayout.strates.begin(),resolvedLayout.strates.end(), const bool hasNativeCavesRecipe=std::any_of(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
[](const auto& stratum){return stratum.generatorKind=="Caves" [](const auto& stratum){return VoxelForgeCore::IsNativeCaveFieldStratum(stratum)
&&stratum.recipeId=="Core.Caves.PrimitiveField"&&stratum.recipeVersion==1
&&stratum.passageCapabilities.hasNativeCaveField;}); &&stratum.passageCapabilities.hasNativeCaveField;});
const auto actualRecipe=hasNativeCavesRecipe?std::string_view("caves") 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")
:hasFloatingIslands?std::string_view("floating-islands") :hasFloatingIslands?std::string_view("floating-islands")
:hasSurfaceWorldPlan?std::string_view("surface-world") :hasSurfaceWorldPlan?std::string_view("surface-world")
:std::string_view("missing"); :std::string_view("missing");
@@ -1834,5 +2150,6 @@ int main(int argc,char** argv)
options.AuditMotifIntervals, options.AuditMotifIntervals,
options.ExactMidpointDensityMemo,options.ActiveLaneBatching, options.ExactMidpointDensityMemo,options.ActiveLaneBatching,
options.ProofBlockCells,options.HasSignEdgeRepairOverride options.ProofBlockCells,options.HasSignEdgeRepairOverride
?options.ParallelSignEdgeRepair:options.FloatingIslandsRecipe); ?options.ParallelSignEdgeRepair:options.FloatingIslandsRecipe,
options.AuditNativeLattice,options.LatticeOrphanControl);
} }
+346 -1
View File
@@ -1403,6 +1403,342 @@ namespace StandaloneStreamBenchmark
return reachable&&intervalOk&&measured>=0.5; return reachable&&intervalOk&&measured>=0.5;
} }
inline bool AuditNativeLatticeField(const VoxelForgeCore::World& shape,
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
std::uint32_t voxelMicrometres,bool orphanControl)
{
using namespace VoxelForgeCore;
constexpr double Pi=3.1415926535897932384626433832795;
constexpr double Q=static_cast<double>(WorldAssemblySubvoxelsPerVoxel);
constexpr double MouthCenterHeight=1.13;
const double vm=std::max<std::uint32_t>(1,voxelMicrometres)/1.0e6;
const auto metres=[](const std::array<std::int64_t,3>& p)
{return std::array<double,3>{p[0]/1.0e6,p[1]/1.0e6,p[2]/1.0e6};};
const auto density=[&](double x,double y,double z)
{return static_cast<double>(shape.densityAt({x/vm,y/vm,z/vm}))*vm;};
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)
{std::printf("[LatticeAudit] recipe=%s graph=%u status=FAIL\n",
lattice==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==lattice->strateIndex)
{if(arrival)return false;arrival=&passage;}
if(passage.upperStrateIndex==lattice->strateIndex)
{if(departure)return false;departure=&passage;}
}
if(!arrival||!departure)return false;
const auto mouthMetres=[&](const std::array<std::int64_t,3>& point)
{return std::array<double,3>{point[0]*vm/Q,point[1]*vm/Q,
point[2]*vm/Q-MouthCenterHeight};};
const auto arrivalPoint=mouthMetres(arrival->lowerMouthSubvoxels);
const auto departurePoint=mouthMetres(departure->upperMouthSubvoxels);
std::vector<const ResolvedCavernEdgeV1*> mazeEdges;
const ResolvedCavernEdgeV1* arrivalPortal=nullptr;
const ResolvedCavernEdgeV1* helix=nullptr;
const ResolvedCavernEdgeV1* departurePortal=nullptr;
for(const auto& edge:layout.cavernEdges)
{
if(edge.strateIndex!=lattice->strateIndex)continue;
if(edge.stableId=="lattice/spine/00-arrival-portal-v1")arrivalPortal=&edge;
else if(edge.stableId=="lattice/spine/01-helix-ramp-v1")helix=&edge;
else if(edge.stableId=="lattice/spine/99-departure-portal-v1")departurePortal=&edge;
else if(edge.stableId.starts_with("lattice/spine/maze-edge-"))mazeEdges.push_back(&edge);
}
std::sort(mazeEdges.begin(),mazeEdges.end(),[](const auto* a,const auto* b)
{return a->stableId<b->stableId;});
if(!arrivalPortal||!helix||!departurePortal||mazeEdges.size()<4
||layout.cavernRooms.size()<mazeEdges.size()+1)return false;
std::vector<const ResolvedCavernEdgeV1*> ordered={arrivalPortal,helix};
ordered.insert(ordered.end(),mazeEdges.begin(),mazeEdges.end());
ordered.push_back(departurePortal);
std::vector<std::array<double,3>> route;
bool continuous=true;
std::size_t pointCount=0;
for(const auto* edge:ordered)
{
if(edge->polylineMicrometres.size()<2)return false;
for(std::size_t i=0;i<edge->polylineMicrometres.size();++i)
{
const auto point=metres(edge->polylineMicrometres[i]);
if(!route.empty())
{
const auto& prior=route.back();
const double gap=std::hypot(std::hypot(point[0]-prior[0],point[1]-prior[1]),
point[2]-prior[2]);
if(i==0&&gap>0.05)continuous=false;
if(i==0&&gap<=0.05)continue;
}
route.push_back(point);++pointCount;
}
}
const auto endpointGap=[](const auto& a,const auto& b)
{return std::hypot(std::hypot(a[0]-b[0],a[1]-b[1]),a[2]-b[2]);};
const double arrivalGap=endpointGap(metres(arrivalPortal->polylineMicrometres.front()),
arrivalPoint);
const double departureGap=endpointGap(metres(departurePortal->polylineMicrometres.back()),
departurePoint);
continuous=continuous&&arrivalGap<=0.05&&departureGap<=0.05;
double maximumGrade=0.0;
for(std::size_t i=1;i<route.size();++i)
{
const double horizontal=std::hypot(route[i][0]-route[i-1][0],
route[i][1]-route[i-1][1]);
const double vertical=std::abs(route[i][2]-route[i-1][2]);
if(horizontal<1.0e-9&&vertical>1.0e-6)maximumGrade=90.0;
else if(horizontal>1.0e-9)
maximumGrade=std::max(maximumGrade,std::atan2(vertical,horizontal)*180.0/Pi);
}
constexpr double CenterlineRadius=6.0,ClearWidth=3.0,Pitch=5.0;
const double helixInnerGrade=std::atan(Pitch/
(2.0*Pi*(CenterlineRadius-ClearWidth*0.5)))*180.0/Pi;
maximumGrade=std::max(maximumGrade,helixInnerGrade);
std::uint64_t stationCount=0,capsuleSamples=0,supportSamples=0,headSamples=0;
std::uint64_t capsuleFailures=0,supportFailures=0,headFailures=0;
std::array<double,3> firstCapsuleFailure{},firstSupportFailure{},firstHeadFailure{};
bool hasCapsuleFailure=false,hasSupportFailure=false,hasHeadFailure=false;
double minimumHeadroom=std::numeric_limits<double>::infinity();
bool capsuleClear=continuous,allSupported=continuous,headroomClear=continuous;
if(continuous)
{
constexpr double CapsuleRadius=0.34,CapsuleHeight=1.76,StationStep=0.25;
constexpr std::array<std::array<double,2>,5> offsets={{{0.0,0.0},{0.20,0.0},
{-0.20,0.0},{0.0,0.20},{0.0,-0.20}}};
for(std::size_t segment=1;segment<route.size();++segment)
{
const auto& a=route[segment-1];const auto& b=route[segment];
const double dx=b[0]-a[0],dy=b[1]-a[1],dz=b[2]-a[2];
const double length=std::hypot(std::hypot(dx,dy),dz);
const int steps=std::max(1,static_cast<int>(std::ceil(length/StationStep)));
for(int step=0;step<steps;++step)
{
const double t=static_cast<double>(step)/steps;
const double x=a[0]+dx*t,y=a[1]+dy*t,z=a[2]+dz*t;
const double horizontal=std::hypot(dx,dy);
const double nx=horizontal>1.0e-9?-dy/horizontal:0.0;
const double ny=horizontal>1.0e-9?dx/horizontal:1.0;
++stationCount;
for(double h=0.10;h<=CapsuleHeight+1.0e-9;h+=0.17)
{
double cap=0.0;
if(h<CapsuleRadius)cap=CapsuleRadius-h;
else if(h>CapsuleHeight-CapsuleRadius)
cap=h-(CapsuleHeight-CapsuleRadius);
const double reach=std::sqrt(std::max(0.0,
CapsuleRadius*CapsuleRadius-cap*cap));
for(const auto& offset:offsets)
{
if(std::hypot(offset[0],offset[1])>reach+1.0e-8)continue;
++capsuleSamples;
const bool clear=density(x+nx*offset[0]-ny*offset[1],
y+ny*offset[0]+nx*offset[1],z+0.12+h)>=0.0;
capsuleClear=capsuleClear&&clear;
if(!clear)
{
++capsuleFailures;
if(!hasCapsuleFailure)
{firstCapsuleFailure={x,y,z+0.12+h};hasCapsuleFailure=true;}
}
}
}
constexpr std::array<std::array<double,2>,5> supportOffsets={{{0.0,0.0},
{0.28,0.0},{-0.28,0.0},{0.0,0.28},{0.0,-0.28}}};
for(const auto& offset:supportOffsets)
{
++supportSamples;bool supported=false;
for(double depth=0.08;depth<=0.62+1.0e-9;depth+=0.06)
supported=supported||density(x+nx*offset[0]-ny*offset[1],
y+ny*offset[0]+nx*offset[1],z-depth)<0.0;
allSupported=allSupported&&supported;
if(!supported)
{
++supportFailures;
if(!hasSupportFailure)
{firstSupportFailure={x+nx*offset[0]-ny*offset[1],
y+ny*offset[0]+nx*offset[1],z};hasSupportFailure=true;}
}
}
for(const auto& offset:offsets)
{
++headSamples;
const double clearance=density(x+nx*offset[0]-ny*offset[1],
y+ny*offset[0]+nx*offset[1],z+2.20);
headroomClear=headroomClear&&clearance>=0.0;
if(clearance<0.0)
{
++headFailures;
if(!hasHeadFailure)
{firstHeadFailure={x+nx*offset[0]-ny*offset[1],
y+ny*offset[0]+nx*offset[1],z+2.20};hasHeadFailure=true;}
}
if(clearance>=0.0)minimumHeadroom=std::min(minimumHeadroom,2.20);
else minimumHeadroom=std::min(minimumHeadroom,0.0);
}
}
}
}
if(hasSupportFailure)
{
std::printf("[LatticeSupportProbe] first_xyz_m=(%.3f,%.3f,%.3f) density_below_m=",
firstSupportFailure[0],firstSupportFailure[1],firstSupportFailure[2]);
for(double depth=0.08;depth<=0.62+1.0e-9;depth+=0.06)
std::printf("%s%.3f",depth>0.08?",":"",
density(firstSupportFailure[0],firstSupportFailure[1],
firstSupportFailure[2]-depth));
std::printf("\n");
}
const auto filletEdge=mazeEdges.front();
const auto p0=metres(filletEdge->polylineMicrometres[
filletEdge->polylineMicrometres.size()/2-1]);
const auto p1=metres(filletEdge->polylineMicrometres[
filletEdge->polylineMicrometres.size()/2]);
const double tx=p1[0]-p0[0],ty=p1[1]-p0[1];
const double horizontal=std::hypot(tx,ty);
const double grade=horizontal>1.0e-9?(p1[2]-p0[2])/horizontal:0.0;
const double width=filletEdge->smoothWidthMicrometres/1.0e6;
double measuredFillet=0.0;
if(horizontal>1.0e-9&&width>0.0)
{
const double nx=-ty/horizontal,ny=tx/horizontal;
const double inverseLength=1.0/std::sqrt(1.0+grade*grade);
const double upX=-grade*tx/horizontal*inverseLength;
const double upY=-grade*ty/horizontal*inverseLength;
const double upZ=inverseLength;
const double cx=(p0[0]+p1[0])*0.5,cy=(p0[1]+p1[1])*0.5,
cz=(p0[2]+p1[2])*0.5;
const double radius=filletEdge->boreRadiusMicrometres/1.0e6;
const auto contour=[&](double v)
{
double lo=0.0,hi=width;
const auto sample=[&](double u)
{
const double inside=(u+v)/std::sqrt(2.0);
const double rise=(u-v)/std::sqrt(2.0);
return density(cx+nx*(radius-inside)+upX*rise,
cy+ny*(radius-inside)+upY*rise,cz+upZ*rise);
};
if(!(sample(lo)<0.0&&sample(hi)>=0.0))
return std::numeric_limits<double>::quiet_NaN();
for(int iteration=0;iteration<28;++iteration)
{const double mid=(lo+hi)*0.5;if(sample(mid)<0.0)lo=mid;else hi=mid;}
return (lo+hi)*0.5;
};
constexpr double delta=0.02;
const double left=contour(-delta),center=contour(0.0),right=contour(delta);
if(std::isfinite(left)&&std::isfinite(center)&&std::isfinite(right))
measuredFillet=delta*delta/(left+right-2.0*center);
}
const double idealFillet=width/(2.0*std::sqrt(2.0));
struct FIntervalCheck {const char* name;std::array<double,3> p;};
const auto mazeA=metres(mazeEdges.front()->polylineMicrometres.front());
const auto mazeB=metres(mazeEdges.front()->polylineMicrometres.back());
const std::array<double,3> mazeMid={(mazeA[0]+mazeB[0])*0.5,
(mazeA[1]+mazeB[1])*0.5,(mazeA[2]+mazeB[2])*0.5};
const double mazeLength=std::hypot(mazeB[0]-mazeA[0],mazeB[1]-mazeA[1]);
const double wallX=mazeLength>1.0e-9?-(mazeB[1]-mazeA[1])/mazeLength:1.0;
const double wallY=mazeLength>1.0e-9?(mazeB[0]-mazeA[0])/mazeLength:0.0;
double maxX=-std::numeric_limits<double>::infinity();
double minY=std::numeric_limits<double>::infinity();
double maxY=-std::numeric_limits<double>::infinity();
double midZ=mazeMid[2];
for(const auto& edge:layout.cavernEdges)if(edge.strateIndex==lattice->strateIndex)
for(const auto& point:edge.polylineMicrometres)
{const auto p=metres(point);maxX=std::max(maxX,p[0]);minY=std::min(minY,p[1]);maxY=std::max(maxY,p[1]);}
const std::array<FIntervalCheck,4> checks={{{"host-rock",{maxX+8.0,
(minY+maxY)*0.5,midZ+1.2}},
{"maze-air",{mazeMid[0],mazeMid[1],mazeMid[2]+1.25}},
{"maze-wall-foot",{mazeMid[0]+wallX*1.30,mazeMid[1]+wallY*1.30,
mazeMid[2]+0.25}},
{"helix-air",{helix->polylineMicrometres[helix->polylineMicrometres.size()/2][0]/1.0e6,
helix->polylineMicrometres[helix->polylineMicrometres.size()/2][1]/1.0e6,
helix->polylineMicrometres[helix->polylineMicrometres.size()/2][2]/1.0e6+1.25}}}};
std::uint64_t boxes=0,samples=0,misses=0,allAir=0,allSolid=0,mixed=0;
bool falseAllAirRejected=false,falseAllSolidRejected=false,wallMixed=false;
bool isTileShapeFreeAware=false,latticeTileFree=false;
for(const auto& check:checks)
{
const Vec3i origin{static_cast<int>(std::llround(check.p[0]/vm)),
static_cast<int>(std::llround(check.p[1]/vm)),
static_cast<int>(std::llround(check.p[2]/vm))};
const TileClass verdict=shape.classifyBox(origin,1,1);
bool bruteAir=true,bruteSolid=true;
for(int z=0;z<=6;++z)for(int y=0;y<=6;++y)for(int x=0;x<=6;++x)
{
const double d=density((origin.x-1.0+0.5*x)*vm,
(origin.y-1.0+0.5*y)*vm,(origin.z-1.0+0.5*z)*vm);
bruteAir=bruteAir&&d>=0.0;bruteSolid=bruteSolid&&d<0.0;++samples;
if((verdict==TileClass::allAir&&d<0.0)
||(verdict==TileClass::allSolid&&d>=0.0))++misses;
}
++boxes;allAir+=verdict==TileClass::allAir;
allSolid+=verdict==TileClass::allSolid;mixed+=verdict==TileClass::mixed;
falseAllAirRejected|=bruteSolid&&verdict!=TileClass::allAir;
falseAllSolidRejected|=bruteAir&&verdict!=TileClass::allSolid;
if(std::string_view(check.name)=="maze-wall-foot")wallMixed=verdict==TileClass::mixed;
if(std::string_view(check.name)=="maze-air")
{
const int32 tileWidth=CoreTileVoxelCount;
const TileKey key{{static_cast<std::int32_t>(std::floor(
static_cast<double>(origin.x)/tileWidth)),
static_cast<std::int32_t>(std::floor(static_cast<double>(origin.y)/tileWidth)),
static_cast<std::int32_t>(std::floor(static_cast<double>(origin.z)/tileWidth))},0};
latticeTileFree=shape.isTileShapeFree(key);
isTileShapeFreeAware=!latticeTileFree;
}
std::printf("[LatticeIntervalBox] id=%s proof=%s brute_air=%s brute_solid=%s miss=%s\n",
check.name,verdict==TileClass::allAir?"all-air":
verdict==TileClass::allSolid?"all-solid":"mixed",
bruteAir?"yes":"no",bruteSolid?"yes":"no",
(verdict==TileClass::allAir?!bruteAir:
verdict==TileClass::allSolid?!bruteSolid:false)?"yes":"no");
}
const bool intervalOk=boxes>=4&&allSolid>=1&&mixed>=1
&&misses==0&&falseAllAirRejected&&falseAllSolidRejected&&wallMixed
&&isTileShapeFreeAware;
const bool pathOk=continuous&&capsuleClear&&allSupported&&headroomClear
&&maximumGrade<=15.0&&minimumHeadroom>=2.2;
std::printf("[LatticeCapsuleReachability] arrival=%s departure=%s edges=%zu path_points=%zu stations=%llu capsule_samples=%llu capsule_failures=%llu first_capsule_failure_m=(%.3f,%.3f,%.3f) support_samples=%llu support_failures=%llu first_support_failure_m=(%.3f,%.3f,%.3f) head_samples=%llu head_failures=%llu first_head_failure_m=(%.3f,%.3f,%.3f) max_grade_deg=%.3f min_headroom_m=%.3f required_headroom_m=2.200 support=every-station reachable=%s status=%s\n",
arrival->stableId.c_str(),departure->stableId.c_str(),ordered.size(),pointCount,
static_cast<unsigned long long>(stationCount),
static_cast<unsigned long long>(capsuleSamples),
static_cast<unsigned long long>(capsuleFailures),firstCapsuleFailure[0],
firstCapsuleFailure[1],firstCapsuleFailure[2],
static_cast<unsigned long long>(supportSamples),
static_cast<unsigned long long>(supportFailures),firstSupportFailure[0],
firstSupportFailure[1],firstSupportFailure[2],
static_cast<unsigned long long>(headSamples),
static_cast<unsigned long long>(headFailures),firstHeadFailure[0],
firstHeadFailure[1],firstHeadFailure[2],maximumGrade,
minimumHeadroom, pathOk?"yes":"no",pathOk?"PASS":"FAIL");
std::printf("[LatticeFilletAudit] smooth_max=cubic width_m=%.6f measured_radius_m=%.6f ideal_radius_m=%.6f status=%s\n",
width,measuredFillet,idealFillet,measuredFillet>=0.5?"PASS":"FAIL");
std::printf("[LatticeIntervalOracle] boxes=%llu all_air=%llu all_solid=%llu mixed=%llu samples=%llu misses=%llu false_all_air=%s false_all_solid=%s wall_foot=%s isTileShapeFree=%s tile_free=%s status=%s\n",
static_cast<unsigned long long>(boxes),static_cast<unsigned long long>(allAir),
static_cast<unsigned long long>(allSolid),static_cast<unsigned long long>(mixed),
static_cast<unsigned long long>(samples),static_cast<unsigned long long>(misses),
falseAllAirRejected?"REJECTED":"FAILED",
falseAllSolidRejected?"REJECTED":"FAILED",wallMixed?"mixed":"not-mixed",
isTileShapeFreeAware?"BLOCKED":"FAILED",latticeTileFree?"yes":"no",
intervalOk?"PASS":"FAIL");
if(orphanControl)
{
const bool rejected=!continuous||!pathOk;
std::printf("[LatticeReachabilityControl] orphaned_shaft=true result=%s expected=FAIL status=%s\n",
rejected?"FAIL":"PASS",rejected?"PASS":"FAIL");
return rejected&&intervalOk&&measuredFillet>=0.5;
}
return pathOk&&intervalOk&&measuredFillet>=0.5;
}
inline bool AuditMotifIntervals(const VoxelForgeCore::World& shape, inline bool AuditMotifIntervals(const VoxelForgeCore::World& shape,
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout, const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
std::uint32_t voxelMicrometres) std::uint32_t voxelMicrometres)
@@ -1535,7 +1871,8 @@ namespace StandaloneStreamBenchmark
bool bInstrumentTileDrain=false,bool bAuditMotifIntervals=false, bool bInstrumentTileDrain=false,bool bAuditMotifIntervals=false,
bool bExactMidpointDensityMemo=false, bool bExactMidpointDensityMemo=false,
bool bActiveLaneBatching=false,int32 ProofBlockCells=8, bool bActiveLaneBatching=false,int32 ProofBlockCells=8,
bool bParallelSignEdgeRepair=false) bool bParallelSignEdgeRepair=false,bool bAuditNativeLattice=false,
bool bLatticeOrphanControl=false)
{ {
using Clock=std::chrono::steady_clock; using Clock=std::chrono::steady_clock;
FStandaloneOwnerWorld& World=*OwnerWorld; FStandaloneOwnerWorld& World=*OwnerWorld;
@@ -1741,6 +2078,14 @@ namespace StandaloneStreamBenchmark
{std::fprintf(stderr,"[CavesAudit] missing resolved layout\n");return 2;} {std::fprintf(stderr,"[CavesAudit] missing resolved layout\n");return 2;}
return AuditNativeCavesField(Shape,*ResolvedLayout,World.CoreVoxelSizeMicrons)?0:14; return AuditNativeCavesField(Shape,*ResolvedLayout,World.CoreVoxelSizeMicrons)?0:14;
} }
if(bAuditNativeLattice)
{
Streamer.Stop();
if(!ResolvedLayout||!CoreSnapshot.resolvedLayout())
{std::fprintf(stderr,"[LatticeAudit] missing resolved layout\n");return 2;}
return AuditNativeLatticeField(Shape,*ResolvedLayout,
World.CoreVoxelSizeMicrons,bLatticeOrphanControl)?0:14;
}
if(bAuditMotifIntervals) if(bAuditMotifIntervals)
{ {
Streamer.Stop(); Streamer.Stop();
+5 -3
View File
@@ -1109,7 +1109,6 @@ namespace StandaloneWorldAssemblyPrototype
{ {
using namespace VoxelForgeCore; using namespace VoxelForgeCore;
if(!owner.Manager){error="resolved layout builder requires the initialized director";return false;} if(!owner.Manager){error="resolved layout builder requires the initialized director";return false;}
(void)recipeVariant;
out={};out.worldIdentity="owner-world/seed/"+std::to_string(owner.Seed); out={};out.worldIdentity="owner-world/seed/"+std::to_string(owner.Seed);
out.ownerVoxelSizeMicrometres=owner.OwnerVoxelSizeMicrons; out.ownerVoxelSizeMicrometres=owner.OwnerVoxelSizeMicrons;
out.coreVoxelSizeMicrometres=owner.CoreVoxelSizeMicrons; out.coreVoxelSizeMicrometres=owner.CoreVoxelSizeMicrons;
@@ -1138,6 +1137,8 @@ namespace StandaloneWorldAssemblyPrototype
const auto authored=sealAndDensity(*slot.Definition,type); const auto authored=sealAndDensity(*slot.Definition,type);
const bool nativeCaves=recipeVariant=="caves" const bool nativeCaves=recipeVariant=="caves"
&&type==ECaveGeneratorType::SurfaceWorld; &&type==ECaveGeneratorType::SurfaceWorld;
const bool nativeLattice=recipeVariant==("lattice/"+std::to_string(slot.StrateIndex))
&&(type==ECaveGeneratorType::Maze||type==ECaveGeneratorType::VerticalShafts);
const std::int64_t bottom=static_cast<std::int64_t>(slot.BottomChunkZ)*owner.StrateLayoutChunkSizeMicrometres; const std::int64_t bottom=static_cast<std::int64_t>(slot.BottomChunkZ)*owner.StrateLayoutChunkSizeMicrometres;
const std::int64_t top=static_cast<std::int64_t>(slot.TopChunkZ+1)*owner.StrateLayoutChunkSizeMicrometres; const std::int64_t top=static_cast<std::int64_t>(slot.TopChunkZ+1)*owner.StrateLayoutChunkSizeMicrometres;
const std::int64_t seal=static_cast<std::int64_t>(std::llround( const std::int64_t seal=static_cast<std::int64_t>(std::llround(
@@ -1148,10 +1149,11 @@ namespace StandaloneWorldAssemblyPrototype
s.generatorKind=nativeCaves?"Caves":GeneratorKind(type); s.generatorKind=nativeCaves?"Caves":GeneratorKind(type);
s.stableId=out.worldIdentity+"/strate/"+std::to_string(slot.StrateIndex) s.stableId=out.worldIdentity+"/strate/"+std::to_string(slot.StrateIndex)
+"/generator/"+s.generatorKind; +"/generator/"+s.generatorKind;
s.recipeId=nativeCaves?"Core.Caves.PrimitiveField" s.recipeId=nativeLattice?"Core.Lattice.PrimitiveField"
:nativeCaves?"Core.Caves.PrimitiveField"
:type==ECaveGeneratorType::SurfaceWorld?"SurfaceWorld.Heightfield2D" :type==ECaveGeneratorType::SurfaceWorld?"SurfaceWorld.Heightfield2D"
:type==ECaveGeneratorType::FloatingIslands?"FloatingIslands.ClusterField":"legacy."+std::string(GeneratorKind(type)); :type==ECaveGeneratorType::FloatingIslands?"FloatingIslands.ClusterField":"legacy."+std::string(GeneratorKind(type));
s.recipeVersion=nativeCaves?1 s.recipeVersion=(nativeCaves||nativeLattice)?1
:type==ECaveGeneratorType::SurfaceWorld?assembly.recipe.schemaVersion :type==ECaveGeneratorType::SurfaceWorld?assembly.recipe.schemaVersion
:type==ECaveGeneratorType::FloatingIslands&&!assembly.floatingIslands.empty() :type==ECaveGeneratorType::FloatingIslands&&!assembly.floatingIslands.empty()
?assembly.floatingIslands.front().manifest.recipe.geometryVersion:0; ?assembly.floatingIslands.front().manifest.recipe.geometryVersion:0;
+303
View File
@@ -27,6 +27,7 @@ LINUX47 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux47"
LINUX53 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux53" LINUX53 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux53"
LINUX54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54" LINUX54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54"
LINUX56 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux56" LINUX56 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux56"
LINUX58 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux58"
OWNER = ROOT / "Standalone/data/owner_world.json" OWNER = ROOT / "Standalone/data/owner_world.json"
OUT_DEFAULT = ROOT / "Saved/Linux/l42" OUT_DEFAULT = ROOT / "Saved/Linux/l42"
@@ -65,6 +66,7 @@ TILE_MATERIAL_HASH_RE = re.compile(
CAMERA_SWEEP_SEED = 0x4C3434 CAMERA_SWEEP_SEED = 0x4C3434
PROFILE_CAMERA_SWEEP_SEED = 0x4C3533 PROFILE_CAMERA_SWEEP_SEED = 0x4C3533
CAVES_CAMERA_SWEEP_SEED = 0x4C3534 CAVES_CAMERA_SWEEP_SEED = 0x4C3534
LATTICE_CAMERA_SWEEP_SEED = 0x4C3538
PROFILE_NAMES = ("periodicHills", "mirroredRelief", "flatField") PROFILE_NAMES = ("periodicHills", "mirroredRelief", "flatField")
RECIPE_RE = re.compile( RECIPE_RE = re.compile(
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M) r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M)
@@ -669,6 +671,242 @@ def run_native_caves_field_audit(runner: VerificationRunner, binary: Path) -> tu
return fixture, layout_path return fixture, layout_path
def build_lattice_fixture(runner: VerificationRunner, binary: Path) -> Path:
fixture = OUT / "fixtures" / "lattice-l58.json"
builder = LINUX58 / "BuildLatticeFixtureL58.py"
runner.run_stage("build-native-lattice-fixture-l58", builder,
["--owner", str(OWNER), "--out", str(fixture)], binary=binary,
data=OWNER, recipe="lattice", style=None, env=env_for(),
script_inputs=[builder, OWNER])
return fixture
def run_native_lattice_field_audit(runner: VerificationRunner, binary: Path) -> tuple[Path, Path]:
fixture = build_lattice_fixture(runner, binary)
compiler = compiler_name(binary)
layout_path = OUT / "layouts" / f"native-lattice-field-{compiler}.json"
styles = {"0": 2, "1": 1, "2": 1}
inputs = [LINUX58 / "BuildLatticeFixtureL58.py"]
row, output = runner.run_cpp(f"native-lattice-field-{compiler}", binary, fixture,
"lattice", ["--lattice-strate=1", "--audit-native-lattice",
"--max-clip-level=0", "--render-distance-chunks=0"],
expected_style=None, expected_strate_styles=styles, workers=12,
max_level=0, layout_path=layout_path, script_inputs=inputs)
plan = re.search(r"^\[LatticeSpinePlan\] .*recipe=Core\.Lattice\.PrimitiveField version=1 "
r"strate=1 .*spine_first=true .*centerline_radius_m=6\.000 clear_width_m=3\.000 "
r"pitch_m=5\.000 turns=4 floor_to_soffit_m=2\.600 .*inner_edge_grade_deg=([0-9.]+) "
r"floor_blend_m=1\.900 union_envelope_m=0\.050 maze_cells=([0-9]+) dead_ends=2 .*status=PASS$",
output, re.M)
runner.require(f"native-lattice-field-{compiler}:recipe-and-spine", bool(plan),
"native opt-in version-1 lattice, actual mouths first, four-turn ramp and two branches")
fillet = re.search(r"^\[LatticeFilletAudit\].*measured_radius_m=([0-9.]+) .*status=PASS$",
output, re.M)
runner.require(f"native-lattice-field-{compiler}:measured-fillet",
bool(fillet and float(fillet.group(1)) >= 0.5),
f"measured_radius_m={fillet.group(1) if fillet else 'MISSING'} required>=0.500")
reach = re.search(r"^\[LatticeCapsuleReachability\].*max_grade_deg=([0-9.]+) "
r"min_headroom_m=([0-9.]+) required_headroom_m=2\.200 .*reachable=yes status=PASS$",
output, re.M)
runner.require(f"native-lattice-field-{compiler}:mouth-to-mouth-reachability",
bool(reach and float(reach.group(1)) <= 15.0 and float(reach.group(2)) >= 2.2),
f"grade_deg={reach.group(1) if reach else 'MISSING'} "
f"headroom_m={reach.group(2) if reach else 'MISSING'}")
runner.require(f"native-lattice-field-{compiler}:interval-oracle-and-shape-free",
all_match(output, r"^\[LatticeIntervalOracle\].*misses=0 "
r"false_all_air=REJECTED false_all_solid=REJECTED .*isTileShapeFree=BLOCKED .*status=PASS$"),
"BVH intervals match brute-force samples, reject both false proofs and block occupied tiles")
orphan_name = f"native-lattice-orphan-control-{compiler}"
orphan_layout = OUT / "layouts" / f"{orphan_name}.json"
_, orphan_output = runner.run_cpp(orphan_name, binary, fixture, "lattice",
["--lattice-strate=1", "--audit-native-lattice", "--lattice-orphan-control",
"--max-clip-level=0", "--render-distance-chunks=0"],
expected_style=None, expected_strate_styles=styles, workers=12,
max_level=0, layout_path=orphan_layout, script_inputs=inputs)
runner.require(f"{orphan_name}:disconnect-rejected",
all_match(orphan_output, r"^\[LatticeReachabilityControl\] orphaned_shaft=true "
r"result=FAIL expected=FAIL status=PASS$"),
"a helix shaft shifted off both spine junctions fails final-field reachability")
layout = json.loads(layout_path.read_text(encoding="utf-8"))
edges = layout.get("cavern_edges", [])
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
and len(edges) >= 9 and len(rooms) >= 8
and len(lattice_rows) == 1
and lattice_rows[0].get("generator_kind") == "Maze"
and lattice_rows[0].get("recipe_id") == "Core.Lattice.PrimitiveField"
and all(len(edge.get("polyline_um", [])) >= 2 for edge in edges))
runner.require(f"native-lattice-field-{compiler}:resolved-records", graph_ok,
f"vocabulary={layout.get('cavern_graph_vocabulary_version')} "
f"rooms={len(rooms)} edges={len(edges)} maze_recipe="
f"{lattice_rows[0].get('recipe_id') if lattice_rows else 'MISSING'}")
if plan:
row["helix_inner_edge_grade_degrees"] = float(plan.group(1))
row["maze_cells"] = int(plan.group(2))
if fillet:
row["measured_fillet_radius_m"] = float(fillet.group(1))
if reach:
row["max_route_grade_degrees"] = float(reach.group(1))
row["minimum_headroom_m"] = float(reach.group(2))
return fixture, layout_path
def lattice_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"])
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:
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", [])]
if len(helix_points) < 4:
raise VerificationFailure(f"lattice helix polyline is missing in {layout_path}")
rng = _SplitMix64(LATTICE_CAMERA_SWEEP_SEED)
cameras: list[tuple[float, float, float, float, float]] = []
for camera_index in range(count):
if camera_index == 0:
index = len(helix_points) // 2
point = helix_points[index]
tangent = [helix_points[index + 1][axis] - helix_points[index - 1][axis]
for axis in range(3)]
lateral = rng.between(-0.35, 0.35)
horizontal = max(1.0e-9, math.hypot(tangent[0], tangent[1]))
x = point[0] - tangent[1] / horizontal * lateral
y = point[1] + tangent[0] / horizontal * lateral
z = point[2] + rng.between(1.35, 1.55)
else:
route_edges = [edge for edge in edges if edge["stable_id"].startswith(
"lattice/spine/maze-edge-")]
edge = route_edges[min(len(route_edges) - 1,
int(rng.unit() * len(route_edges)))]
points = [[value / 1_000_000.0 for value in point]
for point in edge.get("polyline_um", [])]
index = min(len(points) - 2, int(rng.unit() * (len(points) - 1)))
t = rng.between(0.25, 0.75)
point = [points[index][axis] + (points[index + 1][axis] - points[index][axis]) * t
for axis in range(3)]
tangent = [points[index + 1][axis] - points[index][axis] for axis in range(3)]
horizontal = max(1.0e-9, math.hypot(tangent[0], tangent[1]))
side = rng.between(-0.45, 0.45)
x = point[0] - tangent[1] / horizontal * side
y = point[1] + tangent[0] / horizontal * side
z = point[2] + rng.between(1.35, 1.55)
yaw = math.degrees(math.atan2(tangent[0], tangent[1])) + rng.between(-14.0, 14.0)
pitch = rng.between(-6.0, 6.0)
cameras.append((x, y, z, yaw, pitch))
return cameras
def lattice_graph_digest(layout_path: Path) -> str:
layout = json.loads(layout_path.read_text(encoding="utf-8"))
graph = {key: layout.get(key) for key in (
"cavern_graph_vocabulary_version", "cavern_rooms", "cavern_edges")}
encoded = json.dumps(graph, sort_keys=True, separators=(",", ":")).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def run_lattice_determinism(runner: VerificationRunner, binary: Path,
fixture: Path, layout_path: Path, *,
compare_reference: bool) -> None:
compiler = compiler_name(binary)
reference_path = OUT / "lattice-determinism-reference.json"
if compare_reference:
try:
reference = json.loads(reference_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise VerificationFailure(f"missing Clang lattice peer reference: {error}")
else:
reference = None
current: dict[str, dict[str, object]] = {}
styles = {"0": 2, "1": 1, "2": 1}
for camera_index, camera in enumerate(lattice_camera_positions(layout_path)):
x, y, z, yaw, pitch = camera
camera_flag = f"--camera={x:.6f},{y:.6f},{z:.6f},{yaw:.3f},{pitch:.3f}"
signatures: dict[int, dict[tuple[int, int, int, int], tuple[str, ...]]] = {}
peer_layouts: dict[int, str] = {}
key = f"p{camera_index:02d}"
current[key] = {"camera_m": [round(value, 6) for value in camera[:3]],
"inside_helix": camera_index == 0}
for workers in (1, 8, 12):
name = (f"lattice-camera-{compiler}-s{LATTICE_CAMERA_SWEEP_SEED:06x}-"
f"p{camera_index:02d}-w{workers}")
run_layout = OUT / "layouts" / f"{name}.json"
_, output = runner.run_cpp(name, binary, fixture, "lattice",
["--lattice-strate=1", "--stream-hash-tiles", camera_flag,
"--clip-radius=3", "--max-clip-level=0", "--max-level=0",
"--render-distance-chunks=0"], expected_style=None,
expected_strate_styles=styles, workers=workers, max_level=0,
layout_path=run_layout,
script_inputs=[LINUX58 / "BuildLatticeFixtureL58.py"])
tiles = parse_tile_hashes(output)
whole = re.search(r"^\[PipelineWholeViewHash\] tiles=(\d+)", output, re.M)
expected_rows = int(whole.group(1)) if whole else -1
runner.require(f"{name}:tile-hash-rows", bool(tiles) and len(tiles) == expected_rows,
f"rows={len(tiles)} expected={expected_rows} camera_m={camera[:3]}")
signatures[workers] = tiles
peer_layouts[workers] = lattice_graph_digest(run_layout)
current[key][str(workers)] = {
"/".join(map(str, tile)): list(value)
for tile, value in sorted(tiles.items())}
for left, right in ((1, 8), (1, 12), (8, 12)):
runner.require(f"lattice-camera-{compiler}-{key}:W{left}-W{right}",
signatures[left] == signatures[right],
f"camera_m=({x:.3f},{y:.3f},{z:.3f}) W{left}=W{right} tile hashes match")
runner.require(f"lattice-camera-{compiler}-{key}:record-peers",
len(set(peer_layouts.values())) == 1,
f"canonical room/edge records match across W1/W8/W12: {peer_layouts}")
current[key]["graph_sha256"] = next(iter(peer_layouts.values()))
if compare_reference:
runner.require("lattice-camera-gcc:matches-clang-peers", current == reference,
"GCC random-camera tile hashes and canonical lattice records match Clang")
else:
reference_path.write_text(json.dumps(current, indent=2, sort_keys=True) + "\n",
encoding="utf-8")
def run_lattice_performance(runner: VerificationRunner, binary: Path,
fixture: Path, layout_path: Path) -> None:
compiler = compiler_name(binary)
camera = lattice_camera_positions(layout_path, count=1)[0]
x, y, z, yaw, pitch = camera
camera_flag = f"--camera={x:.6f},{y:.6f},{z:.6f},{yaw:.3f},{pitch:.3f}"
name = f"lattice-speed-{compiler}-w12"
row, output = runner.run_cpp(name, binary, fixture, "lattice",
["--lattice-strate=1", camera_flag, "--clip-radius=4", "--max-clip-level=0",
"--max-level=0", "--render-distance-chunks=0", "--camera-step=24,0,0",
"--region-crossing"], expected_style=None,
expected_strate_styles={"0": 2, "1": 1, "2": 1}, workers=12,
max_level=0, layout_path=OUT / "layouts" / f"{name}.json",
script_inputs=[LINUX58 / "BuildLatticeFixtureL58.py"])
stream = re.search(r"^\[StreamPipeline\] .*first_tile_ready=([0-9.]+)s "
r".*neighborhood_ready=([0-9.]+)s .*whole_view_ready=([0-9.]+)s .*cache_hits=0",
output, re.M)
moving = re.search(r"^\[MovingTileLatency\] entering_tiles=(\d+) "
r"p50_ms=([0-9.]+) p95_ms=([0-9.]+) metric=submit_to_completion_observed$",
output, re.M)
runner.require(f"{name}:cold-and-moving-records",
bool(stream and moving and int(moving.group(1)) > 0),
f"first_tile_s={stream.group(1) if stream else 'MISSING'} "
f"near_ring_s={stream.group(2) if stream else 'MISSING'} "
f"whole_view_s={stream.group(3) if stream else 'MISSING'} "
f"moving_p50_ms={moving.group(2) if moving else 'MISSING'} "
f"moving_p95_ms={moving.group(3) if moving else 'MISSING'}")
if stream and moving:
row["speed_metrics"] = {
"cold_first_tile_seconds": float(stream.group(1)),
"near_ring_seconds": float(stream.group(2)),
"cold_whole_view_seconds": float(stream.group(3)),
"moving_entering_tiles": int(moving.group(1)),
"moving_p50_ms": float(moving.group(2)),
"moving_p95_ms": float(moving.group(3)),
"workers": 12, "camera_m": [round(value, 6) for value in camera[:3]],
}
def cave_camera_positions(layout_path: Path, count: int = 4) -> list[tuple[float, float, float, float, float]]: 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")) layout = json.loads(layout_path.read_text(encoding="utf-8"))
edges = sorted(layout.get("cavern_edges", []), key=lambda edge: edge["stable_id"]) edges = sorted(layout.get("cavern_edges", []), key=lambda edge: edge["stable_id"])
@@ -863,6 +1101,58 @@ def run_caves_surface_evidence(runner: VerificationRunner, fixture: Path,
f"observed={match.group(1) if match else 'MISSING'}") f"observed={match.group(1) if match else 'MISSING'}")
def run_lattice_surface_evidence(runner: VerificationRunner, fixture: Path,
layout_path: Path) -> None:
capture_script = LINUX58 / "CaptureNativeLatticeL58.py"
artifact_dir = LINUX58 / "captures"
_, capture_output = runner.run_stage("capture-native-lattice-l58", capture_script,
["--out", str(OUT), "--fixture", str(fixture), "--layout", str(layout_path),
"--headless", str(CLANG), "--renderer", str(RENDERER),
"--artifact-dir", str(artifact_dir)], binary=CLANG, data=fixture,
recipe="lattice", style=2, env=env_for(capture=True),
script_inputs=[capture_script, LINUX54 / "CaptureNativeCavesL54.py",
VERIFY_DIR / "capture_quality.py"])
capture_rows = re.findall(r"^\[L58LatticeCapture\] view=([^ ]+) file=(\S+) "
r"bytes=(\d+) sha256=([0-9a-f]{64}) status=PASS$", capture_output, re.M)
expected_views = {"helix-top-down", "ramp-eye-next-turn", "maze-eye-level",
"cutaway-overview"}
runner.require("capture-native-lattice-l58:coverage-and-size",
{row[0] for row in capture_rows} == expected_views and len(capture_rows) == 4
and all(int(row[2]) <= 300_000 for row in capture_rows),
f"views={[(row[0], int(row[2])) for row in capture_rows]}")
surface_script = LINUX58 / "AuditLatticeSurfaceL58.py"
audit_layout = ROOT / "Saved/Linux/l48/layouts/landmark-reachability-clang.json"
candidate_mesh = OUT / "lattice-meshes" / "lattice-whole-view.vfmesh"
controls = (
("L45-ramp-control", ROOT / "Saved/Linux/l45/landmark-l45.vfmesh", "fail"),
("L46-entrance-control", ROOT / "Saved/Linux/l46/landmark-l46.vfmesh", "fail"),
("L47-wall-foot-control", ROOT / "Saved/Linux/l47/landmark-l47.vfmesh", "fail"),
("plain-native-control", ROOT / "Saved/Linux/l48/control-plain-native.vfmesh", "pass"),
)
args = ["--json", str(OUT / "whole-surface-lattice-l58.json"),
"--svg-dir", str(artifact_dir), "--case", "lattice-whole",
str(candidate_mesh), str(layout_path), "measure", "whole"]
for label, mesh, expectation in controls:
args.extend(("--case", label, str(mesh), str(audit_layout), expectation, "whole"))
_, audit_output = runner.run_stage("whole-surface-lattice-l58", surface_script,
args, binary=CLANG, data=fixture, recipe="lattice", style=2,
layout_path=layout_path, env=env_for(),
script_inputs=[surface_script, LINUX54 / "AuditWholeSurfaceL54.py",
candidate_mesh, layout_path, audit_layout, *(item[1] for item in controls)])
candidate = re.search(r"^\[WholeSurfaceAudit\] label=lattice-whole .*"
r"observed=(PASS|FAIL) expected=MEASURE status=PASS$", audit_output, re.M)
runner.require("whole-surface-lattice-l58:helix-and-maze",
bool(candidate and candidate.group(1) == "PASS"),
f"observed={candidate.group(1) if candidate else 'MISSING'} over helix ramp edges and maze wall feet")
for label, _mesh, expectation in controls:
match = re.search(rf"^\[WholeSurfaceAudit\] label={re.escape(label)} .*"
rf"observed=(PASS|FAIL) expected={expectation.upper()} status=PASS$",
audit_output, re.M)
runner.require(f"whole-surface-lattice-l58:{label}", bool(match),
f"control_expectation={expectation} observed={match.group(1) if match else 'MISSING'}")
def run_random_camera_sweep(runner: VerificationRunner, binaries: tuple[Path, ...], def run_random_camera_sweep(runner: VerificationRunner, binaries: tuple[Path, ...],
camera_count: int, camera_count: int,
configurations: tuple[tuple[int, int, int], ...]) -> None: configurations: tuple[tuple[int, int, int], ...]) -> None:
@@ -1911,6 +2201,11 @@ def run_medium_suite(runner: VerificationRunner, *, both_compilers: bool = False
run_caves_determinism(runner, CLANG, fixture, layout_path, compare_reference=False) run_caves_determinism(runner, CLANG, fixture, layout_path, compare_reference=False)
run_caves_performance(runner, CLANG, fixture, layout_path) run_caves_performance(runner, CLANG, fixture, layout_path)
run_caves_surface_evidence(runner, fixture, layout_path) run_caves_surface_evidence(runner, fixture, layout_path)
lattice_fixture, lattice_layout = run_native_lattice_field_audit(runner, CLANG)
run_lattice_determinism(runner, CLANG, lattice_fixture, lattice_layout,
compare_reference=False)
run_lattice_performance(runner, CLANG, lattice_fixture, lattice_layout)
run_lattice_surface_evidence(runner, lattice_fixture, lattice_layout)
_motif_fixture, motif_layout = run_motif_fixture_audits(runner, CLANG) _motif_fixture, motif_layout = run_motif_fixture_audits(runner, CLANG)
run_motif_determinism(runner, CLANG, compare_reference=False) run_motif_determinism(runner, CLANG, compare_reference=False)
run_motif_timing(runner, CLANG, motif_layout) run_motif_timing(runner, CLANG, motif_layout)
@@ -1932,6 +2227,13 @@ def run_gcc_supplement(runner: VerificationRunner) -> None:
if not fixture.is_file() or not layout_path.is_file(): if not fixture.is_file() or not layout_path.is_file():
fixture, layout_path = run_native_caves_field_audit(runner, CLANG) fixture, layout_path = run_native_caves_field_audit(runner, CLANG)
run_caves_determinism(runner, GCC, fixture, layout_path, compare_reference=True) run_caves_determinism(runner, GCC, fixture, layout_path, compare_reference=True)
lattice_fixture = OUT / "fixtures" / "lattice-l58.json"
lattice_layout = OUT / "layouts" / "native-lattice-field-clang.json"
if not lattice_fixture.is_file() or not lattice_layout.is_file():
lattice_fixture, lattice_layout = run_native_lattice_field_audit(runner, CLANG)
run_native_lattice_field_audit(runner, GCC)
run_lattice_determinism(runner, GCC, lattice_fixture, lattice_layout,
compare_reference=True)
run_motif_fixture_audits(runner, GCC) run_motif_fixture_audits(runner, GCC)
run_motif_determinism(runner, GCC, compare_reference=True) run_motif_determinism(runner, GCC, compare_reference=True)
@@ -1963,6 +2265,7 @@ def main() -> int:
run_random_camera_sweep(runner, (CLANG,), 3, ((3, 0, 0),)) run_random_camera_sweep(runner, (CLANG,), 3, ((3, 0, 0),))
run_ring(runner, full=False) run_ring(runner, full=False)
run_native_caves_field_audit(runner, CLANG) run_native_caves_field_audit(runner, CLANG)
run_native_lattice_field_audit(runner, CLANG)
run_motif_fixture_audits(runner, CLANG) run_motif_fixture_audits(runner, CLANG)
elif ARGS.tier == "medium": elif ARGS.tier == "medium":
run_medium_suite(runner, both_compilers=False) run_medium_suite(runner, both_compilers=False)