Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e443bf81c | ||
|
|
8be1c4d9ef | ||
|
|
ba017077d1 | ||
|
|
49e29fcd36 | ||
|
|
b7a47bef95 | ||
|
|
d0158c6019 | ||
|
|
28069f0d98 | ||
|
|
103d8b471e | ||
|
|
63c7e563d0 |
No files matched your search
@@ -1883,8 +1883,9 @@ Each record separates the nominal ceiling, content, and bedrock bands. A strict
|
|||||||
sequence for every strate and requires bedrock(N) to touch the next strate's ceiling(N+1). The two
|
sequence for every strate and requires bedrock(N) to touch the next strate's ceiling(N+1). The two
|
||||||
bands remain separately owned. Gradient and Interleaved interfaces may declare interpenetration only
|
bands remain separately owned. Gradient and Interleaved interfaces may declare interpenetration only
|
||||||
inside their explicit transition zone; Hard has zero effective width. The manager's `TransitionBlendChunks`
|
inside their explicit transition zone; Hard has zero effective width. The manager's `TransitionBlendChunks`
|
||||||
is expressed in owner-chunk metres. Interleaved's existing two-chunk domain warp extends its possible
|
is expressed in owner-chunk metres. L21 recorded the older Interleaved allowance as blend width plus
|
||||||
fingers beyond the blend width and is recorded separately. Compatibility validation reports current
|
warp amplitude; L29 now treats the declared blend width as the complete physical crossing allowance.
|
||||||
|
Domain warp changes finger placement, but cannot extend overlap beyond that width. Compatibility validation reports current
|
||||||
gaps, overlaps, ambiguities, inactive/missing bands, and unhonoured transitions while accepting them;
|
gaps, overlaps, ambiguities, inactive/missing bands, and unhonoured transitions while accepting them;
|
||||||
strict validation rejects them. Core dispatches per-strate recipe fields without applying that
|
strict validation rejects them. Core dispatches per-strate recipe fields without applying that
|
||||||
parameter blend, even though the legacy manager helper implements it. Both band types default diggable,
|
parameter blend, even though the legacy manager helper implements it. Both band types default diggable,
|
||||||
@@ -1943,6 +1944,7 @@ opens a band; the support strip stays below the player capsule. The lower mouth
|
|||||||
monotone descent from the short route-bound landing tail, whose height may vary with the route.
|
monotone descent from the short route-bound landing tail, whose height may vary with the route.
|
||||||
Both route and passage audits sample the 0.34 m by 1.76 m capsule, support probes, and grade; the
|
Both route and passage audits sample the 0.34 m by 1.76 m capsule, support probes, and grade; the
|
||||||
negative control compiles the same world without passage carving and must detect a blocked capsule.
|
negative control compiles the same world without passage carving and must detect a blocked capsule.
|
||||||
|
|
||||||
The L23 shared-field change is `WorldAssemblyEvaluatorVersion=9`. It does not change the L22 bands
|
The L23 shared-field change is `WorldAssemblyEvaluatorVersion=9`. It does not change the L22 bands
|
||||||
or any generator's field away from a passage. The L23 report includes exact-query/hash comparisons,
|
or any generator's field away from a passage. The L23 report includes exact-query/hash comparisons,
|
||||||
the blocked control, captures, and the Windows adapter checklist. §8.10's cache, proof, streaming,
|
the blocked control, captures, and the Windows adapter checklist. §8.10's cache, proof, streaming,
|
||||||
@@ -2051,6 +2053,115 @@ proof bounds preserve the L27 output.
|
|||||||
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md)
|
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md)
|
||||||
records counts, parity, costs and audit caveats.
|
records counts, parity, costs and audit caveats.
|
||||||
|
|
||||||
|
### 8.46 Honored bedrock/ceiling transitions — World Assembler L29
|
||||||
|
|
||||||
|
The upper strate's `EVoxelStrateTransition` controls the shared interface between its bedrock band
|
||||||
|
and the next strate's ceiling band in the Core composition path. `TransitionBlendChunks` is converted
|
||||||
|
using the resolved layout chunk width (8 m by default). Hard preserves the flat interface. Gradient
|
||||||
|
moves the shared surface with a smooth 2D hash field whose peak-to-peak range is at most one quarter
|
||||||
|
of the declared width. Interleaved uses a seed/interface/coordinate-keyed, domain-warped 3D hash field
|
||||||
|
to set actual overlap between the facing bands from zero to the declared width. The two faces move
|
||||||
|
inward by half that overlap; the warp changes finger placement without increasing the crossing
|
||||||
|
allowance. Both bands keep authored thickness, and their solid intervals touch or overlap in every
|
||||||
|
XY column. Core does not blend unrelated generator fields.
|
||||||
|
|
||||||
|
Each boundary band keeps at least **two Core voxels** of solid thickness (0.5 m at the default 25 cm
|
||||||
|
voxel size). Strict validation rejects thinner bands and declared interpenetration beyond the
|
||||||
|
transition width. The world audit samples a 512 m square XY area, measures actual crossing depth and
|
||||||
|
minimum band thickness, and sweeps every non-passage column through both bands at half-voxel spacing.
|
||||||
|
Passage columns are excluded from the hole sweep. Near an interface mouth, the shared face returns to
|
||||||
|
its nominal height inside the mouth radius plus a 1 m support margin, then fades back to the selected
|
||||||
|
transition over 2 m so the authored landing floor remains supported. Negative controls reject a
|
||||||
|
transition one micrometre wider than declared and a band one micrometre below the minimum. See
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md)
|
||||||
|
for audit measurements, hashes, timings, captures and run history.
|
||||||
|
|
||||||
|
### 8.47 Ground-bound SurfaceWorld departure routes — World Assembler M3 Linux L33
|
||||||
|
|
||||||
|
An intra-strate SurfaceWorld route follows the resolved terrain ground profile. Its route-point Z
|
||||||
|
is the ground height plus the player's half-height; the route does not descend to the strate's
|
||||||
|
bedrock boundary. Bedrock belongs to the separate inter-strate passage, which begins at the
|
||||||
|
SurfaceWorld departure mouth on the ground and tunnels outward and down through native rock and
|
||||||
|
the boundary into the next strate.
|
||||||
|
|
||||||
|
The old fixed candidate envelope used a 64 m chord and 16 m centre spacing. On the L33 upper-SW
|
||||||
|
fixture, replaying that envelope sampled ground from −507.767 to −464.857 m; 366/648 candidate paths
|
||||||
|
exceeded the 15° grade limit. The worst failures clustered near the outer west/northwest terrain
|
||||||
|
shoulder (up to 79.971°) and the steep approach slopes around the origin. Candidate diagnostics
|
||||||
|
separate grade failures from endpoints outside the bounded arrival/departure placement disks.
|
||||||
|
The bounded search now limits its longest chord to the two mouth radii minus a 3 m clearance
|
||||||
|
margin, checks centres every 8 m, and ranks endpoints near the route anchors. It finds a 21.5 m
|
||||||
|
route from (−2.75, 0) to (18.75, 0), sampled on ground and accepted by the full capsule/support/
|
||||||
|
grade audit.
|
||||||
|
|
||||||
|
The SurfaceWorld departure passage starts at the ground mouth with a 1.5 m portal, then smoothsteps
|
||||||
|
to a 3.5 m rock tunnel over 9 m. Its ring is sized and oriented outward from the mouth, so the
|
||||||
|
approach does not cut back under the walk route. The `FloatingIslands → SurfaceWorld → TunnelNetwork`
|
||||||
|
fixture passes all four passage styles, including both interfaces, capsule clearance, support,
|
||||||
|
grade, walkability, naturalness, open-air, column ends, long-bridge and route-binding audits.
|
||||||
|
|
||||||
|
`FloorProfileBlend` previously compared each curved floor profile with its straight chord and
|
||||||
|
applied a 2-voxel limit. That is a curvature measurement, not a seam measurement. It now checks
|
||||||
|
endpoint height and tangent continuity at 0.01 voxel and 0.001 voxel/m; the 3-voxel step and
|
||||||
|
0.25 voxel/m tangent-step controls are both detected. SurfaceWorld owner hashes remain identical
|
||||||
|
to L32 because the route-mouth, expanded column sizing and short-span support changes apply only
|
||||||
|
when SurfaceWorld is the upper strate. No evaluator or recipe version changed.
|
||||||
|
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md)
|
||||||
|
records the four-style audits, captures, determinism, owner hashes, costs and retries.
|
||||||
|
|
||||||
|
### 8.48 Irregular passage-wall relief and grounded tread shoulders — World Assembler M3 L35
|
||||||
|
|
||||||
|
L34's owner-SW profile had a 7.438 m radius repeat with 0.0361 m (3.61 cm) amplitude; an independent
|
||||||
|
mesh-normal scan also found a 4.750 m period at 0.02682 (1.537°). The geometry came from piecewise
|
||||||
|
capsule joins, while broad value-noise bands added a second regular pattern. The shared carve now
|
||||||
|
uses Catmull–Rom centerline interpolation on eligible interior segments, excluding short spans and
|
||||||
|
terminal controls. Passage walls use deterministic, rotated 43/29/19 m relief with reduced terrain
|
||||||
|
band contribution; other `VF_FieldSurfaceRelief` users, including grounded bridge columns, are
|
||||||
|
unchanged. The shared evaluator is version 21.
|
||||||
|
|
||||||
|
The residual radius scan on owner-SW style 0 measures a 7.062 m period at 0.0109 m (1.09 cm),
|
||||||
|
with 0.0044 m (0.44 cm) coherent amplitude. The independent L35 mesh-normal scan passes at 8 m
|
||||||
|
and 0.02496 axial amplitude, below its 0.0255 threshold. The ripple audit samples both side-wall
|
||||||
|
radii every 0.125 m in 32 m windows, scans 0.5–8 m periods, and rejects amplitudes of 0.035 m or
|
||||||
|
more. Its synthetic control uses 2 m radius spheres spaced every 1.5 m; it measures 0.0599 m and
|
||||||
|
must fail.
|
||||||
|
|
||||||
|
At the FI mouth, the flat apron is the shared passage support tread, not `LandingExtensionQ`, a tile
|
||||||
|
edge or a separate support layer. The tread is required by capsule and support probes. L35 gives its
|
||||||
|
outer entry shoulder a shallow taper and seeded grain while preserving the supported walk core and
|
||||||
|
lower-mouth overlap width. The shallow quarter-weight support-height/radius spline keeps the tread
|
||||||
|
continuous without moving its XY footprint.
|
||||||
|
|
||||||
|
Cascade routes also replace identical full-depth cosine lobes with a seeded shallow profile on each
|
||||||
|
turn. Passage, capsule, support, open-air, bridge, binding, strict layout, capture, determinism, ring,
|
||||||
|
and cost evidence are recorded in
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md).
|
||||||
|
|
||||||
|
### 8.49 Walk-floor washboard and FI mouth naturalness — World Assembler M3 L36
|
||||||
|
|
||||||
|
L35's exported owner-SW mid-tunnel floor measured a 7.250 m repeat at 0.02252 m amplitude; its
|
||||||
|
upper-SW fixture arrival measured a 1.438 m repeat at 0.02482 m. Both came from segment-built,
|
||||||
|
one-sided support treads meeting the native terrain zero crossing at per-segment joins. The floor
|
||||||
|
probe samples the exported walk mesh along the resolved route, separately from the wall-radius scan.
|
||||||
|
Eligible tread sections overlap by up to 2 m and all sparse route segments now use a bounded
|
||||||
|
signed-distance slab, including SurfaceWorld-to-TunnelNetwork routes. This keeps the tread top stable
|
||||||
|
across segment joins while capsule-clearance and native-solid gates retain walkability and open air.
|
||||||
|
Regular tread tops remain seated 5 cm lower and ease back to 0.20 m within 20 m of a lower landing;
|
||||||
|
seeded shoulder grain is removed, and gravity-facing relief is reduced while side-wall and ceiling
|
||||||
|
relief remain irregular.
|
||||||
|
|
||||||
|
On v23, the same mesh probes measure 7.188 m / 0.00067 m (0.067 cm) on owner-SW and 6.500 m /
|
||||||
|
0.00057 m (0.057 cm) on the upper fixture. The density audit checks five 32 m walk-floor windows at
|
||||||
|
0.125 m spacing, rejects amplitudes of 0.015 m or more, and detects its beaded control. The FI mouth
|
||||||
|
uses a local 2 m symmetric signed-field filter with the route protected and joins route-mouth support
|
||||||
|
to native terrain with a 1.5 m smooth signed-distance union. Its mesh audit reduces the L35
|
||||||
|
42.27° fade-ring jump to 27.13° and finds no detached component above the 0.0001 m² area floor; one
|
||||||
|
8-triangle subthreshold component at 0.0000266 m² is recorded. The evaluator is version 23. Full
|
||||||
|
recipe/style audits, route support, open-air, strict layout, legacy validators, determinism, capture and
|
||||||
|
paired-cost results are in
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ remains `0`, and lateral regions remain gated off.
|
|||||||
| `../../Standalone/Core/src/CoreNamespacePreamble.h` | First include of every Core TU. Forces FP contraction off per compiler (Clang/MSVC/GCC pragmas; unknown compiler = `#error`) and defines `vfcore::fp_contract::selfTest`, which `World::compile` runs before building: a fused multiply-add (e.g. Clang `-ffp-contract=fast`) refuses world generation. The world depends on contraction (Linux2/Linux3 reports), so never remove either. |
|
| `../../Standalone/Core/src/CoreNamespacePreamble.h` | First include of every Core TU. Forces FP contraction off per compiler (Clang/MSVC/GCC pragmas; unknown compiler = `#error`) and defines `vfcore::fp_contract::selfTest`, which `World::compile` runs before building: a fused multiply-add (e.g. Clang `-ffp-contract=fast`) refuses world generation. The world depends on contraction (Linux2/Linux3 reports), so never remove either. |
|
||||||
| `../../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, transition and dispatch provenance, 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.41. |
|
| `../../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/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`. |
|
||||||
@@ -1160,7 +1160,7 @@ and [`Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md`](Standalone/Resear
|
|||||||
| Symbol | Canonical file | Role |
|
| Symbol | Canonical file | Role |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PlanResolvedLayoutPassagesV1`, `ResolvedLayoutPassageV1::lowerMouthControlIndex` | `Standalone/Core/src/ResolvedLayoutInternal.h`, public record in `VoxelForgeCore.h` | Plans deterministic fixed-metre passages from adjacent director-resolved strata, binds route departure/arrival mouths, and records the derived split between the monotone descent and route-bound landing tail. |
|
| `PlanResolvedLayoutPassagesV1`, `ResolvedLayoutPassageV1::lowerMouthControlIndex` | `Standalone/Core/src/ResolvedLayoutInternal.h`, public record in `VoxelForgeCore.h` | Plans deterministic fixed-metre passages from adjacent director-resolved strata, binds route departure/arrival mouths, and records the derived split between the monotone descent and route-bound landing tail. |
|
||||||
| `FStandaloneShapeWorld::ApplyResolvedStrateComposition`, `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies boundary bands first, then opens the passage capsule and supported floor along the passage for every native generator. |
|
| `FStandaloneShapeWorld::ApplyResolvedStrateComposition`, `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies resolved boundary bands first, then opens the passage capsule and supported floor along the passage for every native generator. |
|
||||||
| `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples passage capsules, floor support, mouth ownership/radius, in-strate route bindings, and a blocked-passage negative control. |
|
| `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples passage capsules, floor support, mouth ownership/radius, in-strate route bindings, and a blocked-passage negative control. |
|
||||||
|
|
||||||
The evaluator version is 9. See ARCHITECTURE §8.41 and
|
The evaluator version is 9. See ARCHITECTURE §8.41 and
|
||||||
@@ -1217,6 +1217,57 @@ schema V3 is unchanged. See ARCHITECTURE §8.44 and
|
|||||||
See ARCHITECTURE §8.45 and
|
See ARCHITECTURE §8.45 and
|
||||||
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md).
|
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md).
|
||||||
|
|
||||||
|
### 3.29 Honored bedrock/ceiling transitions — World Assembler L29
|
||||||
|
|
||||||
|
| Symbol | Canonical file | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `EvaluateResolvedLayoutTransitionBandsV1`, `AuditResolvedStrateTransitions` | Public Core header / `Standalone/Core/src/ResolvedLayoutInternal.h`; `Standalone/Source/StreamBenchmarkPipeline.h` | Resolve the deterministic mode field from seed, interface and coordinates; audit actual crossing depth, minimum band thickness, passage-excluded holes, and strict negative controls. |
|
||||||
|
| `FStandaloneShapeWorld::ApplyResolvedStrateBoundaryBand` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies Hard, Gradient or Interleaved to both facing bands in the shared Core composition path; tapers locally at passage mouths to preserve landing support. |
|
||||||
|
|
||||||
|
The shared evaluator is version 15. Recipe and resolved-layout schema versions are unchanged.
|
||||||
|
The Unreal adapter now forwards the authored transition type and chunk width; those Unreal edits
|
||||||
|
were not compiled here. See ARCHITECTURE §8.46 and
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md).
|
||||||
|
|
||||||
|
### 3.30 SurfaceWorld as an upper strate and floor-profile joins — World Assembler L33
|
||||||
|
|
||||||
|
| Symbol | Canonical file | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `BuildFiniteStrateRoute` | `Standalone/Source/WorldAssemblyPrototype.h` | Samples route heights from the composed terrain ground; bounds candidate chords by the arrival/departure placement radii and records per-segment grade failures. |
|
||||||
|
| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Starts an upper-SurfaceWorld departure at its terrain mouth, opens a small portal and smoothsteps to the rock tunnel while orienting the ring outward. |
|
||||||
|
| `BuildPassageVoidCrossingPlan`, `PassageBridgeSignedDistance` | `Standalone/Core/src/ShapeWorldInternal.h` | Sizes unsupported ranges from the full control-segment window only for upper SurfaceWorld; other upper generators keep the previous bracketing-sample bounds. |
|
||||||
|
| `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Uses floor height rather than bore-centre height for naturalness and records failed-only wall probes for column audits. |
|
||||||
|
| `FloorProfileBlend` | `Standalone/Core/src/ShapeWorldInternal.h` | Measures endpoint height/tangent continuity; chord deviation is informational, with step and tangent-step negative controls. |
|
||||||
|
|
||||||
|
The owner SurfaceWorld hashes match L32 and evaluator/recipe versions are unchanged. See
|
||||||
|
ARCHITECTURE §8.47 and
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md).
|
||||||
|
|
||||||
|
### 3.31 Passage-wall ripple and FI tread-shoulder audit — World Assembler M3 L35
|
||||||
|
|
||||||
|
| Symbol | Canonical file | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `VF_FieldPassageWallRelief` | `Standalone/Core/src/VoxelForgeFieldShared.h` | Supplies passage-only irregular wall relief at rotated 43/29/19 m scales. Other `VF_FieldSurfaceRelief` users stay unchanged. |
|
||||||
|
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Catmull–Rom smooths eligible interior sweep joins, applies passage-only wall relief, and tapers/grains the entry support shoulder below the capsule/support probe footprint. |
|
||||||
|
| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Gives each Cascade turn a seeded shallow radial profile instead of identical repeated full-depth cosine lobes. |
|
||||||
|
| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples wall radii and walk-floor zero crossings at 0.125 m spacing, scans periods 0.5–8 m in 32 m windows, and rejects wall/floor amplitudes ≥0.035/0.015 m. Sphere and beaded-floor controls must be detected. |
|
||||||
|
|
||||||
|
`WorldAssemblyEvaluatorVersion=21`. See ARCHITECTURE §8.48 and
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md).
|
||||||
|
|
||||||
|
### 3.32 Walk-floor washboard and FI mouth blend — World Assembler M3 L36
|
||||||
|
|
||||||
|
| Symbol | Canonical file | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Extends eligible tread sections by up to 2 m and uses bounded signed-distance support on every sparse route, including SurfaceWorld-to-TunnelNetwork, removes seeded shoulder grain, and seats regular tread tops 5 cm lower with a smooth return at the lower landing. Capsule/open-air gates remain. Gravity-facing wall relief is reduced; side and ceiling relief remain. |
|
||||||
|
| `ApplyResolvedStratePassageCarves` mouth support | `Standalone/Core/src/ShapeWorldInternal.h` | Builds exact signed-distance route-mouth tread boxes and smooth-unions them with native terrain over 1.5 m. |
|
||||||
|
| `NativeFloatingIslandDensityAt` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies a local 2 m symmetric signed-field filter around FI passage mouths, exempts resolved route support, and removes isolated negative samples thinner than 2 m on all axes. |
|
||||||
|
| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Keeps the wall-radius audit and adds walk-floor zero-crossing profiles at 0.125 m, with five 32 m windows and a 0.015 m limit plus a beaded-floor negative control. |
|
||||||
|
| `RunMouthFragmentAuditL36.py`, `ProbeWalkFloorProfileL36.py` | `Standalone/Research/WorldAssemblerM3/Linux36/` | Compare L35 floor and FI-mouth controls with L36; the mouth check fails on detached components at or above 0.0001 m² and checks normal jumps across the fade ring. |
|
||||||
|
|
||||||
|
`WorldAssemblyEvaluatorVersion=23`. See ARCHITECTURE §8.49 and
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@@ -486,6 +486,7 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
|||||||
IslandPlan.bottomChunkZ = Slot.BottomChunkZ;
|
IslandPlan.bottomChunkZ = Slot.BottomChunkZ;
|
||||||
auto& Recipe = IslandPlan.manifest.recipe;
|
auto& Recipe = IslandPlan.manifest.recipe;
|
||||||
Recipe.geometryVersion = 8;
|
Recipe.geometryVersion = 8;
|
||||||
|
Recipe.terrainFollowingRoute = static_cast<std::uint32_t>(Passage.Style) == 0;
|
||||||
Recipe.layoutChunkWidthSubvoxels = LayoutChunkWidthSubvoxels;
|
Recipe.layoutChunkWidthSubvoxels = LayoutChunkWidthSubvoxels;
|
||||||
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
||||||
Recipe.ownerVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0));
|
Recipe.ownerVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0));
|
||||||
@@ -1106,13 +1107,21 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
|||||||
Item.transitionWidthMicrometres = static_cast<std::int64_t>(Item.transitionBlendChunks)
|
Item.transitionWidthMicrometres = static_cast<std::int64_t>(Item.transitionBlendChunks)
|
||||||
* Item.managerChunkWidthMicrometres;
|
* Item.managerChunkWidthMicrometres;
|
||||||
Item.transitionWarpAmplitudeMicrometres = Item.transitionMode == "Interleaved"
|
Item.transitionWarpAmplitudeMicrometres = Item.transitionMode == "Interleaved"
|
||||||
? 2 * Item.managerChunkWidthMicrometres : 0;
|
? std::min(Item.transitionWidthMicrometres / 2, Item.managerChunkWidthMicrometres) : 0;
|
||||||
Item.transitionInterpenetrationMicrometres = 0;
|
Item.transitionInterpenetrationMicrometres = Item.transitionMode == "Interleaved"
|
||||||
Item.transitionHonouredInCore = Slot.Definition->TransitionType == EVoxelStrateTransition::Hard;
|
? Item.transitionWidthMicrometres
|
||||||
Item.transitionFallsBackToHard = Slot.Definition->TransitionType != EVoxelStrateTransition::Hard;
|
: Item.transitionMode == "Gradient" ? Item.transitionWidthMicrometres / 4 : 0;
|
||||||
|
Item.transitionFallsBackToHard = Item.transitionMode != "Hard"
|
||||||
|
&& Item.transitionWidthMicrometres <= 0;
|
||||||
|
Item.transitionHonouredInCore = Item.transitionMode == "Hard"
|
||||||
|
|| !Item.transitionFallsBackToHard;
|
||||||
Item.transitionDispatchPath = Item.transitionFallsBackToHard
|
Item.transitionDispatchPath = Item.transitionFallsBackToHard
|
||||||
? "declared Gradient/Interleaved recorded; Core uses an explicit Hard transition fallback with zero interpenetration"
|
? "declared zero-width transition uses an explicit Hard fallback"
|
||||||
: "declared Hard transition is applied at this interface";
|
: Item.transitionMode == "Hard"
|
||||||
|
? "declared Hard transition preserves the flat shared bedrock/ceiling interface"
|
||||||
|
: Item.transitionMode == "Interleaved"
|
||||||
|
? "shared composition overlaps both facing bands with deterministic domain-warped fingers inside the declared width"
|
||||||
|
: "shared composition moves both band faces together with the smooth declared-width transition field";
|
||||||
Item.managerLookup = "UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate";
|
Item.managerLookup = "UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate";
|
||||||
if (Type == ECaveGeneratorType::SurfaceWorld)
|
if (Type == ECaveGeneratorType::SurfaceWorld)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -201,15 +201,19 @@ enum class ECaveGeneratorType : uint8
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EVoxelStrateTransition — How two adjacent strates blend at their shared boundary.
|
* EVoxelStrateTransition — How two adjacent strates meet at their shared boundary.
|
||||||
*
|
*
|
||||||
* Each strate boundary can use a different transition style. The transition type
|
* Each strate boundary can use a different transition style. The transition type
|
||||||
* is defined on the UPPER strate's definition and controls the boundary between
|
* is defined on the UPPER strate's definition and controls the boundary between
|
||||||
* itself and the strate below it.
|
* itself and the strate below it.
|
||||||
*
|
*
|
||||||
* Gradient: Smooth linear interpolation of generation params across TransitionBlendChunks.
|
* In the Core composition path, TransitionBlendChunks defines the full permitted
|
||||||
* This is the classic approach — no seam visible, but also no dramatic
|
* crossing depth between the upper bedrock and lower ceiling bands. Each band keeps
|
||||||
* geological feature at the boundary. Best for similar-looking strates.
|
* its authored thickness and the solid bands always overlap or touch. The legacy manager
|
||||||
|
* may also blend generation parameters for Gradient; Core does not blend unrelated fields.
|
||||||
|
*
|
||||||
|
* Gradient: A low-amplitude smooth height field moves the shared interface and both
|
||||||
|
* content-facing band surfaces. Best for gentle geological variation.
|
||||||
*
|
*
|
||||||
* Hard: No blending at all. The strate's params apply right up to the boundary,
|
* Hard: No blending at all. The strate's params apply right up to the boundary,
|
||||||
* then instantly switch to the neighbor's params. The abrupt change in
|
* then instantly switch to the neighbor's params. The abrupt change in
|
||||||
@@ -217,26 +221,21 @@ enum class ECaveGeneratorType : uint8
|
|||||||
* discontinuity at the boundary. Best for strates that should feel like
|
* discontinuity at the boundary. Best for strates that should feel like
|
||||||
* distinct geological layers with a visible dividing line.
|
* distinct geological layers with a visible dividing line.
|
||||||
*
|
*
|
||||||
* Interleaved: The boundary position is warped by a 3D noise field, so "fingers"
|
* Interleaved: A deterministic 3D domain-warped hash field sets the overlap depth per XY
|
||||||
* of one strate reach into the other. Some XY columns transition early,
|
* column from zero to the declared width, creating irregular fingers.
|
||||||
* others late, creating an irregular, interlocking boundary. The warp
|
* Best for strates that should feel grown together.
|
||||||
* amplitude is ~2 chunks, and Z frequency is lower than XY to produce
|
|
||||||
* horizontal finger-like intrusions. Best for strates that should feel
|
|
||||||
* like they grew into each other over geological time.
|
|
||||||
*/
|
*/
|
||||||
UENUM(BlueprintType)
|
UENUM(BlueprintType)
|
||||||
enum class EVoxelStrateTransition : uint8
|
enum class EVoxelStrateTransition : uint8
|
||||||
{
|
{
|
||||||
// Smooth linear interpolation of all generation params across the blend zone.
|
// Smoothly undulates the shared bedrock/ceiling interface in the Core composition path.
|
||||||
// No visible boundary — params change gradually over TransitionBlendChunks distance.
|
|
||||||
Gradient UMETA(DisplayName = "Gradient (smooth blend)"),
|
Gradient UMETA(DisplayName = "Gradient (smooth blend)"),
|
||||||
|
|
||||||
// No blending — params switch instantly at the boundary.
|
// No blending — params switch instantly at the boundary.
|
||||||
// Creates natural cliffs, ledges, or material discontinuities.
|
// Creates natural cliffs, ledges, or material discontinuities.
|
||||||
Hard UMETA(DisplayName = "Hard (sharp boundary with cliff)"),
|
Hard UMETA(DisplayName = "Hard (sharp boundary with cliff)"),
|
||||||
|
|
||||||
// 3D noise warps the boundary position per XY column.
|
// A 3D domain-warped hash field creates finger-like intrusions within the declared width.
|
||||||
// Creates finger-like intrusions of one strate into the other.
|
|
||||||
Interleaved UMETA(DisplayName = "Interleaved (domain-warped fingers)")
|
Interleaved UMETA(DisplayName = "Interleaved (domain-warped fingers)")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -249,8 +249,127 @@ namespace VoxelForgeCore
|
|||||||
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
|
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
|
||||||
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
||||||
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
||||||
// Version 14 adds shared passage columns for long native open-air crossings.
|
// Version 14 adds shared passage columns; version 16 smooths owner Gradient faces;
|
||||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 14;
|
// version 20 restores full Gradient amplitude and uses a continuous signed-distance band union;
|
||||||
|
// version 22 overlaps tread sections and smooths FloatingIslands passage-mouth features;
|
||||||
|
// version 23 uses signed-distance support for sparse SurfaceWorld-to-TunnelNetwork treads;
|
||||||
|
// version 24 narrows passage-mouth support and filters native ground detail;
|
||||||
|
// version 25 joins open-mouth treads to native terrain relief before flattening inward;
|
||||||
|
// version 26 tapers mouth width and bounds the support to the native terrain surface;
|
||||||
|
// version 27 keeps the native blend through the entrance ramp and narrows its filter;
|
||||||
|
// version 28 tapers the open end to a worn path and blends it into walkable support;
|
||||||
|
// version 29 curves the supported and carved path through both mouth shoulders;
|
||||||
|
// version 30 narrows the flat core and blends support through a noisy natural shoulder;
|
||||||
|
// version 31 adds shared, capsule-safe field relief to both open-mouth walking surfaces;
|
||||||
|
// version 32 lowers the mouth core into the bore and removes thin filtered-field remnants;
|
||||||
|
// version 33 fades relief before small detached field remnants;
|
||||||
|
// version 34 keeps the open-mouth tread within native terrain curvature and noise;
|
||||||
|
// version 35 follows the native heightfield farther into the mouth ramp;
|
||||||
|
// version 36 protects the walking line with a gradual filter fade;
|
||||||
|
// version 37 lengthens that fade and restores the last capsule margin;
|
||||||
|
// version 38 keeps native ground at the lip and blends the tread in below it;
|
||||||
|
// version 39 bends the mouth tread across the native field and restores the open bore;
|
||||||
|
// version 40 uses broad route-only filtering and smooth, low-frequency tread relief;
|
||||||
|
// version 41 rounds the mouth tread and removes filter route masks;
|
||||||
|
// version 42 clips the mouth bore at the native walking surface;
|
||||||
|
// version 43 localizes the mouth field filter without masking the surface route;
|
||||||
|
// version 44 widens the natural shoulder blend and follows lower-frequency terrain;
|
||||||
|
// version 45 tests the passage tread independently of route-anchor support;
|
||||||
|
// version 46 fades the native filter broadly along the authored surface path;
|
||||||
|
// version 47 carries the full winding route with a narrow native-height tread;
|
||||||
|
// version 48 fades route support out before the surface mouth;
|
||||||
|
// version 49 keeps the mouth filtered and restores detail on the outer route;
|
||||||
|
// version 50 limits route-field restoration to 65% of the convolution weight;
|
||||||
|
// version 51 narrows the route fade to 8 m horizontal and 6 m vertical;
|
||||||
|
// version 52 evaluates mouth support in positive-density route tiles;
|
||||||
|
// version 53 fades the supported passage tread in from native ground at both mouths;
|
||||||
|
// version 54 tapers mouth-support width and depth;
|
||||||
|
// version 55 blends that tread from native ground and applies route support after carving;
|
||||||
|
// version 56 seats route support at the audited floor;
|
||||||
|
// version 57's raised lip overfilled the mouth;
|
||||||
|
// version 58 restores the native-height lip and explicitly covers route-support queries;
|
||||||
|
// version 59 fixes route-support distance units and lengthens the worn-mouth taper;
|
||||||
|
// version 60 brings the narrow terrain-following route tread in before the outer mouth lip;
|
||||||
|
// version 61 finishes that tread by five metres and preserves its exact walking boundary;
|
||||||
|
// version 62 fades the route tread along the authored path and seats it below the capsule;
|
||||||
|
// version 63 only adds that tread through native-air gaps and fades it over two path metres;
|
||||||
|
// version 64 rounds the path-to-ground join while keeping its top below the capsule;
|
||||||
|
// version 65 tests rounded tread cross-sections and softer native-height following;
|
||||||
|
// version 66 fades the mouth carve and support together so native crater ground forms the lip;
|
||||||
|
// version 67 follows the native route height more closely and completes the entrance fade sooner;
|
||||||
|
// version 68 narrows the flat walk core and eases a shallow worn ramp into the cave floor;
|
||||||
|
// version 69 lets capsule-cleared mouth treads bridge positive-density floor gaps;
|
||||||
|
// version 70 lowers the mouth tread enough to clear the capsule's toe margin;
|
||||||
|
// version 71 keeps its rounded top inside the route's shallow support-probe band;
|
||||||
|
// version 72 adds mouth support only below its signed floor, avoiding capsule-space fill;
|
||||||
|
// version 73 carries the terrain-following route tread to the actual passage mouth;
|
||||||
|
// version 74 seats that short connection in the audited shallow support band;
|
||||||
|
// version 75 keeps the rounded path edge outside the capsule's shallow floor probes;
|
||||||
|
// version 76 carries the endpoint tread through the passage's first support-probe depth;
|
||||||
|
// version 77 joins the mouth floor to native ground through a noisy walk ramp;
|
||||||
|
// version 78 rounds the ramp shoulder and clears its initial capsule envelope;
|
||||||
|
// version 79 tested the unbuilt native-ground mouth;
|
||||||
|
// version 80 narrows the walk tread;
|
||||||
|
// version 81 adds bounded path relief and a wavering, worn shoulder;
|
||||||
|
// version 82 lowers the relief into the tread to keep its capsule clearance;
|
||||||
|
// version 83 increases the natural edge warp and recessed surface variation;
|
||||||
|
// version 84 moves that variation to the actual mouth and restores support width;
|
||||||
|
// version 85 keeps the established lower-mouth capsule profile while shaping entry mouths;
|
||||||
|
// version 86 restricts route reinforcement to native solid and recesses the worn top;
|
||||||
|
// version 87 restores its pre-carve route placement.
|
||||||
|
// version 88 removes the independent mouth cap; the native crater surface forms the entry ramp.
|
||||||
|
// version 89 removes the one-sided entry tread band as well.
|
||||||
|
// version 90 lengthens the entrance floor-carve fade to retain native ground beneath the route.
|
||||||
|
// version 91 lets a narrow mouth support follow the full native surface relief.
|
||||||
|
// version 92 derives mouth floor alignment from the entrance path for first-segment samples too.
|
||||||
|
// version 93 blends the carve against native density so a zero fade really preserves ground.
|
||||||
|
// version 94 applies the mouth floor fade only beneath the walking surface.
|
||||||
|
// version 95 seats retained native ground just below the capsule's foot clearance.
|
||||||
|
// version 96 limits that preservation to the shallow support band, away from inner coils.
|
||||||
|
// version 97 extends the native support band down to the sampled rock envelope.
|
||||||
|
// version 98 raises the support band back to the route floor while keeping it below the capsule.
|
||||||
|
// version 99 restores native-solid route support after the passage carve;
|
||||||
|
// version 100 restores a shallow, noise-shaped entrance tread under its capsule gate;
|
||||||
|
// version 101 preserves the native FI field around the resolved walking line;
|
||||||
|
// version 102 tapers the mouth filter from its 1.5 m core to the same 12.5 m edge;
|
||||||
|
// version 103 blends route protection over a metre-scale shoulder;
|
||||||
|
// version 104 composes overlapping route masks once, by maximum influence;
|
||||||
|
// version 105 widens the route blend to soften the native/filter seam;
|
||||||
|
// version 106 preserves raw negative rock under the route without unfiltering its crown;
|
||||||
|
// version 107 softens that restore and broadens the narrow upper-coil walk core;
|
||||||
|
// version 108 deepens relief and lowers route restoration to soften the remaining crease;
|
||||||
|
// version 109 blended 60% of the two-metre mouth field convolution;
|
||||||
|
// version 110 restores the full filtered contour and widens the upper walk core;
|
||||||
|
// version 111 permits the capsule-cleared signed tread to bridge the upper mouth gap;
|
||||||
|
// version 112 adds a tenth-metre margin to the rounded upper tread edge.
|
||||||
|
// version 139 keeps native ground only in a capsule-scale mouth lane and carries
|
||||||
|
// the seeded terrain contour through the full upper-mouth support;
|
||||||
|
// versions 140-145 tested short-span decks, mouth fade length, route phase and tread SDF;
|
||||||
|
// versions 146-148 tested a sunk chord, near-side phase and all-style FI spiral.
|
||||||
|
// version 149 routes non-spiral FI mouths into a tight near-side ring.
|
||||||
|
// version 150 widened that ring but left a central pillar.
|
||||||
|
// version 151 keeps the ring half a metre outside the passage bore.
|
||||||
|
// version 152 fills capsule-clear positive-density gaps under the narrow mouth tread.
|
||||||
|
// version 153 tested a one-metre entry-turn sink; it left the raised route visible and rippled the floor.
|
||||||
|
// version 154 tightens the FI ring and uses a steeper, still walkable descent to separate hidden coils.
|
||||||
|
// version 155's tighter coil removed the fin but made sharp fragments and route collisions.
|
||||||
|
// version 156 keeps the 3 m ring, overlaps the bore by 0.25 m, and seats the native tread below the capsule.
|
||||||
|
// version 157 joins the near-side FI ring at a tangent so its entrance does not kink across the crater.
|
||||||
|
// version 158 tested a descending entry coil; its exposed canopy is rejected.
|
||||||
|
// version 159 keeps the tangent and uses a steeper sub-15-degree FI descent to separate its coils.
|
||||||
|
// version 160 removed the mouth walk lane; it left the bridge and lost one support sample.
|
||||||
|
// version 161 tapered the anchored entry radius to 3 m; a fin remained at the crater lip.
|
||||||
|
// version 162 disabled native-tread restoration alone; the carve fade still preserved the ridge.
|
||||||
|
// version 163 disabled both passage restorations; it removed the path support.
|
||||||
|
// version 164 narrowed the first two inner FI route points; it enlarged the lip fin.
|
||||||
|
// version 165 disabled native-rock blending around the FI stratum route; the ridge remained.
|
||||||
|
// version 166 disabled the FI mouth support handoff; route support failed on the island approach.
|
||||||
|
// version 167 tested centerline footing on the FI approach; its bridge remained and floor ripple rose.
|
||||||
|
// version 168 removed the connector's 1 m lift; the crater crossing remained exposed.
|
||||||
|
// version 169 sank every connector 2 m; it left four FI route support probes low.
|
||||||
|
// version 170 sinks only connectors touching the anchored root by 1 m.
|
||||||
|
// Version 208 keeps the crater tread scoped to style-zero FI mouths.
|
||||||
|
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 208;
|
||||||
|
|
||||||
enum class FieldChannel : std::uint32_t
|
enum class FieldChannel : std::uint32_t
|
||||||
{
|
{
|
||||||
@@ -444,6 +563,8 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t anchorIslandMinimumRadiusSubvoxels = 0;
|
std::int64_t anchorIslandMinimumRadiusSubvoxels = 0;
|
||||||
std::int64_t anchorRootBottomZSubvoxels = 0;
|
std::int64_t anchorRootBottomZSubvoxels = 0;
|
||||||
std::int64_t anchorMouthOffsetSubvoxels = 0;
|
std::int64_t anchorMouthOffsetSubvoxels = 0;
|
||||||
|
// Style-zero mouths follow the sampled island surface into the passage lip.
|
||||||
|
bool terrainFollowingRoute = false;
|
||||||
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
||||||
std::uint64_t contentHash = 0;
|
std::uint64_t contentHash = 0;
|
||||||
};
|
};
|
||||||
@@ -639,6 +760,18 @@ namespace VoxelForgeCore
|
|||||||
std::vector<ResolvedLayoutAnchorV2> anchors;
|
std::vector<ResolvedLayoutAnchorV2> anchors;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Per-column resolved bands for one adjacent bedrock/ceiling interface.
|
||||||
|
* crossingDepthMicrometres is the actual overlap depth between the two facing bands.
|
||||||
|
*/
|
||||||
|
struct ResolvedLayoutTransitionBandsV1
|
||||||
|
{
|
||||||
|
std::int64_t crossingDepthMicrometres = 0;
|
||||||
|
std::int64_t upperBedrockBottomMicrometres = 0;
|
||||||
|
std::int64_t upperBedrockTopMicrometres = 0;
|
||||||
|
std::int64_t lowerCeilingBottomMicrometres = 0;
|
||||||
|
std::int64_t lowerCeilingTopMicrometres = 0;
|
||||||
|
};
|
||||||
|
|
||||||
struct ResolvedLayoutIssueV2
|
struct ResolvedLayoutIssueV2
|
||||||
{
|
{
|
||||||
std::string code;
|
std::string code;
|
||||||
@@ -656,6 +789,8 @@ namespace VoxelForgeCore
|
|||||||
std::string stableId;
|
std::string stableId;
|
||||||
std::int32_t upperStrateIndex = -1;
|
std::int32_t upperStrateIndex = -1;
|
||||||
std::int32_t lowerStrateIndex = -1;
|
std::int32_t lowerStrateIndex = -1;
|
||||||
|
// EVoxelPassageStyle numeric value used by style-specific passage support.
|
||||||
|
std::uint32_t style = 1;
|
||||||
std::int64_t placementRadiusSubvoxels = 0;
|
std::int64_t placementRadiusSubvoxels = 0;
|
||||||
std::array<std::int64_t, 3> upperMouthSubvoxels{};
|
std::array<std::int64_t, 3> upperMouthSubvoxels{};
|
||||||
std::array<std::int64_t, 3> lowerMouthSubvoxels{};
|
std::array<std::int64_t, 3> lowerMouthSubvoxels{};
|
||||||
@@ -730,6 +865,17 @@ namespace VoxelForgeCore
|
|||||||
std::string_view json, std::string& error);
|
std::string_view json, std::string& error);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the deterministic band faces at one XY column. Gradient shares one smooth
|
||||||
|
* displaced face; Interleaved moves the faces inward by a total crossing no larger than
|
||||||
|
* the declared width. Both bands retain authored thickness and touch or overlap, so no
|
||||||
|
* air gap can form.
|
||||||
|
*/
|
||||||
|
VFCORE_API ResolvedLayoutTransitionBandsV1 EvaluateResolvedLayoutTransitionBandsV1(
|
||||||
|
const ResolvedLayoutStratumV2& upper, const ResolvedLayoutStratumV2& lower,
|
||||||
|
std::int32_t worldSeed, std::int64_t layoutChunkSizeMicrometres,
|
||||||
|
std::int64_t xMicrometres, std::int64_t yMicrometres);
|
||||||
|
|
||||||
struct ResolvedLayoutValidatorAuditV3
|
struct ResolvedLayoutValidatorAuditV3
|
||||||
{
|
{
|
||||||
std::uint32_t controlsRun = 0;
|
std::uint32_t controlsRun = 0;
|
||||||
|
|||||||
@@ -1217,6 +1217,82 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
|||||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill only underside cells that marching cubes missed. Active transitions use the
|
||||||
|
// same displaced face and passage-mouth fade as the signed-distance field.
|
||||||
|
struct FTransitionFaceReplacement
|
||||||
|
{
|
||||||
|
double BaseZVoxels=0.0;
|
||||||
|
double MaximumDisplacementVoxels=0.0;
|
||||||
|
std::vector<uint8> Cells;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline int32 RemoveCoarseTransitionFaceTriangles(FVoxelMeshData& Mesh,
|
||||||
|
const FIntVector& Origin,int32 Step,int32 GridSide,double VoxelSizeCm,
|
||||||
|
const std::vector<FTransitionFaceReplacement>& Replacements,bool bUpward)
|
||||||
|
{
|
||||||
|
if(Step<=0||GridSide<2||VoxelSizeCm<=0.0||Replacements.empty())return 0;
|
||||||
|
const int32 TriangleCount=Mesh.Triangles.Num()/3;
|
||||||
|
const int32 OldCeilingStart=std::max(0,TriangleCount-Mesh.NumCeilingTriangles);
|
||||||
|
TArray<uint8> OldMasks=Mesh.TriangleSkirtFaceMasks;
|
||||||
|
OldMasks.SetNum(TriangleCount);
|
||||||
|
TArray<int32> KeptTriangles;
|
||||||
|
TArray<uint8> KeptMasks;
|
||||||
|
KeptTriangles.Reserve(Mesh.Triangles.Num());
|
||||||
|
KeptMasks.Reserve(TriangleCount);
|
||||||
|
int32 Removed=0,RemovedCeiling=0;
|
||||||
|
const int32 CellSide=GridSide-1;
|
||||||
|
for(int32 Triangle=0;Triangle<TriangleCount;++Triangle)
|
||||||
|
{
|
||||||
|
const int32 IA=Mesh.Triangles[Triangle*3];
|
||||||
|
const int32 IB=Mesh.Triangles[Triangle*3+1];
|
||||||
|
const int32 IC=Mesh.Triangles[Triangle*3+2];
|
||||||
|
bool bRemove=false;
|
||||||
|
if(OldMasks[Triangle]==0&&IA>=0&&IB>=0&&IC>=0
|
||||||
|
&&IA<Mesh.Vertices.Num()&&IB<Mesh.Vertices.Num()&&IC<Mesh.Vertices.Num())
|
||||||
|
{
|
||||||
|
const FVector& A=Mesh.Vertices[IA];
|
||||||
|
const FVector& B=Mesh.Vertices[IB];
|
||||||
|
const FVector& C=Mesh.Vertices[IC];
|
||||||
|
const FVector GeometricNormal=FVector::CrossProduct(B-A,C-A);
|
||||||
|
const double NormalLength=GeometricNormal.Size();
|
||||||
|
const double Facing=NormalLength>1.0e-12?GeometricNormal.Z/NormalLength:0.0;
|
||||||
|
if((bUpward&&Facing>0.35)||(!bUpward&&Facing< -0.35))
|
||||||
|
{
|
||||||
|
const double X=(A.X+B.X+C.X)/(3.0*VoxelSizeCm);
|
||||||
|
const double Y=(A.Y+B.Y+C.Y)/(3.0*VoxelSizeCm);
|
||||||
|
const double Z=(A.Z+B.Z+C.Z)/(3.0*VoxelSizeCm);
|
||||||
|
const int32 CellX=static_cast<int32>(std::floor((X-Origin.X)/Step));
|
||||||
|
const int32 CellY=static_cast<int32>(std::floor((Y-Origin.Y)/Step));
|
||||||
|
if(CellX>=0&&CellX<CellSide&&CellY>=0&&CellY<CellSide)
|
||||||
|
{
|
||||||
|
const std::size_t Cell=static_cast<std::size_t>(CellY)*CellSide+CellX;
|
||||||
|
for(const auto& Replacement:Replacements)
|
||||||
|
{
|
||||||
|
if(Cell>=Replacement.Cells.size()||!Replacement.Cells[Cell])continue;
|
||||||
|
const double Margin=Replacement.MaximumDisplacementVoxels+4.0;
|
||||||
|
if(Z>=Replacement.BaseZVoxels-Margin
|
||||||
|
&&Z<=Replacement.BaseZVoxels+Margin)
|
||||||
|
{bRemove=true;break;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(bRemove)
|
||||||
|
{
|
||||||
|
++Removed;
|
||||||
|
if(Triangle>=OldCeilingStart)++RemovedCeiling;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
KeptTriangles.Add(IA);KeptTriangles.Add(IB);KeptTriangles.Add(IC);
|
||||||
|
KeptMasks.Add(OldMasks[Triangle]);
|
||||||
|
}
|
||||||
|
if(Removed==0)return 0;
|
||||||
|
Mesh.Triangles=std::move(KeptTriangles);
|
||||||
|
Mesh.TriangleSkirtFaceMasks=std::move(KeptMasks);
|
||||||
|
Mesh.NumCeilingTriangles=std::max(0,Mesh.NumCeilingTriangles-RemovedCeiling);
|
||||||
|
return Removed;
|
||||||
|
}
|
||||||
|
|
||||||
inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
||||||
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
||||||
{
|
{
|
||||||
@@ -1230,9 +1306,14 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
{
|
{
|
||||||
double FaceZ=0.0;
|
double FaceZ=0.0;
|
||||||
double Thickness=0.0;
|
double Thickness=0.0;
|
||||||
|
double MaximumDisplacementVoxels=0.0;
|
||||||
|
std::vector<double> FaceHeights;
|
||||||
std::vector<uint8> SolidAbove;
|
std::vector<uint8> SolidAbove;
|
||||||
|
const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr;
|
||||||
|
bool bTransition=false;
|
||||||
};
|
};
|
||||||
std::vector<FCandidate> Candidates;
|
std::vector<FCandidate> Candidates;
|
||||||
|
std::vector<FTransitionFaceReplacement> Replacements;
|
||||||
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
||||||
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
||||||
{
|
{
|
||||||
@@ -1243,19 +1324,34 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
Candidate.FaceZ=static_cast<double>(Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
Candidate.FaceZ=static_cast<double>(Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
||||||
Candidate.Thickness=static_cast<double>(Stratum->ceiling.topMicrometres
|
Candidate.Thickness=static_cast<double>(Stratum->ceiling.topMicrometres
|
||||||
-Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
-Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
||||||
if(Candidate.FaceZ<Origin.Z-1.0e-6||Candidate.FaceZ>Origin.Z+Extent+1.0e-6)continue;
|
Candidate.Stratum=Stratum;
|
||||||
|
const auto* transitionUpper=Shape.FindResolvedLayoutStratumByOrder(Stratum->order-1);
|
||||||
|
const bool bTransition=transitionUpper
|
||||||
|
&&Shape.IsResolvedLayoutTransitionActive(*transitionUpper);
|
||||||
|
const double displacement=bTransition?static_cast<double>(
|
||||||
|
Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(*transitionUpper,*Stratum))
|
||||||
|
/CoreMicrons:0.0;
|
||||||
|
if(Candidate.FaceZ<Origin.Z-displacement-1.0e-6
|
||||||
|
||Candidate.FaceZ>Origin.Z+Extent+displacement+1.0e-6)continue;
|
||||||
|
Candidate.bTransition=bTransition;
|
||||||
|
Candidate.MaximumDisplacementVoxels=displacement;
|
||||||
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
||||||
if(Epsilon<=0.0)continue;
|
if(Epsilon<=0.0)continue;
|
||||||
Candidate.SolidAbove.resize(static_cast<std::size_t>(GridSide)*GridSide,0);
|
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
||||||
|
Candidate.FaceHeights.resize(GridCount,Candidate.FaceZ);
|
||||||
|
Candidate.SolidAbove.resize(GridCount,0);
|
||||||
bool AnySolidAbove=false;
|
bool AnySolidAbove=false;
|
||||||
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
||||||
{
|
{
|
||||||
const double WorldX=Origin.X+X*GridStep;
|
const double WorldX=Origin.X+X*GridStep;
|
||||||
const double WorldY=Origin.Y+Y*GridStep;
|
const double WorldY=Origin.Y+Y*GridStep;
|
||||||
|
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
|
Candidate.bTransition=Shape.ResolvedCeilingBottomAt(*Candidate.Stratum,
|
||||||
|
WorldX,WorldY,Candidate.FaceHeights[GridIndex])||Candidate.bTransition;
|
||||||
const float Above=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
const float Above=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
||||||
static_cast<float>(Candidate.FaceZ+Epsilon));
|
static_cast<float>(Candidate.FaceHeights[GridIndex]+Epsilon));
|
||||||
const bool bSolidAbove=std::isfinite(Above)&&Above<0.0f;
|
const bool bSolidAbove=std::isfinite(Above)&&Above<0.0f;
|
||||||
Candidate.SolidAbove[static_cast<std::size_t>(Y*GridSide+X)]=bSolidAbove?1:0;
|
Candidate.SolidAbove[GridIndex]=bSolidAbove?1:0;
|
||||||
AnySolidAbove=AnySolidAbove||bSolidAbove;
|
AnySolidAbove=AnySolidAbove||bSolidAbove;
|
||||||
}
|
}
|
||||||
if(AnySolidAbove)Candidates.push_back(std::move(Candidate));
|
if(AnySolidAbove)Candidates.push_back(std::move(Candidate));
|
||||||
@@ -1277,8 +1373,19 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
std::vector<int32> NewTriangles;
|
std::vector<int32> NewTriangles;
|
||||||
for(const FCandidate& Candidate:Candidates)
|
for(const FCandidate& Candidate:Candidates)
|
||||||
{
|
{
|
||||||
std::vector<int32> VertexMap(static_cast<std::size_t>(GridSide)*GridSide,INDEX_NONE);
|
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
||||||
const double CeilingZ=Candidate.FaceZ;
|
std::vector<int32> VertexMap(GridCount,INDEX_NONE);
|
||||||
|
FTransitionFaceReplacement Replacement;
|
||||||
|
Replacement.BaseZVoxels=Candidate.FaceZ;
|
||||||
|
Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels;
|
||||||
|
Replacement.Cells.resize(static_cast<std::size_t>(GridSide-1)*(GridSide-1),0);
|
||||||
|
const auto heightAt=[&](double X,double Y)
|
||||||
|
{
|
||||||
|
if(!Candidate.bTransition)return Candidate.FaceZ;
|
||||||
|
double height=Candidate.FaceZ;
|
||||||
|
Shape.ResolvedCeilingBottomAt(*Candidate.Stratum,X,Y,height);
|
||||||
|
return height;
|
||||||
|
};
|
||||||
const auto AddVertex=[&](int32 X,int32 Y)
|
const auto AddVertex=[&](int32 X,int32 Y)
|
||||||
{
|
{
|
||||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
@@ -1286,8 +1393,20 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
if(Mapped!=INDEX_NONE)return Mapped;
|
if(Mapped!=INDEX_NONE)return Mapped;
|
||||||
Mapped=static_cast<int32>(NewVertices.size());
|
Mapped=static_cast<int32>(NewVertices.size());
|
||||||
const double WorldX=Origin.X+X*GridStep,WorldY=Origin.Y+Y*GridStep;
|
const double WorldX=Origin.X+X*GridStep,WorldY=Origin.Y+Y*GridStep;
|
||||||
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,CeilingZ*VoxelSizeCm);
|
const double FaceZ=Candidate.FaceHeights[GridIndex];
|
||||||
NewNormals.emplace_back(0.0,0.0,-1.0);
|
FVector Normal(0.0,0.0,-1.0);
|
||||||
|
if(Candidate.bTransition)
|
||||||
|
{
|
||||||
|
const double Delta=std::max(1.0,std::min<double>(GridStep,4.0));
|
||||||
|
const double DX=(heightAt(WorldX+Delta,WorldY)-heightAt(WorldX-Delta,WorldY))
|
||||||
|
/(2.0*Delta);
|
||||||
|
const double DY=(heightAt(WorldX,WorldY+Delta)-heightAt(WorldX,WorldY-Delta))
|
||||||
|
/(2.0*Delta);
|
||||||
|
Normal=FVector(DX,DY,-1.0);
|
||||||
|
Normal.Normalize();
|
||||||
|
}
|
||||||
|
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,FaceZ*VoxelSizeCm);
|
||||||
|
NewNormals.push_back(Normal);
|
||||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
||||||
if(bHasColors)NewColors.push_back(Color);
|
if(bHasColors)NewColors.push_back(Color);
|
||||||
return Mapped;
|
return Mapped;
|
||||||
@@ -1300,15 +1419,40 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
||!Candidate.SolidAbove[I01]||!Candidate.SolidAbove[I11])continue;
|
||!Candidate.SolidAbove[I01]||!Candidate.SolidAbove[I11])continue;
|
||||||
const double CellX0=Origin.X+X*GridStep,CellY0=Origin.Y+Y*GridStep;
|
const double CellX0=Origin.X+X*GridStep,CellY0=Origin.Y+Y*GridStep;
|
||||||
const double CellX1=CellX0+GridStep,CellY1=CellY0+GridStep;
|
const double CellX1=CellX0+GridStep,CellY1=CellY0+GridStep;
|
||||||
// Suppress a coincident plane only when it covers the full cell. A hit at just the
|
bool fullyCovered=false;
|
||||||
// center of a curved shell triangle is not enough to leave the rest of this cap cell open.
|
if(!Candidate.bTransition)
|
||||||
if(MeshIndex.HasDownwardCellCoverage(CellX0,CellY0,CellX1,CellY1,CeilingZ,0.001))continue;
|
fullyCovered=MeshIndex.HasDownwardCellCoverage(
|
||||||
|
CellX0,CellY0,CellX1,CellY1,Candidate.FaceZ,0.001);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fullyCovered=true;
|
||||||
|
constexpr double CoverageSamples[5][2]={
|
||||||
|
{0.0,0.0},{1.0,0.0},{0.0,1.0},{1.0,1.0},{0.5,0.5}};
|
||||||
|
for(const auto& Sample:CoverageSamples)
|
||||||
|
{
|
||||||
|
const double U=Sample[0],V=Sample[1];
|
||||||
|
const double SampleX=CellX0+(CellX1-CellX0)*U;
|
||||||
|
const double SampleY=CellY0+(CellY1-CellY0)*V;
|
||||||
|
const double SampleZ=heightAt(SampleX,SampleY);
|
||||||
|
if(!MeshIndex.HasDownwardHit(SampleX,SampleY,SampleZ,0.25))
|
||||||
|
fullyCovered=false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(fullyCovered)continue;
|
||||||
|
if(Key.Level>=4&&Candidate.bTransition)
|
||||||
|
Replacement.Cells[static_cast<std::size_t>(Y)*(GridSide-1)+X]=1;
|
||||||
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
||||||
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
||||||
NewTriangles.insert(NewTriangles.end(),{A,C,B,A,D,C});
|
NewTriangles.insert(NewTriangles.end(),{A,C,B,A,D,C});
|
||||||
}
|
}
|
||||||
|
if(Key.Level>=4&&Candidate.bTransition
|
||||||
|
&&std::any_of(Replacement.Cells.begin(),Replacement.Cells.end(),
|
||||||
|
[](uint8 Value){return Value!=0;}))
|
||||||
|
Replacements.push_back(std::move(Replacement));
|
||||||
}
|
}
|
||||||
if(NewTriangles.empty())return;
|
if(NewTriangles.empty())return;
|
||||||
|
const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,GridStep,
|
||||||
|
GridSide,VoxelSizeCm,Replacements,false);
|
||||||
|
|
||||||
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
||||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
||||||
@@ -1347,9 +1491,8 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
Mesh.NumCeilingTriangles+=static_cast<int32>(NewTriangles.size()/3);
|
Mesh.NumCeilingTriangles+=static_cast<int32>(NewTriangles.size()/3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the interior-facing top of each bedrock band. The density composition already
|
// Fill only top cells that marching cubes missed. Active transitions use the same
|
||||||
// makes this band solid and editable; this planar closure keeps its visible floor
|
// displaced face and passage-mouth fade as the signed-distance field.
|
||||||
// continuous when a coarse marching-cubes step would otherwise skip a sub-voxel band.
|
|
||||||
inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
||||||
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
||||||
{
|
{
|
||||||
@@ -1358,8 +1501,16 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
const int32 Extent=Key.ExtentVoxels();
|
const int32 Extent=Key.ExtentVoxels();
|
||||||
if(Extent<=0||Extent%Step!=0)return;
|
if(Extent<=0||Extent%Step!=0)return;
|
||||||
const int32 GridSide=Extent/Step+1;
|
const int32 GridSide=Extent/Step+1;
|
||||||
struct FCandidate { double FloorZ=0.0; double Thickness=0.0; };
|
struct FCandidate
|
||||||
|
{
|
||||||
|
double FloorZ=0.0;
|
||||||
|
double Thickness=0.0;
|
||||||
|
double MaximumDisplacementVoxels=0.0;
|
||||||
|
const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr;
|
||||||
|
bool bTransition=false;
|
||||||
|
};
|
||||||
std::vector<FCandidate> Candidates;
|
std::vector<FCandidate> Candidates;
|
||||||
|
std::vector<FTransitionFaceReplacement> Replacements;
|
||||||
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
||||||
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
||||||
{
|
{
|
||||||
@@ -1367,9 +1518,17 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
if(!Stratum||!Stratum->bedrock.presentInCurrentField
|
if(!Stratum||!Stratum->bedrock.presentInCurrentField
|
||||||
||Stratum->bedrock.topMicrometres<=Stratum->bedrock.bottomMicrometres)continue;
|
||Stratum->bedrock.topMicrometres<=Stratum->bedrock.bottomMicrometres)continue;
|
||||||
const double FloorZ=static_cast<double>(Stratum->bedrock.topMicrometres)/CoreMicrons;
|
const double FloorZ=static_cast<double>(Stratum->bedrock.topMicrometres)/CoreMicrons;
|
||||||
if(FloorZ<Origin.Z-1.0e-6||FloorZ>Origin.Z+Extent+1.0e-6)continue;
|
const auto* transitionLower=Shape.FindResolvedLayoutStratumByOrder(Stratum->order+1);
|
||||||
|
const bool bTransition=transitionLower
|
||||||
|
&&Shape.IsResolvedLayoutTransitionActive(*Stratum);
|
||||||
|
const double displacement=bTransition
|
||||||
|
?static_cast<double>(Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
||||||
|
*Stratum,*transitionLower))/CoreMicrons
|
||||||
|
:0.0;
|
||||||
|
if(FloorZ<Origin.Z-displacement-1.0e-6
|
||||||
|
||FloorZ>Origin.Z+Extent+displacement+1.0e-6)continue;
|
||||||
Candidates.push_back({FloorZ,static_cast<double>(Stratum->bedrock.topMicrometres
|
Candidates.push_back({FloorZ,static_cast<double>(Stratum->bedrock.topMicrometres
|
||||||
-Stratum->bedrock.bottomMicrometres)/CoreMicrons});
|
-Stratum->bedrock.bottomMicrometres)/CoreMicrons,displacement,Stratum,bTransition});
|
||||||
}
|
}
|
||||||
if(Candidates.empty())return;
|
if(Candidates.empty())return;
|
||||||
|
|
||||||
@@ -1390,16 +1549,34 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
{
|
{
|
||||||
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
||||||
if(Epsilon<=0.0)continue;
|
if(Epsilon<=0.0)continue;
|
||||||
std::vector<uint8> SolidBelow(static_cast<std::size_t>(GridSide)*GridSide,0);
|
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
||||||
|
std::vector<double> faceHeights(GridCount,Candidate.FloorZ);
|
||||||
|
std::vector<uint8> solidBelow(GridCount,0);
|
||||||
|
FTransitionFaceReplacement Replacement;
|
||||||
|
Replacement.BaseZVoxels=Candidate.FloorZ;
|
||||||
|
Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels;
|
||||||
|
Replacement.Cells.resize(static_cast<std::size_t>(GridSide-1)*(GridSide-1),0);
|
||||||
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
||||||
{
|
{
|
||||||
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
||||||
|
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
|
if(Candidate.bTransition)
|
||||||
|
Shape.ResolvedBedrockTopAt(*Candidate.Stratum,WorldX,WorldY,
|
||||||
|
faceHeights[GridIndex]);
|
||||||
const float Density=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
const float Density=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
||||||
static_cast<float>(Candidate.FloorZ-Epsilon));
|
static_cast<float>(faceHeights[GridIndex]-Epsilon));
|
||||||
SolidBelow[static_cast<std::size_t>(Y*GridSide+X)]=
|
solidBelow[GridIndex]=
|
||||||
std::isfinite(Density)&&Density<0.0f?1:0;
|
std::isfinite(Density)&&Density<0.0f?1:0;
|
||||||
}
|
}
|
||||||
std::vector<int32> VertexMap(static_cast<std::size_t>(GridSide)*GridSide,INDEX_NONE);
|
const auto heightAt=[&](double X,double Y)
|
||||||
|
{
|
||||||
|
if(!Candidate.bTransition)return Candidate.FloorZ;
|
||||||
|
double height=Candidate.FloorZ;
|
||||||
|
if(Candidate.bTransition)
|
||||||
|
Shape.ResolvedBedrockTopAt(*Candidate.Stratum,X,Y,height);
|
||||||
|
return height;
|
||||||
|
};
|
||||||
|
std::vector<int32> VertexMap(GridCount,INDEX_NONE);
|
||||||
const auto AddVertex=[&](int32 X,int32 Y)
|
const auto AddVertex=[&](int32 X,int32 Y)
|
||||||
{
|
{
|
||||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
@@ -1407,9 +1584,20 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
if(Mapped!=INDEX_NONE)return Mapped;
|
if(Mapped!=INDEX_NONE)return Mapped;
|
||||||
Mapped=static_cast<int32>(NewVertices.size());
|
Mapped=static_cast<int32>(NewVertices.size());
|
||||||
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
||||||
|
FVector Normal(0.0,0.0,1.0);
|
||||||
|
if(Candidate.bTransition)
|
||||||
|
{
|
||||||
|
const double Delta=std::max(1.0,std::min<double>(Step,4.0));
|
||||||
|
const double DX=(heightAt(WorldX+Delta,WorldY)-heightAt(WorldX-Delta,WorldY))
|
||||||
|
/(2.0*Delta);
|
||||||
|
const double DY=(heightAt(WorldX,WorldY+Delta)-heightAt(WorldX,WorldY-Delta))
|
||||||
|
/(2.0*Delta);
|
||||||
|
Normal=FVector(-DX,-DY,1.0);
|
||||||
|
Normal.Normalize();
|
||||||
|
}
|
||||||
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,
|
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,
|
||||||
Candidate.FloorZ*VoxelSizeCm);
|
faceHeights[GridIndex]*VoxelSizeCm);
|
||||||
NewNormals.emplace_back(0.0,0.0,1.0);
|
NewNormals.push_back(Normal);
|
||||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
||||||
if(bHasColors)NewColors.push_back(Color);
|
if(bHasColors)NewColors.push_back(Color);
|
||||||
return Mapped;
|
return Mapped;
|
||||||
@@ -1418,16 +1606,43 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
{
|
{
|
||||||
const std::size_t I00=static_cast<std::size_t>(Y*GridSide+X);
|
const std::size_t I00=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1;
|
const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1;
|
||||||
if(!SolidBelow[I00]||!SolidBelow[I10]||!SolidBelow[I01]||!SolidBelow[I11])continue;
|
if(!solidBelow[I00]||!solidBelow[I10]||!solidBelow[I01]||!solidBelow[I11])continue;
|
||||||
const double X0=Origin.X+X*Step,Y0=Origin.Y+Y*Step;
|
const double X0=Origin.X+X*Step,Y0=Origin.Y+Y*Step;
|
||||||
const double X1=X0+Step,Y1=Y0+Step;
|
const double X1=X0+Step,Y1=Y0+Step;
|
||||||
if(MeshIndex.HasUpwardCellCoverage(X0,Y0,X1,Y1,Candidate.FloorZ,0.001))continue;
|
bool fullyCovered=false;
|
||||||
|
if(!Candidate.bTransition)
|
||||||
|
fullyCovered=MeshIndex.HasUpwardCellCoverage(
|
||||||
|
X0,Y0,X1,Y1,Candidate.FloorZ,0.001);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fullyCovered=true;
|
||||||
|
constexpr double CoverageSamples[5][2]={
|
||||||
|
{0.0,0.0},{1.0,0.0},{0.0,1.0},{1.0,1.0},{0.5,0.5}};
|
||||||
|
for(const auto& Sample:CoverageSamples)
|
||||||
|
{
|
||||||
|
const double U=Sample[0],V=Sample[1];
|
||||||
|
const double SampleX=X0+(X1-X0)*U;
|
||||||
|
const double SampleY=Y0+(Y1-Y0)*V;
|
||||||
|
const double SampleZ=heightAt(SampleX,SampleY);
|
||||||
|
if(!MeshIndex.HasUpwardHit(SampleX,SampleY,SampleZ,0.25))
|
||||||
|
fullyCovered=false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(fullyCovered)continue;
|
||||||
|
if(Key.Level>=4&&Candidate.bTransition)
|
||||||
|
Replacement.Cells[static_cast<std::size_t>(Y)*(GridSide-1)+X]=1;
|
||||||
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
||||||
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
||||||
NewTriangles.insert(NewTriangles.end(),{A,B,C,A,C,D});
|
NewTriangles.insert(NewTriangles.end(),{A,B,C,A,C,D});
|
||||||
}
|
}
|
||||||
|
if(Key.Level>=4&&Candidate.bTransition
|
||||||
|
&&std::any_of(Replacement.Cells.begin(),Replacement.Cells.end(),
|
||||||
|
[](uint8 Value){return Value!=0;}))
|
||||||
|
Replacements.push_back(std::move(Replacement));
|
||||||
}
|
}
|
||||||
if(NewTriangles.empty())return;
|
if(NewTriangles.empty())return;
|
||||||
|
const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,Step,
|
||||||
|
GridSide,VoxelSizeCm,Replacements,true);
|
||||||
|
|
||||||
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
||||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ namespace floating_island_detail
|
|||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.boundarySealSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.boundarySealSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeHalfWidthSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeHalfWidthSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeThicknessSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeThicknessSubvoxels), 8);
|
||||||
|
if(recipe.terrainFollowingRoute)HashWord(hash,1,1);
|
||||||
HashWord(hash, HashRouteConstraints(recipe.routeConstraints), 8);
|
HashWord(hash, HashRouteConstraints(recipe.routeConstraints), 8);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
@@ -805,9 +806,39 @@ namespace floating_island_detail
|
|||||||
const double t = std::clamp(along / length, 0.0, 1.0);
|
const double t = std::clamp(along / length, 0.0, 1.0);
|
||||||
if (alongFraction) *alongFraction = t;
|
if (alongFraction) *alongFraction = t;
|
||||||
const double deck = az + (static_cast<double>(edge.endSubvoxels[2]) - az) * t;
|
const double deck = az + (static_cast<double>(edge.endSubvoxels[2]) - az) * t;
|
||||||
|
const std::uint64_t westernEdgeId = StableId(recipe.seed, 0x200000000ull,
|
||||||
|
0x425249444745ull);
|
||||||
|
const bool westernSupportConnector=edge.stableId==westernEdgeId;
|
||||||
|
if(recipe.geometryVersion>=2&&WorldAssemblyEvaluatorVersion>=183
|
||||||
|
&&westernSupportConnector)
|
||||||
|
{
|
||||||
|
const double phase=static_cast<double>(Roll(recipe.seed,edge.stableId,0x5749445448ull)
|
||||||
|
&0xffffffffull)*(2.0*Pi/4294967296.0);
|
||||||
|
const double localHalfWidth=std::max(
|
||||||
|
static_cast<double>(recipe.routeConstraints.capsuleRadiusSubvoxels+Q),
|
||||||
|
static_cast<double>(edge.halfWidthSubvoxels)
|
||||||
|
+0.48*static_cast<double>(Q)*std::sin(6.0*Pi*t+phase));
|
||||||
|
const double localThickness=static_cast<double>(edge.thicknessSubvoxels);
|
||||||
|
const double rounding=std::min({1.5*static_cast<double>(Q),
|
||||||
|
localHalfWidth*0.6,localThickness*0.45});
|
||||||
|
const double roundedX=side-(localHalfWidth-rounding);
|
||||||
|
const double roundedY=std::abs(along-0.5*length)-(0.5*length-rounding);
|
||||||
|
const double roundedZ=std::abs(z-(deck-0.5*localThickness))
|
||||||
|
-(0.5*localThickness-rounding);
|
||||||
|
const double roundedOutside=std::sqrt(
|
||||||
|
std::pow(std::max(roundedX,0.0),2)
|
||||||
|
+std::pow(std::max(roundedY,0.0),2)
|
||||||
|
+std::pow(std::max(roundedZ,0.0),2));
|
||||||
|
const double roundedInside=std::min(std::max(roundedX,
|
||||||
|
std::max(roundedY,roundedZ)),0.0);
|
||||||
|
if(topSurface)*topSurface=z-deck>=std::max(horizontal,
|
||||||
|
deck-localThickness-z);
|
||||||
|
return roundedOutside+roundedInside-rounding;
|
||||||
|
}
|
||||||
const double arch = recipe.geometryVersion >= 2
|
const double arch = recipe.geometryVersion >= 2
|
||||||
? 2.0 * static_cast<double>(edge.thicknessSubvoxels) * (4.0 * t * (1.0 - t))
|
&& WorldAssemblyEvaluatorVersion < 172
|
||||||
: 0.0;
|
? 2.0 * static_cast<double>(edge.thicknessSubvoxels) * (4.0 * t * (1.0 - t))
|
||||||
|
: 0.0;
|
||||||
const double upper = z - deck;
|
const double upper = z - deck;
|
||||||
const double lower = deck - static_cast<double>(edge.thicknessSubvoxels) - arch - z;
|
const double lower = deck - static_cast<double>(edge.thicknessSubvoxels) - arch - z;
|
||||||
if (topSurface) *topSurface = upper >= std::max(horizontal, lower);
|
if (topSurface) *topSurface = upper >= std::max(horizontal, lower);
|
||||||
@@ -1174,8 +1205,8 @@ namespace floating_island_detail
|
|||||||
// Land each connector well inside the broad top so the full capsule footprint remains
|
// Land each connector well inside the broad top so the full capsule footprint remains
|
||||||
// supported across the bridge-to-island transition, including bounded surface roughness.
|
// supported across the bridge-to-island transition, including bounded surface roughness.
|
||||||
// Les connecteurs recouvrent le plateau pour soutenir toute la capsule au raccord.
|
// Les connecteurs recouvrent le plateau pour soutenir toute la capsule au raccord.
|
||||||
// Keep the narrow span exposed to open sky; a smaller fraction buries most of it in the island tops.
|
// Keep the capsule footprint supported while seating L37 connectors just below the native island tops.
|
||||||
// Garder la passerelle etroite visible dans le vide ; une fraction plus faible l'enfouit dans les plateaux.
|
// Garder l'appui de la capsule tout en enfouissant les connecteurs L37 juste sous les plateaux.
|
||||||
const double landingFraction = recipe.geometryVersion >= 2 ? 0.52 : 0.62;
|
const double landingFraction = recipe.geometryVersion >= 2 ? 0.52 : 0.62;
|
||||||
const double fromReach = static_cast<double>(from.radiusSubvoxels) * landingFraction;
|
const double fromReach = static_cast<double>(from.radiusSubvoxels) * landingFraction;
|
||||||
const double toReach = static_cast<double>(to.radiusSubvoxels) * landingFraction;
|
const double toReach = static_cast<double>(to.radiusSubvoxels) * landingFraction;
|
||||||
@@ -1183,7 +1214,19 @@ namespace floating_island_detail
|
|||||||
const double sy = from.centerSubvoxels[1] + uy * fromReach;
|
const double sy = from.centerSubvoxels[1] + uy * fromReach;
|
||||||
const double ex = to.centerSubvoxels[0] - ux * toReach;
|
const double ex = to.centerSubvoxels[0] - ux * toReach;
|
||||||
const double ey = to.centerSubvoxels[1] - uy * toReach;
|
const double ey = to.centerSubvoxels[1] - uy * toReach;
|
||||||
const double routeDeckLift = recipe.geometryVersion >= 2 ? 1.0 * Q : 0.0;
|
const bool craterMouthConnector=WorldAssemblyEvaluatorVersion>=174
|
||||||
|
&&(from.ordinal==recipe.anchorIslandOrdinal
|
||||||
|
||to.ordinal==recipe.anchorIslandOrdinal);
|
||||||
|
const bool westernSupportConnector=WorldAssemblyEvaluatorVersion>=178
|
||||||
|
&&((from.ordinal==0&&to.ordinal==1)||(from.ordinal==1&&to.ordinal==0));
|
||||||
|
const double routeDeckLift=recipe.geometryVersion>=2
|
||||||
|
?(WorldAssemblyEvaluatorVersion>=178
|
||||||
|
?(westernSupportConnector
|
||||||
|
?(WorldAssemblyEvaluatorVersion>=181?-1.0
|
||||||
|
:(WorldAssemblyEvaluatorVersion>=180?-1.25
|
||||||
|
:(WorldAssemblyEvaluatorVersion>=179?-1.5:-1.0))):-2.0)
|
||||||
|
:(craterMouthConnector?-2.0
|
||||||
|
:(WorldAssemblyEvaluatorVersion>=172?-1.0:1.0)))*Q:0.0;
|
||||||
const double sz = IslandTopSurface(recipe, from, sx, sy) + routeDeckLift;
|
const double sz = IslandTopSurface(recipe, from, sx, sy) + routeDeckLift;
|
||||||
const double ez = IslandTopSurface(recipe, to, ex, ey) + routeDeckLift;
|
const double ez = IslandTopSurface(recipe, to, ex, ey) + routeDeckLift;
|
||||||
edge.startSubvoxels = {RoundQ(sx), RoundQ(sy), RoundQ(sz)};
|
edge.startSubvoxels = {RoundQ(sx), RoundQ(sy), RoundQ(sz)};
|
||||||
@@ -1336,6 +1379,7 @@ std::optional<FloatingIslandManifestV1> FloatingIslandManifestV1::FromJson(
|
|||||||
manifest.recipe.anchorMouthOffsetSubvoxels))
|
manifest.recipe.anchorMouthOffsetSubvoxels))
|
||||||
{error="floating-island geometry version 7 is missing a passage-rooted anchor field";return std::nullopt;}
|
{error="floating-island geometry version 7 is missing a passage-rooted anchor field";return std::nullopt;}
|
||||||
}
|
}
|
||||||
|
manifest.recipe.terrainFollowingRoute=recipeValue.Get("terrain_following_route").Boolean(false);
|
||||||
if (!floating_island_detail::ReadU32(root.Get("schema_version"), schema)
|
if (!floating_island_detail::ReadU32(root.Get("schema_version"), schema)
|
||||||
|| !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash)
|
|| !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash)
|
||||||
|| !floating_island_detail::ReadU32(recipeValue.Get("schema_version"), manifest.recipe.schemaVersion)
|
|| !floating_island_detail::ReadU32(recipeValue.Get("schema_version"), manifest.recipe.schemaVersion)
|
||||||
@@ -1495,6 +1539,7 @@ std::string FloatingIslandManifestV1::ToJson() const
|
|||||||
<< ",\"anchor_island_minimum_radius_q\":" << recipe.anchorIslandMinimumRadiusSubvoxels
|
<< ",\"anchor_island_minimum_radius_q\":" << recipe.anchorIslandMinimumRadiusSubvoxels
|
||||||
<< ",\"anchor_root_bottom_z_q\":" << recipe.anchorRootBottomZSubvoxels
|
<< ",\"anchor_root_bottom_z_q\":" << recipe.anchorRootBottomZSubvoxels
|
||||||
<< ",\"anchor_mouth_offset_q\":" << recipe.anchorMouthOffsetSubvoxels;
|
<< ",\"anchor_mouth_offset_q\":" << recipe.anchorMouthOffsetSubvoxels;
|
||||||
|
if(recipe.terrainFollowingRoute)out << ",\"terrain_following_route\":true";
|
||||||
out << ",\"blend_radius_q\":" << recipe.blendRadiusSubvoxels
|
out << ",\"blend_radius_q\":" << recipe.blendRadiusSubvoxels
|
||||||
<< ",\"contour_warp_q\":" << recipe.contourWarpSubvoxels
|
<< ",\"contour_warp_q\":" << recipe.contourWarpSubvoxels
|
||||||
<< ",\"roughness_q\":" << recipe.roughnessSubvoxels
|
<< ",\"roughness_q\":" << recipe.roughnessSubvoxels
|
||||||
@@ -1746,6 +1791,26 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|||||||
const std::int64_t y = floating_island_detail::RoundQ(
|
const std::int64_t y = floating_island_detail::RoundQ(
|
||||||
static_cast<double>(a.centerSubvoxels[1]) + dy * t);
|
static_cast<double>(a.centerSubvoxels[1]) + dy * t);
|
||||||
std::int64_t routeX=x,routeY=y;
|
std::int64_t routeX=x,routeY=y;
|
||||||
|
if(WorldAssemblyEvaluatorVersion>=192&&recipe.terrainFollowingRoute
|
||||||
|
&&recipe.geometryVersion>=5
|
||||||
|
&&segment+2==count&&pieces>1)
|
||||||
|
{
|
||||||
|
const auto& anchor=islands[recipe.anchorIslandOrdinal];
|
||||||
|
const std::uint32_t side=static_cast<std::uint32_t>(
|
||||||
|
floating_island_detail::UnitQ16(recipe.seed,anchor.stableId,
|
||||||
|
0x414e43484f524d50ull)&1u);
|
||||||
|
const double length=std::hypot(dx,dy);
|
||||||
|
const double bend=10.0*floating_island_detail::Q
|
||||||
|
*std::pow(std::sin(floating_island_detail::Pi*t),2.0);
|
||||||
|
const double sign=side?-1.0:1.0;
|
||||||
|
if(length>0.0)
|
||||||
|
{
|
||||||
|
routeX=floating_island_detail::RoundQ(
|
||||||
|
static_cast<double>(routeX)+sign*(-dy/length)*bend);
|
||||||
|
routeY=floating_island_detail::RoundQ(
|
||||||
|
static_cast<double>(routeY)+sign*(dx/length)*bend);
|
||||||
|
}
|
||||||
|
}
|
||||||
if(recipe.geometryVersion>=5&&segment+2==count&&piece==pieces)
|
if(recipe.geometryVersion>=5&&segment+2==count&&piece==pieces)
|
||||||
{
|
{
|
||||||
const auto& anchor=islands[recipe.anchorIslandOrdinal];
|
const auto& anchor=islands[recipe.anchorIslandOrdinal];
|
||||||
@@ -1762,8 +1827,11 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|||||||
routeX=floating_island_detail::RoundQ(mouthX);
|
routeX=floating_island_detail::RoundQ(mouthX);
|
||||||
routeY=floating_island_detail::RoundQ(mouthY);
|
routeY=floating_island_detail::RoundQ(mouthY);
|
||||||
}
|
}
|
||||||
const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
const auto surface = WorldAssemblyEvaluatorVersion>=199
|
||||||
recipe, manifest.plan, routeX, routeY);
|
&&recipe.terrainFollowingRoute
|
||||||
|
?floating_island_detail::FindSurfaceZ(recipe,manifest.plan,routeX,routeY)
|
||||||
|
:floating_island_detail::FindCapsuleFootingTopZ(
|
||||||
|
recipe,manifest.plan,routeX,routeY);
|
||||||
if (!surface)
|
if (!surface)
|
||||||
{ error = "island planner could not find a full capsule-width support surface along its connector chain"; return std::nullopt; }
|
{ error = "island planner could not find a full capsule-width support surface along its connector chain"; return std::nullopt; }
|
||||||
WorldAssemblyRoutePointV1 point;
|
WorldAssemblyRoutePointV1 point;
|
||||||
|
|||||||
@@ -13,6 +13,111 @@ namespace VoxelForgeCore
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
constexpr std::int64_t TransitionQ16 = 1ll << 16;
|
||||||
|
|
||||||
|
void TransitionHashWord(std::uint64_t& hash, std::uint64_t word)
|
||||||
|
{
|
||||||
|
for (int byte = 0; byte < 8; ++byte)
|
||||||
|
{
|
||||||
|
hash ^= static_cast<std::uint8_t>(word >> (byte * 8));
|
||||||
|
hash *= 1099511628211ull;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint64_t TransitionHash(std::int32_t seed, std::int32_t interfaceIndex,
|
||||||
|
std::int64_t x, std::int64_t y, std::int64_t z, std::uint32_t salt)
|
||||||
|
{
|
||||||
|
std::uint64_t hash = 14695981039346656037ull;
|
||||||
|
TransitionHashWord(hash, static_cast<std::uint32_t>(seed));
|
||||||
|
TransitionHashWord(hash, static_cast<std::uint32_t>(interfaceIndex));
|
||||||
|
TransitionHashWord(hash, static_cast<std::uint64_t>(x));
|
||||||
|
TransitionHashWord(hash, static_cast<std::uint64_t>(y));
|
||||||
|
TransitionHashWord(hash, static_cast<std::uint64_t>(z));
|
||||||
|
TransitionHashWord(hash, salt);
|
||||||
|
hash ^= hash >> 30; hash *= 0xbf58476d1ce4e5b9ull;
|
||||||
|
hash ^= hash >> 27; hash *= 0x94d049bb133111ebull;
|
||||||
|
hash ^= hash >> 31;
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int64_t TransitionNoiseValue(std::int32_t seed, std::int32_t interfaceIndex,
|
||||||
|
std::int64_t x, std::int64_t y, std::int64_t z, std::uint32_t salt)
|
||||||
|
{
|
||||||
|
const std::uint32_t bits = static_cast<std::uint32_t>(
|
||||||
|
TransitionHash(seed, interfaceIndex, x, y, z, salt) >> 32);
|
||||||
|
return static_cast<std::int64_t>(bits >> 15) - TransitionQ16;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int64_t TransitionFloorDiv(std::int64_t value, std::int64_t divisor)
|
||||||
|
{
|
||||||
|
const std::int64_t quotient = value / divisor;
|
||||||
|
const std::int64_t remainder = value % divisor;
|
||||||
|
return quotient - (remainder < 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int64_t TransitionFade(std::int64_t valueQ16)
|
||||||
|
{
|
||||||
|
valueQ16 = std::clamp(valueQ16, std::int64_t{0}, TransitionQ16);
|
||||||
|
const std::int64_t square = (valueQ16 * valueQ16) / TransitionQ16;
|
||||||
|
return (square * (3 * TransitionQ16 - 2 * valueQ16)) / TransitionQ16;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int64_t TransitionLerp(std::int64_t a, std::int64_t b, std::int64_t tQ16)
|
||||||
|
{ return a + ((b - a) * tQ16) / TransitionQ16; }
|
||||||
|
|
||||||
|
std::int64_t TransitionValueNoise2D(std::int32_t seed, std::int32_t interfaceIndex,
|
||||||
|
std::int64_t x, std::int64_t y, std::int64_t cell, std::uint32_t salt)
|
||||||
|
{
|
||||||
|
const std::int64_t ix = TransitionFloorDiv(x, cell), iy = TransitionFloorDiv(y, cell);
|
||||||
|
const std::int64_t tx = TransitionFade(((x - ix * cell) * TransitionQ16) / cell);
|
||||||
|
const std::int64_t ty = TransitionFade(((y - iy * cell) * TransitionQ16) / cell);
|
||||||
|
std::uint64_t prefix=14695981039346656037ull;
|
||||||
|
TransitionHashWord(prefix,static_cast<std::uint32_t>(seed));
|
||||||
|
TransitionHashWord(prefix,static_cast<std::uint32_t>(interfaceIndex));
|
||||||
|
const auto at = [&](std::int64_t dx, std::int64_t dy)
|
||||||
|
{
|
||||||
|
std::uint64_t hash=prefix;
|
||||||
|
TransitionHashWord(hash,static_cast<std::uint64_t>(ix+dx));
|
||||||
|
TransitionHashWord(hash,static_cast<std::uint64_t>(iy+dy));
|
||||||
|
TransitionHashWord(hash,0);
|
||||||
|
TransitionHashWord(hash,salt);
|
||||||
|
hash ^= hash >> 30; hash *= 0xbf58476d1ce4e5b9ull;
|
||||||
|
hash ^= hash >> 27; hash *= 0x94d049bb133111ebull;
|
||||||
|
hash ^= hash >> 31;
|
||||||
|
const std::uint32_t bits=static_cast<std::uint32_t>(hash>>32);
|
||||||
|
return static_cast<std::int64_t>(bits>>15)-TransitionQ16;
|
||||||
|
};
|
||||||
|
return TransitionLerp(TransitionLerp(at(0, 0), at(1, 0), tx),
|
||||||
|
TransitionLerp(at(0, 1), at(1, 1), tx), ty);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int64_t TransitionValueNoise3D(std::int32_t seed, std::int32_t interfaceIndex,
|
||||||
|
std::int64_t x, std::int64_t y, std::int64_t z, std::int64_t cell,
|
||||||
|
std::uint32_t salt)
|
||||||
|
{
|
||||||
|
const std::int64_t ix = TransitionFloorDiv(x, cell), iy = TransitionFloorDiv(y, cell);
|
||||||
|
const std::int64_t iz = TransitionFloorDiv(z, cell);
|
||||||
|
const std::int64_t tx = TransitionFade(((x - ix * cell) * TransitionQ16) / cell);
|
||||||
|
const std::int64_t ty = TransitionFade(((y - iy * cell) * TransitionQ16) / cell);
|
||||||
|
const std::int64_t tz = TransitionFade(((z - iz * cell) * TransitionQ16) / cell);
|
||||||
|
const auto at = [&](std::int64_t dx, std::int64_t dy, std::int64_t dz)
|
||||||
|
{ return TransitionNoiseValue(seed, interfaceIndex, ix + dx, iy + dy, iz + dz, salt); };
|
||||||
|
const std::int64_t z0 = TransitionLerp(
|
||||||
|
TransitionLerp(at(0, 0, 0), at(1, 0, 0), tx),
|
||||||
|
TransitionLerp(at(0, 1, 0), at(1, 1, 0), tx), ty);
|
||||||
|
const std::int64_t z1 = TransitionLerp(
|
||||||
|
TransitionLerp(at(0, 0, 1), at(1, 0, 1), tx),
|
||||||
|
TransitionLerp(at(0, 1, 1), at(1, 1, 1), tx), ty);
|
||||||
|
return TransitionLerp(z0, z1, tz);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int64_t LayoutTransitionLimit(const ResolvedLayoutStratumV2& stratum)
|
||||||
|
{
|
||||||
|
if (stratum.transitionMode == "Hard" || stratum.transitionFallsBackToHard)
|
||||||
|
return 0;
|
||||||
|
return std::max<std::int64_t>(0, stratum.transitionWidthMicrometres);
|
||||||
|
}
|
||||||
|
|
||||||
std::string LayoutEscape(std::string_view value)
|
std::string LayoutEscape(std::string_view value)
|
||||||
{
|
{
|
||||||
std::string out="\"";
|
std::string out="\"";
|
||||||
@@ -41,12 +146,6 @@ namespace
|
|||||||
}
|
}
|
||||||
std::string LayoutHex(std::uint64_t value)
|
std::string LayoutHex(std::uint64_t value)
|
||||||
{std::ostringstream out;out<<std::hex<<std::setfill('0')<<std::setw(16)<<value;return out.str();}
|
{std::ostringstream out;out<<std::hex<<std::setfill('0')<<std::setw(16)<<value;return out.str();}
|
||||||
std::int64_t LayoutTransitionLimit(const ResolvedLayoutStratumV2& stratum)
|
|
||||||
{
|
|
||||||
if(stratum.transitionMode=="Hard"||stratum.transitionFallsBackToHard)return 0;
|
|
||||||
return stratum.transitionWidthMicrometres
|
|
||||||
+(stratum.transitionMode=="Interleaved"?stratum.transitionWarpAmplitudeMicrometres:0);
|
|
||||||
}
|
|
||||||
std::string LayoutBandJson(const ResolvedLayoutBandV2& band)
|
std::string LayoutBandJson(const ResolvedLayoutBandV2& band)
|
||||||
{
|
{
|
||||||
std::ostringstream out;out<<"{\"kind\":"<<LayoutEscape(band.kind)
|
std::ostringstream out;out<<"{\"kind\":"<<LayoutEscape(band.kind)
|
||||||
@@ -122,6 +221,7 @@ namespace
|
|||||||
out<<"{\"stable_id\":"<<LayoutEscape(p.stableId)
|
out<<"{\"stable_id\":"<<LayoutEscape(p.stableId)
|
||||||
<<",\"upper_strate_index\":"<<p.upperStrateIndex
|
<<",\"upper_strate_index\":"<<p.upperStrateIndex
|
||||||
<<",\"lower_strate_index\":"<<p.lowerStrateIndex
|
<<",\"lower_strate_index\":"<<p.lowerStrateIndex
|
||||||
|
<<",\"style\":"<<p.style
|
||||||
<<",\"placement_radius_q\":"<<p.placementRadiusSubvoxels
|
<<",\"placement_radius_q\":"<<p.placementRadiusSubvoxels
|
||||||
<<",\"upper_mouth_q\":["<<p.upperMouthSubvoxels[0]<<','
|
<<",\"upper_mouth_q\":["<<p.upperMouthSubvoxels[0]<<','
|
||||||
<<p.upperMouthSubvoxels[1]<<','<<p.upperMouthSubvoxels[2]
|
<<p.upperMouthSubvoxels[1]<<','<<p.upperMouthSubvoxels[2]
|
||||||
@@ -205,6 +305,84 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResolvedLayoutTransitionBandsV1 EvaluateResolvedLayoutTransitionBandsV1(
|
||||||
|
const ResolvedLayoutStratumV2& upper, const ResolvedLayoutStratumV2& lower,
|
||||||
|
std::int32_t worldSeed, std::int64_t layoutChunkSizeMicrometres,
|
||||||
|
std::int64_t xMicrometres, std::int64_t yMicrometres)
|
||||||
|
{
|
||||||
|
ResolvedLayoutTransitionBandsV1 result;
|
||||||
|
const std::int64_t seam = upper.bedrock.bottomMicrometres;
|
||||||
|
const std::int64_t upperThickness = upper.bedrock.topMicrometres - seam;
|
||||||
|
const std::int64_t lowerThickness = lower.ceiling.topMicrometres
|
||||||
|
- lower.ceiling.bottomMicrometres;
|
||||||
|
result.upperBedrockBottomMicrometres = seam;
|
||||||
|
result.upperBedrockTopMicrometres = seam + std::max<std::int64_t>(0, upperThickness);
|
||||||
|
result.lowerCeilingBottomMicrometres = seam - std::max<std::int64_t>(0, lowerThickness);
|
||||||
|
result.lowerCeilingTopMicrometres = seam;
|
||||||
|
|
||||||
|
const std::int64_t width = upper.transitionFallsBackToHard
|
||||||
|
? 0 : std::max<std::int64_t>(0, upper.transitionWidthMicrometres);
|
||||||
|
if (upper.transitionMode == "Hard" || width == 0
|
||||||
|
|| (upper.transitionMode != "Gradient" && upper.transitionMode != "Interleaved")
|
||||||
|
|| !upper.bedrock.presentInCurrentField || !lower.ceiling.presentInCurrentField
|
||||||
|
|| upperThickness <= 0 || lowerThickness <= 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
if (width <= 0) return result;
|
||||||
|
|
||||||
|
std::int64_t noise = 0;
|
||||||
|
if (upper.transitionMode == "Gradient")
|
||||||
|
{
|
||||||
|
const std::int64_t wavelength = std::max<std::int64_t>(
|
||||||
|
1000000, std::max<std::int64_t>(1, layoutChunkSizeMicrometres) * 4);
|
||||||
|
noise = TransitionValueNoise2D(worldSeed, upper.strateIndex,
|
||||||
|
xMicrometres, yMicrometres, wavelength, 0x47524144u);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// A face crossing wider than the sum of the authored bands would separate
|
||||||
|
// their far edges and leave an air slab. Limit the physical crossing to the
|
||||||
|
// smaller of the declared transition width and the combined band thickness.
|
||||||
|
const std::int64_t maximumContinuousCrossing=upperThickness+lowerThickness;
|
||||||
|
const std::int64_t crossingLimit=std::min(width,maximumContinuousCrossing);
|
||||||
|
const std::int64_t cell = std::max<std::int64_t>(1000000,
|
||||||
|
std::max<std::int64_t>(1, layoutChunkSizeMicrometres) * 2);
|
||||||
|
const std::int64_t warp = std::min(crossingLimit / 2, cell / 2);
|
||||||
|
const std::int64_t warpX = (TransitionValueNoise3D(worldSeed, upper.strateIndex,
|
||||||
|
xMicrometres, yMicrometres, seam, cell, 0x49574158u) * warp) / TransitionQ16;
|
||||||
|
const std::int64_t warpY = (TransitionValueNoise3D(worldSeed, upper.strateIndex,
|
||||||
|
xMicrometres, yMicrometres, seam, cell, 0x49574159u) * warp) / TransitionQ16;
|
||||||
|
const std::int64_t warpZ = (TransitionValueNoise3D(worldSeed, upper.strateIndex,
|
||||||
|
xMicrometres, yMicrometres, seam, cell, 0x4957415au) * warp) / TransitionQ16;
|
||||||
|
noise = TransitionValueNoise3D(worldSeed, upper.strateIndex,
|
||||||
|
xMicrometres + warpX, yMicrometres + warpY, seam + warpZ,
|
||||||
|
cell, 0x494e544cu);
|
||||||
|
|
||||||
|
// Interleaved means physical crossing: the two faces enter each other's
|
||||||
|
// bands by a total depth in [0, declared width]. Split that overlap across
|
||||||
|
// both faces so neither stratum owns the entire displacement. Since both
|
||||||
|
// resulting intervals are solid and overlap, the transition cannot open a gap.
|
||||||
|
const std::int64_t overlap = ((noise + TransitionQ16) * crossingLimit)
|
||||||
|
/ (2 * TransitionQ16);
|
||||||
|
const std::int64_t upperInset = overlap / 2;
|
||||||
|
const std::int64_t lowerInset = overlap - upperInset;
|
||||||
|
result.crossingDepthMicrometres = overlap;
|
||||||
|
result.upperBedrockBottomMicrometres = seam - upperInset;
|
||||||
|
result.upperBedrockTopMicrometres = result.upperBedrockBottomMicrometres + upperThickness;
|
||||||
|
result.lowerCeilingTopMicrometres = seam + lowerInset;
|
||||||
|
result.lowerCeilingBottomMicrometres = result.lowerCeilingTopMicrometres - lowerThickness;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::int64_t displacement = (noise * (width / 8)) / TransitionQ16;
|
||||||
|
const std::int64_t interfaceZ = seam + displacement;
|
||||||
|
result.upperBedrockBottomMicrometres = interfaceZ;
|
||||||
|
result.upperBedrockTopMicrometres = interfaceZ + upperThickness;
|
||||||
|
result.lowerCeilingBottomMicrometres = interfaceZ - lowerThickness;
|
||||||
|
result.lowerCeilingTopMicrometres = interfaceZ;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
std::string ResolvedLayoutSnapshotV3::ToJson() const
|
std::string ResolvedLayoutSnapshotV3::ToJson() const
|
||||||
{
|
{
|
||||||
const std::string body=LayoutBodyJson(*this);
|
const std::string body=LayoutBodyJson(*this);
|
||||||
@@ -216,7 +394,8 @@ bool ResolvedLayoutSnapshotV3::finalize(std::string& error)
|
|||||||
{
|
{
|
||||||
error.clear();
|
error.clear();
|
||||||
if(schemaVersion!=ResolvedLayoutSchemaVersionV3||worldIdentity.empty()||strates.empty()
|
if(schemaVersion!=ResolvedLayoutSchemaVersionV3||worldIdentity.empty()||strates.empty()
|
||||||
||strateLayoutChunkSizeMicrometres==0||strateLayoutChunkWidthSubvoxels<=0)
|
||coreVoxelSizeMicrometres==0||strateLayoutChunkSizeMicrometres==0
|
||||||
|
||strateLayoutChunkWidthSubvoxels<=0)
|
||||||
{error="resolved layout has an unsupported version, empty world identity, or no strates";return false;}
|
{error="resolved layout has an unsupported version, empty world identity, or no strates";return false;}
|
||||||
std::stable_sort(strates.begin(),strates.end(),[](const auto& a,const auto& b)
|
std::stable_sort(strates.begin(),strates.end(),[](const auto& a,const auto& b)
|
||||||
{return a.interiorTopMicrometres!=b.interiorTopMicrometres
|
{return a.interiorTopMicrometres!=b.interiorTopMicrometres
|
||||||
@@ -257,7 +436,8 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
|||||||
{
|
{
|
||||||
issues.clear();error.clear();
|
issues.clear();error.clear();
|
||||||
if(schemaVersion!=ResolvedLayoutSchemaVersionV3||worldIdentity.empty()||strates.empty()
|
if(schemaVersion!=ResolvedLayoutSchemaVersionV3||worldIdentity.empty()||strates.empty()
|
||||||
||strateLayoutChunkSizeMicrometres==0||strateLayoutChunkWidthSubvoxels<=0)
|
||coreVoxelSizeMicrometres==0||strateLayoutChunkSizeMicrometres==0
|
||||||
|
||strateLayoutChunkWidthSubvoxels<=0)
|
||||||
{error="resolved layout schema/world/strata header is invalid";return false;}
|
{error="resolved layout schema/world/strata header is invalid";return false;}
|
||||||
const auto add=[&](std::string code,std::string detail)
|
const auto add=[&](std::string code,std::string detail)
|
||||||
{issues.push_back({std::move(code),std::move(detail)});};
|
{issues.push_back({std::move(code),std::move(detail)});};
|
||||||
@@ -271,6 +451,8 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
|||||||
if(s.transitionInterpenetrationMicrometres>effectiveTransitionWidth
|
if(s.transitionInterpenetrationMicrometres>effectiveTransitionWidth
|
||||||
||s.transitionInterpenetrationMicrometres<0)
|
||s.transitionInterpenetrationMicrometres<0)
|
||||||
add("transition_overlap_exceeds_width",s.stableId+" transition interpenetration exceeds its declared width");
|
add("transition_overlap_exceeds_width",s.stableId+" transition interpenetration exceeds its declared width");
|
||||||
|
if(s.transitionWidthMicrometres<0||s.transitionBlendChunks<0)
|
||||||
|
add("invalid_transition_width",s.stableId+" has a negative transition width");
|
||||||
if(s.managerChunkWidthMicrometres>0&&s.dispatchChunkWidthMicrometres>0
|
if(s.managerChunkWidthMicrometres>0&&s.dispatchChunkWidthMicrometres>0
|
||||||
&&s.managerChunkWidthMicrometres!=s.dispatchChunkWidthMicrometres)
|
&&s.managerChunkWidthMicrometres!=s.dispatchChunkWidthMicrometres)
|
||||||
add("ambiguous_dispatch_units",s.stableId+" manager chunk width differs from Core dispatch chunk width");
|
add("ambiguous_dispatch_units",s.stableId+" manager chunk width differs from Core dispatch chunk width");
|
||||||
@@ -290,6 +472,10 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
|||||||
{
|
{
|
||||||
if(b.kind!=expected||b.topMicrometres<=b.bottomMicrometres)
|
if(b.kind!=expected||b.topMicrometres<=b.bottomMicrometres)
|
||||||
add("wrong_order",s.stableId+" has an invalid "+std::string(expected)+" band");
|
add("wrong_order",s.stableId+" has an invalid "+std::string(expected)+" band");
|
||||||
|
const std::int64_t minimumThickness=2ll*coreVoxelSizeMicrometres;
|
||||||
|
if(b.topMicrometres-b.bottomMicrometres<minimumThickness)
|
||||||
|
add("band_thinner_than_minimum",s.stableId+" "+std::string(expected)
|
||||||
|
+" is thinner than the two-Core-voxel minimum");
|
||||||
if(b.protectedBand&&b.diggable)
|
if(b.protectedBand&&b.diggable)
|
||||||
add("invalid_protection",s.stableId+" band is both protected and diggable");
|
add("invalid_protection",s.stableId+" band is both protected and diggable");
|
||||||
if(b.solidDensityQ16<=0)
|
if(b.solidDensityQ16<=0)
|
||||||
@@ -498,6 +684,8 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
|||||||
ResolvedLayoutPassageV1 p;p.stableId=LayoutString(v,"stable_id");
|
ResolvedLayoutPassageV1 p;p.stableId=LayoutString(v,"stable_id");
|
||||||
p.upperStrateIndex=static_cast<std::int32_t>(LayoutInt(v,"upper_strate_index"));
|
p.upperStrateIndex=static_cast<std::int32_t>(LayoutInt(v,"upper_strate_index"));
|
||||||
p.lowerStrateIndex=static_cast<std::int32_t>(LayoutInt(v,"lower_strate_index"));
|
p.lowerStrateIndex=static_cast<std::int32_t>(LayoutInt(v,"lower_strate_index"));
|
||||||
|
if(v.Get("style").IsNumber())
|
||||||
|
p.style=static_cast<std::uint32_t>(LayoutInt(v,"style"));
|
||||||
p.placementRadiusSubvoxels=LayoutInt(v,"placement_radius_q");
|
p.placementRadiusSubvoxels=LayoutInt(v,"placement_radius_q");
|
||||||
p.mouthRadiusSubvoxels=LayoutInt(v,"mouth_radius_q");p.midRadiusSubvoxels=LayoutInt(v,"mid_radius_q");
|
p.mouthRadiusSubvoxels=LayoutInt(v,"mouth_radius_q");p.midRadiusSubvoxels=LayoutInt(v,"mid_radius_q");
|
||||||
const auto& upper=v.Get("upper_mouth_q").AsArray();
|
const auto& upper=v.Get("upper_mouth_q").AsArray();
|
||||||
@@ -603,10 +791,13 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
cfg->distanceMaximumSubvoxels):8*qPerMetre;
|
cfg->distanceMaximumSubvoxels):8*qPerMetre;
|
||||||
const std::int64_t placementRadius=std::max<std::int64_t>(1,outer);
|
const std::int64_t placementRadius=std::max<std::int64_t>(1,outer);
|
||||||
// Leave room for the capsule and bounded wall relief at the floor/wall seam.
|
// Leave room for the capsule and bounded wall relief at the floor/wall seam.
|
||||||
const std::int64_t mouthRadius=std::max<std::int64_t>(static_cast<std::int64_t>(std::llround(1.5*qPerMetre)),
|
const bool upperIsSurfaceWorld=upper.generatorKind=="SurfaceWorld";
|
||||||
|
const std::int64_t minimumMouthRadius=static_cast<std::int64_t>(std::llround(
|
||||||
|
(upperIsSurfaceWorld?2.0:1.5)*qPerMetre));
|
||||||
|
const std::int64_t mouthRadius=std::max<std::int64_t>(minimumMouthRadius,
|
||||||
cfg?cfg->mouthRadiusSubvoxels:0);
|
cfg?cfg->mouthRadiusSubvoxels:0);
|
||||||
const std::int64_t midRadius=std::max<std::int64_t>(
|
const std::int64_t midRadius=std::max<std::int64_t>(
|
||||||
static_cast<std::int64_t>(std::llround(1.5*qPerMetre)),
|
static_cast<std::int64_t>(std::llround((upperIsSurfaceWorld?3.5:1.5)*qPerMetre)),
|
||||||
cfg?cfg->midRadiusSubvoxels:mouthRadius);
|
cfg?cfg->midRadiusSubvoxels:mouthRadius);
|
||||||
const std::int64_t reachMinimum=cfg?std::max<std::int64_t>(0,cfg->reachMinimumSubvoxels):8*qPerMetre;
|
const std::int64_t reachMinimum=cfg?std::max<std::int64_t>(0,cfg->reachMinimumSubvoxels):8*qPerMetre;
|
||||||
const std::int64_t reachMaximum=cfg?std::max<std::int64_t>(reachMinimum,cfg->reachMaximumSubvoxels):24*qPerMetre;
|
const std::int64_t reachMaximum=cfg?std::max<std::int64_t>(reachMinimum,cfg->reachMaximumSubvoxels):24*qPerMetre;
|
||||||
@@ -620,6 +811,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
for(std::uint32_t connection=0;connection<connectionCount;++connection)
|
for(std::uint32_t connection=0;connection<connectionCount;++connection)
|
||||||
{
|
{
|
||||||
ResolvedLayoutPassageV1 passage;
|
ResolvedLayoutPassageV1 passage;
|
||||||
|
passage.style=style;
|
||||||
passage.stableId="passage/"+std::to_string(upper.strateIndex)+"-"
|
passage.stableId="passage/"+std::to_string(upper.strateIndex)+"-"
|
||||||
+std::to_string(lower.strateIndex)+"/"+std::to_string(connection);
|
+std::to_string(lower.strateIndex)+"/"+std::to_string(connection);
|
||||||
passage.upperStrateIndex=upper.strateIndex;passage.lowerStrateIndex=lower.strateIndex;
|
passage.upperStrateIndex=upper.strateIndex;passage.lowerStrateIndex=lower.strateIndex;
|
||||||
@@ -628,6 +820,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
std::array<std::int64_t,3> upperMouth{},lowerMouth{};
|
std::array<std::int64_t,3> upperMouth{},lowerMouth{};
|
||||||
const bool hasUpperRoute=connection==0&&routeAnchor(upper,true,upperMouth);
|
const bool hasUpperRoute=connection==0&&routeAnchor(upper,true,upperMouth);
|
||||||
const bool hasLowerRoute=connection==0&&routeAnchor(lower,false,lowerMouth);
|
const bool hasLowerRoute=connection==0&&routeAnchor(lower,false,lowerMouth);
|
||||||
|
const bool surfaceGroundDeparture=hasUpperRoute&&upperIsSurfaceWorld;
|
||||||
const ResolvedLayoutAnchorV2* passageAnchor=nullptr;
|
const ResolvedLayoutAnchorV2* passageAnchor=nullptr;
|
||||||
if(connection==0)
|
if(connection==0)
|
||||||
for(const auto& anchor:upper.anchors)
|
for(const auto& anchor:upper.anchors)
|
||||||
@@ -685,29 +878,91 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
std::max<std::int64_t>(0,cfg?cfg->cascadeLedgeSubvoxels:0)
|
std::max<std::int64_t>(0,cfg?cfg->cascadeLedgeSubvoxels:0)
|
||||||
*static_cast<std::int64_t>(cfg?cfg->cascadeSteps:4)),
|
*static_cast<std::int64_t>(cfg?cfg->cascadeSteps:4)),
|
||||||
mouthRadius+2*qPerMetre,outerRingRadius);
|
mouthRadius+2*qPerMetre,outerRingRadius);
|
||||||
|
if(surfaceGroundDeparture)
|
||||||
|
{
|
||||||
|
const double departureRadius=std::hypot(static_cast<double>(upperMouth[0]),
|
||||||
|
static_cast<double>(upperMouth[1]));
|
||||||
|
const std::int64_t clearRingRadius=static_cast<std::int64_t>(std::ceil(
|
||||||
|
departureRadius+mouthRadius+0.5*qPerMetre));
|
||||||
|
const std::int64_t maximumRingRadius=std::max<std::int64_t>(1,
|
||||||
|
placementRadius-qPerMetre);
|
||||||
|
ringRadius=std::min(maximumRingRadius,
|
||||||
|
std::max(ringRadius,clearRingRadius));
|
||||||
|
}
|
||||||
if(anchoredIslandPassage)
|
if(anchoredIslandPassage)
|
||||||
ringRadius=std::min(ringRadius,mouthRadius+qPerMetre/2);
|
{
|
||||||
|
const std::int64_t craterRimRouteRadius=VoxelForgeCore::WorldAssemblyEvaluatorVersion>=185
|
||||||
|
&&style==0&&!anchoredSpiral&&passageAnchor&&passageAnchor->values.size()>3
|
||||||
|
?std::max<std::int64_t>(midRadius,
|
||||||
|
passageAnchor->values[3]+2*qPerMetre)
|
||||||
|
:mouthRadius+qPerMetre/2;
|
||||||
|
ringRadius=std::min(ringRadius,anchoredSpiral
|
||||||
|
?mouthRadius+qPerMetre/2:craterRimRouteRadius);
|
||||||
|
}
|
||||||
std::uint32_t phase=static_cast<std::uint32_t>(PassageSeed(seed,
|
std::uint32_t phase=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||||
upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16);
|
upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16);
|
||||||
if(hasUpperRoute)
|
if(hasUpperRoute)
|
||||||
{
|
{
|
||||||
std::int64_t bestAlignment=std::numeric_limits<std::int64_t>::min();
|
std::int64_t bestAlignment=std::numeric_limits<std::int64_t>::min();
|
||||||
const std::uint32_t hashedPhase=phase;
|
const std::uint32_t hashedPhase=phase;
|
||||||
|
const std::int64_t phaseX=style==0&&anchoredIslandPassage
|
||||||
|
&&!anchoredSpiral&&passageAnchor
|
||||||
|
?upperMouth[0]-passageAnchor->values[0]
|
||||||
|
:(hasUpperTangent?upperTangent[0]:upperMouth[0]);
|
||||||
|
const std::int64_t phaseY=style==0&&anchoredIslandPassage
|
||||||
|
&&!anchoredSpiral&&passageAnchor
|
||||||
|
?upperMouth[1]-passageAnchor->values[1]
|
||||||
|
:(hasUpperTangent?upperTangent[1]:upperMouth[1]);
|
||||||
for(std::uint32_t offset=0;offset<16;++offset)
|
for(std::uint32_t offset=0;offset<16;++offset)
|
||||||
{
|
{
|
||||||
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
||||||
(hashedPhase+offset)%16);
|
(hashedPhase+offset)%16);
|
||||||
const std::int64_t alignment=(hasUpperTangent?upperTangent[0]:upperMouth[0])
|
const std::int64_t alignment=phaseX*directions[candidate][0]
|
||||||
*directions[candidate][0]
|
+phaseY*directions[candidate][1];
|
||||||
+(hasUpperTangent?upperTangent[1]:upperMouth[1])
|
if(style==0&&anchoredIslandPassage&&!anchoredSpiral&&passageAnchor)
|
||||||
*directions[candidate][1];
|
{
|
||||||
if(alignment>bestAlignment)
|
const std::int64_t targetProjection=ringRadius*65536;
|
||||||
|
if(alignment<=0)continue;
|
||||||
|
const std::int64_t tangentError=std::abs(alignment-targetProjection);
|
||||||
|
if(tangentError<bestAlignment
|
||||||
|
||bestAlignment==std::numeric_limits<std::int64_t>::min())
|
||||||
|
{bestAlignment=tangentError;phase=candidate;}
|
||||||
|
}
|
||||||
|
else if(alignment>bestAlignment)
|
||||||
{bestAlignment=alignment;phase=candidate;}
|
{bestAlignment=alignment;phase=candidate;}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(hasUpperRoute&&upperIsSurfaceWorld)
|
||||||
|
{
|
||||||
|
const auto& directionAtPhase=directions[phase];
|
||||||
|
const std::int64_t radialAlignment=upperMouth[0]*directionAtPhase[0]
|
||||||
|
+upperMouth[1]*directionAtPhase[1];
|
||||||
|
if(radialAlignment<0)
|
||||||
|
{
|
||||||
|
std::int64_t bestOutwardAlignment=std::numeric_limits<std::int64_t>::min();
|
||||||
|
for(std::uint32_t candidate=0;candidate<directions.size();++candidate)
|
||||||
|
{
|
||||||
|
const std::int64_t alignment=upperMouth[0]*directions[candidate][0]
|
||||||
|
+upperMouth[1]*directions[candidate][1];
|
||||||
|
if(alignment>bestOutwardAlignment)
|
||||||
|
{bestOutwardAlignment=alignment;phase=candidate;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
||||||
connection,0x44495245u)&1u)!=0;
|
connection,0x44495245u)&1u)!=0;
|
||||||
if(anchoredSpiral&&passageAnchor&&hasUpperTangent)
|
if(style==0&&anchoredIslandPassage&&!anchoredSpiral&&passageAnchor)
|
||||||
|
{
|
||||||
|
const auto& ringDirection=directions[phase];
|
||||||
|
const double radiusX=static_cast<double>(ringRadius)*ringDirection[0]/65536.0;
|
||||||
|
const double radiusY=static_cast<double>(ringRadius)*ringDirection[1]/65536.0;
|
||||||
|
const double anchorX=static_cast<double>(passageAnchor->values[0]);
|
||||||
|
const double anchorY=static_cast<double>(passageAnchor->values[1]);
|
||||||
|
const double connectorX=anchorX+radiusX-upperMouth[0];
|
||||||
|
const double connectorY=anchorY+radiusY-upperMouth[1];
|
||||||
|
clockwise=connectorX*(-radiusY)+connectorY*radiusX>=0.0;
|
||||||
|
}
|
||||||
|
else if(anchoredSpiral&&passageAnchor&&hasUpperTangent)
|
||||||
{
|
{
|
||||||
const double radialX=static_cast<double>(upperMouth[0]-passageAnchor->values[0]);
|
const double radialX=static_cast<double>(upperMouth[0]-passageAnchor->values[0]);
|
||||||
const double radialY=static_cast<double>(upperMouth[1]-passageAnchor->values[1]);
|
const double radialY=static_cast<double>(upperMouth[1]-passageAnchor->values[1]);
|
||||||
@@ -748,7 +1003,32 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
const double ledge=std::min<double>(ringRadius/4,
|
const double ledge=std::min<double>(ringRadius/4,
|
||||||
static_cast<double>(cfg?std::max<std::int64_t>(0,
|
static_cast<double>(cfg?std::max<std::int64_t>(0,
|
||||||
cfg->cascadeLedgeSubvoxels):0));
|
cfg->cascadeLedgeSubvoxels):0));
|
||||||
radius-=ledge*(0.5-0.5*std::cos(angle*steps));
|
// Give each turn a seeded, shallow angular profile so the same cosine
|
||||||
|
// scallop cannot repeat at full ledge depth on every revolution.
|
||||||
|
const std::uint32_t controlCount=std::clamp(steps*2u,8u,
|
||||||
|
segments/2u);
|
||||||
|
const double coordinate=angle*controlCount
|
||||||
|
/(2.0*3.14159265358979323846);
|
||||||
|
const std::uint32_t control0=static_cast<std::uint32_t>(std::floor(coordinate));
|
||||||
|
const std::uint32_t control1=(control0+1u)%controlCount;
|
||||||
|
const double blend=coordinate-std::floor(coordinate);
|
||||||
|
const auto controlValue=[&](std::uint32_t control)
|
||||||
|
{
|
||||||
|
const std::uint32_t salt=0x43415343u
|
||||||
|
^(loopIndex*0x9e3779b9u)^(control*0x85ebca6bu);
|
||||||
|
const std::uint32_t bits=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||||
|
upper.strateIndex,lower.strateIndex,connection,salt)>>40);
|
||||||
|
return 2.0*static_cast<double>(bits)/16777215.0-1.0;
|
||||||
|
};
|
||||||
|
const double smooth=blend*blend*(3.0-2.0*blend);
|
||||||
|
const double irregular=std::lerp(controlValue(control0),
|
||||||
|
controlValue(control1),smooth);
|
||||||
|
const std::uint32_t loopHash=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||||
|
upper.strateIndex,lower.strateIndex,connection,
|
||||||
|
0x4c454447u+loopIndex)>>40);
|
||||||
|
const double amplitude=0.60+0.40
|
||||||
|
*static_cast<double>(loopHash)/16777215.0;
|
||||||
|
radius-=ledge*amplitude*(0.24+0.10*irregular);
|
||||||
}
|
}
|
||||||
if(pathStyle!=1u&&anchoredIslandPassage&&passageAnchor)
|
if(pathStyle!=1u&&anchoredIslandPassage&&passageAnchor)
|
||||||
{
|
{
|
||||||
@@ -826,7 +1106,8 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
hasAnchorHelix=helixEndIndex>0;
|
hasAnchorHelix=helixEndIndex>0;
|
||||||
}
|
}
|
||||||
const std::int64_t routeGradeQ16=hasAnchorHelix
|
const std::int64_t routeGradeQ16=hasAnchorHelix
|
||||||
&&lower.generatorKind=="SurfaceWorld"?8000:TargetGradeQ16;
|
&&lower.generatorKind=="SurfaceWorld"?8000
|
||||||
|
:style==0&&anchoredIslandPassage&&!anchoredSpiral?15500:TargetGradeQ16;
|
||||||
const std::array<std::int64_t,2> firstRing=ringPoint(phase);
|
const std::array<std::int64_t,2> firstRing=ringPoint(phase);
|
||||||
if(hasAnchorHelix&&pathStyle==1u)
|
if(hasAnchorHelix&&pathStyle==1u)
|
||||||
{
|
{
|
||||||
@@ -926,7 +1207,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
pushXY(ringPoint(lowerPhase));
|
pushXY(ringPoint(lowerPhase));
|
||||||
}
|
}
|
||||||
pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath();
|
pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath();
|
||||||
const std::size_t lowerMouthIndex=xy.size()-1;
|
std::size_t lowerMouthIndex=xy.size()-1;
|
||||||
std::vector<std::array<std::int64_t,3>> lowerLandingPoints;
|
std::vector<std::array<std::int64_t,3>> lowerLandingPoints;
|
||||||
if(hasLowerTangent)
|
if(hasLowerTangent)
|
||||||
{
|
{
|
||||||
@@ -960,6 +1241,27 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
if(lowerLandingPoints.empty())
|
if(lowerLandingPoints.empty())
|
||||||
{error=passage.stableId+" lower route has no landing length inside PassageConfig.DistanceMax";return false;}
|
{error=passage.stableId+" lower route has no landing length inside PassageConfig.DistanceMax";return false;}
|
||||||
}
|
}
|
||||||
|
if(VoxelForgeCore::WorldAssemblyEvaluatorVersion>=188
|
||||||
|
&&style==0&&anchoredIslandPassage&&!anchoredSpiral&&xy.size()>4)
|
||||||
|
{
|
||||||
|
std::vector<std::array<std::int64_t,2>> refined;
|
||||||
|
refined.reserve(xy.size()*2);
|
||||||
|
for(std::size_t index=0;index+1<xy.size();++index)
|
||||||
|
{
|
||||||
|
refined.push_back(xy[index]);
|
||||||
|
if(index<3)continue;
|
||||||
|
refined.push_back({
|
||||||
|
static_cast<std::int64_t>(std::llround(0.5*(
|
||||||
|
static_cast<double>(xy[index][0])
|
||||||
|
+static_cast<double>(xy[index+1][0])))),
|
||||||
|
static_cast<std::int64_t>(std::llround(0.5*(
|
||||||
|
static_cast<double>(xy[index][1])
|
||||||
|
+static_cast<double>(xy[index+1][1]))))});
|
||||||
|
}
|
||||||
|
refined.push_back(xy.back());
|
||||||
|
if(lowerMouthIndex>3)lowerMouthIndex+=lowerMouthIndex-3;
|
||||||
|
xy.swap(refined);
|
||||||
|
}
|
||||||
totalHorizontal=measurePath();
|
totalHorizontal=measurePath();
|
||||||
if(loops>=256||totalHorizontal==0)
|
if(loops>=256||totalHorizontal==0)
|
||||||
{error=passage.stableId+" could not fit a supported grade inside the resolved placement radius";return false;}
|
{error=passage.stableId+" could not fit a supported grade inside the resolved placement radius";return false;}
|
||||||
@@ -974,10 +1276,23 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
if(hasAnchorHelix&&helixHorizontal==0)
|
if(hasAnchorHelix&&helixHorizontal==0)
|
||||||
{error=passage.stableId+" anchor spiral has no horizontal reach";return false;}
|
{error=passage.stableId+" anchor spiral has no horizontal reach";return false;}
|
||||||
passage.controlPoints.reserve(xy.size());
|
passage.controlPoints.reserve(xy.size());
|
||||||
|
const std::int64_t surfacePortalRadius=surfaceGroundDeparture
|
||||||
|
?std::min(mouthRadius,static_cast<std::int64_t>(std::llround(1.5*qPerMetre)))
|
||||||
|
:mouthRadius;
|
||||||
|
const std::uint64_t surfaceMouthBlendLength=static_cast<std::uint64_t>(
|
||||||
|
std::max<std::int64_t>(8*qPerMetre,2*mouthRadius));
|
||||||
for(std::size_t i=0;i<xy.size();++i)
|
for(std::size_t i=0;i<xy.size();++i)
|
||||||
{
|
{
|
||||||
const std::int64_t radius=(i<=1||i==lowerMouthIndex
|
std::int64_t radius=(i<=1||i==lowerMouthIndex
|
||||||
||i+1>=xy.size()-1)?mouthRadius:midRadius;
|
||i+1>=xy.size()-1)?mouthRadius:midRadius;
|
||||||
|
if(surfaceGroundDeparture&&i<lowerMouthIndex)
|
||||||
|
{
|
||||||
|
const double t=std::clamp(static_cast<double>(cumulative[i])
|
||||||
|
/surfaceMouthBlendLength,0.0,1.0);
|
||||||
|
const double smooth=t*t*(3.0-2.0*t);
|
||||||
|
radius=surfacePortalRadius+static_cast<std::int64_t>(std::llround(
|
||||||
|
(midRadius-surfacePortalRadius)*smooth));
|
||||||
|
}
|
||||||
std::int64_t playerZ=0;
|
std::int64_t playerZ=0;
|
||||||
if(i>lowerMouthIndex)
|
if(i>lowerMouthIndex)
|
||||||
playerZ=lowerLandingPoints[i-lowerMouthIndex-1][2];
|
playerZ=lowerLandingPoints[i-lowerMouthIndex-1][2];
|
||||||
@@ -998,9 +1313,30 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
point.positionSubvoxels={xy[i][0],xy[i][1],playerZ+radius-playerCenterAboveFloorQ};
|
point.positionSubvoxels={xy[i][0],xy[i][1],playerZ+radius-playerCenterAboveFloorQ};
|
||||||
point.radiusSubvoxels=radius;passage.controlPoints.push_back(point);
|
point.radiusSubvoxels=radius;passage.controlPoints.push_back(point);
|
||||||
}
|
}
|
||||||
passage.controlPoints.front().positionSubvoxels[2]=upperMouth[2]+mouthRadius-playerCenterAboveFloorQ;
|
if(surfaceGroundDeparture)
|
||||||
passage.controlPoints[lowerMouthIndex].positionSubvoxels[2]=
|
std::printf("[ResolvedSurfaceWorldDepartureMouth] passage=%s ground_floor_z_m=%.3f portal_radius_m=%.3f tunnel_radius_m=%.3f blend_length_m=%.3f departure_xy_m=(%.3f,%.3f) first_tunnel_xy_m=(%.3f,%.3f) first_tunnel_floor_z_m=%.3f first_tunnel_radius_m=%.3f\n",
|
||||||
lowerMouth[2]+mouthRadius-playerCenterAboveFloorQ;
|
passage.stableId.c_str(),(upperMouth[2]-static_cast<std::int64_t>(std::llround(0.88*qPerMetre)))/static_cast<double>(qPerMetre),
|
||||||
|
surfacePortalRadius/static_cast<double>(qPerMetre),
|
||||||
|
midRadius/static_cast<double>(qPerMetre),
|
||||||
|
surfaceMouthBlendLength/static_cast<double>(qPerMetre),
|
||||||
|
upperMouth[0]/static_cast<double>(qPerMetre),
|
||||||
|
upperMouth[1]/static_cast<double>(qPerMetre),
|
||||||
|
passage.controlPoints.size()>1?passage.controlPoints[1].positionSubvoxels[0]
|
||||||
|
/static_cast<double>(qPerMetre):0.0,
|
||||||
|
passage.controlPoints.size()>1?passage.controlPoints[1].positionSubvoxels[1]
|
||||||
|
/static_cast<double>(qPerMetre):0.0,
|
||||||
|
passage.controlPoints.size()>1?
|
||||||
|
(passage.controlPoints[1].positionSubvoxels[2]
|
||||||
|
-passage.controlPoints[1].radiusSubvoxels+playerCenterAboveFloorQ
|
||||||
|
-static_cast<std::int64_t>(std::llround(0.88*qPerMetre)))
|
||||||
|
/static_cast<double>(qPerMetre):0.0,
|
||||||
|
passage.controlPoints.size()>1?passage.controlPoints[1].radiusSubvoxels
|
||||||
|
/static_cast<double>(qPerMetre):0.0);
|
||||||
|
passage.controlPoints.front().positionSubvoxels[2]=upperMouth[2]
|
||||||
|
+passage.controlPoints.front().radiusSubvoxels-playerCenterAboveFloorQ;
|
||||||
|
passage.controlPoints[lowerMouthIndex].positionSubvoxels[2]=lowerMouth[2]
|
||||||
|
+passage.controlPoints[lowerMouthIndex].radiusSubvoxels
|
||||||
|
-playerCenterAboveFloorQ;
|
||||||
layout.passages.push_back(std::move(passage));
|
layout.passages.push_back(std::move(passage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1055,6 +1391,11 @@ ResolvedLayoutValidatorAuditV3 AuditResolvedLayoutValidatorV3()
|
|||||||
auto missingCeiling=base;missingCeiling.strates[0].ceiling.presentInCurrentField=false;expectReject("missing ceiling",missingCeiling);
|
auto missingCeiling=base;missingCeiling.strates[0].ceiling.presentInCurrentField=false;expectReject("missing ceiling",missingCeiling);
|
||||||
auto wrongOrder=base;wrongOrder.strates[0].content.kind="bedrock";expectReject("wrong band order",wrongOrder);
|
auto wrongOrder=base;wrongOrder.strates[0].content.kind="bedrock";expectReject("wrong band order",wrongOrder);
|
||||||
auto largeOverlap=base;largeOverlap.strates[0].transitionInterpenetrationMicrometres=4000001;expectReject("interpenetration exceeds transition width",largeOverlap);
|
auto largeOverlap=base;largeOverlap.strates[0].transitionInterpenetrationMicrometres=4000001;expectReject("interpenetration exceeds transition width",largeOverlap);
|
||||||
|
auto thinBand=base;
|
||||||
|
thinBand.strates[0].bedrock.topMicrometres=thinBand.strates[0].bedrock.bottomMicrometres
|
||||||
|
+2ll*base.coreVoxelSizeMicrometres-1;
|
||||||
|
thinBand.strates[0].content.bottomMicrometres=thinBand.strates[0].bedrock.topMicrometres;
|
||||||
|
expectReject("bedrock band below minimum thickness",thinBand);
|
||||||
auto genericOverlap=base;genericOverlap.strates[0].bedrock.bottomMicrometres-=1;expectReject("nominal band overlap",genericOverlap);
|
auto genericOverlap=base;genericOverlap.strates[0].bedrock.bottomMicrometres-=1;expectReject("nominal band overlap",genericOverlap);
|
||||||
auto genericGap=base;genericGap.strates[0].bedrock.bottomMicrometres+=1;expectReject("nominal band gap",genericGap);
|
auto genericGap=base;genericGap.strates[0].bedrock.bottomMicrometres+=1;expectReject("nominal band gap",genericGap);
|
||||||
result.passed=result.controlsRun==result.controlsRejectedAsExpected&&result.failures.empty();return result;
|
result.passed=result.controlsRun==result.controlsRejectedAsExpected&&result.failures.empty();return result;
|
||||||
|
|||||||
@@ -40,6 +40,34 @@ namespace VoxelForgeCore
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||||
|
void PrintL30CostCounters(const FStandaloneShapeWorld::FClassificationThreadStats& stats)
|
||||||
|
{
|
||||||
|
if(stats.BoundaryBandCalls==0&&stats.ColumnZOverlapCalls==0)return;
|
||||||
|
std::printf("[L30CostTile] boundary_band_calls=%llu transition_window_checks=%llu "
|
||||||
|
"transition_window_hits=%llu transition_evaluations=%llu passage_fade_calls=%llu "
|
||||||
|
"passage_fade_mouth_tests=%llu column_z_overlap_calls=%llu column_guard_calls=%llu "
|
||||||
|
"column_guard_nonpositive_skips=%llu column_guard_radial_rejects=%llu "
|
||||||
|
"boundary_tile_cull_rejects=%llu boundary_point_cull_rejects=%llu "
|
||||||
|
"transition_passage_tile_skips=%llu "
|
||||||
|
"column_sdf_calls=%llu\n",
|
||||||
|
static_cast<unsigned long long>(stats.BoundaryBandCalls),
|
||||||
|
static_cast<unsigned long long>(stats.TransitionWindowChecks),
|
||||||
|
static_cast<unsigned long long>(stats.TransitionWindowHits),
|
||||||
|
static_cast<unsigned long long>(stats.TransitionEvaluatorCalls),
|
||||||
|
static_cast<unsigned long long>(stats.TransitionPassageFadeCalls),
|
||||||
|
static_cast<unsigned long long>(stats.TransitionPassageMouthTests),
|
||||||
|
static_cast<unsigned long long>(stats.ColumnZOverlapCalls),
|
||||||
|
static_cast<unsigned long long>(stats.ColumnGuardCalls),
|
||||||
|
static_cast<unsigned long long>(stats.ColumnGuardNonpositiveSkips),
|
||||||
|
static_cast<unsigned long long>(stats.ColumnGuardRadialRejects),
|
||||||
|
static_cast<unsigned long long>(stats.BoundaryTileCullRejects),
|
||||||
|
static_cast<unsigned long long>(stats.BoundaryPointCullRejects),
|
||||||
|
static_cast<unsigned long long>(stats.TransitionPassageTileSkips),
|
||||||
|
static_cast<unsigned long long>(stats.ColumnSignedDistanceCalls));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
FIntVector ToNative(Vec3i value) { return {value.x,value.y,value.z}; }
|
FIntVector ToNative(Vec3i value) { return {value.x,value.y,value.z}; }
|
||||||
FVector ToNative(Vec3d value) { return {value.x,value.y,value.z}; }
|
FVector ToNative(Vec3d value) { return {value.x,value.y,value.z}; }
|
||||||
Vec3d FromNative(const FVector& value) { return {value.X,value.Y,value.Z}; }
|
Vec3d FromNative(const FVector& value) { return {value.X,value.Y,value.Z}; }
|
||||||
@@ -1995,6 +2023,9 @@ namespace VoxelForgeCore
|
|||||||
out.diagnostics.classificationExclusiveMilliseconds=std::max(0.0,
|
out.diagnostics.classificationExclusiveMilliseconds=std::max(0.0,
|
||||||
Counts.Milliseconds-classificationColumnComputeMilliseconds);
|
Counts.Milliseconds-classificationColumnComputeMilliseconds);
|
||||||
out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds;
|
out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds;
|
||||||
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||||
|
PrintL30CostCounters(Counts);
|
||||||
|
#endif
|
||||||
if(options.captureColumnCoordinates)
|
if(options.captureColumnCoordinates)
|
||||||
{
|
{
|
||||||
out.diagnostics.columnEvaluationWorkerHash=static_cast<std::uint64_t>(
|
out.diagnostics.columnEvaluationWorkerHash=static_cast<std::uint64_t>(
|
||||||
@@ -2356,6 +2387,9 @@ namespace VoxelForgeCore
|
|||||||
if(bHasWorldAssembly)
|
if(bHasWorldAssembly)
|
||||||
{
|
{
|
||||||
const auto Counts=FStandaloneShapeWorld::GetThreadClassificationStats();
|
const auto Counts=FStandaloneShapeWorld::GetThreadClassificationStats();
|
||||||
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||||
|
PrintL30CostCounters(Counts);
|
||||||
|
#endif
|
||||||
const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
|
const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
|
||||||
out.diagnostics.fieldCalls=Columns.pointCalls;
|
out.diagnostics.fieldCalls=Columns.pointCalls;
|
||||||
out.diagnostics.floatFieldCalls=Counts.NativeFloatFieldCalls;
|
out.diagnostics.floatFieldCalls=Counts.NativeFloatFieldCalls;
|
||||||
|
|||||||
@@ -331,6 +331,28 @@ VF_FIELD_INLINE float VF_FieldPerlin3D(float x,float y,float z)
|
|||||||
return VF_FieldPerlinLerp(y0,y1,sw);
|
return VF_FieldPerlinLerp(y0,y1,sw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VF_FIELD_INLINE float VF_FieldPassageWallRelief(float x,float y,float z,VF_FieldU32 seed)
|
||||||
|
{
|
||||||
|
// Use long, non-commensurate rotated scales. The shared passage wall keeps broad,
|
||||||
|
// uneven rock relief without a short wavelength that can read as repeated rings.
|
||||||
|
const float broad=VF_FieldValueNoise(x+17.0f,y-23.0f,z+11.0f,43,
|
||||||
|
seed^0xB5297A4Du);
|
||||||
|
const float mid=VF_FieldValueNoise(y-7.0f,z+19.0f,x-31.0f,29,
|
||||||
|
seed^0x63D83595u);
|
||||||
|
const float fine=VF_FieldValueNoise(z+5.0f,x+29.0f,y-13.0f,19,
|
||||||
|
seed^0xA511E9B3u);
|
||||||
|
return 0.50f*broad+0.35f*mid+0.15f*fine;
|
||||||
|
}
|
||||||
|
|
||||||
|
VF_FIELD_INLINE float VF_FieldPassageGroundGrain(float x,float y,VF_FieldU32 seed)
|
||||||
|
{
|
||||||
|
const float broad=VF_FieldValueNoise(x+3.25f,y-7.5f,0.0f,8,
|
||||||
|
seed^0xC2B2AE35u);
|
||||||
|
const float fine=VF_FieldValueNoise(y+11.75f,x-5.0f,0.0f,5,
|
||||||
|
seed^0x27D4EB2Fu);
|
||||||
|
return 0.65f*broad+0.35f*fine;
|
||||||
|
}
|
||||||
|
|
||||||
VF_FIELD_INLINE float VF_FieldOwnerFractal(float x,float y,float z,VF_FieldI32 octaves,
|
VF_FIELD_INLINE float VF_FieldOwnerFractal(float x,float y,float z,VF_FieldI32 octaves,
|
||||||
VF_FieldI32 mode)
|
VF_FieldI32 mode)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ Costs below are design estimates, not timings. Low means bounded analytic featur
|
|||||||
|
|
||||||
A passage planner must produce a traversable route or pair a non-walkable visual passage with another guaranteed route. A narrow crack or drop cannot be assumed walkable because its centerline is air.
|
A passage planner must produce a traversable route or pair a non-walkable visual passage with another guaranteed route. A narrow crack or drop cannot be assumed walkable because its centerline is air.
|
||||||
|
|
||||||
|
For a SurfaceWorld strate, the supported arrival-to-departure route follows the sampled terrain
|
||||||
|
ground and stays within the mouth placement bounds. The inter-strate departure passage owns the
|
||||||
|
descent from that ground mouth through rock and bedrock. Treating the bottom of a tall SurfaceWorld
|
||||||
|
strate as the route's departure height would ask the walk planner to cross open terrain air; it is
|
||||||
|
the wrong endpoint for the intra-strate route.
|
||||||
|
|
||||||
| Passage type | Planner output | Bound and likely cost |
|
| Passage type | Planner output | Bound and likely cost |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| SlopedTunnel | Straight-style path with switchbacks as needed, capsule clearance, supported floor, maximum grade and landing geometry. | Swept capsule bounds plus floor envelope; low–medium. |
|
| SlopedTunnel | Straight-style path with switchbacks as needed, capsule clearance, supported floor, maximum grade and landing geometry. | Swept capsule bounds plus floor envelope; low–medium. |
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# World Assembler L29 — bedrock/ceiling transitions
|
||||||
|
|
||||||
|
**Core now honors the upper strate's `EVoxelStrateTransition` at the shared interface.** The owner
|
||||||
|
world's upper two-chunk Gradient resolves to 16 m at the 8 m layout chunk size. SurfaceWorld's
|
||||||
|
one-chunk setting remains in the resolved strate record. Core does not blend unrelated generator
|
||||||
|
parameters. `WorldAssemblyEvaluatorVersion` is 15; recipe versions and resolved-layout schema V3
|
||||||
|
are unchanged.
|
||||||
|
|
||||||
|
## Appearance and safety
|
||||||
|
|
||||||
|
| Mode | Result |
|
||||||
|
|---|---|
|
||||||
|
| Hard | The original flat, touching boundary. No displacement or overlap. |
|
||||||
|
| Gradient | Both faces follow one smooth 2D hash surface. On the 16 m interface, the measured peak-to-peak undulation is 3.988 m. |
|
||||||
|
| Interleaved | A deterministic 3D domain-warped hash field moves both faces inward, producing fingered overlap. Measured peak crossing was 6.487 m in FloatingIslands and 6.952 m in SurfaceWorld, within the declared 16 m. |
|
||||||
|
|
||||||
|
Each boundary band keeps at least **two Core voxels** of solid thickness: 0.5 m at the default
|
||||||
|
25 cm voxel size. The owner bands measured 0.5–1.0 m on the upper side and 6.25 m on the lower
|
||||||
|
side. Interleaved is capped by both the declared width and the bands' combined thickness, so the
|
||||||
|
solid intervals still touch or overlap.
|
||||||
|
|
||||||
|
The strict interface audit covers a 512 m square: 4,221 columns per interface, with four passage
|
||||||
|
columns excluded from the hole sweep. Hard, Gradient and Interleaved each report zero hole columns
|
||||||
|
and zero air samples. Both negative controls reject as expected: crossing one micrometre wider than
|
||||||
|
declared, and either band one micrometre thinner than the minimum. All six recipe/mode runs pass.
|
||||||
|
|
||||||
|
Transition faces return to the nominal seam near passage mouths: the protected radius is the mouth
|
||||||
|
radius plus 1 m, followed by a 2 m smooth fade. This kept the style-2 FI landing supported; its
|
||||||
|
final capsule/support audit passes.
|
||||||
|
|
||||||
|
## Passage and parity audits
|
||||||
|
|
||||||
|
The final matrix has 4 styles × 2 recipes × 2 compilers (16 runs). Capsule and walkability,
|
||||||
|
open-air, naturalness, no-long-bridge, route bindings, strict-validator controls, L13 route parity,
|
||||||
|
and L9 proof audits pass. Open-air reports zero violations; validator controls reject 10/10;
|
||||||
|
route-parity and proof audits report zero mismatches/violations. The L23/L24/L26 negative controls
|
||||||
|
are detected.
|
||||||
|
|
||||||
|
The L28 `FloorProfileBlend` diagnostic still exceeds its 2-voxel threshold in styles 1 and 3:
|
||||||
|
2.544 and 3.645 voxels. L28 records the same values on both compilers, so this is inherited and
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
## Hashes
|
||||||
|
|
||||||
|
The L25 W1/W8/W12 hash matrix is identical across Clang and GCC for both recipes: full-detail,
|
||||||
|
whole-view, all LOD rows and exact-query hashes match. Hash order below is density / mesh / payload /
|
||||||
|
topology.
|
||||||
|
|
||||||
|
| Recipe | Full detail (960 tiles) | Whole view (2,370 tiles) | Exact-query density / detail / collision |
|
||||||
|
|---|---|---|---|
|
||||||
|
| SurfaceWorld | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `0e8c5af01df96b14 / 70099e89b77811ed / dbce0fdcf6764126 / ddaf81a1228f9b4b` | `c949c4df2ce6f9d9 / 114c501e2d830d94 / e6011d3a248d4849` |
|
||||||
|
| FloatingIslands | `ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `a492eefa4f534975 / 141f00f3e411a0df / 93f50c569526c5cb / d83b53c8e53df81c` | `7b7c50ac24ccadcc / efcba20c0c70c415 / e604823a249029bf` |
|
||||||
|
|
||||||
|
The 24 m moving crossing processed 314 entering tiles per recipe; Clang/GCC crossing hashes match.
|
||||||
|
SurfaceWorld's away-from-interface probe at z = −400 m also matches L28: all 960 full-detail
|
||||||
|
tiles and the exact-query density/detail/collision hashes are identical. The broader whole-view
|
||||||
|
hash differs because its far-LOD coverage also includes the changed interface.
|
||||||
|
|
||||||
|
## Cost
|
||||||
|
|
||||||
|
Five clean, alternating W12 pairs against
|
||||||
|
`Saved/Linux/bin/VoxelForgeCoreHeadless-linux28-clang-r28`, hashing off:
|
||||||
|
|
||||||
|
| Recipe | L28 whole-view ready | L29 whole-view ready | Change |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| SurfaceWorld | 1.581 s | 1.644 s | +3.97% |
|
||||||
|
| FloatingIslands | 16.605 s | 16.814 s | +1.26% |
|
||||||
|
|
||||||
|
FloatingIslands is within the ~3% target. SurfaceWorld is about one percentage point above it.
|
||||||
|
|
||||||
|
## Captures
|
||||||
|
|
||||||
|
All are 1280×720 JPEGs, each under 21 KB (well below 300 KB):
|
||||||
|
|
||||||
|
- `captures/fi-floor-gradient-above.jpg` — FI floor from above, inside FI.
|
||||||
|
- `captures/sw-ceiling-gradient-below.jpg` — SW ceiling from below, inside SW.
|
||||||
|
- `captures/interface-hard-side-cutaway.jpg`
|
||||||
|
- `captures/interface-gradient-side-cutaway.jpg`
|
||||||
|
- `captures/interface-interleaved-side-cutaway.jpg`
|
||||||
|
|
||||||
|
## Changes and run history
|
||||||
|
|
||||||
|
Core transition evaluation/validation and the shared composition path changed, along with the
|
||||||
|
headless transition audit, architecture/codemap, and the two Unreal files
|
||||||
|
`Source/VoxelForge/Public/VoxelStrateTypes.h` and
|
||||||
|
`Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`. Unreal was unavailable here, so those edits
|
||||||
|
were not compiled. No commit, push or stash was made.
|
||||||
|
|
||||||
|
Failed or retried runs:
|
||||||
|
|
||||||
|
- An early Clang build used a nonexistent dispatch helper; corrected to the current query method and rebuilt with Clang and GCC.
|
||||||
|
- The first Interleaved overlap used the full sum of band thicknesses and opened gaps (1,085 columns / 20,313 air samples). Capping overlap to `min(declared width, combined thickness)` removed the gaps.
|
||||||
|
- An initial hole sweep sampled a half-open band edge and reported 752 boundary-rounding holes. Sampling half a voxel inside each band removed those edge artifacts; final sweeps report zero holes.
|
||||||
|
- The first FI style-2 passage audit had 11 unsupported probes after Gradient moved the mouth face. The local mouth taper fixed it; all 16 final passage runs pass.
|
||||||
|
- A resolved-layout-only command initially omitted required pipeline flags and exited 2; rerun with the required flags passed. Early cutaway camera/clip settings produced blank framing; the final orientation and clip plane produced the captures above.
|
||||||
|
- One cost folder contained a duplicated timing sequence and was discarded. The five-pair clean run above was sequential. A cache experiment did not improve cost and was removed. A cost-extraction `sed` typo was rerun with Python; benchmark logs were unaffected.
|
||||||
|
|
||||||
|
No build or audit exceeded 30 minutes, and no step remained stuck for 15 minutes. All builds and runs
|
||||||
|
used CPUs 0–11, at most 12 workers, normal priority, and the 6 GiB address-space cap.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged: transitions honoured safely (0 holes over 4,221 columns per interface, negative controls reject, all
|
||||||
|
passage audits pass, deterministic).
|
||||||
|
- **The look is not verified by the captures:** `interface-interleaved-side-cutaway.jpg` shows flat faces
|
||||||
|
(black = uncapped solid between faces), with no visible fingers; `sw-ceiling-gradient-below.jpg` is a flat
|
||||||
|
plane; `fi-floor-gradient-above.jpg` shows **concentric dashed rings** on the FI floor, which looks like an
|
||||||
|
artifact (possibly the passage-mouth protected-radius fade, or quantised undulation), not a smooth Gradient.
|
||||||
|
- Cumulative SW cost vs L24 is now ~+8% (L28 +3.7% plus L29 +4%). L30 covers the ring artifact, readable
|
||||||
|
transition captures, and cost recovery.
|
||||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,239 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rasterize upward mesh triangles into a top-down field height map."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
Q16 = 1 << 16
|
||||||
|
MASK64 = (1 << 64) - 1
|
||||||
|
|
||||||
|
|
||||||
|
def trunc_div(numerator, denominator):
|
||||||
|
return numerator // denominator if numerator >= 0 else -((-numerator) // denominator)
|
||||||
|
|
||||||
|
|
||||||
|
def transition_hash(seed, interface, x, y, z, salt):
|
||||||
|
value = 14695981039346656037
|
||||||
|
for word in (seed & 0xffffffff, interface & 0xffffffff,
|
||||||
|
x & MASK64, y & MASK64, z & MASK64, salt):
|
||||||
|
for byte in range(8):
|
||||||
|
value ^= (word >> (byte * 8)) & 0xff
|
||||||
|
value = (value * 1099511628211) & MASK64
|
||||||
|
value ^= value >> 30
|
||||||
|
value = (value * 0xbf58476d1ce4e5b9) & MASK64
|
||||||
|
value ^= value >> 27
|
||||||
|
value = (value * 0x94d049bb133111eb) & MASK64
|
||||||
|
value ^= value >> 31
|
||||||
|
return ((value >> 32) >> 15) - Q16
|
||||||
|
|
||||||
|
|
||||||
|
def transition_fade(value):
|
||||||
|
value = max(0, min(Q16, value))
|
||||||
|
square = value * value // Q16
|
||||||
|
return square * (3 * Q16 - 2 * value) // Q16
|
||||||
|
|
||||||
|
|
||||||
|
def gradient_height_um(layout, x_um, y_um):
|
||||||
|
upper = next(item for item in layout["strates"] if item["order"] == 0)
|
||||||
|
lower = next(item for item in layout["strates"] if item["order"] == 1)
|
||||||
|
width = upper["transition_width_um"]
|
||||||
|
cell = max(1_000_000, layout["strate_layout_chunk_size_um"] * 4)
|
||||||
|
ix = x_um // cell
|
||||||
|
iy = y_um // cell
|
||||||
|
tx = transition_fade(((x_um - ix * cell) * Q16) // cell)
|
||||||
|
ty = transition_fade(((y_um - iy * cell) * Q16) // cell)
|
||||||
|
|
||||||
|
def at(dx, dy):
|
||||||
|
noise = transition_hash(0, upper["strate_index"], ix + dx, iy + dy, 0,
|
||||||
|
0x47524144)
|
||||||
|
return noise
|
||||||
|
|
||||||
|
def lerp(a, b, t):
|
||||||
|
return a + trunc_div((b - a) * t, Q16)
|
||||||
|
|
||||||
|
low = lerp(lerp(at(0, 0), at(1, 0), tx),
|
||||||
|
lerp(at(0, 1), at(1, 1), tx), ty)
|
||||||
|
displacement = trunc_div(low * (width // 8), Q16)
|
||||||
|
seam = upper["bedrock"]["bottom_um"]
|
||||||
|
thickness = upper["bedrock"]["top_um"] - seam
|
||||||
|
return seam + displacement + thickness
|
||||||
|
|
||||||
|
|
||||||
|
def read_scene(path):
|
||||||
|
triangles = []
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit(f"{path}: invalid scene header")
|
||||||
|
version, voxel_um, count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.read(40)
|
||||||
|
if version != 1 or count > 100000:
|
||||||
|
raise SystemExit("unsupported scene")
|
||||||
|
for _ in range(count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
for index in range(0, index_count, 3):
|
||||||
|
if masks and masks[index // 3]:
|
||||||
|
continue
|
||||||
|
a, b, c = indices[index:index + 3]
|
||||||
|
if max(a, b, c) >= surface_vertices:
|
||||||
|
continue
|
||||||
|
nz = (normals[a * 3 + 2] + normals[b * 3 + 2]
|
||||||
|
+ normals[c * 3 + 2]) / 3.0
|
||||||
|
if nz < 0.35:
|
||||||
|
continue
|
||||||
|
vertices = tuple((positions[v * 3], positions[v * 3 + 1],
|
||||||
|
positions[v * 3 + 2]) for v in (a, b, c))
|
||||||
|
triangles.append(vertices)
|
||||||
|
return voxel_um, triangles
|
||||||
|
|
||||||
|
|
||||||
|
def rasterize(triangles, bounds, width, height):
|
||||||
|
x0, x1, y0, y1 = bounds
|
||||||
|
values = [None] * (width * height)
|
||||||
|
sx, sy = width / (x1 - x0), height / (y1 - y0)
|
||||||
|
for tri in triangles:
|
||||||
|
(ax, ay, az), (bx, by, bz), (cx, cy, cz) = tri
|
||||||
|
den = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
|
||||||
|
if abs(den) < 1.0e-12:
|
||||||
|
continue
|
||||||
|
left = max(0, int(math.floor((min(ax, bx, cx) - x0) * sx)))
|
||||||
|
right = min(width - 1, int(math.ceil((max(ax, bx, cx) - x0) * sx)))
|
||||||
|
top = max(0, int(math.floor((y1 - max(ay, by, cy)) * sy)))
|
||||||
|
bottom = min(height - 1, int(math.ceil((y1 - min(ay, by, cy)) * sy)))
|
||||||
|
for py in range(top, bottom + 1):
|
||||||
|
y = y1 - (py + 0.5) / sy
|
||||||
|
row = py * width
|
||||||
|
for px in range(left, right + 1):
|
||||||
|
x = x0 + (px + 0.5) / sx
|
||||||
|
u = ((by - cy) * (x - cx) + (cx - bx) * (y - cy)) / den
|
||||||
|
v = ((cy - ay) * (x - cx) + (ax - cx) * (y - cy)) / den
|
||||||
|
w = 1.0 - u - v
|
||||||
|
if min(u, v, w) < -1.0e-6:
|
||||||
|
continue
|
||||||
|
z = u * az + v * bz + w * cz
|
||||||
|
idx = row + px
|
||||||
|
if values[idx] is None or z > values[idx]:
|
||||||
|
values[idx] = z
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("output_ppm")
|
||||||
|
parser.add_argument("--bounds", nargs=4, type=float, required=True,
|
||||||
|
metavar=("XMIN", "XMAX", "YMIN", "YMAX"))
|
||||||
|
parser.add_argument("--size", nargs=2, type=int, default=(640, 640))
|
||||||
|
parser.add_argument("--range", nargs=2, type=float, default=(-122.0, -118.0),
|
||||||
|
metavar=("ZMIN", "ZMAX"))
|
||||||
|
parser.add_argument("--ring-center", nargs=2, type=float,
|
||||||
|
metavar=("X", "Y"))
|
||||||
|
parser.add_argument("--layout")
|
||||||
|
args = parser.parse_args()
|
||||||
|
voxel_um, triangles = read_scene(args.scene)
|
||||||
|
width, height = args.size
|
||||||
|
sx, sy = width / (args.bounds[1] - args.bounds[0]), height / (args.bounds[3] - args.bounds[2])
|
||||||
|
values = rasterize(triangles, args.bounds, width, height)
|
||||||
|
finite = sorted(value for value in values if value is not None)
|
||||||
|
if not finite:
|
||||||
|
raise SystemExit("no upward-facing field samples in bounds")
|
||||||
|
low, high = args.range
|
||||||
|
if high <= low:
|
||||||
|
raise SystemExit("height range must increase")
|
||||||
|
stops = ((0.00, (31, 52, 92)), (0.24, (41, 119, 153)),
|
||||||
|
(0.50, (85, 177, 143)), (0.73, (226, 207, 105)),
|
||||||
|
(1.00, (181, 71, 49)))
|
||||||
|
with open(args.output_ppm, "wb") as output:
|
||||||
|
output.write(f"P6\n{width} {height}\n255\n".encode())
|
||||||
|
for value in values:
|
||||||
|
if value is None:
|
||||||
|
output.write(bytes((18, 22, 30)))
|
||||||
|
continue
|
||||||
|
t = max(0.0, min(1.0, (value - low) / (high - low)))
|
||||||
|
for index in range(len(stops) - 1):
|
||||||
|
if t <= stops[index + 1][0]:
|
||||||
|
ta, ca = stops[index]
|
||||||
|
tb, cb = stops[index + 1]
|
||||||
|
blend = (t - ta) / (tb - ta)
|
||||||
|
rgb = tuple(round(ca[c] + blend * (cb[c] - ca[c]))
|
||||||
|
for c in range(3))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rgb = stops[-1][1]
|
||||||
|
output.write(bytes(rgb))
|
||||||
|
def quantile(p):
|
||||||
|
return finite[round((len(finite) - 1) * p)]
|
||||||
|
print(f"[RingFieldRaster] voxel_um={voxel_um} triangles={len(triangles)} "
|
||||||
|
f"pixels={len(finite)}/{len(values)} height_min_m={finite[0]:.6f} "
|
||||||
|
f"height_p05_m={quantile(.05):.6f} height_median_m={quantile(.5):.6f} "
|
||||||
|
f"height_p95_m={quantile(.95):.6f} height_max_m={finite[-1]:.6f} "
|
||||||
|
f"height_range_m={finite[-1]-finite[0]:.6f} "
|
||||||
|
f"map={args.output_ppm}")
|
||||||
|
if args.ring_center:
|
||||||
|
cx, cy = args.ring_center
|
||||||
|
def at_world(x, y):
|
||||||
|
fx = (x - args.bounds[0]) * sx - 0.5
|
||||||
|
fy = (args.bounds[3] - y) * sy - 0.5
|
||||||
|
ix, iy = math.floor(fx), math.floor(fy)
|
||||||
|
tx, ty = fx - ix, fy - iy
|
||||||
|
samples = []
|
||||||
|
for px, py in ((ix, iy), (ix + 1, iy), (ix, iy + 1), (ix + 1, iy + 1)):
|
||||||
|
if not (0 <= px < width and 0 <= py < height):
|
||||||
|
return None
|
||||||
|
value = values[py * width + px]
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
samples.append(value)
|
||||||
|
return ((samples[0] * (1 - tx) + samples[1] * tx) * (1 - ty)
|
||||||
|
+ (samples[2] * (1 - tx) + samples[3] * tx) * ty)
|
||||||
|
print(f"[RingRadialProbe] center_m=({cx:.3f},{cy:.3f}) "
|
||||||
|
"radius_m mean_m angular_sd_m min_m max_m samples")
|
||||||
|
for radius in range(1, 17):
|
||||||
|
row = [at_world(cx + radius * math.cos(a * math.tau / 32),
|
||||||
|
cy + radius * math.sin(a * math.tau / 32))
|
||||||
|
for a in range(32)]
|
||||||
|
row = [value for value in row if value is not None]
|
||||||
|
if row:
|
||||||
|
mean = sum(row) / len(row)
|
||||||
|
sd = math.sqrt(sum((value - mean) ** 2 for value in row) / len(row))
|
||||||
|
print(f" {radius:>2} {mean:.6f} {sd:.6f} {min(row):.6f} "
|
||||||
|
f"{max(row):.6f} {len(row)}")
|
||||||
|
if args.layout:
|
||||||
|
layout = json.load(open(args.layout, encoding="utf-8"))
|
||||||
|
if next(item for item in layout["strates"] if item["order"] == 0)["transition_mode"] != "Gradient":
|
||||||
|
raise SystemExit("--layout must describe a Gradient owner interface")
|
||||||
|
errors = []
|
||||||
|
for index, actual in enumerate(values):
|
||||||
|
if actual is None:
|
||||||
|
continue
|
||||||
|
px, py = index % width, index // width
|
||||||
|
x = args.bounds[0] + (px + 0.5) / sx
|
||||||
|
y = args.bounds[3] - (py + 0.5) / sy
|
||||||
|
expected = gradient_height_um(layout, round(x * 1_000_000),
|
||||||
|
round(y * 1_000_000)) / 1_000_000.0
|
||||||
|
errors.append(actual - expected)
|
||||||
|
absolute = sorted(abs(error) for error in errors)
|
||||||
|
rms = math.sqrt(sum(error * error for error in errors) / len(errors))
|
||||||
|
print(f"[RingGradientMatch] samples={len(errors)} rms_m={rms:.6f} "
|
||||||
|
f"p95_abs_m={absolute[round((len(absolute)-1)*.95)]:.6f} "
|
||||||
|
f"max_abs_m={absolute[-1]:.6f} "
|
||||||
|
"expected=owner-resolved-2d-hash-value-noise")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#include "CoreNamespacePreamble.h"
|
||||||
|
#include "ResolvedLayoutInternal.h"
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
VoxelForgeCore::ResolvedLayoutStratumV2 upper, lower;
|
||||||
|
upper.strateIndex=0;upper.order=0;upper.transitionMode="Gradient";
|
||||||
|
upper.transitionWidthMicrometres=16000000;
|
||||||
|
upper.bedrock.bottomMicrometres=-120000000;
|
||||||
|
upper.bedrock.topMicrometres=-119500000;
|
||||||
|
upper.bedrock.presentInCurrentField=true;
|
||||||
|
lower.strateIndex=1;lower.order=1;
|
||||||
|
lower.ceiling.bottomMicrometres=-126250000;
|
||||||
|
lower.ceiling.topMicrometres=-120000000;
|
||||||
|
lower.ceiling.presentInCurrentField=true;
|
||||||
|
for(const auto& point:std::array<std::array<std::int64_t,2>,5>{
|
||||||
|
std::array<std::int64_t,2>{64000000,-32000000},
|
||||||
|
{65000000,-32000000},{64000000,-31000000},{70000000,-32000000},
|
||||||
|
{80000000,-35000000}})
|
||||||
|
{
|
||||||
|
const auto bands=VoxelForgeCore::EvaluateResolvedLayoutTransitionBandsV1(
|
||||||
|
upper,lower,0,8000000,point[0],point[1]);
|
||||||
|
std::printf("x_m=%.3f y_m=%.3f top_m=%.6f bottom_m=%.6f\n",
|
||||||
|
point[0]/1000000.0,point[1]/1000000.0,
|
||||||
|
bands.upperBedrockTopMicrometres/1000000.0,
|
||||||
|
bands.upperBedrockBottomMicrometres/1000000.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# VoxelForge L30
|
||||||
|
|
||||||
|
Work is on `assembler/world`. The speed and look patches are saved separately under
|
||||||
|
`Saved/Linux/l30/`; evaluator version is now 16 for the field change. No Unreal files were edited.
|
||||||
|
Nothing was committed, pushed, or stashed.
|
||||||
|
|
||||||
|
## Ring measurement and fix
|
||||||
|
|
||||||
|
I rasterized upward-facing triangles from the L29 mesh (skirt triangles excluded) over the same
|
||||||
|
409,600 pixels as the after field. At `(64, -32)`, the L29 field stayed almost flat at `-120.063 m` from
|
||||||
|
radii 1–11 m, then broke at 12 m. The expected Gradient at radius 1 m is `-121.360 m`. This is a
|
||||||
|
field defect, not shading: the probe is about 67 m from the passage mouth, and the raster has no
|
||||||
|
skirts or readable-shading tint. The passage column flare is at that distant mouth as well.
|
||||||
|
|
||||||
|
The upper FloatingIslands generator could leave native solid density in the air-side gap above a
|
||||||
|
negatively displaced Gradient face. The constant band densities also snapped the face to Core
|
||||||
|
voxel levels. The owner Gradient path now clears that gap with positive signed distance and writes
|
||||||
|
negative signed distance below the face, allowing the mesh to interpolate its height. The after
|
||||||
|
profile follows the resolved hash surface at every sampled radius. Match error fell from RMS
|
||||||
|
`0.361765 m`, p95 `1.016178 m`, max `1.304169 m` to RMS `0.110357 m`, p95 `0.218531 m`, max
|
||||||
|
`0.248818 m` (one Core voxel is `0.25 m`).
|
||||||
|
|
||||||
|
Before/after captures use the same color ramp and bounds:
|
||||||
|
|
||||||
|
- [L29 height map](captures/fi-floor-gradient-before-heightmap.jpg), [fixed height map](captures/fi-floor-gradient-after-heightmap.jpg)
|
||||||
|
- [L29 readable render](captures/fi-floor-gradient-before.jpg), [fixed readable render](captures/fi-floor-gradient-after.jpg)
|
||||||
|
|
||||||
|
## Transition captures
|
||||||
|
|
||||||
|
The maps cover the full 640×640 surface and are all below 300 KB. Gradient is the owner world
|
||||||
|
without a mode override. Hard and Interleaved use fixture overrides; the resolved layouts record
|
||||||
|
those modes.
|
||||||
|
|
||||||
|
- [Hard](captures/transition-hard-heightmap.jpg) — flat interface.
|
||||||
|
- [Gradient](captures/transition-gradient-heightmap.jpg) — owner field.
|
||||||
|
- [Interleaved](captures/transition-interleaved-heightmap.jpg) — fixture override.
|
||||||
|
|
||||||
|
## Cost
|
||||||
|
|
||||||
|
Five alternating W12 pairs, hashing off and `cache_hits=0`. First table is the separable speed-only
|
||||||
|
build, whose hashes remain L29-identical. Second table is the final version-16 build.
|
||||||
|
|
||||||
|
| Recipe | L24 | L29 | L30 speed-only | vs L24 | L30 final | vs L24 |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| SurfaceWorld | 1.566 s | 1.653 s | 1.606 s | +2.55% | 1.530 s | +2.68% |
|
||||||
|
| FloatingIslands | 16.625 s | 16.779 s | 16.470 s | −0.93% | 16.543 s | +0.21% |
|
||||||
|
|
||||||
|
Counter-enabled runs on the same plain W12 workload locate the saved work:
|
||||||
|
|
||||||
|
| Recipe | Boundary calls | Window checks → evaluations | Fade calls | Boundary tile / point culls | Column Z tests → guards → radial rejects | SDF calls |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| SurfaceWorld | 19.95 M | 3.54 M → 739 k | 8.2 k | 8.99 M / 8.96 M | 1.87 M → 1.85 M → 1.58 M | 18.4 k |
|
||||||
|
| FloatingIslands | 66.33 M | 8.00 M → 2.68 M | 8.6 k | 38.25 M / 23.63 M | 8.17 M → 8.14 M → 6.82 M | 83.4 k |
|
||||||
|
|
||||||
|
The fade-tile cull skips 731 k SurfaceWorld and 2.67 M FloatingIslands fade evaluations; the
|
||||||
|
column guard exits on nonpositive field values 272 k and 1.27 M times. Timing comes from
|
||||||
|
uninstrumented binaries; the counts above use counter logging.
|
||||||
|
|
||||||
|
## Hashes and audits
|
||||||
|
|
||||||
|
The speed-only v15 build matches all 12 L29 hash records for each recipe/compiler/worker matrix
|
||||||
|
(W1/W8/W12), and all 12 records for each 24 m crossing matrix. That is 24 exact comparisons;
|
||||||
|
all pass. The final v16 build is deterministic across both compilers and all worker counts, in the
|
||||||
|
normal and 24 m crossing runs. Representative W12 hashes (`density / mesh / payload / topology`):
|
||||||
|
|
||||||
|
| Recipe | Full detail | Whole view |
|
||||||
|
|---|---|---|
|
||||||
|
| SurfaceWorld | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `0e8c5af01df96b14 / 70099e89b77811ed / dbce0fdcf6764126 / ddaf81a1228f9b4b` |
|
||||||
|
| FloatingIslands | `ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `4ee19ea6b6406ca6 / be648f94c899b47e / 310b525f31604f98 / 10366eb6dfce2384` |
|
||||||
|
|
||||||
|
Exact-query hashes also match across v16 runs: SurfaceWorld
|
||||||
|
`c949c4df2ce6f9d9 / 114c501e2d830d94 / e6011d3a248d4849`; FloatingIslands
|
||||||
|
`7b7c50ac24ccadcc / efcba20c0c70c415 / e604823a249029bf`.
|
||||||
|
|
||||||
|
- Interface audit: all 12 compiler/recipe/mode combinations pass; 4,221 columns each, zero holes
|
||||||
|
and air samples, both negative controls reject.
|
||||||
|
- The 16 L29 fixture-matrix replays pass passage safety/open-air, validator controls (10/10), L13
|
||||||
|
route parity, L9 proof checks (zero violations), and walkability. `FloorProfileBlend` still
|
||||||
|
reports 2.544 vox in style 1 and 3.645 vox in style 3, unchanged from L29.
|
||||||
|
- Audit scope caveat: the L29 files named `floating-islands` were run with
|
||||||
|
`--recipe=surface-world`. With the actual `--recipe=floating-islands` option, the rock-column
|
||||||
|
audit fails on all four styles on both L29 and L30: connected ends are 1/2, 3/4, 1/2, 3/4.
|
||||||
|
The open-air audit passes. This is inherited, but means the L29 report's “two recipes” matrix did
|
||||||
|
not exercise the FloatingIslands CLI recipe.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- Broad air-gap experiments first lost a SurfaceWorld style-2 support sample. Route/radius masks
|
||||||
|
and support restoration then caused open-air violations. Widening the fade restored that case,
|
||||||
|
but measurement placed the ring far outside the fade, so those variants were discarded. The
|
||||||
|
final owner-only gate and signed-distance face pass the targeted support audit.
|
||||||
|
- Early speed variants missed the target: tile-only `+4.04%`, Z-point `+3.11%`, scalar path
|
||||||
|
`+4.33%` vs L24. The final band and fade-tile culls meet the target.
|
||||||
|
- The first counter-instrumentation compile failed; the counter hook was corrected and rebuilt
|
||||||
|
with Clang and GCC. One duplicated timing sequence was discarded; only the clean five-pair runs
|
||||||
|
are reported.
|
||||||
|
- The first 24 m crossing comparison supplied an extra camera override and mismatched. Rerunning
|
||||||
|
with the L29 camera setup and only `--camera-step=24,0,0` matches all hashes.
|
||||||
|
- An early Hard export hit `std::bad_alloc`; retry passed. The first EGL render used an unwritable
|
||||||
|
cache; retry with `XDG_CACHE_HOME` under `Saved/Linux/l30` passed. The first map conversion also
|
||||||
|
stopped before the Hard/Interleaved maps; both were rerun individually and have full raster
|
||||||
|
coverage.
|
||||||
|
- The first passage-matrix summary script assumed a different marker order and reported failure;
|
||||||
|
checking each audit marker by name confirms the core matrix passes. A `check=True` true-recipe
|
||||||
|
control runner stopped on the expected L29 exit code 10; rerunning while recording return codes
|
||||||
|
confirmed the same four inherited rock-column failures on L29 and L30.
|
||||||
|
|
||||||
|
No build or run exceeded 30 minutes or remained stuck for 15 minutes. Builds and runs used CPUs
|
||||||
|
0–11, at most 12 workers, normal priority, and the 6 GiB address-space cap. No Unreal build was
|
||||||
|
attempted.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged: the ring was a real field defect (air-side gap left solid, face snapped to voxel levels), now fixed
|
||||||
|
(RMS 0.36 → 0.11 m). Cost back to SW +2.7% / FI +0.2% vs L24. The speed part is bit-identical; final v16 is
|
||||||
|
deterministic.
|
||||||
|
- Still visible: `fi-floor-gradient-after.jpg` shows faint concentric lines. Unknown whether that is residual
|
||||||
|
geometry (≤0.25 m steps) or the readable shader's height-tint banding; L31 measures it.
|
||||||
|
- **Important finding (thanks to Luna's caveat):** L29's "both recipes" passage matrix ran SurfaceWorld twice. On
|
||||||
|
the real `--recipe=floating-islands`, the rock-column connected-ends audit fails in all 4 styles (1/2 or 3/4).
|
||||||
|
Since L27, FI column results were never really tested. L31 priority.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Interleave five W12 hash-off whole-view cost triplets for L24/L29/L30."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l30"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"l24": ROOT / "Saved/Linux/l24-source/bin/linux/VoxelForgeCoreHeadless-linux24-clang19",
|
||||||
|
"l29": ROOT / "Saved/Linux/bin/VoxelForgeCoreHeadless-linux29-clang",
|
||||||
|
"l30": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade",
|
||||||
|
}
|
||||||
|
ORDERS = (
|
||||||
|
("l24", "l29", "l30"),
|
||||||
|
("l29", "l30", "l24"),
|
||||||
|
("l30", "l24", "l29"),
|
||||||
|
("l24", "l30", "l29"),
|
||||||
|
("l29", "l24", "l30"),
|
||||||
|
)
|
||||||
|
WHOLE_READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||||
|
rows = []
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for pair, order in enumerate(ORDERS, 1):
|
||||||
|
for build in order:
|
||||||
|
name = f"cost-speedfade-{recipe}-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"prlimit", "--as=6442450944", "--", str(BINARIES[build]),
|
||||||
|
str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
check=True, timeout=1800)
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matches = WHOLE_READY.findall(text)
|
||||||
|
if len(matches) != 1 or "hash=off" not in text or "cache_hits=0" not in text:
|
||||||
|
raise SystemExit(f"invalid cost run: {name}; see {logfile}")
|
||||||
|
ready = float(matches[0])
|
||||||
|
rows.append((recipe, pair, build, ready, elapsed))
|
||||||
|
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.3f}s", flush=True)
|
||||||
|
|
||||||
|
summary = OUT / "cost-runs-speedfade.csv"
|
||||||
|
summary.write_text("recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds\n" +
|
||||||
|
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.6f}\n"
|
||||||
|
for recipe, pair, build, ready, elapsed in rows), encoding="utf-8")
|
||||||
|
print(f"wrote {summary}", flush=True)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify speed-only L30 hashes on the 24 m crossing against L29."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l30"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"clang": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade",
|
||||||
|
"gcc": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade-gcc",
|
||||||
|
}
|
||||||
|
PATTERN = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
passed = True
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
reference_path = ROOT / f"Saved/Linux/linux29-final-crossing24-{compiler}-{recipe}.log"
|
||||||
|
reference = PATTERN.findall(reference_path.read_text(encoding="utf-8"))
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
name = f"crossing-speedonly-default-{compiler}-{recipe}-w{workers}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"prlimit", "--as=6442450944", "--", str(binary),
|
||||||
|
str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", f"--workers={workers}",
|
||||||
|
"--camera-step=24,0,0",
|
||||||
|
"--region-crossing", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
check=True, timeout=1800)
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
rows = PATTERN.findall(logfile.read_text(encoding="utf-8", errors="replace"))
|
||||||
|
ok = len(reference) == 12 and rows == reference
|
||||||
|
passed &= ok
|
||||||
|
print(f"{compiler} {recipe} W{workers} elapsed={elapsed:.3f}s "
|
||||||
|
f"hash_rows={len(rows)} {'MATCH' if ok else 'MISMATCH'}", flush=True)
|
||||||
|
print(f"[CrossingSpeedOnly] runs=12 status={'PASS' if passed else 'FAIL'}", flush=True)
|
||||||
|
raise SystemExit(0 if passed else 1)
|
||||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Combine the initial and repeat L31 interleaved cost batches."""
|
||||||
|
import csv
|
||||||
|
import statistics
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
sources = (
|
||||||
|
("first", 0, OUT / "cost-first/cost-runs.csv"),
|
||||||
|
("repeat", 5, OUT / "cost-runs.csv"),
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
for batch, offset, path in sources:
|
||||||
|
with path.open(newline="", encoding="utf-8") as stream:
|
||||||
|
for row in csv.DictReader(stream):
|
||||||
|
row["pair"] = str(int(row["pair"]) + offset)
|
||||||
|
row["source_batch"] = batch
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
with (OUT / "cost-combined.csv").open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.DictWriter(stream, fieldnames=(
|
||||||
|
"recipe", "pair", "build", "whole_view_ready_seconds", "process_elapsed_seconds",
|
||||||
|
"exit_code", "status", "source_batch"))
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
print("recipe pairs median_l30_s median_l31_s median_change_pct paired_median_pct range_pct")
|
||||||
|
failed = False
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
by_build = {
|
||||||
|
build: [float(row["whole_view_ready_seconds"]) for row in rows
|
||||||
|
if row["recipe"] == recipe and row["build"] == build]
|
||||||
|
for build in ("l30", "l31")
|
||||||
|
}
|
||||||
|
paired = []
|
||||||
|
for pair in range(1, 11):
|
||||||
|
times = {
|
||||||
|
row["build"]: float(row["whole_view_ready_seconds"])
|
||||||
|
for row in rows if row["recipe"] == recipe and int(row["pair"]) == pair
|
||||||
|
}
|
||||||
|
paired.append((times["l31"] / times["l30"] - 1.0) * 100.0)
|
||||||
|
median_l30 = statistics.median(by_build["l30"])
|
||||||
|
median_l31 = statistics.median(by_build["l31"])
|
||||||
|
median_change = (median_l31 / median_l30 - 1.0) * 100.0
|
||||||
|
print(f"{recipe} 10 {median_l30:.3f} {median_l31:.3f} {median_change:+.2f} "
|
||||||
|
f"{statistics.median(paired):+.2f} {min(paired):+.2f}..{max(paired):+.2f}")
|
||||||
|
failed |= len(by_build["l30"]) != 10 or len(by_build["l31"]) != 10
|
||||||
|
print(f"[L31CostCombined] pairs_per_recipe=10 runs={len(rows)} "
|
||||||
|
f"status={'FAIL' if failed else 'PASS'} summary={OUT / 'cost-combined.csv'}")
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 40 else 0)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare L25 and 24 m crossing hash records for matching recipe/compiler/workers."""
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
pattern = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
lines = []
|
||||||
|
failed = False
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
left = pattern.findall((OUT / f"l25-{compiler}-{recipe}-w{workers}.log").read_text())
|
||||||
|
right = pattern.findall((OUT / f"crossing24-{compiler}-{recipe}-w{workers}.log").read_text())
|
||||||
|
same = bool(left) and len(left) == 12 and left == right
|
||||||
|
failed |= not same
|
||||||
|
lines.append(f"{recipe},{compiler},W{workers},{'MATCH' if same else 'MISMATCH'}")
|
||||||
|
|
||||||
|
path = OUT / "determinism-crossing-compare.log"
|
||||||
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
print("\n".join(lines))
|
||||||
|
print(f"[L31CrossingHashCompare] comparisons={len(lines)} "
|
||||||
|
f"pass={sum(line.endswith('MATCH') for line in lines)} fail={sum(line.endswith('MISMATCH') for line in lines)}")
|
||||||
|
raise SystemExit(1 if failed else 0)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Encode the six requested L31 screenshots as small, pinned JPEGs."""
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
SOURCE = ROOT / "Saved/Linux/l31"
|
||||||
|
DEST = Path(__file__).resolve().parent / "captures"
|
||||||
|
DEST.mkdir(parents=True, exist_ok=True)
|
||||||
|
names = (
|
||||||
|
"fi-column-top-close-v19",
|
||||||
|
"fi-column-top-wide-v19",
|
||||||
|
"fi-column-base-close-v19",
|
||||||
|
"fi-column-base-wide-v19",
|
||||||
|
"fi-floor-v19-tint-off",
|
||||||
|
"fi-floor-v19-tint-on",
|
||||||
|
)
|
||||||
|
failed = False
|
||||||
|
for name in names:
|
||||||
|
source = SOURCE / f"{name}.png"
|
||||||
|
destination = DEST / f"{name.removesuffix('-v19').replace('-v19-', '-')}.jpg"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", "ffmpeg",
|
||||||
|
"-y", "-hide_banner", "-loglevel", "error", "-i", str(source),
|
||||||
|
"-frames:v", "1", "-q:v", "4", str(destination),
|
||||||
|
]
|
||||||
|
result = subprocess.run(command, check=False)
|
||||||
|
size = destination.stat().st_size if destination.exists() else 0
|
||||||
|
ok = result.returncode == 0 and size <= 300_000
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[L31CaptureJPEG] source={source.name} output={destination.name} "
|
||||||
|
f"bytes={size} status={'PASS' if ok else 'FAIL'}")
|
||||||
|
raise SystemExit(1 if failed else 0)
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Measure surface heights and normals on an eastward FI floor radial line."""
|
||||||
|
import array
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def read_surface_vertices(path):
|
||||||
|
vertices = []
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit(f"{path}: invalid scene header")
|
||||||
|
version, voxel_um, count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.read(40)
|
||||||
|
if version != 1 or count > 100000:
|
||||||
|
raise SystemExit("unsupported scene")
|
||||||
|
for _ in range(count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
used = set()
|
||||||
|
for tri in range(0, index_count, 3):
|
||||||
|
if masks and masks[tri // 3]:
|
||||||
|
continue
|
||||||
|
for index in indices[tri:tri + 3]:
|
||||||
|
if index < surface_count:
|
||||||
|
used.add(index)
|
||||||
|
for index in used:
|
||||||
|
offset = index * 3
|
||||||
|
vertices.append((positions[offset], positions[offset + 1],
|
||||||
|
positions[offset + 2], normals[offset],
|
||||||
|
normals[offset + 1], normals[offset + 2]))
|
||||||
|
return voxel_um, vertices
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
scene = sys.argv[1]
|
||||||
|
center_x, center_y = 64.0, -32.0
|
||||||
|
voxel_um, vertices = read_surface_vertices(scene)
|
||||||
|
floor = [v for v in vertices if abs(v[2] + 119.5) < 0.05
|
||||||
|
and math.hypot(v[0] - center_x, v[1] - center_y) <= 16.5]
|
||||||
|
if not floor:
|
||||||
|
raise SystemExit("no floor surface vertices near expected FI bedrock top")
|
||||||
|
mean = tuple(sum(v[index] for v in floor) / len(floor) for index in (3, 4, 5))
|
||||||
|
normal_rms = math.sqrt(sum(sum((v[index] - mean[axis]) ** 2
|
||||||
|
for axis, index in enumerate((3, 4, 5)))
|
||||||
|
for v in floor) / len(floor))
|
||||||
|
min_z = min(v[2] for v in floor)
|
||||||
|
max_z = max(v[2] for v in floor)
|
||||||
|
print(f"[RingNormalField] voxel_um={voxel_um} center_m=({center_x:.3f},{center_y:.3f}) "
|
||||||
|
f"vertices={len(floor)} height_min_m={min_z:.6f} height_max_m={max_z:.6f} "
|
||||||
|
f"mean_normal=({mean[0]:.8f},{mean[1]:.8f},{mean[2]:.8f}) "
|
||||||
|
f"normal_rms={normal_rms:.9f}")
|
||||||
|
print("[RingNormalRadialLine] radius_m sampled_radius_m height_m nx ny nz")
|
||||||
|
for radius in range(1, 17):
|
||||||
|
x, y = center_x + radius, center_y
|
||||||
|
vertex = min(floor, key=lambda v: (v[0] - x) ** 2 + (v[1] - y) ** 2)
|
||||||
|
sampled_radius = math.hypot(vertex[0] - center_x, vertex[1] - center_y)
|
||||||
|
if math.hypot(vertex[0] - x, vertex[1] - y) > 0.5:
|
||||||
|
raise SystemExit(f"missing near vertex at radius {radius} m")
|
||||||
|
print(f"{radius:2d} {sampled_radius:.6f} {vertex[2]:.6f} "
|
||||||
|
f"{vertex[3]:.8f} {vertex[4]:.8f} {vertex[5]:.8f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rasterize upward mesh triangles into a top-down field height map."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
Q16 = 1 << 16
|
||||||
|
MASK64 = (1 << 64) - 1
|
||||||
|
|
||||||
|
|
||||||
|
def trunc_div(numerator, denominator):
|
||||||
|
return numerator // denominator if numerator >= 0 else -((-numerator) // denominator)
|
||||||
|
|
||||||
|
|
||||||
|
def transition_hash(seed, interface, x, y, z, salt):
|
||||||
|
value = 14695981039346656037
|
||||||
|
for word in (seed & 0xffffffff, interface & 0xffffffff,
|
||||||
|
x & MASK64, y & MASK64, z & MASK64, salt):
|
||||||
|
for byte in range(8):
|
||||||
|
value ^= (word >> (byte * 8)) & 0xff
|
||||||
|
value = (value * 1099511628211) & MASK64
|
||||||
|
value ^= value >> 30
|
||||||
|
value = (value * 0xbf58476d1ce4e5b9) & MASK64
|
||||||
|
value ^= value >> 27
|
||||||
|
value = (value * 0x94d049bb133111eb) & MASK64
|
||||||
|
value ^= value >> 31
|
||||||
|
return ((value >> 32) >> 15) - Q16
|
||||||
|
|
||||||
|
|
||||||
|
def transition_fade(value):
|
||||||
|
value = max(0, min(Q16, value))
|
||||||
|
square = value * value // Q16
|
||||||
|
return square * (3 * Q16 - 2 * value) // Q16
|
||||||
|
|
||||||
|
|
||||||
|
def gradient_height_um(layout, x_um, y_um):
|
||||||
|
upper = next(item for item in layout["strates"] if item["order"] == 0)
|
||||||
|
lower = next(item for item in layout["strates"] if item["order"] == 1)
|
||||||
|
width = upper["transition_width_um"]
|
||||||
|
cell = max(1_000_000, layout["strate_layout_chunk_size_um"] * 4)
|
||||||
|
ix = x_um // cell
|
||||||
|
iy = y_um // cell
|
||||||
|
tx = transition_fade(((x_um - ix * cell) * Q16) // cell)
|
||||||
|
ty = transition_fade(((y_um - iy * cell) * Q16) // cell)
|
||||||
|
|
||||||
|
def at(dx, dy):
|
||||||
|
noise = transition_hash(0, upper["strate_index"], ix + dx, iy + dy, 0,
|
||||||
|
0x47524144)
|
||||||
|
return noise
|
||||||
|
|
||||||
|
def lerp(a, b, t):
|
||||||
|
return a + trunc_div((b - a) * t, Q16)
|
||||||
|
|
||||||
|
low = lerp(lerp(at(0, 0), at(1, 0), tx),
|
||||||
|
lerp(at(0, 1), at(1, 1), tx), ty)
|
||||||
|
amplitude_divisor = 256 if upper.get("generator_kind") == "FloatingIslands" else 8
|
||||||
|
displacement = trunc_div(low * (width // amplitude_divisor), Q16)
|
||||||
|
seam = upper["bedrock"]["bottom_um"]
|
||||||
|
thickness = upper["bedrock"]["top_um"] - seam
|
||||||
|
return seam + displacement + thickness
|
||||||
|
|
||||||
|
|
||||||
|
def read_scene(path):
|
||||||
|
triangles = []
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit(f"{path}: invalid scene header")
|
||||||
|
version, voxel_um, count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.read(40)
|
||||||
|
if version != 1 or count > 100000:
|
||||||
|
raise SystemExit("unsupported scene")
|
||||||
|
for _ in range(count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
for index in range(0, index_count, 3):
|
||||||
|
if masks and masks[index // 3]:
|
||||||
|
continue
|
||||||
|
a, b, c = indices[index:index + 3]
|
||||||
|
if max(a, b, c) >= surface_vertices:
|
||||||
|
continue
|
||||||
|
nz = (normals[a * 3 + 2] + normals[b * 3 + 2]
|
||||||
|
+ normals[c * 3 + 2]) / 3.0
|
||||||
|
if nz < 0.35:
|
||||||
|
continue
|
||||||
|
vertices = tuple((positions[v * 3], positions[v * 3 + 1],
|
||||||
|
positions[v * 3 + 2]) for v in (a, b, c))
|
||||||
|
triangles.append(vertices)
|
||||||
|
return voxel_um, triangles
|
||||||
|
|
||||||
|
|
||||||
|
def rasterize(triangles, bounds, width, height):
|
||||||
|
x0, x1, y0, y1 = bounds
|
||||||
|
values = [None] * (width * height)
|
||||||
|
sx, sy = width / (x1 - x0), height / (y1 - y0)
|
||||||
|
for tri in triangles:
|
||||||
|
(ax, ay, az), (bx, by, bz), (cx, cy, cz) = tri
|
||||||
|
den = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
|
||||||
|
if abs(den) < 1.0e-12:
|
||||||
|
continue
|
||||||
|
left = max(0, int(math.floor((min(ax, bx, cx) - x0) * sx)))
|
||||||
|
right = min(width - 1, int(math.ceil((max(ax, bx, cx) - x0) * sx)))
|
||||||
|
top = max(0, int(math.floor((y1 - max(ay, by, cy)) * sy)))
|
||||||
|
bottom = min(height - 1, int(math.ceil((y1 - min(ay, by, cy)) * sy)))
|
||||||
|
for py in range(top, bottom + 1):
|
||||||
|
y = y1 - (py + 0.5) / sy
|
||||||
|
row = py * width
|
||||||
|
for px in range(left, right + 1):
|
||||||
|
x = x0 + (px + 0.5) / sx
|
||||||
|
u = ((by - cy) * (x - cx) + (cx - bx) * (y - cy)) / den
|
||||||
|
v = ((cy - ay) * (x - cx) + (ax - cx) * (y - cy)) / den
|
||||||
|
w = 1.0 - u - v
|
||||||
|
if min(u, v, w) < -1.0e-6:
|
||||||
|
continue
|
||||||
|
z = u * az + v * bz + w * cz
|
||||||
|
idx = row + px
|
||||||
|
if values[idx] is None or z > values[idx]:
|
||||||
|
values[idx] = z
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("output_ppm")
|
||||||
|
parser.add_argument("--bounds", nargs=4, type=float, required=True,
|
||||||
|
metavar=("XMIN", "XMAX", "YMIN", "YMAX"))
|
||||||
|
parser.add_argument("--size", nargs=2, type=int, default=(640, 640))
|
||||||
|
parser.add_argument("--range", nargs=2, type=float, default=(-122.0, -118.0),
|
||||||
|
metavar=("ZMIN", "ZMAX"))
|
||||||
|
parser.add_argument("--ring-center", nargs=2, type=float,
|
||||||
|
metavar=("X", "Y"))
|
||||||
|
parser.add_argument("--probe-max-radius", type=int, default=16)
|
||||||
|
parser.add_argument("--layout")
|
||||||
|
args = parser.parse_args()
|
||||||
|
voxel_um, triangles = read_scene(args.scene)
|
||||||
|
width, height = args.size
|
||||||
|
sx, sy = width / (args.bounds[1] - args.bounds[0]), height / (args.bounds[3] - args.bounds[2])
|
||||||
|
values = rasterize(triangles, args.bounds, width, height)
|
||||||
|
finite = sorted(value for value in values if value is not None)
|
||||||
|
if not finite:
|
||||||
|
raise SystemExit("no upward-facing field samples in bounds")
|
||||||
|
low, high = args.range
|
||||||
|
if high <= low:
|
||||||
|
raise SystemExit("height range must increase")
|
||||||
|
stops = ((0.00, (31, 52, 92)), (0.24, (41, 119, 153)),
|
||||||
|
(0.50, (85, 177, 143)), (0.73, (226, 207, 105)),
|
||||||
|
(1.00, (181, 71, 49)))
|
||||||
|
with open(args.output_ppm, "wb") as output:
|
||||||
|
output.write(f"P6\n{width} {height}\n255\n".encode())
|
||||||
|
for value in values:
|
||||||
|
if value is None:
|
||||||
|
output.write(bytes((18, 22, 30)))
|
||||||
|
continue
|
||||||
|
t = max(0.0, min(1.0, (value - low) / (high - low)))
|
||||||
|
for index in range(len(stops) - 1):
|
||||||
|
if t <= stops[index + 1][0]:
|
||||||
|
ta, ca = stops[index]
|
||||||
|
tb, cb = stops[index + 1]
|
||||||
|
blend = (t - ta) / (tb - ta)
|
||||||
|
rgb = tuple(round(ca[c] + blend * (cb[c] - ca[c]))
|
||||||
|
for c in range(3))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rgb = stops[-1][1]
|
||||||
|
output.write(bytes(rgb))
|
||||||
|
def quantile(p):
|
||||||
|
return finite[round((len(finite) - 1) * p)]
|
||||||
|
print(f"[RingFieldRaster] voxel_um={voxel_um} triangles={len(triangles)} "
|
||||||
|
f"pixels={len(finite)}/{len(values)} height_min_m={finite[0]:.6f} "
|
||||||
|
f"height_p05_m={quantile(.05):.6f} height_median_m={quantile(.5):.6f} "
|
||||||
|
f"height_p95_m={quantile(.95):.6f} height_max_m={finite[-1]:.6f} "
|
||||||
|
f"height_range_m={finite[-1]-finite[0]:.6f} "
|
||||||
|
f"map={args.output_ppm}")
|
||||||
|
if args.ring_center:
|
||||||
|
cx, cy = args.ring_center
|
||||||
|
def at_world(x, y):
|
||||||
|
fx = (x - args.bounds[0]) * sx - 0.5
|
||||||
|
fy = (args.bounds[3] - y) * sy - 0.5
|
||||||
|
ix, iy = math.floor(fx), math.floor(fy)
|
||||||
|
tx, ty = fx - ix, fy - iy
|
||||||
|
samples = []
|
||||||
|
for px, py in ((ix, iy), (ix + 1, iy), (ix, iy + 1), (ix + 1, iy + 1)):
|
||||||
|
if not (0 <= px < width and 0 <= py < height):
|
||||||
|
return None
|
||||||
|
value = values[py * width + px]
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
samples.append(value)
|
||||||
|
return ((samples[0] * (1 - tx) + samples[1] * tx) * (1 - ty)
|
||||||
|
+ (samples[2] * (1 - tx) + samples[3] * tx) * ty)
|
||||||
|
print(f"[RingRadialProbe] center_m=({cx:.3f},{cy:.3f}) "
|
||||||
|
"radius_m mean_m angular_sd_m min_m max_m samples")
|
||||||
|
for radius in range(1, args.probe_max_radius + 1):
|
||||||
|
row = [at_world(cx + radius * math.cos(a * math.tau / 32),
|
||||||
|
cy + radius * math.sin(a * math.tau / 32))
|
||||||
|
for a in range(32)]
|
||||||
|
row = [value for value in row if value is not None]
|
||||||
|
if row:
|
||||||
|
mean = sum(row) / len(row)
|
||||||
|
sd = math.sqrt(sum((value - mean) ** 2 for value in row) / len(row))
|
||||||
|
print(f" {radius:>2} {mean:.6f} {sd:.6f} {min(row):.6f} "
|
||||||
|
f"{max(row):.6f} {len(row)}")
|
||||||
|
if args.layout:
|
||||||
|
layout = json.load(open(args.layout, encoding="utf-8"))
|
||||||
|
if next(item for item in layout["strates"] if item["order"] == 0)["transition_mode"] != "Gradient":
|
||||||
|
raise SystemExit("--layout must describe a Gradient owner interface")
|
||||||
|
errors = []
|
||||||
|
for index, actual in enumerate(values):
|
||||||
|
if actual is None:
|
||||||
|
continue
|
||||||
|
px, py = index % width, index // width
|
||||||
|
x = args.bounds[0] + (px + 0.5) / sx
|
||||||
|
y = args.bounds[3] - (py + 0.5) / sy
|
||||||
|
expected = gradient_height_um(layout, round(x * 1_000_000),
|
||||||
|
round(y * 1_000_000)) / 1_000_000.0
|
||||||
|
errors.append(actual - expected)
|
||||||
|
absolute = sorted(abs(error) for error in errors)
|
||||||
|
rms = math.sqrt(sum(error * error for error in errors) / len(errors))
|
||||||
|
print(f"[RingGradientMatch] samples={len(errors)} rms_m={rms:.6f} "
|
||||||
|
f"p95_abs_m={absolute[round((len(absolute)-1)*.95)]:.6f} "
|
||||||
|
f"max_abs_m={absolute[-1]:.6f} "
|
||||||
|
"expected=owner-resolved-2d-hash-value-noise")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Linux31 report
|
||||||
|
|
||||||
|
## FI column finding
|
||||||
|
|
||||||
|
The earlier L29/L30 passage table ran SurfaceWorld twice. On the real FloatingIslands recipe, the old column check under-counted connected ends because it treated support at the route foot as the column cap. The route foot is near z = −116 m; the actual lower cap is near z = −122 m, inside native bedrock whose top is z = −119.5 m. The upper caps sit inside anchor-island rock near z = −62 to −69 m. There is no floating column end or unintended gap. The open passage mouth is intentional.
|
||||||
|
|
||||||
|
The audit now samples each planned cap against the native density field. Negative density is solid rock. Every main column cap overlaps native rock within 0.25 m. The style 3 short lower cap overlaps within 0.50 m.
|
||||||
|
|
||||||
|
| FI style | Cap | Z (m) | Native overlap / samples | Minimum native density |
|
||||||
|
|---:|---|---:|---:|---:|
|
||||||
|
| 0 | Main upper / anchor island | −65.846 | 5,051 / 10,005 | −14.761719 |
|
||||||
|
| 0 | Main lower / bedrock | −122.390 | 6,149 / 12,588 | −8.000000 |
|
||||||
|
| 1 | Short upper | −61.574 | 9,725 / 11,033 | −23.464844 |
|
||||||
|
| 1 | Short lower | −75.637 | 3,915 / 11,500 | −15.554688 |
|
||||||
|
| 1 | Main upper / anchor island | −66.485 | 4,497 / 11,118 | −15.613281 |
|
||||||
|
| 1 | Main lower / bedrock | −122.416 | 6,673 / 13,735 | −8.000000 |
|
||||||
|
| 2 | Main upper / anchor island | −68.891 | 3,986 / 8,523 | −15.546875 |
|
||||||
|
| 2 | Main lower / bedrock | −122.382 | 5,396 / 10,885 | −8.000000 |
|
||||||
|
| 3 | Short upper | −65.805 | 5,095 / 10,064 | −14.671875 |
|
||||||
|
| 3 | Short lower | −80.054 | 2,498 / 10,413 | −11.910156 |
|
||||||
|
| 3 | Main upper / anchor island | −69.478 | 3,902 / 9,985 | −15.597656 |
|
||||||
|
| 3 | Main lower / bedrock | −122.436 | 6,130 / 12,453 | −8.000000 |
|
||||||
|
|
||||||
|
The negative control moves the style 0 lower cap into the air gap at z = −96.390 m: 0/3,120 native overlaps, minimum density +22.273438, and `detected=true`. The audit still catches a genuinely disconnected end.
|
||||||
|
|
||||||
|
## Recipe-stamped passage matrix
|
||||||
|
|
||||||
|
Each row is checked against the requested and resolved recipe stamp. The columns are capsule blocked/unsupported counts and mouth checks, open-air violations, naturalness, longest uncolumned span / 12.5 m limit, connected column ends, route bindings / failures, and walkability samples.
|
||||||
|
|
||||||
|
For all eight FI rows, `FloatingIslandRouteAudit` also passes: 78,064 capsule samples, 0 blocked, 0 unsupported, and 0 grade failures. The runner now asserts both capsule audits.
|
||||||
|
|
||||||
|
| Requested → actual | Style | Compiler | Capsule | Open air | Natural | Long bridge | Column ends | Bindings | Walkability |
|
||||||
|
|---|---:|---|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| surface-world → surface-world | 0 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 5642/5642 |
|
||||||
|
| surface-world → surface-world | 0 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 5642/5642 |
|
||||||
|
| surface-world → surface-world | 1 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1490/1490 |
|
||||||
|
| surface-world → surface-world | 1 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1490/1490 |
|
||||||
|
| surface-world → surface-world | 2 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1545/1545 |
|
||||||
|
| surface-world → surface-world | 2 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1545/1545 |
|
||||||
|
| surface-world → surface-world | 3 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1495/1495 |
|
||||||
|
| surface-world → surface-world | 3 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1495/1495 |
|
||||||
|
| floating-islands → floating-islands | 0 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.464/12.500 m | 2/2 | 2 (0 failures) | 4000/4000 |
|
||||||
|
| floating-islands → floating-islands | 0 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.464/12.500 m | 2/2 | 2 (0 failures) | 4000/4000 |
|
||||||
|
| floating-islands → floating-islands | 1 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 3.851/12.500 m | 4/4 | 2 (0 failures) | 1047/1047 |
|
||||||
|
| floating-islands → floating-islands | 1 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 3.851/12.500 m | 4/4 | 2 (0 failures) | 1047/1047 |
|
||||||
|
| floating-islands → floating-islands | 2 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 2 (0 failures) | 1135/1135 |
|
||||||
|
| floating-islands → floating-islands | 2 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 2 (0 failures) | 1135/1135 |
|
||||||
|
| floating-islands → floating-islands | 3 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.243/12.500 m | 4/4 | 2 (0 failures) | 1061/1061 |
|
||||||
|
| floating-islands → floating-islands | 3 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.243/12.500 m | 4/4 | 2 (0 failures) | 1061/1061 |
|
||||||
|
|
||||||
|
All 16 rows pass. The runner rejects a recipe mismatch. Its negative control requests SurfaceWorld while the resolved layout is FloatingIslands; the log records `requested=surface-world actual=floating-islands status=FAIL` and the process exits 2. Logs and the machine-readable matrix are under `Saved/Linux/l31/`.
|
||||||
|
|
||||||
|
## FI floor lines
|
||||||
|
|
||||||
|
The L30 rings persisted with height tint off, so they were geometry. Evaluator v19 reduces FloatingIslands Gradient displacement to `transition_width / 256` (one quarter voxel at the 16 m transition width), and the layout validator uses the same bound. The v18 one-voxel cap still left a 0.251819 m radial height range and a visible contour.
|
||||||
|
|
||||||
|
On the east radial line from (64, −32), samples at radii 1–16 m all have height −119.500000 m and normal (0, 0, 1). The extended mesh raster has a 0.000479 m total height range. The readable shader now applies a cubic smooth tint (`t²(3−2t)`). Tint-off and tint-on captures show a flat floor without contour bands.
|
||||||
|
|
||||||
|
## Captures and hashes
|
||||||
|
|
||||||
|
All six JPEGs are recipe-stamped from FloatingIslands and under 300 KB.
|
||||||
|
|
||||||
|
| Capture | Size | SHA-256 |
|
||||||
|
|---|---:|---|
|
||||||
|
| [FI column top, close](captures/fi-column-top-close.jpg) | 85,696 B | `6b651655fc2127bb5b3502a1a99c024167db2edc21bfe7241f6335a858bb8575` |
|
||||||
|
| [FI column top, wide](captures/fi-column-top-wide.jpg) | 67,043 B | `926193affd463492095b97310c62416910e12fc94e75d8548a01ad1f9601b6ec` |
|
||||||
|
| [FI column bedrock base, close](captures/fi-column-base-close.jpg) | 60,491 B | `3e524e0f6c506413b4b5b29d8835792f8b5240f95787c6a08d12ebe6e46aadd5` |
|
||||||
|
| [FI column bedrock base, wide](captures/fi-column-base-wide.jpg) | 61,543 B | `27ef1a72874c7f8e257969c5e69d614f336edce84693ee80b98783b9f610a4b2` |
|
||||||
|
| [FI floor, tint off](captures/fi-floor-tint-off.jpg) | 43,405 B | `41b9180c9696c7025e23009e6efdb2e86ace5900b08c293c92371797f9a8596d` |
|
||||||
|
| [FI floor, tint on](captures/fi-floor-tint-on.jpg) | 43,405 B | `0672e2ddc93fb6a4fe4f6adfdf4cdee896bdbc90dddfb7cf65050d978f86f32b` |
|
||||||
|
|
||||||
|
Evaluator version is 19. Binary SHA-256:
|
||||||
|
|
||||||
|
- Clang: `a796d433b7e31fa05abba970dbd486db1a904d13222d9c02b9054c394375e4f7`
|
||||||
|
- GCC: `c2ed12ddf4db5ce97fcd2d9c8f32e4515a4fba6d36d4d8a4b5fbb07a8ab8ae73`
|
||||||
|
|
||||||
|
## Determinism and cost
|
||||||
|
|
||||||
|
The L25 command and 24 m crossing each passed 12 cases: Clang and GCC at W1/W8/W12 for both recipes. All 12 hash records per log were stable across compilers and worker counts. SW matches the L30 baseline; FI changes as expected with the v19 geometry. L25 and crossing hashes match for all 12 recipe/compiler/worker combinations.
|
||||||
|
|
||||||
|
| Recipe | Full-detail density / mesh / payload / topology | Whole-view density / mesh / payload / topology |
|
||||||
|
|---|---|---|
|
||||||
|
| SurfaceWorld | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `0e8c5af01df96b14 / 70099e89b77811ed / dbce0fdcf6764126 / ddaf81a1228f9b4b` |
|
||||||
|
| FloatingIslands | `ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `52e0b9f95da4291e / 7c6e9e18346278a4 / 59160fe3df02a0ae / ef7f17088fe3253e` |
|
||||||
|
|
||||||
|
Cost compared the Clang L31 binary with the L30 `linux30-speed-fade` binary: 10 interleaved pairs per recipe, 40 runs total, with zero cache hits. Combined median whole-view time was 1.772 s → 1.784 s for SW (+0.71%) and 17.468 s → 17.482 s for FI (+0.08%). The second five-pair batch was +1.20% SW and +0.01% FI; the larger early SW deltas settled as runs warmed. The paired medians across all 10 were +1.86% SW and +0.55% FI, within the observed run variation.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- Five low-clip floor exports (`export-fi-floor.log`, `export-fi-floor-probe.log`, `export-fi-floor-without-mesh.log`, `export-fi-floor-probe-retry.log`, `fi-floor-no-camera-probe.log`) stopped with `std::bad_alloc`. Export succeeded with `--max-clip-level=8 --max-level=0`.
|
||||||
|
- A first radial analyzer invocation omitted the second `--range` value and was rejected by argparse; it was rerun with both bounds.
|
||||||
|
- A proof/parity probe used custom camera positions and failed SW parity because the sampled tile set differed from the established default-camera audit. That probe was stopped; the default-camera proof and transition suite passed 16/16.
|
||||||
|
- The first determinism launch ended before completing the matrix. The full 24-case run passed on retry. A first cost batch had an early SW timing outlier, so five additional interleaved pairs per recipe were run and retained in the combined summary.
|
||||||
|
- Early capture framing probes cropped the FI axis or placed the camera inside rock. A v19 wide floor view exposed only the export skirt edge; the final centered column and square top-down floor views were used.
|
||||||
|
- The first GCC build attempt was interrupted at 19 objects and retried successfully. One v18 diagnostic build used the default Standalone build path; those artifacts were removed, and final v19 build outputs are under `Saved/Linux/l31/`.
|
||||||
|
- Two one-line post-processing commands had quoting/pair-index errors; the checked-in `CompareDeterminismL31.py` and `AggregateCostL31.py` helpers produced the final passing summaries.
|
||||||
|
|
||||||
|
No run exceeded 30 minutes or remained stuck for 15 minutes. No Unreal files were edited. Nothing was committed, pushed, or stashed.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged as an intermediate step: correct FI column-cap audit (with negative control), a recipe-stamped runner that
|
||||||
|
rejects mismatches, all 16 recipe/style/compiler rows passing, smooth shader tint, determinism.
|
||||||
|
- **Not accepted as the final floor fix:** v19 caps the FI Gradient displacement at `width/256` (≈6 cm), which
|
||||||
|
removes the owner's Gradient transition rather than making it smooth; the floor is now flat (radial
|
||||||
|
height range 0.0005 m). The real fix is a smooth, non-terraced undulation at the L29 amplitude (≈4 m
|
||||||
|
peak-to-peak). L32 restores it.
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the recipe-stamped L31 passage audit matrix and reject mislabeled runs."""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-linux31-gcc",
|
||||||
|
}
|
||||||
|
MARKERS = (
|
||||||
|
"[ResolvedPassageAuditSummary]",
|
||||||
|
"[PassageOpenAirAuditSummary]",
|
||||||
|
"[PassageNaturalnessSummary]",
|
||||||
|
"[PassageNoLongBridgeAudit]",
|
||||||
|
"[PassageRockColumnAuditSummary]",
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for style in range(4):
|
||||||
|
data = ROOT / f"Saved/Linux/linux25-owner-{recipe}-style{style}.json"
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"passage-{recipe}-style{style}-{compiler}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate",
|
||||||
|
"--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
recipe_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
markers_ok = all(re.search(re.escape(marker) + r".*status=PASS", text)
|
||||||
|
for marker in MARKERS)
|
||||||
|
capsule_ok = bool(re.search(
|
||||||
|
r"^\[ResolvedPassageAudit\].* blocked=0 support_samples=\d+ "
|
||||||
|
r"unsupported=0 grade_failures=0 mouth_content=2/2 mouth_radius=2/2 "
|
||||||
|
r"walkability=PASS$", text, re.M))
|
||||||
|
island_route_ok = (recipe != "floating-islands" or bool(re.search(
|
||||||
|
r"^\[FloatingIslandRouteAudit\].* blocked=0 support_samples=\d+ "
|
||||||
|
r"unsupported=0 grade_failures=0 status=PASS$", text, re.M)))
|
||||||
|
binding_ok = bool(re.search(
|
||||||
|
r"\[ResolvedPassageAuditSummary\].*route_bindings=[1-9][0-9]* "
|
||||||
|
r"route_binding_failures=0 .*status=PASS", text))
|
||||||
|
walk_ok = "[Walkability] PASS" in text
|
||||||
|
if recipe == "floating-islands":
|
||||||
|
control_ok = "[PassageColumnDisconnectedEndControl]" in text and bool(
|
||||||
|
re.search(r"\[PassageColumnDisconnectedEndControl\].*detected=true", text))
|
||||||
|
else:
|
||||||
|
control_ok = True
|
||||||
|
ok = (result is not None and result.returncode == 0 and recipe_ok
|
||||||
|
and markers_ok and capsule_ok and island_route_ok and binding_ok
|
||||||
|
and walk_ok and control_ok)
|
||||||
|
actual_recipe = stamps[0][1] if len(stamps) == 1 else "MISSING"
|
||||||
|
rows.append((recipe, actual_recipe, style, compiler, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s exit={result.returncode if result else 'TIMEOUT'} "
|
||||||
|
f"recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"capsule={'PASS' if capsule_ok and island_route_ok else 'FAIL'} "
|
||||||
|
f"column_control={'PASS' if control_ok else 'FAIL'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
if not ok:
|
||||||
|
failed = True
|
||||||
|
|
||||||
|
summary = OUT / "passage-audit-matrix.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"requested_recipe,actual_recipe,style,compiler,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{requested},{actual},{style},{compiler},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for requested, actual, style, compiler, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
control_log = OUT / "recipe-mismatch-control.log"
|
||||||
|
control_owner = OUT / "recipe-mismatch-control-owner.json"
|
||||||
|
control_owner_data = json.loads(
|
||||||
|
(ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json").read_text(encoding="utf-8"))
|
||||||
|
top_owner_stratum = max(control_owner_data["strates"], key=lambda stratum: stratum["top_chunk_z"])
|
||||||
|
top_owner_stratum["asset"]["GeneratorType"] = 6 # ECaveGeneratorType::FloatingIslands
|
||||||
|
control_owner.write_text(json.dumps(control_owner_data, separators=(",", ":")), encoding="utf-8")
|
||||||
|
control_command = [
|
||||||
|
str(BINARIES["clang"]),
|
||||||
|
str(control_owner),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", "--recipe=surface-world",
|
||||||
|
"--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
with control_log.open("w", encoding="utf-8") as stream:
|
||||||
|
control = subprocess.run(control_command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
control_text = control_log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
control_ok = (control.returncode == 2 and
|
||||||
|
"[WorldAssemblerRecipeAudit] requested=surface-world actual=floating-islands "
|
||||||
|
"top_generator=FloatingIslands " in control_text and
|
||||||
|
"floating_islands_stratum=present status=FAIL" in control_text)
|
||||||
|
print(f"[L31RecipeMismatchControl] requested=surface-world actual=floating-islands "
|
||||||
|
f"exit={control.returncode} detected={'true' if control_ok else 'false'} "
|
||||||
|
f"{'PASS' if control_ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L31PassageMatrix] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or not control_ok or len(rows) != 16 else 0)
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run five interleaved W12 whole-view pairs against the final L30 binary."""
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"l30": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade",
|
||||||
|
"l31": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
}
|
||||||
|
ORDERS = (("l30", "l31"), ("l31", "l30"), ("l30", "l31"),
|
||||||
|
("l31", "l30"), ("l30", "l31"))
|
||||||
|
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for pair, order in enumerate(ORDERS, 1):
|
||||||
|
for build in order:
|
||||||
|
name = f"cost-{recipe}-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
str(BINARIES[build]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matches = READY.findall(text)
|
||||||
|
if build == "l31":
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
actual_recipe = stamps[0][1] if len(stamps) == 1 else "missing"
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
else:
|
||||||
|
configured = re.findall(
|
||||||
|
r"^\[HeadlessConfig\].*\brecipe=([^\s]+)", text, re.M)
|
||||||
|
actual_recipe = configured[0] if len(configured) == 1 else "missing"
|
||||||
|
floating_plan = "[FloatingIslandWorldAssembly]" in text
|
||||||
|
surface_plan = "[WorldAssemblerPrototype]" in text
|
||||||
|
plan_matches = ((actual_recipe == "floating-islands" and floating_plan)
|
||||||
|
or (actual_recipe == "surface-world" and not floating_plan
|
||||||
|
and surface_plan))
|
||||||
|
marker = (f"[L31BaselineRecipeAudit] requested={recipe} actual={actual_recipe} "
|
||||||
|
f"headless_recipe={configured[0] if len(configured) == 1 else 'missing'} "
|
||||||
|
f"floating_island_plan={'present' if floating_plan else 'absent'} "
|
||||||
|
f"surface_world_plan={'present' if surface_plan else 'absent'} "
|
||||||
|
f"status={'PASS' if actual_recipe == recipe and plan_matches else 'FAIL'}\n")
|
||||||
|
with logfile.open("a", encoding="utf-8") as stream:
|
||||||
|
stream.write(marker)
|
||||||
|
text += marker
|
||||||
|
stamp_ok = (actual_recipe == recipe and plan_matches
|
||||||
|
and len(configured) == 1)
|
||||||
|
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||||
|
and "hash=off" in text and "cache_hits=0" in text)
|
||||||
|
if build == "l31":
|
||||||
|
valid = valid and stamp_ok and actual_recipe == recipe
|
||||||
|
else:
|
||||||
|
valid = valid and stamp_ok and "status=PASS" in marker
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else float("nan")
|
||||||
|
rows.append((recipe, pair, build, ready, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", valid))
|
||||||
|
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True)
|
||||||
|
failed |= not valid
|
||||||
|
|
||||||
|
summary = OUT / "cost-runs.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for recipe, pair, build, ready, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build]
|
||||||
|
for build in BINARIES}
|
||||||
|
l30 = statistics.median(by_build["l30"])
|
||||||
|
l31 = statistics.median(by_build["l31"])
|
||||||
|
paired = []
|
||||||
|
for pair in range(1, 6):
|
||||||
|
values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair}
|
||||||
|
paired.append((values["l31"] - values["l30"]) / values["l30"] * 100.0)
|
||||||
|
print(f"[L31CostSummary] recipe={recipe} pairs=5 median_l30_s={l30:.3f} "
|
||||||
|
f"median_l31_s={l31:.3f} median_change_pct={(l31/l30-1)*100.0:+.2f} "
|
||||||
|
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L31Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 20 else 0)
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Re-run the L25 hash command and 24 m crossing on the L31 compiler matrix."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-linux31-gcc",
|
||||||
|
}
|
||||||
|
HASH_PATTERN = re.compile(
|
||||||
|
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
candidate_reference = {}
|
||||||
|
fi_hash_changes = {"l25": False, "crossing24": False}
|
||||||
|
|
||||||
|
|
||||||
|
def hashes(path):
|
||||||
|
return HASH_PATTERN.findall(path.read_text(encoding="utf-8", errors="replace"))
|
||||||
|
|
||||||
|
|
||||||
|
def run(kind, compiler, recipe, workers):
|
||||||
|
global failed
|
||||||
|
name = f"{kind}-{compiler}-{recipe}-w{workers}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
str(BINARIES[compiler]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
"--far-horizon-lod8-step=0", f"--workers={workers}", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
if kind == "crossing24":
|
||||||
|
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
current = hashes(logfile)
|
||||||
|
# The normal L30 log stem is ringfix-hash; crossing uses ringfix-crossing24.
|
||||||
|
baseline = ROOT / "Saved/Linux/l30" / (
|
||||||
|
f"ringfix-crossing24-{compiler}-{recipe}-w{workers}.log" if kind == "crossing24"
|
||||||
|
else f"ringfix-hash-{compiler}-{recipe}-w{workers}.log")
|
||||||
|
reference = hashes(baseline) if baseline.is_file() else []
|
||||||
|
key = (kind, recipe)
|
||||||
|
prior = candidate_reference.get(key)
|
||||||
|
stable = prior is None or current == prior
|
||||||
|
if prior is None and current:
|
||||||
|
candidate_reference[key] = current
|
||||||
|
baseline_match = bool(reference) and current == reference
|
||||||
|
baseline_changed = bool(reference) and current != reference
|
||||||
|
if recipe == "surface-world":
|
||||||
|
baseline_ok = baseline_match
|
||||||
|
comparison = "MATCH" if baseline_match else "MISMATCH"
|
||||||
|
else:
|
||||||
|
baseline_ok = baseline_changed
|
||||||
|
comparison = "EXPECTED_CHANGE" if baseline_changed else "UNCHANGED_OR_MISSING"
|
||||||
|
fi_hash_changes[kind] |= baseline_changed
|
||||||
|
ok = (result is not None and result.returncode == 0
|
||||||
|
and stamps == [(recipe, recipe, "PASS")]
|
||||||
|
and "hash=on" in text and len(current) == 12 and stable and baseline_ok)
|
||||||
|
rows.append((kind, recipe, compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", len(current), ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"hashes={len(current)} "
|
||||||
|
f"L30={comparison} candidate_stable={'yes' if stable else 'no'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
if not ok:
|
||||||
|
failed = True
|
||||||
|
|
||||||
|
|
||||||
|
for kind in ("l25", "crossing24"):
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
run("crossing24" if kind == "crossing24" else "l25", compiler, recipe, workers)
|
||||||
|
|
||||||
|
summary = OUT / "determinism.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"kind,recipe,compiler,workers,elapsed_seconds,exit_code,hash_rows,status\n" +
|
||||||
|
"".join(f"{kind},{recipe},{compiler},{workers},{elapsed:.3f},{code},{count},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for kind, recipe, compiler, workers, elapsed, code, count, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
for kind, changed in fi_hash_changes.items():
|
||||||
|
if not changed:
|
||||||
|
failed = True
|
||||||
|
print(f"[L31ExpectedGeometryHashChange] kind={kind} detected=false FAIL", flush=True)
|
||||||
|
else:
|
||||||
|
print(f"[L31ExpectedGeometryHashChange] kind={kind} detected=true PASS", flush=True)
|
||||||
|
print(f"[L31Determinism] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 24 else 0)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run L31 route/proof and transition invariants with recipe-stamped logs."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-linux31-gcc",
|
||||||
|
}
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, binary, data, recipe, extra, checks):
|
||||||
|
global failed
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate",
|
||||||
|
"--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}", *extra,
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
checks_ok = all(check(text) for check in checks)
|
||||||
|
ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok
|
||||||
|
rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
owner = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[RouteParityAudit\].*status=PASS$", text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))])
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for mode in ("hard", "gradient", "interleaved"):
|
||||||
|
name = f"invariant-transition-{recipe}-{mode}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||||
|
text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))])
|
||||||
|
|
||||||
|
summary = OUT / "invariant-audits.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"run,recipe,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for name, recipe, elapsed, code, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L31InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 16 else 0)
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract the validated recipe-stamped passage matrix into a compact table."""
|
||||||
|
import csv
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
rows = []
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for style in range(4):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
name = f"passage-{recipe}-style{style}-{compiler}"
|
||||||
|
path = OUT / f"{name}.log"
|
||||||
|
text = path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
def match(pattern):
|
||||||
|
found = re.search(pattern, text, re.M)
|
||||||
|
if not found:
|
||||||
|
raise SystemExit(f"{path}: missing metric {pattern}")
|
||||||
|
return found
|
||||||
|
|
||||||
|
stamp = match(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=(PASS|FAIL)$")
|
||||||
|
capsule = match(
|
||||||
|
r"^\[ResolvedPassageAudit\].*capsule_samples=(\d+) blocked=(\d+) "
|
||||||
|
r"support_samples=(\d+) unsupported=(\d+) grade_failures=(\d+) "
|
||||||
|
r"mouth_content=([^ ]+) mouth_radius=([^ ]+) .*$")
|
||||||
|
air = match(r"^\[PassageOpenAirAuditSummary\].*violations=(\d+).*status=(PASS|FAIL)$")
|
||||||
|
natural = match(r"^\[PassageNaturalnessSummary\] passed=(\d+)/(\d+).*$")
|
||||||
|
bridge = match(
|
||||||
|
r"^\[PassageNoLongBridgeAudit\].*longest_uncolumned_m=([0-9.]+) "
|
||||||
|
r"threshold_m=([0-9.]+).*$")
|
||||||
|
column = match(
|
||||||
|
r"^\[PassageRockColumnAuditSummary\] columns=(\d+) connected_ends=([^ ]+) "
|
||||||
|
r"short_spans=(\d+) wall_or_support_failures=(\d+).*status=(PASS|FAIL)$")
|
||||||
|
binding = match(
|
||||||
|
r"^\[ResolvedPassageAuditSummary\].*route_bindings=(\d+) "
|
||||||
|
r"route_binding_failures=(\d+).*status=(PASS|FAIL)$")
|
||||||
|
walk = match(r"^\[Walkability\] PASS (\d+)/(\d+) passage centerline samples")
|
||||||
|
rows.append({
|
||||||
|
"requested": stamp.group(1), "actual": stamp.group(2), "stamp": stamp.group(3),
|
||||||
|
"style": style, "compiler": compiler,
|
||||||
|
"capsule_samples": int(capsule.group(1)), "blocked": int(capsule.group(2)),
|
||||||
|
"support_samples": int(capsule.group(3)),
|
||||||
|
"unsupported": int(capsule.group(4)), "grade_failures": int(capsule.group(5)),
|
||||||
|
"mouth_content": capsule.group(6), "mouth_radius": capsule.group(7),
|
||||||
|
"air_violations": int(air.group(1)), "air_status": air.group(2),
|
||||||
|
"natural_passed": natural.group(1),
|
||||||
|
"natural_total": natural.group(2),
|
||||||
|
"longest_uncolumned_m": bridge.group(1), "bridge_threshold_m": bridge.group(2),
|
||||||
|
"column_count": int(column.group(1)), "connected_ends": column.group(2),
|
||||||
|
"short_spans": int(column.group(3)), "column_failures": int(column.group(4)),
|
||||||
|
"route_bindings": int(binding.group(1)),
|
||||||
|
"binding_failures": int(binding.group(2)),
|
||||||
|
"walk_samples": int(walk.group(1)), "walk_total": int(walk.group(2)),
|
||||||
|
"log": path.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
with (OUT / "passage-audit-metrics.csv").open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"| Requested → actual | Style | Compiler | Capsule | Open air | Natural | Long bridge | Column ends | Bindings | Walkability |",
|
||||||
|
"|---|---:|---|---|---:|---:|---:|---:|---:|---:|",
|
||||||
|
]
|
||||||
|
for row in rows:
|
||||||
|
ok = (row["requested"] == row["actual"] and row["stamp"] == "PASS"
|
||||||
|
and row["blocked"] == row["unsupported"] == row["grade_failures"] == 0
|
||||||
|
and row["mouth_content"] == row["mouth_radius"] == "2/2"
|
||||||
|
and row["air_violations"] == 0 and row["air_status"] == "PASS"
|
||||||
|
and row["natural_passed"] == row["natural_total"] == "1"
|
||||||
|
and float(row["longest_uncolumned_m"]) <= float(row["bridge_threshold_m"])
|
||||||
|
and row["connected_ends"] == f"{2 * row['column_count']}/{2 * row['column_count']}"
|
||||||
|
and row["column_failures"] == 0 and row["binding_failures"] == 0
|
||||||
|
and row["route_bindings"] > 0 and row["walk_samples"] == row["walk_total"])
|
||||||
|
if not ok:
|
||||||
|
raise SystemExit(f"{row['log']}: metric audit failed")
|
||||||
|
cells = (
|
||||||
|
f"{row['blocked']}/{row['unsupported']} blocked/unsupported; mouth 2/2",
|
||||||
|
f"{row['air_violations']} violations",
|
||||||
|
f"{row['natural_passed']}/{row['natural_total']}",
|
||||||
|
f"{float(row['longest_uncolumned_m']):.3f}/{row['bridge_threshold_m']} m",
|
||||||
|
row["connected_ends"],
|
||||||
|
f"{row['route_bindings']} (0 failures)",
|
||||||
|
f"{row['walk_samples']}/{row['walk_total']}",
|
||||||
|
)
|
||||||
|
lines.append(f"| {row['requested']} → {row['actual']} | {row['style']} | {row['compiler']} | "
|
||||||
|
+ " | ".join(cells) + " |")
|
||||||
|
|
||||||
|
table = OUT / "passage-audit-table.md"
|
||||||
|
table.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
print(f"[L31PassageAuditTable] rows={len(rows)} pass={len(rows)} "
|
||||||
|
f"csv={OUT / 'passage-audit-metrics.csv'} markdown={table}")
|
||||||
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Audit the displaced floor across the passage-mouth fade edge."""
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import resource
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import AuditRingTerraceL32 as ring
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, required=True)
|
||||||
|
parser.add_argument("--lod", type=int, default=0)
|
||||||
|
parser.add_argument("--inner-radius-m", type=float, default=5.5)
|
||||||
|
parser.add_argument("--outer-radius-m", type=float, default=9.5)
|
||||||
|
parser.add_argument("--step-m", type=float, default=0.125)
|
||||||
|
parser.add_argument("--height-min-m", type=float, default=-122.5)
|
||||||
|
parser.add_argument("--height-max-m", type=float, default=-117.5)
|
||||||
|
parser.add_argument("--preferred-height-m", type=float, default=-119.5)
|
||||||
|
parser.add_argument("--highest-surface", action="store_true",
|
||||||
|
help="choose the topmost upward face inside the height window")
|
||||||
|
parser.add_argument("--plateau-min-m", type=float, default=0.5)
|
||||||
|
parser.add_argument("--step-threshold-m", type=float, default=0.1)
|
||||||
|
parser.add_argument("--normal-jump-max-deg", type=float, default=20.0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if (args.inner_radius_m < 0.0 or args.outer_radius_m <= args.inner_radius_m
|
||||||
|
or args.step_m <= 0.0):
|
||||||
|
parser.error("invalid radial interval or sample spacing")
|
||||||
|
|
||||||
|
cx, cy = args.center
|
||||||
|
preferred_height = None if args.highest_surface else args.preferred_height_m
|
||||||
|
bounds = (cx - args.outer_radius_m, cx + args.outer_radius_m,
|
||||||
|
cy - args.outer_radius_m, cy + args.outer_radius_m)
|
||||||
|
voxel_um, triangles, selected_tiles = ring.read_scene(
|
||||||
|
args.scene, args.lod, args.height_min_m, args.height_max_m, bounds)
|
||||||
|
bucket_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << args.lod))
|
||||||
|
buckets = ring.build_buckets(triangles, True, bounds, bucket_size)
|
||||||
|
|
||||||
|
count = round((args.outer_radius_m - args.inner_radius_m) / args.step_m)
|
||||||
|
lines = []
|
||||||
|
for angle_degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||||
|
angle = math.radians(angle_degrees)
|
||||||
|
points = [(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||||
|
for radius in (args.inner_radius_m + i * args.step_m
|
||||||
|
for i in range(count + 1))]
|
||||||
|
lines.append((f"radial-{angle_degrees:.0f}deg", points))
|
||||||
|
|
||||||
|
# Tangential sections stay outside the open mouth while crossing the outer
|
||||||
|
# fade edge on both sides of the 6.5 m center radius.
|
||||||
|
tangent_count = round(8.0 / args.step_m)
|
||||||
|
tangent_values = [-4.0 + i * args.step_m for i in range(tangent_count + 1)]
|
||||||
|
for angle_degrees in (225.0, 315.0):
|
||||||
|
angle = math.radians(angle_degrees)
|
||||||
|
normal = (math.cos(angle), math.sin(angle))
|
||||||
|
tangent = (-normal[1], normal[0])
|
||||||
|
points = [(cx + 6.5 * normal[0] + t * tangent[0],
|
||||||
|
cy + 6.5 * normal[1] + t * tangent[1])
|
||||||
|
for t in tangent_values]
|
||||||
|
lines.append((f"straight-tangent-{angle_degrees:.0f}deg", points))
|
||||||
|
|
||||||
|
failed = False
|
||||||
|
max_plateau = 0.0
|
||||||
|
max_step = 0.0
|
||||||
|
max_normal = 0.0
|
||||||
|
min_coverage = 1.0
|
||||||
|
for name, points in lines:
|
||||||
|
profile = ring.audit_profile(points, buckets, True, args.plateau_min_m,
|
||||||
|
0.0005, args.step_threshold_m, bucket_size, preferred_height)
|
||||||
|
samples = [ring.sample_surface(buckets, x, y, True, bucket_size,
|
||||||
|
preferred_height)
|
||||||
|
for x, y in points]
|
||||||
|
for index in range(1, len(samples)):
|
||||||
|
prior, current = samples[index - 1], samples[index]
|
||||||
|
if prior is None or current is None:
|
||||||
|
continue
|
||||||
|
dot = max(-1.0, min(1.0, sum(a * b for a, b in zip(prior[1], current[1]))))
|
||||||
|
delta = math.degrees(math.acos(dot))
|
||||||
|
if delta > args.normal_jump_max_deg:
|
||||||
|
px, py = points[index - 1]
|
||||||
|
x, y = points[index]
|
||||||
|
print(f"[PassageFadeNormalJump] line={name} "
|
||||||
|
f"radius_m={math.hypot(x-cx,y-cy):.3f} "
|
||||||
|
f"from_m=({px:.3f},{py:.3f},{prior[0]:.3f}) "
|
||||||
|
f"to_m=({x:.3f},{y:.3f},{current[0]:.3f}) "
|
||||||
|
f"normal_delta_deg={delta:.3f}")
|
||||||
|
coverage = profile["coverage"]
|
||||||
|
min_coverage = min(min_coverage, coverage)
|
||||||
|
plateau = max((run[0] for run in profile["plateaus"]), default=0.0)
|
||||||
|
step = max((run[1] for run in profile["plateaus"]), default=0.0)
|
||||||
|
angle = profile["max_angle"]
|
||||||
|
max_plateau = max(max_plateau, plateau)
|
||||||
|
max_step = max(max_step, step)
|
||||||
|
max_normal = max(max_normal, angle)
|
||||||
|
ok = (profile["valid"] and plateau < args.plateau_min_m
|
||||||
|
and angle <= args.normal_jump_max_deg)
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[PassageFadeProfile] line={name} coverage={coverage:.3f} "
|
||||||
|
f"height_range_m={profile.get('height_range', 0.0):.6f} "
|
||||||
|
f"flat_step_runs={len(profile['plateaus'])} "
|
||||||
|
f"max_normal_delta_deg={angle:.6f} status={'PASS' if ok else 'FAIL'}")
|
||||||
|
|
||||||
|
passed = not failed and min_coverage >= 0.95
|
||||||
|
print(f"[PassageFadeRingAudit] scene={args.scene} lod={args.lod} "
|
||||||
|
f"center_m=({cx:.6f},{cy:.6f}) mouth_radius_m=4.500 "
|
||||||
|
f"fade_m=5.500..7.500 radial_m={args.inner_radius_m:.3f}.."
|
||||||
|
f"{args.outer_radius_m:.3f} lines={len(lines)} selected_tiles={selected_tiles} "
|
||||||
|
f"coverage_min={min_coverage:.3f} longest_flat_run_m={max_plateau:.6f} "
|
||||||
|
f"following_step_m={max_step:.6f} max_normal_delta_deg={max_normal:.6f} "
|
||||||
|
f"thresholds=(coverage>=0.950,plateau<0.500,step>=0.100,normal<=20.000) "
|
||||||
|
f"status={'PASS' if passed else 'FAIL'}")
|
||||||
|
print(f"[PassageFadeRSS] peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
raise SystemExit(0 if passed else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Audit several bounded face windows and combine their height range."""
|
||||||
|
import argparse
|
||||||
|
import gc
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import resource
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), required=True)
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, action="append", required=True,
|
||||||
|
help="repeat for each bounded local ring")
|
||||||
|
parser.add_argument("--radius", type=float, default=8.0)
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--height-min-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-max-m", type=float, required=True)
|
||||||
|
parser.add_argument("--preferred-height-m", type=float,
|
||||||
|
help="lock overlapping-face selection near this expected surface height")
|
||||||
|
parser.add_argument("--step", type=float, default=0.125)
|
||||||
|
parser.add_argument("--plateau-min-m", type=float, default=0.5)
|
||||||
|
parser.add_argument("--plateau-epsilon-m", type=float, default=0.0005)
|
||||||
|
parser.add_argument("--step-threshold-m", type=float, default=0.1)
|
||||||
|
parser.add_argument("--normal-jump-max-deg", type=float, default=20.0)
|
||||||
|
parser.add_argument("--amplitude-min-m", type=float, default=3.0)
|
||||||
|
parser.add_argument("--min-radial-lines-per-center", type=int, default=4)
|
||||||
|
parser.add_argument("--min-straight-lines-per-center", type=int, default=2)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
audit_path = Path(__file__).with_name("AuditRingTerraceL32.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("ring_audit_l32_set", audit_path)
|
||||||
|
audit = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(audit)
|
||||||
|
|
||||||
|
upward = args.surface == "floor"
|
||||||
|
preferred_height = args.preferred_height_m
|
||||||
|
if preferred_height is None:
|
||||||
|
preferred_height = args.height_max_m if upward else args.height_min_m
|
||||||
|
all_ranges = []
|
||||||
|
overall_max_normal = 0.0
|
||||||
|
overall_max_plateau = 0.0
|
||||||
|
overall_max_step = 0.0
|
||||||
|
overall_min_coverage = 1.0
|
||||||
|
total_valid_lines = 0
|
||||||
|
total_lines = 0
|
||||||
|
failed = False
|
||||||
|
for center in args.center:
|
||||||
|
bounds = (center[0] - args.radius, center[0] + args.radius,
|
||||||
|
center[1] - args.radius, center[1] + args.radius)
|
||||||
|
voxel_um, triangles, selected_tiles = audit.read_scene(
|
||||||
|
args.scene, args.lod, args.height_min_m, args.height_max_m, bounds)
|
||||||
|
max_lod = max((triangle[0] for triangle in triangles), default=0)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << max_lod))
|
||||||
|
buckets = audit.build_buckets(triangles, upward, bounds, cell_size)
|
||||||
|
results = []
|
||||||
|
for name, points in audit.make_lines(*center, args.radius, args.step):
|
||||||
|
result = audit.audit_profile(points, buckets, upward,
|
||||||
|
args.plateau_min_m, args.plateau_epsilon_m,
|
||||||
|
args.step_threshold_m, cell_size, preferred_height)
|
||||||
|
results.append((name, points, result))
|
||||||
|
|
||||||
|
radial = [row for row in results if row[0].startswith("radial-") and row[2]["valid"]]
|
||||||
|
straight = [row for row in results if row[0].startswith("straight-") and row[2]["valid"]]
|
||||||
|
valid = [row for row in results if row[2]["valid"]]
|
||||||
|
local_min = min((row[2]["height_min"] for row in valid), default=float("inf"))
|
||||||
|
local_max = max((row[2]["height_max"] for row in valid), default=float("-inf"))
|
||||||
|
local_amplitude = local_max - local_min
|
||||||
|
local_plateau = max((run[0] for row in valid for run in row[2]["plateaus"]), default=0.0)
|
||||||
|
local_step = max((run[1] for row in valid for run in row[2]["plateaus"]), default=0.0)
|
||||||
|
local_normal = max((row[2]["max_angle"] for row in valid), default=float("inf"))
|
||||||
|
local_coverage = min((row[2]["coverage"] for row in results), default=0.0)
|
||||||
|
local_ok = (len(valid) == len(results)
|
||||||
|
and len(radial) >= args.min_radial_lines_per_center
|
||||||
|
and len(straight) >= args.min_straight_lines_per_center
|
||||||
|
and local_plateau < args.plateau_min_m
|
||||||
|
and local_normal <= args.normal_jump_max_deg)
|
||||||
|
all_ranges.extend((row[2]["height_min"], row[2]["height_max"])
|
||||||
|
for row in valid)
|
||||||
|
overall_max_normal = max(overall_max_normal, local_normal)
|
||||||
|
overall_max_plateau = max(overall_max_plateau, local_plateau)
|
||||||
|
overall_max_step = max(overall_max_step, local_step)
|
||||||
|
overall_min_coverage = min(overall_min_coverage, local_coverage)
|
||||||
|
total_valid_lines += len(valid)
|
||||||
|
total_lines += len(results)
|
||||||
|
print(f"[RingSetWindow] center_m=({center[0]:.3f},{center[1]:.3f}) "
|
||||||
|
f"radius_m={args.radius:.3f} tiles={selected_tiles} lines={len(valid)}/"
|
||||||
|
f"{len(results)} radial={len(radial)} straight={len(straight)} "
|
||||||
|
f"coverage_min={local_coverage:.3f} height_m={local_min:.6f}.."
|
||||||
|
f"{local_max:.6f} amplitude_m={local_amplitude:.6f} "
|
||||||
|
f"longest_flat_m={local_plateau:.6f} following_step_m={local_step:.6f} "
|
||||||
|
f"max_normal_delta_deg={local_normal:.6f} "
|
||||||
|
f"status={'PASS' if local_ok else 'FAIL'}")
|
||||||
|
for name, _, result in results:
|
||||||
|
print(f"[RingSetProfile] center_m=({center[0]:.3f},{center[1]:.3f}) "
|
||||||
|
f"line={name} coverage={result['coverage']:.3f} "
|
||||||
|
f"range_m={result.get('height_range', 0.0):.6f} "
|
||||||
|
f"flat_step_runs={len(result['plateaus'])} "
|
||||||
|
f"max_normal_delta_deg={result['max_angle']:.6f}")
|
||||||
|
failed |= not local_ok
|
||||||
|
del triangles, buckets, results, radial, straight, valid
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
if all_ranges:
|
||||||
|
sampled_min = min(value[0] for value in all_ranges)
|
||||||
|
sampled_max = max(value[1] for value in all_ranges)
|
||||||
|
amplitude = sampled_max - sampled_min
|
||||||
|
else:
|
||||||
|
sampled_min = sampled_max = amplitude = 0.0
|
||||||
|
passed = not failed and amplitude >= args.amplitude_min_m
|
||||||
|
print(f"[RingSetAudit] scene={args.scene} surface={args.surface} lod={args.lod} "
|
||||||
|
f"windows={len(args.center)} radius_m={args.radius:.3f} "
|
||||||
|
f"height_window_m={args.height_min_m:.3f}..{args.height_max_m:.3f} "
|
||||||
|
f"sampled_height_m={sampled_min:.6f}..{sampled_max:.6f} "
|
||||||
|
f"amplitude_range_m={amplitude:.6f} lines={total_valid_lines}/{total_lines} "
|
||||||
|
f"coverage_min={overall_min_coverage:.3f} "
|
||||||
|
f"longest_flat_run_m={overall_max_plateau:.6f} "
|
||||||
|
f"max_step_m={overall_max_step:.6f} "
|
||||||
|
f"max_normal_delta_deg={overall_max_normal:.6f} "
|
||||||
|
f"thresholds=(amplitude>={args.amplitude_min_m:.3f},"
|
||||||
|
f"plateau<{args.plateau_min_m:.3f},"
|
||||||
|
f"normal<={args.normal_jump_max_deg:.3f}) "
|
||||||
|
f"status={'PASS' if passed else 'FAIL'}")
|
||||||
|
print(f"[RingSetRSS] peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
raise SystemExit(0 if passed else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail on contour terraces, stepped profiles, or insufficient Gradient amplitude."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import math
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
def read_scene(path, wanted_lod, height_min, height_max, bounds):
|
||||||
|
triangles = []
|
||||||
|
selected_tiles = 0
|
||||||
|
x_min, x_max, y_min, y_max = bounds
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit(f"{path}: invalid mesh scene header")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.read(40)
|
||||||
|
if version != 1 or tile_count > 100000:
|
||||||
|
raise SystemExit(f"{path}: unsupported mesh scene")
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
tile_extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
tile_x0, tile_y0 = key[0] * tile_extent, key[1] * tile_extent
|
||||||
|
tile_z0 = key[2] * tile_extent
|
||||||
|
if (wanted_lod is not None and key[3] != wanted_lod) or \
|
||||||
|
tile_x0 > x_max or tile_x0 + tile_extent < x_min or \
|
||||||
|
tile_y0 > y_max or tile_y0 + tile_extent < y_min or \
|
||||||
|
tile_z0 > height_max or tile_z0 + tile_extent < height_min:
|
||||||
|
stream.seek(vertex_count * 24 + index_count * 4 + mask_count, 1)
|
||||||
|
continue
|
||||||
|
selected_tiles += 1
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
for offset in range(0, index_count, 3):
|
||||||
|
if masks and masks[offset // 3]:
|
||||||
|
continue
|
||||||
|
ids = indices[offset:offset + 3]
|
||||||
|
if max(ids) >= surface_count:
|
||||||
|
continue
|
||||||
|
verts = []
|
||||||
|
for index in ids:
|
||||||
|
p, n = index * 3, index * 3
|
||||||
|
verts.append(((positions[p], positions[p + 1], positions[p + 2]),
|
||||||
|
(normals[n], normals[n + 1], normals[n + 2])))
|
||||||
|
average_nz = sum(vertex[1][2] for vertex in verts) / 3.0
|
||||||
|
z_values = [vertex[0][2] for vertex in verts]
|
||||||
|
# Select the requested surface branch by its face height. An
|
||||||
|
# overlap or centroid test admits a giant coarse triangle that
|
||||||
|
# merely clips this window and can belong to the neighboring face.
|
||||||
|
mean_z = sum(z_values) / 3.0
|
||||||
|
vertex_margin = voxel_um / 1_000_000.0 * 0.5
|
||||||
|
if (mean_z < height_min or mean_z > height_max or
|
||||||
|
min(z_values) < height_min - vertex_margin or
|
||||||
|
max(z_values) > height_max + vertex_margin):
|
||||||
|
continue
|
||||||
|
if (max(vertex[0][0] for vertex in verts) < x_min or
|
||||||
|
min(vertex[0][0] for vertex in verts) > x_max or
|
||||||
|
max(vertex[0][1] for vertex in verts) < y_min or
|
||||||
|
min(vertex[0][1] for vertex in verts) > y_max):
|
||||||
|
continue
|
||||||
|
triangles.append((key[3], average_nz, tuple(verts)))
|
||||||
|
return voxel_um, triangles, selected_tiles
|
||||||
|
|
||||||
|
|
||||||
|
def build_buckets(triangles, upward, bounds, cell_size=2.0):
|
||||||
|
buckets = defaultdict(list)
|
||||||
|
x_min, x_max, y_min, y_max = bounds
|
||||||
|
for triangle in triangles:
|
||||||
|
_, average_nz, verts = triangle
|
||||||
|
if (upward and average_nz < 0.35) or (not upward and average_nz > -0.35):
|
||||||
|
continue
|
||||||
|
xs = [v[0][0] for v in verts]
|
||||||
|
ys = [v[0][1] for v in verts]
|
||||||
|
x0 = max(math.floor(min(xs) / cell_size), math.floor(x_min / cell_size))
|
||||||
|
x1 = min(math.floor(max(xs) / cell_size), math.floor(x_max / cell_size))
|
||||||
|
y0 = max(math.floor(min(ys) / cell_size), math.floor(y_min / cell_size))
|
||||||
|
y1 = min(math.floor(max(ys) / cell_size), math.floor(y_max / cell_size))
|
||||||
|
for iy in range(y0, y1 + 1):
|
||||||
|
for ix in range(x0, x1 + 1):
|
||||||
|
buckets[(ix, iy)].append(verts)
|
||||||
|
return buckets
|
||||||
|
|
||||||
|
|
||||||
|
def sample_surface(buckets, x, y, upward, cell_size, preferred_height=None):
|
||||||
|
found = None
|
||||||
|
for verts in buckets.get((math.floor(x / cell_size), math.floor(y / cell_size)), ()):
|
||||||
|
(a, na), (b, nb), (c, nc) = verts
|
||||||
|
denominator = (b[1] - c[1]) * (a[0] - c[0]) + (c[0] - b[0]) * (a[1] - c[1])
|
||||||
|
if abs(denominator) < 1.0e-12:
|
||||||
|
continue
|
||||||
|
u = ((b[1] - c[1]) * (x - c[0]) + (c[0] - b[0]) * (y - c[1])) / denominator
|
||||||
|
v = ((c[1] - a[1]) * (x - c[0]) + (a[0] - c[0]) * (y - c[1])) / denominator
|
||||||
|
w = 1.0 - u - v
|
||||||
|
if min(u, v, w) < -1.0e-6:
|
||||||
|
continue
|
||||||
|
z = u * a[2] + v * b[2] + w * c[2]
|
||||||
|
normal = tuple(u * na[axis] + v * nb[axis] + w * nc[axis] for axis in range(3))
|
||||||
|
length = math.sqrt(sum(value * value for value in normal))
|
||||||
|
if length < 1.0e-9:
|
||||||
|
continue
|
||||||
|
normal = tuple(value / length for value in normal)
|
||||||
|
if (normal[2] > 0) != upward:
|
||||||
|
continue
|
||||||
|
if found is None or (abs(z - preferred_height) < abs(found[0] - preferred_height)
|
||||||
|
if preferred_height is not None else z > found[0]):
|
||||||
|
found = (z, normal)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def make_lines(cx, cy, radius, step):
|
||||||
|
lines = []
|
||||||
|
for index in range(8):
|
||||||
|
angle = index * math.tau / 8.0
|
||||||
|
count = round(radius / step)
|
||||||
|
points = [(cx + i * step * math.cos(angle), cy + i * step * math.sin(angle))
|
||||||
|
for i in range(count + 1)]
|
||||||
|
lines.append((f"radial-{index}", points))
|
||||||
|
count = round(2.0 * radius / step)
|
||||||
|
t_values = [-radius + i * step for i in range(count + 1)]
|
||||||
|
for name, points in (
|
||||||
|
("straight-x", [(cx + t, cy) for t in t_values]),
|
||||||
|
("straight-y", [(cx, cy + t) for t in t_values]),
|
||||||
|
("straight-diagonal", [(cx + t / math.sqrt(2.0), cy + t / math.sqrt(2.0))
|
||||||
|
for t in t_values]),
|
||||||
|
):
|
||||||
|
lines.append((name, points))
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def audit_profile(points, buckets, upward, plateau_min_m, plateau_epsilon_m,
|
||||||
|
step_threshold_m, cell_size, preferred_height=None):
|
||||||
|
samples = [sample_surface(buckets, x, y, upward, cell_size, preferred_height)
|
||||||
|
for x, y in points]
|
||||||
|
valid = [(index, value) for index, value in enumerate(samples) if value is not None]
|
||||||
|
if len(valid) < max(8, len(points) * 0.75):
|
||||||
|
return {"coverage": len(valid) / len(points), "plateaus": [], "max_angle": math.inf,
|
||||||
|
"height_range": 0.0, "valid": False}
|
||||||
|
heights = [value[0] for _, value in valid]
|
||||||
|
plateau_runs = []
|
||||||
|
run_start = valid[0][0]
|
||||||
|
run_start_height = valid[0][1][0]
|
||||||
|
sample_spacing = math.hypot(points[1][0] - points[0][0],
|
||||||
|
points[1][1] - points[0][1])
|
||||||
|
for cursor in range(1, len(valid)):
|
||||||
|
prior_index, prior = valid[cursor - 1]
|
||||||
|
index, current = valid[cursor]
|
||||||
|
if index != prior_index + 1:
|
||||||
|
run_start = index
|
||||||
|
run_start_height = current[0]
|
||||||
|
continue
|
||||||
|
if abs(current[0] - run_start_height) <= plateau_epsilon_m:
|
||||||
|
continue
|
||||||
|
run_length = (prior_index - run_start + 1) * sample_spacing
|
||||||
|
if run_length >= plateau_min_m and abs(current[0] - prior[0]) >= step_threshold_m:
|
||||||
|
plateau_runs.append((run_length, abs(current[0] - prior[0]), run_start,
|
||||||
|
prior_index, index, prior[0], current[0]))
|
||||||
|
run_start = index
|
||||||
|
run_start_height = current[0]
|
||||||
|
max_angle = 0.0
|
||||||
|
for cursor in range(1, len(valid)):
|
||||||
|
prior_index, prior = valid[cursor - 1]
|
||||||
|
index, current = valid[cursor]
|
||||||
|
if index != prior_index + 1:
|
||||||
|
continue
|
||||||
|
dot = max(-1.0, min(1.0, sum(a * b for a, b in zip(prior[1], current[1]))))
|
||||||
|
max_angle = max(max_angle, math.degrees(math.acos(dot)))
|
||||||
|
return {"coverage": len(valid) / len(points), "plateaus": plateau_runs,
|
||||||
|
"max_angle": max_angle, "height_range": max(heights) - min(heights),
|
||||||
|
"height_min": min(heights), "height_max": max(heights),
|
||||||
|
"valid": True}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), default="floor")
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, default=(64.0, -32.0))
|
||||||
|
parser.add_argument("--radius", type=float, default=16.0)
|
||||||
|
parser.add_argument("--lod", required=True,
|
||||||
|
help="audit one LOD (0..8) or the streamed composite (`all`)")
|
||||||
|
parser.add_argument("--step", type=float, default=0.125)
|
||||||
|
parser.add_argument("--height-min-m", type=float, default=-130.0)
|
||||||
|
parser.add_argument("--height-max-m", type=float, default=-110.0)
|
||||||
|
parser.add_argument("--plateau-min-m", type=float, default=0.5)
|
||||||
|
parser.add_argument("--plateau-epsilon-m", type=float, default=0.0005)
|
||||||
|
parser.add_argument("--step-threshold-m", type=float, default=0.1)
|
||||||
|
parser.add_argument("--normal-jump-max-deg", type=float, default=20.0)
|
||||||
|
parser.add_argument("--amplitude-min-m", type=float, default=3.0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
wanted_lod = None if args.lod == "all" else int(args.lod)
|
||||||
|
except ValueError:
|
||||||
|
parser.error("--lod must be 0..8 or all")
|
||||||
|
if wanted_lod is not None and not 0 <= wanted_lod <= 8:
|
||||||
|
parser.error("--lod must be 0..8 or all")
|
||||||
|
bounds = (args.center[0] - args.radius, args.center[0] + args.radius,
|
||||||
|
args.center[1] - args.radius, args.center[1] + args.radius)
|
||||||
|
voxel_um, triangles, selected_tiles = read_scene(args.scene, wanted_lod,
|
||||||
|
args.height_min_m, args.height_max_m, bounds)
|
||||||
|
upward = args.surface == "floor"
|
||||||
|
max_lod = max((triangle[0] for triangle in triangles), default=0)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << max_lod))
|
||||||
|
buckets = build_buckets(triangles, upward, bounds, cell_size)
|
||||||
|
results = []
|
||||||
|
# The requested face is the outer boundary of its authored band: highest Z
|
||||||
|
# for a floor, lowest Z for a ceiling. Selecting the band midpoint can jump
|
||||||
|
# between two overlapping downward/upward sheets at a cave edge.
|
||||||
|
preferred_height = args.height_max_m if upward else args.height_min_m
|
||||||
|
for name, points in make_lines(*args.center, args.radius, args.step):
|
||||||
|
results.append((name, points, audit_profile(points, buckets, upward,
|
||||||
|
args.plateau_min_m, args.plateau_epsilon_m,
|
||||||
|
args.step_threshold_m, cell_size, preferred_height)))
|
||||||
|
valid = [result for _, _, result in results if result["valid"]]
|
||||||
|
if not valid:
|
||||||
|
raise SystemExit("no sampled surface profiles; check scene, center, orientation and LOD")
|
||||||
|
amplitude = max(result["height_max"] for result in valid) - min(
|
||||||
|
result["height_min"] for result in valid)
|
||||||
|
sampled_min = min(result["height_min"] for result in valid)
|
||||||
|
sampled_max = max(result["height_max"] for result in valid)
|
||||||
|
longest_plateau = max((run[0] for result in valid for run in result["plateaus"]),
|
||||||
|
default=0.0)
|
||||||
|
largest_step = max((run[1] for result in valid for run in result["plateaus"]),
|
||||||
|
default=0.0)
|
||||||
|
max_normal_jump = max(result["max_angle"] for result in valid)
|
||||||
|
min_coverage = min(result["coverage"] for _, _, result in results)
|
||||||
|
passed = (amplitude >= args.amplitude_min_m and longest_plateau < args.plateau_min_m
|
||||||
|
and max_normal_jump <= args.normal_jump_max_deg and min_coverage >= 0.75)
|
||||||
|
print(f"[RingTerraceAudit] scene={args.scene} surface={args.surface} lod={args.lod} "
|
||||||
|
f"max_lod={max_lod} "
|
||||||
|
f"height_window_m={args.height_min_m:.3f}..{args.height_max_m:.3f} "
|
||||||
|
f"voxel_um={voxel_um} triangles={len(triangles)} lines={len(results)} "
|
||||||
|
f"selected_tiles={selected_tiles} bucket_cell_m={cell_size:.3f} "
|
||||||
|
f"coverage_min={min_coverage:.3f} sampled_height_m={sampled_min:.6f}.."
|
||||||
|
f"{sampled_max:.6f} amplitude_range_m={amplitude:.6f} "
|
||||||
|
f"longest_flat_run_m={longest_plateau:.6f} following_step_m={largest_step:.6f} "
|
||||||
|
f"max_normal_delta_deg={max_normal_jump:.6f} status={'PASS' if passed else 'FAIL'}")
|
||||||
|
for name, points, result in results:
|
||||||
|
print(f"[RingTerraceProfile] line={name} coverage={result['coverage']:.3f} "
|
||||||
|
f"range_m={result['height_range']:.6f} flat_step_runs={len(result['plateaus'])} "
|
||||||
|
f"max_normal_delta_deg={result['max_angle']:.6f}")
|
||||||
|
for run in result["plateaus"]:
|
||||||
|
print(f"[RingTerraceViolation] line={name} flat_start_m=({points[run[2]][0]:.3f},"
|
||||||
|
f"{points[run[2]][1]:.3f}) flat_run_m={run[0]:.6f} "
|
||||||
|
f"step_m={run[1]:.6f} from_height_m={run[5]:.6f} "
|
||||||
|
f"to_height_m={run[6]:.6f}")
|
||||||
|
print(f"[RingTerraceRSS] peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
raise SystemExit(0 if passed else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Encode the near/far tint-on/off L32 captures as compact JPEGs."""
|
||||||
|
import resource
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
SOURCE = ROOT / "Saved/Linux/l32"
|
||||||
|
DEST = Path(__file__).resolve().parent / "captures"
|
||||||
|
DEST.mkdir(parents=True, exist_ok=True)
|
||||||
|
captures = {
|
||||||
|
"fi-floor-near-tint-on": "cap-final-fi-near-on",
|
||||||
|
"fi-floor-near-tint-off": "cap-final-fi-near-off",
|
||||||
|
"fi-floor-far-tint-on": "cap-final-fi-far-on",
|
||||||
|
"fi-floor-far-tint-off": "cap-final-fi-far-off",
|
||||||
|
"sw-ceiling-near-tint-on": "cap-final-sw-near-on",
|
||||||
|
"sw-ceiling-near-tint-off": "cap-final-sw-near-off",
|
||||||
|
"sw-ceiling-far-tint-on": "cap-final-sw-far-on",
|
||||||
|
"sw-ceiling-far-tint-off": "cap-final-sw-far-off",
|
||||||
|
}
|
||||||
|
failed = False
|
||||||
|
for output_name, input_name in captures.items():
|
||||||
|
source = SOURCE / f"{input_name}.png"
|
||||||
|
destination = DEST / f"{output_name}.jpg"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", "ffmpeg",
|
||||||
|
"-y", "-hide_banner", "-loglevel", "error", "-i", str(source),
|
||||||
|
"-frames:v", "1", "-q:v", "4", str(destination),
|
||||||
|
]
|
||||||
|
result = subprocess.run(command, check=False)
|
||||||
|
size = destination.stat().st_size if destination.exists() else 0
|
||||||
|
ok = source.exists() and result.returncode == 0 and size <= 300_000
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[L32CaptureJPEG] source={source.name} output={destination.name} "
|
||||||
|
f"bytes={size} status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
print(f"[L32CaptureJPEG] peak_rss_kib="
|
||||||
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}", flush=True)
|
||||||
|
raise SystemExit(1 if failed else 0)
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Copy only tiles for one LOD and spatial window from a VFMSH scene."""
|
||||||
|
import argparse
|
||||||
|
import resource
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("source")
|
||||||
|
parser.add_argument("destination")
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--bounds", nargs=6, type=float, required=True,
|
||||||
|
metavar=("XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"))
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not 0 <= args.lod <= 8:
|
||||||
|
parser.error("--lod must be between 0 and 8")
|
||||||
|
x_min, x_max, y_min, y_max, z_min, z_max = args.bounds
|
||||||
|
if x_max <= x_min or y_max <= y_min or z_max <= z_min:
|
||||||
|
parser.error("bounds must have positive extents")
|
||||||
|
|
||||||
|
selected = []
|
||||||
|
with open(args.source, "rb") as src:
|
||||||
|
prefix = src.read(60)
|
||||||
|
if len(prefix) != 60 or prefix[:8] != b"VFMSH001":
|
||||||
|
raise SystemExit("source has an invalid VFMSH001 header")
|
||||||
|
version, voxel_um, tile_count = struct.unpack_from("<3I", prefix, 8)
|
||||||
|
if version != 1 or voxel_um == 0 or tile_count > 1_000_000:
|
||||||
|
raise SystemExit("source has an unsupported scene header")
|
||||||
|
for _ in range(tile_count):
|
||||||
|
header = src.read(36)
|
||||||
|
if len(header) != 36:
|
||||||
|
raise SystemExit("truncated tile header")
|
||||||
|
key = struct.unpack_from("<4i", header)
|
||||||
|
_, _, vertex_count, index_count, mask_count = struct.unpack_from("<5I", header, 16)
|
||||||
|
payload_offset = src.tell()
|
||||||
|
payload_size = vertex_count * 24 + index_count * 4 + mask_count
|
||||||
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
tx0, ty0, tz0 = key[0] * extent, key[1] * extent, key[2] * extent
|
||||||
|
intersects = (key[3] == args.lod and tx0 <= x_max and tx0 + extent >= x_min
|
||||||
|
and ty0 <= y_max and ty0 + extent >= y_min
|
||||||
|
and tz0 <= z_max and tz0 + extent >= z_min)
|
||||||
|
if intersects:
|
||||||
|
selected.append((header, payload_offset, payload_size))
|
||||||
|
src.seek(payload_size, 1)
|
||||||
|
if src.read(1):
|
||||||
|
raise SystemExit("source scene has trailing bytes")
|
||||||
|
|
||||||
|
if not selected:
|
||||||
|
raise SystemExit("no tiles matched the requested LOD and bounds")
|
||||||
|
with open(args.source, "rb") as src, open(args.destination, "wb") as dst:
|
||||||
|
prefix = bytearray(src.read(60))
|
||||||
|
struct.pack_into("<I", prefix, 16, len(selected))
|
||||||
|
dst.write(prefix)
|
||||||
|
for header, payload_offset, payload_size in selected:
|
||||||
|
dst.write(header)
|
||||||
|
src.seek(payload_offset)
|
||||||
|
remaining = payload_size
|
||||||
|
while remaining:
|
||||||
|
block = src.read(min(1 << 20, remaining))
|
||||||
|
if not block:
|
||||||
|
raise SystemExit("truncated tile payload")
|
||||||
|
dst.write(block)
|
||||||
|
remaining -= len(block)
|
||||||
|
print(f"[VFMeshLodWindow] source={args.source} lod={args.lod} "
|
||||||
|
f"selected_tiles={len(selected)} output={args.destination} "
|
||||||
|
f"output_bytes={__import__('os').path.getsize(args.destination)} "
|
||||||
|
f"peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss} status=PASS")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Stream VFMSH tiles and retain a focused, near-horizontal surface patch."""
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("source")
|
||||||
|
parser.add_argument("destination")
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, required=True, metavar=("X", "Y"))
|
||||||
|
parser.add_argument("--radius-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-m", nargs=2, type=float, required=True, metavar=("MIN", "MAX"))
|
||||||
|
parser.add_argument("--min-abs-normal-z", type=float, default=0.7)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.radius_m <= 0 or args.height_m[1] <= args.height_m[0]:
|
||||||
|
parser.error("radius and height interval must be positive")
|
||||||
|
if not 0.0 <= args.min_abs_normal_z <= 1.0:
|
||||||
|
parser.error("--min-abs-normal-z must be in [0,1]")
|
||||||
|
|
||||||
|
cx, cy = args.center
|
||||||
|
r2 = args.radius_m * args.radius_m
|
||||||
|
zmin, zmax = args.height_m
|
||||||
|
input_tiles = kept_tiles = kept_triangles = 0
|
||||||
|
with open(args.source, "rb") as src, open(args.destination, "wb+") as dst:
|
||||||
|
prefix = bytearray(src.read(60))
|
||||||
|
if len(prefix) != 60 or prefix[:8] != b"VFMSH001":
|
||||||
|
raise SystemExit("source has an invalid VFMSH001 header")
|
||||||
|
version, voxel_um, tile_count = struct.unpack_from("<3I", prefix, 8)
|
||||||
|
if version != 1 or voxel_um == 0 or tile_count > 1_000_000:
|
||||||
|
raise SystemExit("source has an unsupported scene header")
|
||||||
|
struct.pack_into("<I", prefix, 16, 0)
|
||||||
|
dst.write(prefix)
|
||||||
|
for _ in range(tile_count):
|
||||||
|
header = src.read(36)
|
||||||
|
if len(header) != 36:
|
||||||
|
raise SystemExit("truncated tile header")
|
||||||
|
key = struct.unpack_from("<4i", header)
|
||||||
|
surface_vertices, skirt_count, vertex_count, index_count, mask_count = \
|
||||||
|
struct.unpack_from("<5I", header, 16)
|
||||||
|
if vertex_count > 2_000_000 or index_count > 12_000_000:
|
||||||
|
raise SystemExit("tile exceeds the focused patch memory limit")
|
||||||
|
vertex_bytes = src.read(vertex_count * 24)
|
||||||
|
index_bytes = src.read(index_count * 4)
|
||||||
|
masks = src.read(mask_count)
|
||||||
|
if (len(vertex_bytes) != vertex_count * 24
|
||||||
|
or len(index_bytes) != index_count * 4 or len(masks) != mask_count):
|
||||||
|
raise SystemExit("truncated tile payload")
|
||||||
|
input_tiles += 1
|
||||||
|
positions = list(struct.iter_unpack("<3f", vertex_bytes[:vertex_count * 12]))
|
||||||
|
indices = struct.unpack(f"<{index_count}I", index_bytes) if index_count else ()
|
||||||
|
face_masks = masks if mask_count else bytes(index_count // 12)
|
||||||
|
filtered = []
|
||||||
|
for tri in range(index_count // 3):
|
||||||
|
i0, i1, i2 = indices[tri * 3:tri * 3 + 3]
|
||||||
|
if i0 >= vertex_count or i1 >= vertex_count or i2 >= vertex_count:
|
||||||
|
raise SystemExit("tile index points outside its vertex array")
|
||||||
|
if face_masks[tri] != 0:
|
||||||
|
continue
|
||||||
|
p0, p1, p2 = positions[i0], positions[i1], positions[i2]
|
||||||
|
mx = (p0[0] + p1[0] + p2[0]) / 3.0
|
||||||
|
my = (p0[1] + p1[1] + p2[1]) / 3.0
|
||||||
|
mz = (p0[2] + p1[2] + p2[2]) / 3.0
|
||||||
|
if (mx - cx) ** 2 + (my - cy) ** 2 > r2 or not zmin <= mz <= zmax:
|
||||||
|
continue
|
||||||
|
ab = (p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2])
|
||||||
|
ac = (p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2])
|
||||||
|
nz = ab[0] * ac[1] - ab[1] * ac[0]
|
||||||
|
length = math.sqrt((ab[1] * ac[2] - ab[2] * ac[1]) ** 2
|
||||||
|
+ (ab[2] * ac[0] - ab[0] * ac[2]) ** 2 + nz * nz)
|
||||||
|
if length == 0 or abs(nz) / length < args.min_abs_normal_z:
|
||||||
|
continue
|
||||||
|
filtered.extend((i0, i1, i2))
|
||||||
|
if not filtered:
|
||||||
|
continue
|
||||||
|
tile_header = struct.pack("<4i5I", *key, surface_vertices, 0,
|
||||||
|
vertex_count, len(filtered), 0)
|
||||||
|
dst.write(tile_header)
|
||||||
|
dst.write(vertex_bytes)
|
||||||
|
dst.write(struct.pack(f"<{len(filtered)}I", *filtered))
|
||||||
|
kept_tiles += 1
|
||||||
|
kept_triangles += len(filtered) // 3
|
||||||
|
dst.seek(16)
|
||||||
|
dst.write(struct.pack("<I", kept_tiles))
|
||||||
|
|
||||||
|
if kept_tiles == 0:
|
||||||
|
raise SystemExit("no surface triangles matched the requested patch")
|
||||||
|
print(f"[VFMeshSurfacePatch] source={args.source} input_tiles={input_tiles} "
|
||||||
|
f"tiles={kept_tiles} triangles={kept_triangles} center=({cx:.3f},{cy:.3f}) "
|
||||||
|
f"radius_m={args.radius_m:.3f} height_m={zmin:.3f}..{zmax:.3f} "
|
||||||
|
f"peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss} status=PASS")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Find VFMSH tiles intersecting a small region without reading mesh payloads."""
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("directory")
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--bounds", nargs=6, type=float, required=True,
|
||||||
|
metavar=("XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"))
|
||||||
|
args = parser.parse_args()
|
||||||
|
xmin, xmax, ymin, ymax, zmin, zmax = args.bounds
|
||||||
|
matches = 0
|
||||||
|
for path in sorted(glob.glob(os.path.join(args.directory, "*.vfmesh"))):
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
continue
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.seek(40, 1)
|
||||||
|
if version != 1:
|
||||||
|
continue
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
x0, y0, z0 = (key[axis] * extent for axis in range(3))
|
||||||
|
intersects = (key[3] == args.lod and index_count > 0
|
||||||
|
and x0 <= xmax and x0 + extent >= xmin
|
||||||
|
and y0 <= ymax and y0 + extent >= ymin
|
||||||
|
and z0 <= zmax and z0 + extent >= zmin)
|
||||||
|
if intersects:
|
||||||
|
matches += 1
|
||||||
|
print(f"[VFMeshTileWindow] scene={os.path.basename(path)} key={key} "
|
||||||
|
f"vertices={vertex_count} indices={index_count} "
|
||||||
|
f"extent_m={extent:.3f}")
|
||||||
|
stream.seek(vertex_count * 24 + index_count * 4 + mask_count, 1)
|
||||||
|
print(f"[VFMeshTileWindowSummary] matching_tiles={matches} "
|
||||||
|
f"peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Stream selected-LOD tiles and find face windows that pass the ring audit."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), required=True)
|
||||||
|
parser.add_argument("--height-min-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-max-m", type=float, required=True)
|
||||||
|
parser.add_argument("--preferred-height-m", type=float,
|
||||||
|
help="lock overlapping-face selection near this expected surface height")
|
||||||
|
parser.add_argument("--step-m", type=float, default=0.5)
|
||||||
|
parser.add_argument("--tile-key", nargs=3, type=int,
|
||||||
|
help="audit only this tile's x/y/z key (LOD is supplied separately)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
audit_path = Path(__file__).with_name("AuditRingTerraceL32.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("ring_audit_l32", audit_path)
|
||||||
|
audit = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(audit)
|
||||||
|
want_up = args.surface == "floor"
|
||||||
|
candidates = []
|
||||||
|
with open(args.scene, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit("invalid VFMSH001 scene")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.seek(40, 1)
|
||||||
|
if version != 1:
|
||||||
|
raise SystemExit("unsupported mesh scene")
|
||||||
|
for _ in range(tile_count):
|
||||||
|
header = stream.read(36)
|
||||||
|
key = struct.unpack("<4i", header[:16])
|
||||||
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", header[16:])
|
||||||
|
payload = vertex_count * 24 + index_count * 4 + mask_count
|
||||||
|
if key[3] != args.lod or (args.tile_key and tuple(key[:3]) != tuple(args.tile_key)):
|
||||||
|
stream.seek(payload, 1)
|
||||||
|
continue
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
triangles = []
|
||||||
|
vertex_margin = voxel_um / 1_000_000.0 * 0.5
|
||||||
|
for offset in range(0, index_count, 3):
|
||||||
|
if masks and masks[offset // 3]:
|
||||||
|
continue
|
||||||
|
ids = indices[offset:offset + 3]
|
||||||
|
if max(ids) >= surface_count:
|
||||||
|
continue
|
||||||
|
verts = []
|
||||||
|
for index in ids:
|
||||||
|
p = index * 3
|
||||||
|
verts.append(((positions[p], positions[p + 1], positions[p + 2]),
|
||||||
|
(normals[p], normals[p + 1], normals[p + 2])))
|
||||||
|
average_nz = sum(vertex[1][2] for vertex in verts) / 3.0
|
||||||
|
if (want_up and average_nz < 0.35) or (not want_up and average_nz > -0.35):
|
||||||
|
continue
|
||||||
|
z_values = [vertex[0][2] for vertex in verts]
|
||||||
|
mean_z = sum(z_values) / 3.0
|
||||||
|
if (mean_z < args.height_min_m or mean_z > args.height_max_m or
|
||||||
|
min(z_values) < args.height_min_m - vertex_margin or
|
||||||
|
max(z_values) > args.height_max_m + vertex_margin):
|
||||||
|
continue
|
||||||
|
triangles.append((key[3], average_nz, tuple(verts)))
|
||||||
|
if not triangles:
|
||||||
|
continue
|
||||||
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
cx, cy = (key[0] + 0.5) * extent, (key[1] + 0.5) * extent
|
||||||
|
radius = extent * 0.5
|
||||||
|
bounds = (cx - radius, cx + radius, cy - radius, cy + radius)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << key[3]))
|
||||||
|
buckets = audit.build_buckets(triangles, want_up, bounds, cell_size)
|
||||||
|
profiles = []
|
||||||
|
preferred_height = args.preferred_height_m
|
||||||
|
if preferred_height is None:
|
||||||
|
preferred_height = args.height_max_m if want_up else args.height_min_m
|
||||||
|
for _, points in audit.make_lines(cx, cy, radius, args.step_m):
|
||||||
|
profiles.append(audit.audit_profile(points, buckets, want_up,
|
||||||
|
0.5, 0.0005, 0.1, cell_size, preferred_height))
|
||||||
|
valid = [profile for profile in profiles if profile["valid"]]
|
||||||
|
coverage = min(profile["coverage"] for profile in profiles)
|
||||||
|
amplitude = max((profile["height_max"] for profile in valid), default=0.0) - \
|
||||||
|
min((profile["height_min"] for profile in valid), default=0.0)
|
||||||
|
longest = max((run[0] for profile in valid for run in profile["plateaus"]),
|
||||||
|
default=0.0)
|
||||||
|
max_normal = max((profile["max_angle"] for profile in valid), default=float("inf"))
|
||||||
|
passed = (len(valid) == len(profiles) and coverage >= 0.75 and amplitude >= 3.0
|
||||||
|
and longest < 0.5 and max_normal <= 20.0)
|
||||||
|
candidates.append((passed, amplitude, -max_normal, key, cx, cy,
|
||||||
|
len(triangles), coverage, longest, max_normal))
|
||||||
|
stream.seek(payload - (vertex_count * 24 + index_count * 4 + mask_count), 1)
|
||||||
|
candidates.sort(reverse=True, key=lambda item: (item[0], item[1], item[2]))
|
||||||
|
print(f"[RingWindowSearch] lod={args.lod} surface={args.surface} tiles={len(candidates)} "
|
||||||
|
f"height_window_m={args.height_min_m:.3f}..{args.height_max_m:.3f} "
|
||||||
|
f"step_m={args.step_m:.3f}")
|
||||||
|
for passed, amplitude, neg_normal, key, cx, cy, count, coverage, longest, normal in candidates[:40]:
|
||||||
|
print(f"[RingWindowCandidate] status={'PASS' if passed else 'FAIL'} key={key} "
|
||||||
|
f"center_m=({cx:.3f},{cy:.3f}) triangles={count} coverage={coverage:.3f} "
|
||||||
|
f"amplitude_m={amplitude:.6f} longest_flat_m={longest:.6f} "
|
||||||
|
f"normal_jump_deg={normal:.6f}")
|
||||||
|
print(f"[RingWindowRSS] peak_rss_kib="
|
||||||
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Find open SurfaceWorld columns below the resolved transition ceiling."""
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("probe")
|
||||||
|
parser.add_argument("owner")
|
||||||
|
parser.add_argument("output")
|
||||||
|
parser.add_argument("--z", type=float, default=-130.0)
|
||||||
|
parser.add_argument("--minimum", type=float, default=-96.0)
|
||||||
|
parser.add_argument("--maximum", type=float, default=96.0)
|
||||||
|
parser.add_argument("--spacing", type=float, default=16.0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
probe = str(Path(args.probe).resolve())
|
||||||
|
owner = str(Path(args.owner).resolve())
|
||||||
|
output = Path(args.output)
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
rows = []
|
||||||
|
count = round((args.maximum - args.minimum) / args.spacing)
|
||||||
|
for iy in range(count + 1):
|
||||||
|
y = args.minimum + iy * args.spacing
|
||||||
|
for ix in range(count + 1):
|
||||||
|
x = args.minimum + ix * args.spacing
|
||||||
|
command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
probe, owner, "surface-world", str(x), str(y),
|
||||||
|
str(args.z - 2.0), str(args.z + 2.0), "0.25"]
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=1800,
|
||||||
|
check=False)
|
||||||
|
samples = [line for line in result.stdout.splitlines()
|
||||||
|
if line.startswith(f"{args.z:.6f},")]
|
||||||
|
density = float(samples[0].split(",")[1]) if result.returncode == 0 and samples else float("nan")
|
||||||
|
rows.append((x, y, args.z, density, result.returncode))
|
||||||
|
print(f"[SurfaceWorldAirScan] x_m={x:.1f} y_m={y:.1f} z_m={args.z:.2f} "
|
||||||
|
f"density_vox={density:.6f} exit={result.returncode}", flush=True)
|
||||||
|
with output.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.writer(stream)
|
||||||
|
writer.writerow(("x_m", "y_m", "z_m", "density_vox", "exit_code"))
|
||||||
|
writer.writerows(rows)
|
||||||
|
open_samples = [row for row in rows if row[3] > 0.0]
|
||||||
|
print(f"[SurfaceWorldAirScanSummary] samples={len(rows)} open={len(open_samples)} "
|
||||||
|
f"output={output} status={'PASS' if open_samples else 'FAIL'}")
|
||||||
|
return 0 if open_samples else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#include "Json.h"
|
||||||
|
#include "OwnerWorldLoader.h"
|
||||||
|
#include "StandaloneCoreScheduler.h"
|
||||||
|
#include "WorldAssemblyPrototype.h"
|
||||||
|
#include "StandaloneSnapshotAdapter.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
int main(int argc,char** argv)
|
||||||
|
{
|
||||||
|
if(argc!=8)
|
||||||
|
{
|
||||||
|
std::fprintf(stderr,"usage: %s owner.json surface-world|floating-islands x_m y_m zmin_m zmax_m step_m\n",argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const std::string recipe=argv[2];
|
||||||
|
if(recipe!="surface-world"&&recipe!="floating-islands")return 2;
|
||||||
|
const double x=std::strtod(argv[3],nullptr),y=std::strtod(argv[4],nullptr);
|
||||||
|
const double zmin=std::strtod(argv[5],nullptr),zmax=std::strtod(argv[6],nullptr);
|
||||||
|
const double step=std::strtod(argv[7],nullptr);
|
||||||
|
if(!std::isfinite(x)||!std::isfinite(y)||!std::isfinite(zmin)||!std::isfinite(zmax)
|
||||||
|
||!std::isfinite(step)||zmax<=zmin||step<=0.0)return 2;
|
||||||
|
|
||||||
|
VFJson::Value root;
|
||||||
|
std::string error;
|
||||||
|
{
|
||||||
|
std::ifstream input(argv[1],std::ios::binary);
|
||||||
|
if(!input){std::fprintf(stderr,"owner JSON open failed\n");return 2;}
|
||||||
|
std::string text{std::istreambuf_iterator<char>(input),std::istreambuf_iterator<char>{}};
|
||||||
|
if(!VFJson::Parser(std::move(text)).Parse(root,error))
|
||||||
|
{std::fprintf(stderr,"owner JSON parse failed: %s\n",error.c_str());return 2;}
|
||||||
|
}
|
||||||
|
if(recipe=="floating-islands"
|
||||||
|
&&!StandaloneWorldAssemblyPrototype::SelectTopStrateAsFloatingIslands(root,error))
|
||||||
|
{std::fprintf(stderr,"FI selection failed: %s\n",error.c_str());return 2;}
|
||||||
|
auto owner=std::make_shared<FStandaloneOwnerWorld>();
|
||||||
|
if(!StandaloneOwnerLoader::Resolve(root,*owner,error))
|
||||||
|
{std::fprintf(stderr,"owner resolve failed: %s\n",error.c_str());return 2;}
|
||||||
|
VoxelForgeCore::WorldAssemblyManifest assembly;
|
||||||
|
if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
||||||
|
*owner,root,error,{},&assembly))
|
||||||
|
{std::fprintf(stderr,"assembly setup failed: %s\n",error.c_str());return 2;}
|
||||||
|
if(recipe=="floating-islands"
|
||||||
|
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
||||||
|
*owner,root,assembly,error))
|
||||||
|
{std::fprintf(stderr,"FI setup failed: %s\n",error.c_str());return 2;}
|
||||||
|
VoxelForgeCore::ResolvedLayoutSnapshotV3 resolvedLayout;
|
||||||
|
if(!StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
||||||
|
*owner,assembly,recipe,resolvedLayout,error))
|
||||||
|
{std::fprintf(stderr,"resolved layout build failed: %s\n",error.c_str());return 2;}
|
||||||
|
auto* rootObject=std::get_if<VFJson::Value::Object>(&root.Data);
|
||||||
|
if(!rootObject)return 2;
|
||||||
|
VFJson::Value layoutJson;
|
||||||
|
layoutJson.Data=resolvedLayout.ToJson();
|
||||||
|
(*rootObject)["resolved_layout_json"]=std::move(layoutJson);
|
||||||
|
auto snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(
|
||||||
|
root,owner->Seed,3,8,false,false,error);
|
||||||
|
if(!snapshot)
|
||||||
|
{std::fprintf(stderr,"snapshot resolve failed: %s\n",error.c_str());return 2;}
|
||||||
|
const auto* layout=snapshot.resolvedLayout();
|
||||||
|
if(!layout){std::fprintf(stderr,"resolved layout missing\n");return 2;}
|
||||||
|
for(const auto& stratum:layout->strates)
|
||||||
|
std::printf("[DensityStratum] order=%d strate=%d generator=%s mode=%s width_um=%lld bedrock_um=%lld..%lld ceiling_um=%lld..%lld\n",
|
||||||
|
stratum.order,stratum.strateIndex,stratum.generatorKind.c_str(),
|
||||||
|
stratum.transitionMode.c_str(),static_cast<long long>(stratum.transitionWidthMicrometres),
|
||||||
|
static_cast<long long>(stratum.bedrock.bottomMicrometres),
|
||||||
|
static_cast<long long>(stratum.bedrock.topMicrometres),
|
||||||
|
static_cast<long long>(stratum.ceiling.bottomMicrometres),
|
||||||
|
static_cast<long long>(stratum.ceiling.topMicrometres));
|
||||||
|
const auto interface=std::find_if(layout->strates.begin(),layout->strates.end(),
|
||||||
|
[](const auto& stratum){return stratum.order==0;});
|
||||||
|
if(interface==layout->strates.end())return 2;
|
||||||
|
|
||||||
|
VoxelForgeCore::World world(std::move(snapshot));
|
||||||
|
VoxelForgeCore::ShapeBuildRequest request;
|
||||||
|
request.minVoxel={static_cast<std::int32_t>(std::floor((x-32.0)*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::floor((y-32.0)*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::floor((zmin-4.0)*4.0))};
|
||||||
|
request.maxVoxel={static_cast<std::int32_t>(std::ceil((x+32.0)*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::ceil((y+32.0)*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::ceil((zmax+4.0)*4.0))};
|
||||||
|
request.clipMinVoxel=request.minVoxel;
|
||||||
|
request.clipMaxVoxel=request.maxVoxel;
|
||||||
|
request.workerLimit=12;
|
||||||
|
request.includeFarWorld=false;
|
||||||
|
FStandaloneCoreScheduler scheduler;
|
||||||
|
if(!world.compile(request,scheduler,error))
|
||||||
|
{std::fprintf(stderr,"shape compile failed: %s\n",error.c_str());return 2;}
|
||||||
|
|
||||||
|
std::printf("[DensityLine] recipe=%s mode=%s width_um=%lld seam_um=%lld band_top_um=%lld band_bottom_um=%lld xy_m=%.6f,%.6f core_voxel_m=0.25 status=PASS\n",
|
||||||
|
recipe.c_str(),interface->transitionMode.c_str(),
|
||||||
|
static_cast<long long>(interface->transitionWidthMicrometres),
|
||||||
|
static_cast<long long>(interface->bedrock.bottomMicrometres),
|
||||||
|
static_cast<long long>(interface->bedrock.topMicrometres),
|
||||||
|
static_cast<long long>(interface->bedrock.bottomMicrometres),x,y);
|
||||||
|
std::printf("z_m,density_vox,lattice_vox,unclamped_vox,dispatch\n");
|
||||||
|
const auto count=static_cast<std::size_t>(std::floor((zmax-zmin)/step+1.0e-9));
|
||||||
|
for(std::size_t i=0;i<=count;++i)
|
||||||
|
{
|
||||||
|
const double z=zmin+static_cast<double>(i)*step;
|
||||||
|
const VoxelForgeCore::Vec3d p{x*4.0,y*4.0,z*4.0};
|
||||||
|
const VoxelForgeCore::Vec3i lattice{
|
||||||
|
static_cast<std::int32_t>(std::llround(x*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::llround(y*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::llround(z*4.0))};
|
||||||
|
std::printf("%.6f,%.9f,%.9f,%.9f,%s\n",z,world.densityAt(p),
|
||||||
|
world.densityAtLattice(lattice),world.unclampedDensityAt(p),
|
||||||
|
world.densityDispatchPathAt(p).c_str());
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Dump a bounded surface line from a VFMSH scene using the ring-audit sampler."""
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import resource
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), required=True)
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, required=True)
|
||||||
|
parser.add_argument("--radius", type=float, required=True)
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--height-min-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-max-m", type=float, required=True)
|
||||||
|
parser.add_argument("--line", choices=("x", "y", "diagonal"), default="x")
|
||||||
|
parser.add_argument("--stride-m", type=float, default=8.0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
audit_path = Path(__file__).with_name("AuditRingTerraceL32.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("ring_audit_l32", audit_path)
|
||||||
|
audit = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(audit)
|
||||||
|
cx, cy = args.center
|
||||||
|
bounds = (cx - args.radius, cx + args.radius,
|
||||||
|
cy - args.radius, cy + args.radius)
|
||||||
|
voxel_um, triangles, tiles = audit.read_scene(args.scene, args.lod,
|
||||||
|
args.height_min_m, args.height_max_m, bounds)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << args.lod))
|
||||||
|
buckets = audit.build_buckets(triangles, args.surface == "floor", bounds, cell_size)
|
||||||
|
count = round(2.0 * args.radius / args.stride_m)
|
||||||
|
print(f"[SurfaceProfile] lod={args.lod} surface={args.surface} tiles={tiles} "
|
||||||
|
f"triangles={len(triangles)} line={args.line} stride_m={args.stride_m}")
|
||||||
|
print("index,x_m,y_m,z_m,nx,ny,nz")
|
||||||
|
for index in range(count + 1):
|
||||||
|
t = -args.radius + index * args.stride_m
|
||||||
|
if args.line == "x":
|
||||||
|
x, y = cx + t, cy
|
||||||
|
elif args.line == "y":
|
||||||
|
x, y = cx, cy + t
|
||||||
|
else:
|
||||||
|
x, y = cx + t / 2.0**0.5, cy + t / 2.0**0.5
|
||||||
|
preferred_height = args.height_max_m if args.surface == "floor" else args.height_min_m
|
||||||
|
hit = audit.sample_surface(buckets, x, y, args.surface == "floor", cell_size,
|
||||||
|
preferred_height)
|
||||||
|
if hit is None:
|
||||||
|
print(f"{index},{x:.3f},{y:.3f},nan,nan,nan,nan")
|
||||||
|
else:
|
||||||
|
z, normal = hit
|
||||||
|
print(f"{index},{x:.3f},{y:.3f},{z:.6f}," +
|
||||||
|
",".join(f"{value:.7f}" for value in normal))
|
||||||
|
print(f"[SurfaceProfileRSS] peak_rss_kib="
|
||||||
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare the v20 transition-column cache with the uncached v20 binary."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
BINARIES = {
|
||||||
|
"base": OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20",
|
||||||
|
"cache": OUT / "bin/transition-cache/VoxelForgeCoreHeadless-l32-transition-cache",
|
||||||
|
}
|
||||||
|
ORDER = (("base", "cache"), ("cache", "base"), ("base", "cache"),
|
||||||
|
("cache", "base"), ("base", "cache"))
|
||||||
|
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for pair, builds in enumerate(ORDER, 1):
|
||||||
|
for build in builds:
|
||||||
|
name = f"cache-compare-sw-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(BINARIES[build]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", "--recipe=surface-world",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matches = READY.findall(text)
|
||||||
|
valid = result is not None and result.returncode == 0 and len(matches) == 1
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else float("nan")
|
||||||
|
rows.append((pair, build, ready, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", valid))
|
||||||
|
print(f"pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True)
|
||||||
|
failed |= not valid
|
||||||
|
|
||||||
|
csv = OUT / "transition-cache-compare.csv"
|
||||||
|
csv.write_text(
|
||||||
|
"pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{pair},{build},{ready:.6f},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for pair, build, ready, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
for build in BINARIES:
|
||||||
|
values = [row[2] for row in rows if row[1] == build]
|
||||||
|
values.sort()
|
||||||
|
median = values[len(values) // 2]
|
||||||
|
print(f"[TransitionCacheCost] build={build} median_whole_view_ready_s={median:.3f}")
|
||||||
|
print(f"[TransitionCacheCost] runs={len(rows)} status={'FAIL' if failed else 'PASS'}")
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 10 else 0)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# VoxelForge L32 report
|
||||||
|
|
||||||
|
## Cause and fix
|
||||||
|
|
||||||
|
The old Gradient transition wrote constant solid-density values per band. Those values are not signed distance to the displaced boundary, so Marching Cubes crossed band levels as contour-like terraces. The hash surface quantization was not the cause: sampling across the final FI floor gives density -0.53345 at z=-121.50 m and +0.46655 at -121.25 m; the SW ceiling gives +0.01514 at -126.50 m and -0.98486 at -126.25 m. Both cross linearly at about 4 density units/m.
|
||||||
|
|
||||||
|
L32 restores the width/8 Gradient displacement (2 m each side, about 4 m peak-to-peak at 16 m owner width). Density is now signed distance to the union of the displaced solid intervals, continuous across band boundaries. Cap samples follow the same displaced faces. Passage-mouth fades use cubic smoothstep across a 2 m fade from the protected mouth radius; this preserves the face while blending the column flare. Version/evaluator is v20.
|
||||||
|
|
||||||
|
## Surface and interface audits
|
||||||
|
|
||||||
|
Branch-locked radial and straight profiles pass on both faces at LOD0 through LOD8. Every tested LOD has at least 3.15 m height range; no repeated-height plateau or following step was found. LOD0/1 profiles use 0.125 m samples (SW LOD1 also checked at 0.0625 m); LOD2–8 use 0.5 m samples. The largest adjacent-normal change was 17.50° on the fine SW LOD1 scan, below the 20° audit limit. Peak RSS for the final Python ring scans was 373,596 KiB (FI LOD0); the passage-fade scans peaked at 52,128 KiB. Mesh scans use tile/chunk processing.
|
||||||
|
|
||||||
|
Interleaved also passes 11 radial/straight profiles at each LOD0–8: all selected windows have full coverage, no flats or steps, and at most 0.367° adjacent-normal change. Their local height ranges are 0.032–3.174 m; the 3 m minimum is the Gradient amplitude check, not an Interleaved requirement. Peak RSS for this tile-streamed sweep was 24,468 KiB.
|
||||||
|
|
||||||
|
Both ring negative controls fail as intended: L30 has 0.742 m range, a 1.875 m flat run followed by a 0.241 m step, and a 27.34° normal jump; L31's flat fix has only 0.000046 m range. The passage-mouth audit passes on both overlapping faces: upper max normal change 15.06°, lower 7.96°, with full coverage and no flats or steps.
|
||||||
|
|
||||||
|
Hole/interface and minimum-band audits pass for Hard, Gradient and Interleaved, for both recipes and compilers: zero hole columns and zero air samples. Wider-than-declared and thinner-than-minimum controls are rejected. Minimum bands are 0.5 m FI, 1.0 m SW and 6.25 m lower band; maximum Interleaved crossing is 6.487 m against the 16 m bound. Passage audit passes all 16 recipe-stamped rows; recipe-mismatch and disconnected-end controls are detected.
|
||||||
|
|
||||||
|
Determinism passes 24/24: the L25 command and 24 m crossing, both recipes, Clang and GCC, workers 1/8/12.
|
||||||
|
|
||||||
|
## Captures and hashes
|
||||||
|
|
||||||
|
Tint-on and tint-off JPEGs are in `captures/`; all are under 30 KB (limit 300 KB):
|
||||||
|
|
||||||
|
- `fi-floor-near-tint-on.jpg`, `fi-floor-near-tint-off.jpg`
|
||||||
|
- `fi-floor-far-tint-on.jpg`, `fi-floor-far-tint-off.jpg`
|
||||||
|
- `sw-ceiling-near-tint-on.jpg`, `sw-ceiling-near-tint-off.jpg`
|
||||||
|
- `sw-ceiling-far-tint-on.jpg`, `sw-ceiling-far-tint-off.jpg`
|
||||||
|
|
||||||
|
They show the FI floor and SW ceiling from inside each stratum, with grazing light, at LOD0 and LOD8. Capture SHA256 values:
|
||||||
|
|
||||||
|
| Capture | SHA256 |
|
||||||
|
|---|---|
|
||||||
|
| fi-floor-far-tint-off.jpg | `239b6be70c177e267d2db95f206fdcdc8834779a7948fdaaddd136b4163d1f34` |
|
||||||
|
| fi-floor-far-tint-on.jpg | `3ecefd555275036a7045a541f02bb1873e22302495009dbd80b4ac58ce716b58` |
|
||||||
|
| fi-floor-near-tint-off.jpg | `847f10acfb420c2bbded61bdee7f578cde19469777b2ef4c5a0eba08b406611c` |
|
||||||
|
| fi-floor-near-tint-on.jpg | `4a5d3e3f8e8254639fa1840a2fb4497f2ab19f64bb4734e13abe3c3cfcfb4008` |
|
||||||
|
| sw-ceiling-far-tint-off.jpg | `f5ec099076c507bed5a703302d89b665e139804a8f8895a68c1d5705aabf9f61` |
|
||||||
|
| sw-ceiling-far-tint-on.jpg | `b82618814c8320be2de1ac53ba10e9ab4469299a8b53b776be32c6ca9a659988` |
|
||||||
|
| sw-ceiling-near-tint-off.jpg | `5789ce59a7748aa278c1b95edae45aec87721721260c1a3aefe6c4cc1e47cd7a` |
|
||||||
|
| sw-ceiling-near-tint-on.jpg | `aa4351c5effc5e93fc0879257fe056b3f4625e7842f5118f28588fb2c5c1afe2` |
|
||||||
|
|
||||||
|
Final Core binary SHA256:
|
||||||
|
|
||||||
|
- Clang: `052694a569c92ef407c25bd9a534c7123ef800f46733e5c428e965c32d905fdc`
|
||||||
|
- GCC: `c8656901ce18861f12168849165785937a2495233604314df9e005ed1e92afdd`
|
||||||
|
|
||||||
|
## Cost and edits
|
||||||
|
|
||||||
|
Five interleaved pairs per recipe against L31. Median whole-view-ready time was SW 1.723 s → 1.756 s (+1.92%) and FI 16.963 s → 17.221 s (+1.52%); paired results were mixed and within observed run noise. No Unreal files changed. The five edited files are the Core public header, `FarShellsInternal.h`, `ResolvedLayoutInternal.h`, `ShapeWorldInternal.h` and `LinuxOffscreenCapture.cpp`; Unreal compilation was not available here.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- The initial multi-LOD Python audit was stopped by system RAM pressure (reported peak unavailable). Subsequent mesh audits ran with CPU/address-space caps and streamed or selected tiles; peak RSS is recorded above. The first whole-mesh FI LOD8 candidate scan was interrupted and replaced by a targeted tile scan.
|
||||||
|
- Early ring-window searches and narrow-height audits selected the wrong one of two overlapping faces. The FI LOD3/4 branch probes measured 1.423 m amplitude / a 1.0 m flat run; the SW LOD1 low-face probe reached 20.19° normal change. Face-locked, broad-window scans replaced them and pass. The exploratory `ring-*`, `ringset-*`, `search-*` and `window-search-*` logs preserve rejected window candidates. The L30/L31 failures above are intentional negative controls.
|
||||||
|
- The first Interleaved layout/export probe omitted the runner's required streaming flags and exited with usage; the retry produced the LOD0–8 export. Its broad 0.5 m candidate scan applied the Gradient 3 m window heuristic and rejected LOD0–4 (one LOD1 candidate switched overlapping faces at 62.92°); selected branch-locked profiles at those LODs and passing LOD5–8 windows show no plateaus or steps. An unsupported `--help` probe also returned usage with exit 2.
|
||||||
|
- The exact-source passage audit at midpoint preference switched faces and reported a 47.79° jump; upper- and lower-face locked reruns pass. An early wrong-camera export selected zero tiles; the exact camera export was rerun. Initial multi-LOD capture meshes mixed neighboring shells, so the final captures use focused LOD0/LOD8 surface patches.
|
||||||
|
- One fine-normal probe had a tuple-index error and was corrected. A SW far-mesh filter had an argument/layout parsing error and was corrected. The density CSV was overwritten by verbose output; the probe was rebuilt from final Core objects and rerun.
|
||||||
|
- The transition-cache build failed on a nested `Shape` initializer; the retry built, but the cache showed no gain and was removed. `/usr/bin/time` was unavailable for one capture attempt; the capped renderer was rerun directly. The first determinism run was interrupted and resumed; the final matrix passes 24/24. One log-search command used unavailable `rg`; a capped streaming-Python scan replaced it.
|
||||||
|
- Performance experiments were rejected or superseded: initial transition path +36.11% SW/+7.18% FI, planar query +14.89%/+2.28%, 3x3 coverage +6.06%/+1.65%, 5-point coverage +4.04%/+2.03%, single-transition fast path +3.52%/+1.39%; cache trial was slower (2.188 s vs 2.171 s). The retained hash-prefix version is the five-pair result above. Logs and exports are under `Saved/Linux/l32/`.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged. Captures checked by me: the FI floor (near, tint off) is a smooth rolling surface with no rings or terraces;
|
||||||
|
the SW ceiling and far views are smooth. Gradient is restored at width/8 (about 4 m peak-to-peak) with signed-distance
|
||||||
|
density; the ring audit rejects both L30 (rings) and L31 (flat). The memory-pressure kill mid-round was resumed;
|
||||||
|
audits now run capped and tile-streamed.
|
||||||
|
- Cumulative SW cost vs L24 is roughly +5% after L28–L32 (features: columns, transitions, smooth faces). FI is ≈ +2%.
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the recipe-stamped L31 passage audit matrix and reject mislabeled runs."""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l32-gcc-v20",
|
||||||
|
}
|
||||||
|
MARKERS = (
|
||||||
|
"[ResolvedPassageAuditSummary]",
|
||||||
|
"[PassageOpenAirAuditSummary]",
|
||||||
|
"[PassageNaturalnessSummary]",
|
||||||
|
"[PassageNoLongBridgeAudit]",
|
||||||
|
"[PassageRockColumnAuditSummary]",
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for style in range(4):
|
||||||
|
data = ROOT / f"Saved/Linux/linux25-owner-{recipe}-style{style}.json"
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"passage-{recipe}-style{style}-{compiler}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate",
|
||||||
|
"--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
recipe_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
markers_ok = all(re.search(re.escape(marker) + r".*status=PASS", text)
|
||||||
|
for marker in MARKERS)
|
||||||
|
capsule_ok = bool(re.search(
|
||||||
|
r"^\[ResolvedPassageAudit\].* blocked=0 support_samples=\d+ "
|
||||||
|
r"unsupported=0 grade_failures=0 mouth_content=2/2 mouth_radius=2/2 "
|
||||||
|
r"walkability=PASS$", text, re.M))
|
||||||
|
island_route_ok = (recipe != "floating-islands" or bool(re.search(
|
||||||
|
r"^\[FloatingIslandRouteAudit\].* blocked=0 support_samples=\d+ "
|
||||||
|
r"unsupported=0 grade_failures=0 status=PASS$", text, re.M)))
|
||||||
|
binding_ok = bool(re.search(
|
||||||
|
r"\[ResolvedPassageAuditSummary\].*route_bindings=[1-9][0-9]* "
|
||||||
|
r"route_binding_failures=0 .*status=PASS", text))
|
||||||
|
walk_ok = "[Walkability] PASS" in text
|
||||||
|
if recipe == "floating-islands":
|
||||||
|
control_ok = "[PassageColumnDisconnectedEndControl]" in text and bool(
|
||||||
|
re.search(r"\[PassageColumnDisconnectedEndControl\].*detected=true", text))
|
||||||
|
else:
|
||||||
|
control_ok = True
|
||||||
|
ok = (result is not None and result.returncode == 0 and recipe_ok
|
||||||
|
and markers_ok and capsule_ok and island_route_ok and binding_ok
|
||||||
|
and walk_ok and control_ok)
|
||||||
|
actual_recipe = stamps[0][1] if len(stamps) == 1 else "MISSING"
|
||||||
|
rows.append((recipe, actual_recipe, style, compiler, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s exit={result.returncode if result else 'TIMEOUT'} "
|
||||||
|
f"recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"capsule={'PASS' if capsule_ok and island_route_ok else 'FAIL'} "
|
||||||
|
f"column_control={'PASS' if control_ok else 'FAIL'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
if not ok:
|
||||||
|
failed = True
|
||||||
|
|
||||||
|
summary = OUT / "passage-audit-matrix.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"requested_recipe,actual_recipe,style,compiler,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{requested},{actual},{style},{compiler},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for requested, actual, style, compiler, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
control_log = OUT / "recipe-mismatch-control.log"
|
||||||
|
control_owner = OUT / "recipe-mismatch-control-owner.json"
|
||||||
|
control_owner_data = json.loads(
|
||||||
|
(ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json").read_text(encoding="utf-8"))
|
||||||
|
top_owner_stratum = max(control_owner_data["strates"], key=lambda stratum: stratum["top_chunk_z"])
|
||||||
|
top_owner_stratum["asset"]["GeneratorType"] = 6 # ECaveGeneratorType::FloatingIslands
|
||||||
|
control_owner.write_text(json.dumps(control_owner_data, separators=(",", ":")), encoding="utf-8")
|
||||||
|
control_command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", str(BINARIES["clang"]),
|
||||||
|
str(control_owner),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", "--recipe=surface-world",
|
||||||
|
"--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
with control_log.open("w", encoding="utf-8") as stream:
|
||||||
|
control = subprocess.run(control_command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
control_text = control_log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
control_ok = (control.returncode == 2 and
|
||||||
|
"[WorldAssemblerRecipeAudit] requested=surface-world actual=floating-islands "
|
||||||
|
"top_generator=FloatingIslands " in control_text and
|
||||||
|
"floating_islands_stratum=present status=FAIL" in control_text)
|
||||||
|
print(f"[L32RecipeMismatchControl] requested=surface-world actual=floating-islands "
|
||||||
|
f"exit={control.returncode} detected={'true' if control_ok else 'false'} "
|
||||||
|
f"{'PASS' if control_ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L32PassageMatrix] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or not control_ok or len(rows) != 16 else 0)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run five interleaved whole-view pairs against the L31 binary."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
RUN_TAG = os.environ.get("VOXELFORGE_COST_TAG", "").strip()
|
||||||
|
BINARIES = {
|
||||||
|
"l31": ROOT / "Saved/Linux/l31/bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
"l32": Path(os.environ.get("VOXELFORGE_L32_COST_BINARY",
|
||||||
|
str(OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20"))),
|
||||||
|
}
|
||||||
|
ORDERS = (("l31", "l32"), ("l32", "l31"), ("l31", "l32"),
|
||||||
|
("l32", "l31"), ("l31", "l32"))
|
||||||
|
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for pair, order in enumerate(ORDERS, 1):
|
||||||
|
for build in order:
|
||||||
|
tag = f"-{RUN_TAG}" if RUN_TAG else ""
|
||||||
|
name = f"cost{tag}-{recipe}-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(BINARIES[build]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matches = READY.findall(text)
|
||||||
|
if build == "l32":
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
actual_recipe = stamps[0][1] if len(stamps) == 1 else "missing"
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
else:
|
||||||
|
configured = re.findall(r"^\[HeadlessConfig\].*\brecipe=([^\s]+)", text, re.M)
|
||||||
|
actual_recipe = configured[0] if len(configured) == 1 else "missing"
|
||||||
|
floating_plan = "[FloatingIslandWorldAssembly]" in text
|
||||||
|
surface_plan = "[WorldAssemblerPrototype]" in text
|
||||||
|
plan_matches = ((actual_recipe == "floating-islands" and floating_plan)
|
||||||
|
or (actual_recipe == "surface-world" and not floating_plan
|
||||||
|
and surface_plan))
|
||||||
|
marker = (f"[L32BaselineRecipeAudit] requested={recipe} actual={actual_recipe} "
|
||||||
|
f"headless_recipe={configured[0] if len(configured) == 1 else 'missing'} "
|
||||||
|
f"floating_island_plan={'present' if floating_plan else 'absent'} "
|
||||||
|
f"surface_world_plan={'present' if surface_plan else 'absent'} "
|
||||||
|
f"status={'PASS' if actual_recipe == recipe and plan_matches else 'FAIL'}\n")
|
||||||
|
with logfile.open("a", encoding="utf-8") as stream:
|
||||||
|
stream.write(marker)
|
||||||
|
text += marker
|
||||||
|
stamp_ok = actual_recipe == recipe and plan_matches and len(configured) == 1
|
||||||
|
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||||
|
and "hash=off" in text and "cache_hits=0" in text and stamp_ok)
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else float("nan")
|
||||||
|
rows.append((recipe, pair, build, ready, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", valid))
|
||||||
|
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True)
|
||||||
|
failed |= not valid
|
||||||
|
|
||||||
|
summary = OUT / (f"cost-runs-{RUN_TAG}.csv" if RUN_TAG else "cost-runs.csv")
|
||||||
|
summary.write_text(
|
||||||
|
"recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for recipe, pair, build, ready, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build]
|
||||||
|
for build in BINARIES}
|
||||||
|
l31 = statistics.median(by_build["l31"])
|
||||||
|
l32 = statistics.median(by_build["l32"])
|
||||||
|
paired = []
|
||||||
|
for pair in range(1, 6):
|
||||||
|
values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair}
|
||||||
|
paired.append((values["l32"] - values["l31"]) / values["l31"] * 100.0)
|
||||||
|
print(f"[L32CostSummary] recipe={recipe} pairs=5 median_l31_s={l31:.3f} "
|
||||||
|
f"median_l32_s={l32:.3f} median_change_pct={(l32/l31-1)*100.0:+.2f} "
|
||||||
|
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}")
|
||||||
|
|
||||||
|
print(f"[L32Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}",
|
||||||
|
flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 20 else 0)
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check the L25 hash command and 24 m crossing across compilers and workers."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
RUN_TAG = os.environ.get("VOXELFORGE_DETERMINISM_TAG", "").strip()
|
||||||
|
RUN_RESUME = os.environ.get("VOXELFORGE_DETERMINISM_RESUME", "0") == "1"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": Path(os.environ.get("VOXELFORGE_L32_CLANG_BINARY",
|
||||||
|
str(OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20"))),
|
||||||
|
"gcc": Path(os.environ.get("VOXELFORGE_L32_GCC_BINARY",
|
||||||
|
str(OUT / "bin/VoxelForgeCoreHeadless-l32-gcc-v20"))),
|
||||||
|
}
|
||||||
|
HASH_PATTERN = re.compile(
|
||||||
|
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
candidate_reference = {}
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
prior_elapsed = {}
|
||||||
|
if RUN_RESUME and RUN_TAG:
|
||||||
|
prior_log = OUT / f"determinism-{RUN_TAG}-runner.log"
|
||||||
|
if prior_log.exists():
|
||||||
|
for line in prior_log.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||||
|
match = re.match(r"^(\S+) elapsed=([0-9.]+)s .* PASS$", line)
|
||||||
|
if match:
|
||||||
|
prior_elapsed[match.group(1)] = float(match.group(2))
|
||||||
|
|
||||||
|
|
||||||
|
def run(kind, compiler, recipe, workers):
|
||||||
|
global failed
|
||||||
|
tag = f"-{RUN_TAG}" if RUN_TAG else ""
|
||||||
|
name = f"{kind}{tag}-{compiler}-{recipe}-w{workers}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(BINARIES[compiler]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
"--far-horizon-lod8-step=0", f"--workers={workers}", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
if kind == "crossing24":
|
||||||
|
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||||
|
if RUN_RESUME and name in prior_elapsed and logfile.exists():
|
||||||
|
result = subprocess.CompletedProcess(command, 0)
|
||||||
|
elapsed = prior_elapsed[name]
|
||||||
|
else:
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
hashes = HASH_PATTERN.findall(text)
|
||||||
|
key = (kind, recipe)
|
||||||
|
reference = candidate_reference.get(key)
|
||||||
|
stable = reference is None or hashes == reference
|
||||||
|
if reference is None and hashes:
|
||||||
|
candidate_reference[key] = hashes
|
||||||
|
ok = (result is not None and result.returncode == 0
|
||||||
|
and stamps == [(recipe, recipe, "PASS")]
|
||||||
|
and "hash=on" in text and len(hashes) == 12 and stable)
|
||||||
|
rows.append((kind, recipe, compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", len(hashes), ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"hashes={len(hashes)} stable_across_matrix={'yes' if stable else 'no'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
for kind in ("l25", "crossing24"):
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
run(kind, compiler, recipe, workers)
|
||||||
|
|
||||||
|
summary = OUT / (f"determinism-{RUN_TAG}.csv" if RUN_TAG else "determinism.csv")
|
||||||
|
summary.write_text(
|
||||||
|
"kind,recipe,compiler,workers,elapsed_seconds,exit_code,hash_rows,status\n" +
|
||||||
|
"".join(f"{kind},{recipe},{compiler},{workers},{elapsed:.3f},{code},{count},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for kind, recipe, compiler, workers, elapsed, code, count, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L32Determinism] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 24 else 0)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run L31 route/proof and transition invariants with recipe-stamped logs."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l32-gcc-v20",
|
||||||
|
}
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, binary, data, recipe, extra, checks):
|
||||||
|
global failed
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate",
|
||||||
|
"--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}", *extra,
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
checks_ok = all(check(text) for check in checks)
|
||||||
|
ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok
|
||||||
|
rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
owner = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[RouteParityAudit\].*status=PASS$", text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))])
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for mode in ("hard", "gradient", "interleaved"):
|
||||||
|
name = f"invariant-transition-{recipe}-{mode}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||||
|
text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))])
|
||||||
|
|
||||||
|
summary = OUT / "invariant-audits.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"run,recipe,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for name, recipe, elapsed, code, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L32InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 16 else 0)
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Stream one VFMSH tile at a time to locate LOD/interface patches."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), required=True)
|
||||||
|
parser.add_argument("--height-min-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-max-m", type=float, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
candidates = []
|
||||||
|
scanned = 0
|
||||||
|
with open(args.scene, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit("invalid VFMSH001 scene")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.seek(40, 1)
|
||||||
|
if version != 1:
|
||||||
|
raise SystemExit("unsupported mesh scene")
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
payload = vertex_count * 24 + index_count * 4 + mask_count
|
||||||
|
if key[3] != args.lod:
|
||||||
|
stream.seek(payload, 1)
|
||||||
|
continue
|
||||||
|
scanned += 1
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
hits = 0
|
||||||
|
sum_x = sum_y = sum_z = 0.0
|
||||||
|
min_z, max_z = float("inf"), float("-inf")
|
||||||
|
want_up = args.surface == "floor"
|
||||||
|
for offset in range(0, index_count, 3):
|
||||||
|
if masks and masks[offset // 3]:
|
||||||
|
continue
|
||||||
|
a, b, c = indices[offset:offset + 3]
|
||||||
|
if max(a, b, c) >= surface_count:
|
||||||
|
continue
|
||||||
|
nz = (normals[a * 3 + 2] + normals[b * 3 + 2] + normals[c * 3 + 2]) / 3.0
|
||||||
|
if (nz > 0) != want_up:
|
||||||
|
continue
|
||||||
|
za, zb, zc = positions[a * 3 + 2], positions[b * 3 + 2], positions[c * 3 + 2]
|
||||||
|
if ((za + zb + zc) / 3.0 < args.height_min_m
|
||||||
|
or (za + zb + zc) / 3.0 > args.height_max_m):
|
||||||
|
continue
|
||||||
|
hits += 1
|
||||||
|
sum_x += (positions[a * 3] + positions[b * 3] + positions[c * 3]) / 3.0
|
||||||
|
sum_y += (positions[a * 3 + 1] + positions[b * 3 + 1] + positions[c * 3 + 1]) / 3.0
|
||||||
|
z = (za + zb + zc) / 3.0
|
||||||
|
sum_z += z
|
||||||
|
min_z, max_z = min(min_z, za, zb, zc), max(max_z, za, zb, zc)
|
||||||
|
if hits:
|
||||||
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
candidates.append((hits, key, sum_x / hits, sum_y / hits,
|
||||||
|
sum_z / hits, min_z, max_z, extent))
|
||||||
|
candidates.sort(reverse=True, key=lambda item: item[0])
|
||||||
|
print(f"[VFMeshTileScan] lod={args.lod} surface={args.surface} scanned_tiles={scanned} "
|
||||||
|
f"candidate_tiles={len(candidates)} peak_rss_kib="
|
||||||
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
for hits, key, x, y, z, low, high, extent in candidates[:40]:
|
||||||
|
print(f"[VFMeshTile] key={key} triangles={hits} centroid_m=({x:.3f},{y:.3f},{z:.3f}) "
|
||||||
|
f"z_m={low:.3f}..{high:.3f} extent_m={extent:.3f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build owner-like FloatingIslands -> SurfaceWorld -> TunnelNetwork fixtures."""
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
SOURCE = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
OUT = ROOT / "Saved/Linux/l33/fixtures"
|
||||||
|
|
||||||
|
|
||||||
|
def make_fixture(style: int) -> Path:
|
||||||
|
world = json.loads(SOURCE.read_text(encoding="utf-8"))
|
||||||
|
source_tunnel, source_surface = world["strates"]
|
||||||
|
|
||||||
|
floating = copy.deepcopy(source_tunnel)
|
||||||
|
floating.update(index=0, top_chunk_z=0, bottom_chunk_z=-15, height_chunks=16)
|
||||||
|
floating["asset"]["StrateName"] = "Linux33 Upper FloatingIslands"
|
||||||
|
floating["asset"]["StrateHeightInChunks"] = 16
|
||||||
|
floating["asset"]["GeneratorType"] = 6
|
||||||
|
|
||||||
|
surface = copy.deepcopy(source_surface)
|
||||||
|
surface.update(index=1, top_chunk_z=-16, bottom_chunk_z=-79, height_chunks=64)
|
||||||
|
surface["asset"]["StrateName"] = "Linux33 Middle SurfaceWorld"
|
||||||
|
surface["asset"]["StrateHeightInChunks"] = 64
|
||||||
|
surface["asset"]["PassageConfig"]["Style"] = style
|
||||||
|
surface["passage_config"]["Style"] = style
|
||||||
|
|
||||||
|
tunnel = copy.deepcopy(source_tunnel)
|
||||||
|
tunnel.update(index=2, top_chunk_z=-80, bottom_chunk_z=-143, height_chunks=64)
|
||||||
|
tunnel["asset"]["StrateName"] = "Linux33 Lower TunnelNetwork"
|
||||||
|
tunnel["asset"]["StrateHeightInChunks"] = 64
|
||||||
|
|
||||||
|
world["strates"] = [floating, surface, tunnel]
|
||||||
|
world["total_strates"] = 3
|
||||||
|
world["settings_asset"]["TotalStrates"] = 3
|
||||||
|
world["settings_asset"]["StratePool"] = [
|
||||||
|
"/Standalone/Linux33/UpperFloatingIslands",
|
||||||
|
"/Standalone/Linux33/MiddleSurfaceWorld",
|
||||||
|
"/Standalone/Linux33/LowerTunnelNetwork",
|
||||||
|
]
|
||||||
|
world["passages"] = [] # The L23 legacy coordinates are not part of this resolved stack.
|
||||||
|
world["source"] = "VoxelForge Linux33 owner-like SurfaceWorld-upper fixture"
|
||||||
|
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
path = OUT / f"sw-upper-tunnel-network-style{style}.json"
|
||||||
|
path.write_text(json.dumps(world, separators=(",", ":")), encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for style in range(4):
|
||||||
|
path = make_fixture(style)
|
||||||
|
print(f"style={style} fixture={path}")
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export and render two bounded SurfaceWorld departure views."""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l33"
|
||||||
|
FIXTURE = OUT / "fixtures/sw-upper-tunnel-network-style0.json"
|
||||||
|
HEADLESS = OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final"
|
||||||
|
CAPTURE = OUT / "bin/VoxelForgeOffscreenCapture-clang-l33-final"
|
||||||
|
PNG_DIR = OUT / "capture-png"
|
||||||
|
DEST = Path(__file__).resolve().parent / "captures"
|
||||||
|
PNG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
DEST.mkdir(parents=True, exist_ok=True)
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["LP_NUM_THREADS"] = "12"
|
||||||
|
env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||||
|
|
||||||
|
views = (
|
||||||
|
{
|
||||||
|
"name": "sw-departure-mouth",
|
||||||
|
"export_camera": "18.75,-12,-499,0,-22",
|
||||||
|
"render_camera": "18.75,0,-490",
|
||||||
|
"yaw": "0",
|
||||||
|
"pitch": "-90",
|
||||||
|
"fov": "38",
|
||||||
|
"height_center": "-503.166",
|
||||||
|
"height_range": "4",
|
||||||
|
"light": "0,-1,0.2",
|
||||||
|
"headlamp": "off",
|
||||||
|
"clip": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sw-departure-rock-entry",
|
||||||
|
"export_camera": "26,-16,-499,64,-16",
|
||||||
|
"render_camera": "14,-4,-500",
|
||||||
|
"yaw": "78",
|
||||||
|
"pitch": "-16",
|
||||||
|
"fov": "32",
|
||||||
|
"height_center": "-503.166",
|
||||||
|
"height_range": "4",
|
||||||
|
"light": "0,-1,0.2",
|
||||||
|
"headlamp": "off",
|
||||||
|
"clip": "1,0,0,-18.75",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, command):
|
||||||
|
logfile = OUT / f"capture-{name}.log"
|
||||||
|
wrapped = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s", *command,
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(wrapped, cwd=ROOT, env=env, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||||
|
print(f"[L33CaptureRun] step={name} elapsed={time.monotonic()-started:.1f}s "
|
||||||
|
f"exit={result.returncode} log={logfile}", flush=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise SystemExit(f"{name} failed; see {logfile}")
|
||||||
|
|
||||||
|
|
||||||
|
for view in views:
|
||||||
|
name = view["name"]
|
||||||
|
mesh = OUT / f"{name}.vfmesh"
|
||||||
|
png = PNG_DIR / f"{name}.png"
|
||||||
|
jpg = DEST / f"{name}.jpg"
|
||||||
|
run(f"{name}-export", [
|
||||||
|
str(HEADLESS), str(FIXTURE), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||||
|
"--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0",
|
||||||
|
"--recipe=floating-islands",
|
||||||
|
f"--camera={view['export_camera']}", f"--mesh-export={mesh}",
|
||||||
|
])
|
||||||
|
command = [
|
||||||
|
str(CAPTURE), str(mesh), str(png), "--size=1600x900", f"--fov-deg={view['fov']}",
|
||||||
|
f"--camera-m={view['render_camera']}", f"--yaw-deg={view['yaw']}",
|
||||||
|
f"--pitch-deg={view['pitch']}", "--shade=readable",
|
||||||
|
f"--headlamp={view['headlamp']}", f"--light-direction={view['light']}",
|
||||||
|
"--height-tint=on", f"--height-center-m={view['height_center']}",
|
||||||
|
f"--height-range-m={view['height_range']}", "--tile-edges=off",
|
||||||
|
]
|
||||||
|
if view["clip"]:
|
||||||
|
command.append(f"--clip-plane={view['clip']}")
|
||||||
|
run(f"{name}-render", command)
|
||||||
|
run(f"{name}-jpeg", [
|
||||||
|
"ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y",
|
||||||
|
"-i", str(png), "-frames:v", "1", "-q:v", "4", str(jpg),
|
||||||
|
])
|
||||||
|
size = jpg.stat().st_size if jpg.exists() else 0
|
||||||
|
ok = 0 < size <= 300_000
|
||||||
|
print(f"[L33CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||||
|
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
if not ok:
|
||||||
|
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB")
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check representative owner hashes against L32 before the full matrix."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l33"
|
||||||
|
L32 = ROOT / "Saved/Linux/l32"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-gcc-l33-final",
|
||||||
|
}
|
||||||
|
HASH_PATTERN = re.compile(
|
||||||
|
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
name = f"owner-hash-preflight-{compiler}-{recipe}-w12"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
"--far-horizon-lod8-step=0", "--workers=12", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
hashes = HASH_PATTERN.findall(text)
|
||||||
|
baseline_path = L32 / f"l25-settled-final-{compiler}-{recipe}-w12.log"
|
||||||
|
baseline = HASH_PATTERN.findall(baseline_path.read_text(
|
||||||
|
encoding="utf-8", errors="replace")) if baseline_path.exists() else []
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
same = len(hashes) == 12 and hashes == baseline
|
||||||
|
ok = (result is not None and result.returncode == 0 and same
|
||||||
|
and stamps == [(recipe, recipe, "PASS")])
|
||||||
|
failed |= not ok
|
||||||
|
print(f"{name} elapsed={time.monotonic()-started:.1f}s hashes={len(hashes)}/12 "
|
||||||
|
f"same_as_l32={'yes' if same else 'no'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
raise SystemExit(1 if failed else 0)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Linux33 report
|
||||||
|
|
||||||
|
## Upper SurfaceWorld route
|
||||||
|
|
||||||
|
The old fixed candidate set used 64 m chords, 2 m waypoint spacing and 16 m centre spacing. On the refreshed upper-stack fixture, the legacy replay measured 366/648 candidates above the 15° grade limit; its worst segment was 79.971° at XY (−84,−48) to (−82,−48). These were long paths over steep terrain relief near the outer search area. L23’s earlier fixture reported 648/648 grade rejects; the refreshed fixture gives a different count. The route Z samples are terrain-ground heights (about −503 m at the chosen mouth), not a target at the bottom of the 499 m strate. There was no unit or bedrock-reference error.
|
||||||
|
|
||||||
|
The planner now caps chords at 58.5 m for the 24 m arrival and 37.5 m departure radii, searches six lengths and places route centres on an 8 m grid, with a proximity cost favouring the origin. It found a 21.5 m walk from (−2.75,0) to departure (18.75,0), average grade about 4.2°. The SW passage starts at the ground floor (−503.166 m), with a 1.5 m portal blending to a 3.5 m tunnel over 9 m, then descends through SW rock. The first tunnel point is at (32.5,0), floor −505.637 m for style 0.
|
||||||
|
|
||||||
|
## Upper-stack fixture audits
|
||||||
|
|
||||||
|
Fixture: FloatingIslands → SurfaceWorld → TunnelNetwork; recipe stamp `floating-islands`; layout identity `resolved-layout-v3-fnv64:fc4e73619037f88c`. Clang and GCC passed every row for every style.
|
||||||
|
|
||||||
|
| Passage style | Floor chord deviation (voxels) | Passage and route audits | Open air | Naturalness | Bridge / columns | Bindings / walkability |
|
||||||
|
|---:|---:|---|---|---|---|---|
|
||||||
|
| 0 | 2.081 | 2/2 each pass | 0 violations | 2/2 pass | pass / 0 failures | 0 failures / pass |
|
||||||
|
| 1 | 10.901 | 2/2 each pass | 0 violations | 2/2 pass | pass / 0 failures | 0 failures / pass |
|
||||||
|
| 2 | 14.822 | 2/2 each pass | 0 violations | 2/2 pass | pass / 0 failures | 0 failures / pass |
|
||||||
|
| 3 | 16.374 | 2/2 each pass | 0 violations | 2/2 pass | pass / 0 failures | 0 failures / pass |
|
||||||
|
|
||||||
|
All rows are recipe-stamped and passed on both compilers. Capsule blocking, unsupported samples and grade failures were zero; mouth-content and mouth-radius checks were 2/2. This includes open-air, naturalness, no-long-bridge, rock-column wall/support, route binding and walkability audits.
|
||||||
|
|
||||||
|
## FloorProfileBlend
|
||||||
|
|
||||||
|
The old 2-voxel check measured maximum curve-to-straight-chord deviation, which grows with valid curvature and does not measure a seam. The check now compares endpoint height and tangent residuals. All styles have 0.00000000 voxel height residual and 0.00000000 voxel/m tangent residual (limits 0.010 and 0.001). Negative controls detected a 3-voxel step and a 0.25 voxel/m tangent step. Verdict: check corrected; no floor geometry change was needed.
|
||||||
|
|
||||||
|
## Captures
|
||||||
|
|
||||||
|
Both 1600×900 JPEGs are under 300 KB. The first shows the SW ground mouth; the second is a cutaway through the rock entry.
|
||||||
|
|
||||||
|
Sizes and SHA-256: mouth 26,939 B, `561d3a1e334ea3a3115fb781a1130ec850d8777b74c1be3704c70b865878640a`; rock entry 37,572 B, `f85d150bf44cd2cdae1ee49de023ad9e4c657b8fa7e8d8f8b93abc86586ad6e5`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Hashes, determinism and cost
|
||||||
|
|
||||||
|
Owner-world hashes match L32 exactly: all 12 hash lines per run across Clang/GCC and both recipes passed the owner preflight; the L25 and 24 m crossing matrix also passed at W1/W8/W12 (24/24 owner rows). W12 whole-view hashes: SurfaceWorld density `953eb1df1705f277`, mesh `d8006cb43f44aa5b`, payload `9217bff766970de5`, topology `83e3aa6f3a063e60`; FloatingIslands density `806537cdd98f8afb`, mesh `a4b06dd4d260943d`, payload `763a99aa04a73eae`, topology `baa37d80d3620d87`. The upper fixture was stable across W1/W8/W12 on both compilers. Total determinism: 30/30. Invariant audits: 16/16.
|
||||||
|
|
||||||
|
Five interleaved W12 cost pairs per recipe against L32: median whole-view ready time changed +0.99% for SurfaceWorld and +0.41% for FloatingIslands; paired medians were +0.57% and +0.41%, within run noise. Recipe/evaluator versions remain 2/20. No Unreal files changed; no Unreal build was run. No version bump was needed because the owner hashes are unchanged.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- CLI setup rejected `--compiler clang`; the accepted form was `--compiler=clang++-19`. One shell quoting attempt ended at EOF. A FloatingIslands fixture initially had a `surface-world` recipe stamp and was regenerated with `floating-islands`.
|
||||||
|
- A translated ring centre failed the strict placement disk and was reverted. Fixture audit iterations: r1 had style 1–3 failures (TN capsule pinch, route undercut, naturalness); r2 had 141 open-air violations plus route/naturalness failures; r3 had strict ring-placement and route-support failures; r4 had four edge open-air samples. The broad column-window fix passed passage audits but changed owner hashes. Its first scoped retry dropped supported bracket samples and failed SurfaceWorld hash preflight on both compilers; restoring the original non-SW bracket bounds fixed it.
|
||||||
|
- An intermediate Clang build failed on `coreVoxelSizeMetres` and `gradeSegments`; both names were corrected. Final Clang 19 and GCC 14 `--fp-contract=off`, `-j12` builds succeeded. An early determinism run on stale binaries was stopped after 21 rows and showed the interim hash mismatch; the final binaries passed all 30 rows.
|
||||||
|
- The first capture export included the far ring and grew to 2.49 GB; it was stopped before rendering. The LOD0 streamed export reduced the scene to about 2.2 MB. Capture framings r5–r8 rendered successfully but obscured the mouth; r9 is the inspected final framing.
|
||||||
|
|
||||||
|
No run exceeded 30 minutes and no step remained stuck for 15 minutes. Logs and exports are in `Saved/Linux/l33/`; no commit, push or stash was made.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged: the owner world is unchanged (hashes equal L32); SW-as-upper now plans its route and passes the full audit set
|
||||||
|
on the FI→SW→TN fixture. The FloorProfileBlend check now measures seams (endpoint height/tangent residuals) with
|
||||||
|
step/tangent negative controls. Accepted: chord deviation measured curvature, not a defect.
|
||||||
|
- Captures: `sw-departure-mouth.jpg` is out of focus and unreadable; `sw-departure-rock-entry.jpg` shows a natural opening
|
||||||
|
but **horizontal step-like ledges inside**, reminiscent of the L23 terraces. L34 checks passage mouths and entries for
|
||||||
|
stepping across styles and fixes the blurry capture setup.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run five interleaved W12 whole-view pairs against the L32 binary."""
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l33"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"l32": ROOT / "Saved/Linux/l32/bin/VoxelForgeCoreHeadless-l32-clang-v20",
|
||||||
|
"l33": OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final",
|
||||||
|
}
|
||||||
|
ORDERS = (("l32", "l33"), ("l33", "l32"), ("l32", "l33"),
|
||||||
|
("l33", "l32"), ("l32", "l33"))
|
||||||
|
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for pair, order in enumerate(ORDERS, 1):
|
||||||
|
for build in order:
|
||||||
|
name = f"cost-{recipe}-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(BINARIES[build]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matches = READY.findall(text)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||||
|
and "hash=off" in text and "cache_hits=0" in text and stamp_ok)
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else float("nan")
|
||||||
|
rows.append((recipe, pair, build, ready, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", valid))
|
||||||
|
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True)
|
||||||
|
failed |= not valid
|
||||||
|
|
||||||
|
summary = OUT / "cost-runs.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for recipe, pair, build, ready, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build]
|
||||||
|
for build in BINARIES}
|
||||||
|
baseline = statistics.median(by_build["l32"])
|
||||||
|
current = statistics.median(by_build["l33"])
|
||||||
|
paired = []
|
||||||
|
for pair in range(1, 6):
|
||||||
|
values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair}
|
||||||
|
paired.append((values["l33"] - values["l32"]) / values["l32"] * 100.0)
|
||||||
|
print(f"[L33CostSummary] recipe={recipe} pairs=5 median_l32_s={baseline:.3f} "
|
||||||
|
f"median_l33_s={current:.3f} median_change_pct={(current/baseline-1)*100.0:+.2f} "
|
||||||
|
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L33Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}",
|
||||||
|
flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 20 else 0)
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run owner L25 hashes, 24 m crossings, and upper-SurfaceWorld fixture determinism."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l33"
|
||||||
|
L32 = ROOT / "Saved/Linux/l32"
|
||||||
|
FIXTURE = OUT / "fixtures/sw-upper-tunnel-network-style0.json"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-gcc-l33-final",
|
||||||
|
}
|
||||||
|
HASH_PATTERN = re.compile(
|
||||||
|
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
LAYOUT_PATTERN = re.compile(
|
||||||
|
r"^\[ResolvedLayoutSnapshot\] schema=\d+ stable_identity=([^ ]+) strata=\d+ bytes=\d+ round_trip=PASS$",
|
||||||
|
re.M)
|
||||||
|
PLAN_PATTERN = re.compile(
|
||||||
|
r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*"
|
||||||
|
r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", re.M)
|
||||||
|
ROUTE_PATTERN = re.compile(
|
||||||
|
r"^\[WorldAssemblerRouteAccepted\] arrival_xy_m=\([^)]*\) "
|
||||||
|
r"departure_xy_m=\([^)]*\) endpoint_proximity_cost=\d+ average_grade_q16=\d+$", re.M)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
owner_reference = {}
|
||||||
|
fixture_reference = None
|
||||||
|
|
||||||
|
|
||||||
|
def run(command, logfile):
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
return result, time.monotonic() - started, logfile.read_text(
|
||||||
|
encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
for kind in ("l25", "crossing24"):
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
name = f"{kind}-{compiler}-{recipe}-w{workers}"
|
||||||
|
logfile = OUT / f"determinism-{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--far-horizon-lod8-step=0",
|
||||||
|
f"--workers={workers}", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
if kind == "crossing24":
|
||||||
|
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||||
|
result, elapsed, text = run(command, logfile)
|
||||||
|
hashes = HASH_PATTERN.findall(text)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
ref = L32 / f"{kind}-settled-final-{compiler}-{recipe}-w{workers}.log"
|
||||||
|
reference_hashes = HASH_PATTERN.findall(ref.read_text(
|
||||||
|
encoding="utf-8", errors="replace")) if ref.exists() else []
|
||||||
|
stable_key = (kind, recipe)
|
||||||
|
stable = stable_key not in owner_reference or hashes == owner_reference[stable_key]
|
||||||
|
if stable_key not in owner_reference and hashes:
|
||||||
|
owner_reference[stable_key] = hashes
|
||||||
|
equal_l32 = len(hashes) == 12 and hashes == reference_hashes
|
||||||
|
ok = (result is not None and result.returncode == 0
|
||||||
|
and stamps == [(recipe, recipe, "PASS")]
|
||||||
|
and "hash=on" in text and len(hashes) == 12 and stable and equal_l32)
|
||||||
|
rows.append((kind, recipe, compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s hashes={len(hashes)}/12 "
|
||||||
|
f"same_across_matrix={'yes' if stable else 'no'} "
|
||||||
|
f"same_as_l32={'yes' if equal_l32 else 'no'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
name = f"upper-fixture-{compiler}-w{workers}"
|
||||||
|
logfile = OUT / f"determinism-{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(binary), str(FIXTURE), "--workers=" + str(workers),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
"--max-level=-1", "--recipe=floating-islands", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
result, elapsed, text = run(command, logfile)
|
||||||
|
layouts = LAYOUT_PATTERN.findall(text)
|
||||||
|
plans = PLAN_PATTERN.findall(text)
|
||||||
|
routes = ROUTE_PATTERN.findall(text)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
passage_summary = re.search(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", text, re.M)
|
||||||
|
signature = (tuple(layouts), tuple(plans), tuple(routes))
|
||||||
|
stable = fixture_reference is None or signature == fixture_reference
|
||||||
|
if fixture_reference is None and layouts and plans and routes:
|
||||||
|
fixture_reference = signature
|
||||||
|
ok = (result is not None and result.returncode == 0 and len(layouts) == 1
|
||||||
|
and len(plans) == 1 and len(routes) == 1
|
||||||
|
and stamps == [("floating-islands", "floating-islands", "PASS")]
|
||||||
|
and passage_summary is not None and stable)
|
||||||
|
rows.append(("upper-fixture", "floating-islands", compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s layout={layouts[0] if layouts else 'MISSING'} "
|
||||||
|
f"stable_across_matrix={'yes' if stable else 'no'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
summary = OUT / "determinism-l33.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"kind,recipe,compiler,workers,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{kind},{recipe},{compiler},{workers},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for kind, recipe, compiler, workers, elapsed, code, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L33Determinism] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 30 else 0)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run L33 route/proof and transition invariants with recipe-stamped logs."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l33"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-gcc-l33-final",
|
||||||
|
}
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, binary, data, recipe, extra, checks):
|
||||||
|
global failed
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}", *extra,
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
checks_ok = all(check(text) for check in checks)
|
||||||
|
ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok
|
||||||
|
rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
owner = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[RouteParityAudit\].*status=PASS$", text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))])
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for mode in ("hard", "gradient", "interleaved"):
|
||||||
|
name = f"invariant-transition-{recipe}-{mode}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||||
|
text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))])
|
||||||
|
|
||||||
|
summary = OUT / "invariant-audits.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"run,recipe,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for name, recipe, elapsed, code, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L33InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 16 else 0)
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Recipe-stamped resolved-passage audits for the L33 upper-SurfaceWorld stack."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l33"
|
||||||
|
TAG = os.environ.get("VOXELFORGE_L33_AUDIT_TAG", "clang").strip() or "clang"
|
||||||
|
BINARY = Path(os.environ.get("VOXELFORGE_L33_AUDIT_BINARY",
|
||||||
|
str(OUT / f"bin/VoxelForgeCoreHeadless-{TAG}-l33-final")))
|
||||||
|
ROWS = []
|
||||||
|
FAILED = False
|
||||||
|
|
||||||
|
for style in range(4):
|
||||||
|
name = f"upper-sw-three-strata-{TAG}-style{style}"
|
||||||
|
fixture = OUT / f"fixtures/sw-upper-tunnel-network-style{style}.json"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(BINARY), str(fixture), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate",
|
||||||
|
"--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
"--recipe=floating-islands", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamp_ok = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M) == [("floating-islands", "floating-islands", "PASS")]
|
||||||
|
summary = re.search(r"^\[ResolvedPassageAuditSummary\].*$", text, re.M)
|
||||||
|
summary_ok = bool(summary and "interfaces=2 passed=2 natural=2/2" in summary.group()
|
||||||
|
and "route_walkable=2" in summary.group()
|
||||||
|
and "route_binding_failures=0" in summary.group()
|
||||||
|
and "status=PASS" in summary.group())
|
||||||
|
markers = (
|
||||||
|
r"^\[PassageOpenAirAuditSummary\].*violations=0.*status=PASS$",
|
||||||
|
r"^\[PassageNoLongBridgeAudit\].*status=PASS$",
|
||||||
|
r"^\[PassageRockColumnAuditSummary\].*wall_or_support_failures=0.*status=PASS$",
|
||||||
|
r"^\[PassageNaturalnessSummary\].*status=PASS$",
|
||||||
|
)
|
||||||
|
markers_ok = all(re.search(pattern, text, re.M) for pattern in markers)
|
||||||
|
passage_rows = re.findall(
|
||||||
|
r"^\[ResolvedPassageAudit\].*blocked=0 support_samples=\d+ unsupported=0 "
|
||||||
|
r"grade_failures=0 mouth_content=2/2 mouth_radius=2/2 walkability=PASS$",
|
||||||
|
text, re.M)
|
||||||
|
floor_rows = re.findall(r"^\[FloorProfileBlend\].*status=PASS$", text, re.M)
|
||||||
|
route_rows = re.findall(
|
||||||
|
r"^\[ResolvedStrateRouteAudit\].*blocked=0 support_samples=\d+ unsupported=0 "
|
||||||
|
r"grade_failures=0 mouth_binding=PASS status=PASS$", text, re.M)
|
||||||
|
ok = (result is not None and result.returncode == 0 and stamp_ok and summary_ok
|
||||||
|
and markers_ok and len(passage_rows) == 2 and len(route_rows) == 2
|
||||||
|
and len(floor_rows) >= 1 and "[Walkability] PASS" in text)
|
||||||
|
FAILED |= not ok
|
||||||
|
ROWS.append((style, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s exit={result.returncode if result else 'TIMEOUT'} "
|
||||||
|
f"recipe={'PASS' if stamp_ok else 'FAIL'} passages={len(passage_rows)}/2 "
|
||||||
|
f"routes={len(route_rows)}/2 floor_blend_rows={len(floor_rows)} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
summary_path = OUT / f"upper-sw-three-strata-{TAG}-audits.csv"
|
||||||
|
summary_path.write_text(
|
||||||
|
"style,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{style},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for style, elapsed, code, ok in ROWS), encoding="utf-8")
|
||||||
|
print(f"[L33UpperSurfaceFixtureAudit] compiler={TAG} runs={len(ROWS)} pass={sum(row[-1] for row in ROWS)} "
|
||||||
|
f"fail={sum(not row[-1] for row in ROWS)} summary={summary_path}", flush=True)
|
||||||
|
raise SystemExit(1 if FAILED or len(ROWS) != 4 else 0)
|
||||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,251 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Measure exported passage-floor meshes along the first 12 m at each mouth."""
|
||||||
|
import csv
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
RESEARCH = Path(__file__).resolve().parent
|
||||||
|
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||||
|
SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||||
|
PROFILE = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(PROFILE)
|
||||||
|
Q_TO_M = 1.0 / 256.0
|
||||||
|
SPACING = 0.125
|
||||||
|
PLATEAU = 0.50
|
||||||
|
EPSILON = 0.02
|
||||||
|
STEP = 0.10
|
||||||
|
SCENES = (
|
||||||
|
("owner-fi", "layout-owner-fi.json", "FloatingIslands", False),
|
||||||
|
("owner-sw", "layout-owner-sw.json", "SurfaceWorld", True),
|
||||||
|
("upper-sw-fixture", "layout-upper-sw-fixture.json", "SurfaceWorld", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dist(a, b):
|
||||||
|
return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
|
||||||
|
|
||||||
|
|
||||||
|
def interpolate(path, distance):
|
||||||
|
remaining = max(0.0, distance)
|
||||||
|
for a, b in zip(path, path[1:]):
|
||||||
|
length = dist(a[0], b[0])
|
||||||
|
if length <= 1.0e-9:
|
||||||
|
continue
|
||||||
|
if remaining <= length:
|
||||||
|
t = remaining / length
|
||||||
|
p = tuple(a[0][axis] + (b[0][axis] - a[0][axis]) * t
|
||||||
|
for axis in range(3))
|
||||||
|
return p, a[1] + (b[1] - a[1]) * t
|
||||||
|
remaining -= length
|
||||||
|
return path[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def make_inward_path(layout, passage, reverse):
|
||||||
|
scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
||||||
|
controls = [(tuple(value * scale for value in point["position_q"]),
|
||||||
|
point["radius_q"] * scale) for point in passage["control_points"]]
|
||||||
|
lower = tuple(value * scale for value in passage["lower_mouth_q"])
|
||||||
|
lower_index = min(range(len(controls)), key=lambda i: dist(controls[i][0], lower))
|
||||||
|
path = controls[:lower_index + 1]
|
||||||
|
if reverse:
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def select_passage(layout, generator, prefer_lower):
|
||||||
|
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||||
|
preferred_side = "lower" if prefer_lower else "upper"
|
||||||
|
preferred_candidates = []
|
||||||
|
fallback_candidates = []
|
||||||
|
for passage in layout["passages"]:
|
||||||
|
upper = strata[int(passage["upper_strate_index"])]["generator_kind"]
|
||||||
|
lower = strata[int(passage["lower_strate_index"])]["generator_kind"]
|
||||||
|
sides = []
|
||||||
|
if upper == generator:
|
||||||
|
sides.append("upper")
|
||||||
|
if lower == generator:
|
||||||
|
sides.append("lower")
|
||||||
|
if sides:
|
||||||
|
if preferred_side in sides:
|
||||||
|
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||||
|
else:
|
||||||
|
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||||
|
candidates = preferred_candidates or fallback_candidates
|
||||||
|
if not candidates:
|
||||||
|
raise RuntimeError(f"no route for {generator}")
|
||||||
|
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||||
|
|
||||||
|
|
||||||
|
def surface_candidates(buckets, x, y, cell_size, preferred_height):
|
||||||
|
hits = []
|
||||||
|
for verts in buckets.get((math.floor(x / cell_size), math.floor(y / cell_size)), ()):
|
||||||
|
(a, na), (b, nb), (c, nc) = verts
|
||||||
|
denominator = (b[1] - c[1]) * (a[0] - c[0]) + \
|
||||||
|
(c[1] - b[1]) * (a[0] - c[0])
|
||||||
|
if abs(denominator) < 1.0e-12:
|
||||||
|
continue
|
||||||
|
u = ((b[1] - c[1]) * (x - c[0]) + (c[0] - b[0]) * (y - c[1])) / denominator
|
||||||
|
v = ((c[1] - a[1]) * (x - c[0]) + (a[0] - c[0]) * (y - c[1])) / denominator
|
||||||
|
w = 1.0 - u - v
|
||||||
|
if min(u, v, w) < -1.0e-6:
|
||||||
|
continue
|
||||||
|
normal_z = u * na[2] + v * nb[2] + w * nc[2]
|
||||||
|
if normal_z <= 0.0:
|
||||||
|
continue
|
||||||
|
z = u * a[2] + v * b[2] + w * c[2]
|
||||||
|
if abs(z - preferred_height) <= 1.0:
|
||||||
|
hits.append((z, normal_z))
|
||||||
|
unique = {}
|
||||||
|
for z, normal_z in hits:
|
||||||
|
key = round(z, 4)
|
||||||
|
unique[key] = max(unique.get(key, 0.0), normal_z)
|
||||||
|
return [(z, round(normal_z, 3)) for z, normal_z in sorted(unique.items(),
|
||||||
|
key=lambda item: abs(item[0] - preferred_height))[:12]]
|
||||||
|
|
||||||
|
|
||||||
|
def sample_profile(mesh_path, path):
|
||||||
|
total = sum(dist(a[0], b[0]) for a, b in zip(path, path[1:]))
|
||||||
|
reach = min(12.0, total)
|
||||||
|
count = int(round(reach / SPACING))
|
||||||
|
positions = []
|
||||||
|
for index in range(count + 1):
|
||||||
|
distance = index * SPACING
|
||||||
|
point, radius = interpolate(path, distance)
|
||||||
|
before, _ = interpolate(path, max(0.0, distance - 0.25))
|
||||||
|
after, _ = interpolate(path, min(total, distance + 0.25))
|
||||||
|
tangent = tuple(after[axis] - before[axis] for axis in range(3))
|
||||||
|
length = math.hypot(tangent[0], tangent[1])
|
||||||
|
if length <= 1.0e-8:
|
||||||
|
positions.append((distance, point, radius, None))
|
||||||
|
else:
|
||||||
|
side = (-tangent[1] / length, tangent[0] / length)
|
||||||
|
positions.append((distance, point, radius, side))
|
||||||
|
expected = [point[2] - radius for _, point, radius, _ in positions]
|
||||||
|
bounds = (min(value[1][0] for value in positions) - 14.0,
|
||||||
|
max(value[1][0] for value in positions) + 14.0,
|
||||||
|
min(value[1][1] for value in positions) - 14.0,
|
||||||
|
max(value[1][1] for value in positions) + 14.0)
|
||||||
|
voxel_um, triangles, selected = PROFILE.read_scene(
|
||||||
|
str(mesh_path), None, min(expected) - 2.0, max(expected) + 2.0, bounds)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||||
|
buckets = PROFILE.build_buckets(triangles, True, bounds, cell_size)
|
||||||
|
lane_profiles = []
|
||||||
|
for lateral in (0.0, 0.75, -0.75):
|
||||||
|
values = []
|
||||||
|
for distance, point, radius, side in positions:
|
||||||
|
if side is None:
|
||||||
|
values.append(None)
|
||||||
|
continue
|
||||||
|
x = point[0] + side[0] * lateral
|
||||||
|
y = point[1] + side[1] * lateral
|
||||||
|
hit = PROFILE.sample_surface(buckets, x, y, True, cell_size,
|
||||||
|
preferred_height=point[2] - radius)
|
||||||
|
values.append((distance, hit[0]) if hit else None)
|
||||||
|
lane_profiles.append(values)
|
||||||
|
valid_count = sum(value is not None for profile in lane_profiles for value in profile)
|
||||||
|
expected_count = len(lane_profiles) * len(positions)
|
||||||
|
max_flat, max_step = 0.0, 0.0
|
||||||
|
step_runs = []
|
||||||
|
# The offset lanes are retained for coverage and inspection, but the
|
||||||
|
# centerline is the stable longitudinal profile when a passage bends. A
|
||||||
|
# fixed lateral offset rotates around tight bends and can jump between
|
||||||
|
# adjacent terrain sheets even when the passage floor is smooth.
|
||||||
|
for lane, profile in enumerate(lane_profiles):
|
||||||
|
if lane != 0:
|
||||||
|
continue
|
||||||
|
run_start_height = None
|
||||||
|
run_start_index = None
|
||||||
|
previous = None
|
||||||
|
for index, value in enumerate(profile):
|
||||||
|
if value is None:
|
||||||
|
run_start_height = None
|
||||||
|
run_start_index = None
|
||||||
|
previous = None
|
||||||
|
continue
|
||||||
|
if previous is None:
|
||||||
|
run_start_height = value[1]
|
||||||
|
run_start_index = index
|
||||||
|
previous = value
|
||||||
|
continue
|
||||||
|
if abs(value[1] - run_start_height) <= EPSILON:
|
||||||
|
previous = value
|
||||||
|
continue
|
||||||
|
flat_length = max(0, index - 1 - run_start_index) * SPACING
|
||||||
|
step_size = abs(value[1] - previous[1])
|
||||||
|
if flat_length >= PLATEAU and step_size >= STEP:
|
||||||
|
max_flat = max(max_flat, flat_length)
|
||||||
|
max_step = max(max_step, step_size)
|
||||||
|
step_runs.append((lane, profile[run_start_index][0], previous[0],
|
||||||
|
value[0], previous[1], value[1],
|
||||||
|
expected[index - 1], expected[index]))
|
||||||
|
run_start_height = value[1]
|
||||||
|
run_start_index = index
|
||||||
|
previous = value
|
||||||
|
coverage = valid_count / expected_count if expected_count else 0.0
|
||||||
|
trace = []
|
||||||
|
for lane, _, _, transition_distance, _, _, _, _ in step_runs:
|
||||||
|
transition_index = min(range(len(positions)),
|
||||||
|
key=lambda i: abs(positions[i][0] - transition_distance))
|
||||||
|
for index in range(max(0, transition_index - 10),
|
||||||
|
min(len(positions), transition_index + 4)):
|
||||||
|
value = lane_profiles[lane][index]
|
||||||
|
point = positions[index][1]
|
||||||
|
side = positions[index][3]
|
||||||
|
x = point[0] + side[0] * (0.75 if lane == 1 else -0.75 if lane == 2 else 0.0)
|
||||||
|
y = point[1] + side[1] * (0.75 if lane == 1 else -0.75 if lane == 2 else 0.0)
|
||||||
|
trace.append((lane, positions[index][0], round(x, 3), round(y, 3),
|
||||||
|
expected[index], value[1] if value else None,
|
||||||
|
surface_candidates(buckets, x, y, cell_size, expected[index])))
|
||||||
|
status = "PASS" if coverage >= 0.75 and max_flat == 0.0 else \
|
||||||
|
"NO_COVERAGE" if coverage < 0.75 else "FAIL"
|
||||||
|
return {
|
||||||
|
"mesh": mesh_path.name,
|
||||||
|
"tiles": selected,
|
||||||
|
"floor_samples": valid_count,
|
||||||
|
"floor_expected": expected_count,
|
||||||
|
"coverage": coverage,
|
||||||
|
"max_flat_run_m": max_flat,
|
||||||
|
"following_step_m": max_step,
|
||||||
|
"step_runs": step_runs,
|
||||||
|
"trace": trace,
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for scene, layout_file, generator, prefer_lower in SCENES:
|
||||||
|
layout = json.loads((OUT / layout_file).read_text(encoding="utf-8"))
|
||||||
|
passage, reverse = select_passage(layout, generator, prefer_lower)
|
||||||
|
path = make_inward_path(layout, passage, reverse)
|
||||||
|
views = (("mouth-ground", "mouth", path),
|
||||||
|
("mouth-entry", "mouth", path),
|
||||||
|
("arrival", "arrival", list(reversed(path))))
|
||||||
|
for view_name, endpoint, oriented in views:
|
||||||
|
mesh_path = OUT / f"{scene}-{('ground-mouth' if view_name == 'mouth-ground' else 'rock-entry' if view_name == 'mouth-entry' else 'arrival')}.vfmesh"
|
||||||
|
result = sample_profile(mesh_path, oriented)
|
||||||
|
row = {"scene": scene, "view": view_name, "endpoint": endpoint,
|
||||||
|
"passage": passage["stable_id"], **result}
|
||||||
|
rows.append(row)
|
||||||
|
print(f"[L34MouthMeshProfile] scene={scene} view={view_name} endpoint={endpoint} "
|
||||||
|
f"passage={passage['stable_id']} mesh={result['mesh']} "
|
||||||
|
f"tiles={result['tiles']} floor_samples={result['floor_samples']}/"
|
||||||
|
f"{result['floor_expected']} coverage={result['coverage']:.3f} "
|
||||||
|
f"flat_run_m={result['max_flat_run_m']:.3f} "
|
||||||
|
f"following_step_m={result['following_step_m']:.3f} "
|
||||||
|
f"step_runs={result['step_runs']} "
|
||||||
|
f"trace={result['trace']} "
|
||||||
|
f"status={result['status']}", flush=True)
|
||||||
|
csv_path = OUT / "passage-mouth-mesh-profiles.csv"
|
||||||
|
with csv_path.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
print(f"[L34MouthMeshProfileSummary] profiles={len(rows)} "
|
||||||
|
f"pass={sum(row['status'] == 'PASS' for row in rows)} "
|
||||||
|
f"no_coverage={sum(row['status'] == 'NO_COVERAGE' for row in rows)} "
|
||||||
|
f"fail={sum(row['status'] == 'FAIL' for row in rows)} file={csv_path}", flush=True)
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export and render the standard four-view passage set for L34."""
|
||||||
|
import json
|
||||||
|
import importlib.util
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
BIN = OUT / "bin"
|
||||||
|
CAPTURES = Path(__file__).resolve().parent / "captures"
|
||||||
|
HEADLESS = BIN / "VoxelForgeCoreHeadless-l34-clang"
|
||||||
|
RENDERER = BIN / "VoxelForgeOffscreenCapture-l34-clang"
|
||||||
|
PNG_DIR = OUT / "capture-png"
|
||||||
|
RECIPE_INPUTS = (
|
||||||
|
("owner-fi", ROOT / "Saved/Linux/linux25-owner-floating-islands-style0.json",
|
||||||
|
"floating-islands", "FloatingIslands", "upper"),
|
||||||
|
("owner-sw", ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json",
|
||||||
|
"surface-world", "SurfaceWorld", "lower"),
|
||||||
|
("upper-sw-fixture", ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json",
|
||||||
|
"floating-islands", "SurfaceWorld", "upper"),
|
||||||
|
)
|
||||||
|
SUBVOXELS_PER_VOXEL = 256.0
|
||||||
|
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||||
|
ring_profile = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(ring_profile)
|
||||||
|
|
||||||
|
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
ENV = os.environ.copy()
|
||||||
|
ENV["LP_NUM_THREADS"] = "12"
|
||||||
|
ENV["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||||
|
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||||
|
PARSER.add_argument("--scene", choices=[value[0] for value in RECIPE_INPUTS])
|
||||||
|
PARSER.add_argument("--view", choices=("ground-mouth", "rock-entry", "mid-tunnel", "arrival"))
|
||||||
|
OPTIONS = PARSER.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, command):
|
||||||
|
log = OUT / f"capture-{name}.log"
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s", *map(str, command)]
|
||||||
|
started = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
print(f"[L34CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||||
|
flush=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise SystemExit(f"{name} failed; see {log}")
|
||||||
|
return log
|
||||||
|
|
||||||
|
|
||||||
|
def add(a, b):
|
||||||
|
return tuple(x + y for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def sub(a, b):
|
||||||
|
return tuple(x - y for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def mul(a, scale):
|
||||||
|
return tuple(x * scale for x in a)
|
||||||
|
|
||||||
|
|
||||||
|
def norm(a):
|
||||||
|
length = math.sqrt(sum(x * x for x in a))
|
||||||
|
if length <= 1.0e-9:
|
||||||
|
raise ValueError("passage has a degenerate camera tangent")
|
||||||
|
return tuple(x / length for x in a)
|
||||||
|
|
||||||
|
|
||||||
|
def interpolate_route(points, distance):
|
||||||
|
remaining = max(0.0, distance)
|
||||||
|
for a, b in zip(points, points[1:]):
|
||||||
|
delta = sub(b[0], a[0])
|
||||||
|
length = math.sqrt(sum(x * x for x in delta))
|
||||||
|
if length <= 1.0e-9:
|
||||||
|
continue
|
||||||
|
if remaining <= length:
|
||||||
|
t = remaining / length
|
||||||
|
pos = tuple(a[0][axis] + delta[axis] * t for axis in range(3))
|
||||||
|
radius = a[1] + (b[1] - a[1]) * t
|
||||||
|
return pos, radius
|
||||||
|
remaining -= length
|
||||||
|
return points[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def route_frame(points, distance):
|
||||||
|
center, radius = interpolate_route(points, distance)
|
||||||
|
before, _ = interpolate_route(points, max(0.0, distance - 0.25))
|
||||||
|
after, _ = interpolate_route(points, distance + 0.25)
|
||||||
|
tangent = norm(sub(after, before))
|
||||||
|
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||||
|
side = (-horizontal[1], horizontal[0], 0.0)
|
||||||
|
return center, radius, tangent, side
|
||||||
|
|
||||||
|
|
||||||
|
def camera_angles(camera, target):
|
||||||
|
delta = sub(target, camera)
|
||||||
|
horizontal = math.hypot(delta[0], delta[1])
|
||||||
|
yaw = math.degrees(math.atan2(delta[0], delta[1]))
|
||||||
|
pitch = math.degrees(math.atan2(delta[2], horizontal))
|
||||||
|
return yaw, pitch
|
||||||
|
|
||||||
|
|
||||||
|
def fmt(values):
|
||||||
|
return ",".join(f"{value:.6f}" for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
def terrain_surface_near(mesh, x, y, expected_height):
|
||||||
|
bounds = (x - 2.0, x + 2.0, y - 2.0, y + 2.0)
|
||||||
|
_, triangles, _ = ring_profile.read_scene(
|
||||||
|
str(mesh), None, -1.0e9, 1.0e9, bounds)
|
||||||
|
buckets = ring_profile.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||||
|
hits = []
|
||||||
|
for dy in range(-4, 5):
|
||||||
|
for dx in range(-4, 5):
|
||||||
|
sample_x, sample_y = x + dx * 0.5, y + dy * 0.5
|
||||||
|
hit = ring_profile.sample_surface(buckets, sample_x, sample_y, True, 1.0,
|
||||||
|
preferred_height=expected_height)
|
||||||
|
if hit is not None:
|
||||||
|
hits.append((math.hypot(dx * 0.5, dy * 0.5),
|
||||||
|
abs(hit[0] - expected_height), sample_x, sample_y, hit[0]))
|
||||||
|
if not hits:
|
||||||
|
return None
|
||||||
|
distance, _, sample_x, sample_y, height = min(hits)
|
||||||
|
if distance > 2.0 or abs(height - expected_height) > 8.0:
|
||||||
|
return None
|
||||||
|
return sample_x, sample_y, height
|
||||||
|
|
||||||
|
|
||||||
|
def load_layout(name, source, recipe):
|
||||||
|
path = OUT / f"layout-{name}.json"
|
||||||
|
run(f"layout-{name}", [HEADLESS, source, "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||||
|
"--max-clip-level=0", "--max-level=0", f"--recipe={recipe}",
|
||||||
|
f"--resolved-layout-output={path}", "--resolved-layout-only"])
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def select_passage(layout, mouth_generator, preferred_side):
|
||||||
|
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||||
|
preferred_candidates = []
|
||||||
|
fallback_candidates = []
|
||||||
|
for passage in layout["passages"]:
|
||||||
|
upper = strata[int(passage["upper_strate_index"])]
|
||||||
|
lower = strata[int(passage["lower_strate_index"])]
|
||||||
|
sides = []
|
||||||
|
if upper["generator_kind"] == mouth_generator:
|
||||||
|
sides.append("upper")
|
||||||
|
if lower["generator_kind"] == mouth_generator:
|
||||||
|
sides.append("lower")
|
||||||
|
if preferred_side in sides:
|
||||||
|
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||||
|
elif sides:
|
||||||
|
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||||
|
candidates = preferred_candidates or fallback_candidates
|
||||||
|
if not candidates:
|
||||||
|
raise RuntimeError(f"no passage mouth in generator {mouth_generator}")
|
||||||
|
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||||
|
|
||||||
|
|
||||||
|
def make_views(layout, passage, reverse):
|
||||||
|
q_to_m = layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0)
|
||||||
|
controls = [
|
||||||
|
(tuple(value * q_to_m for value in point["position_q"]),
|
||||||
|
point["radius_q"] * q_to_m)
|
||||||
|
for point in passage["control_points"]
|
||||||
|
]
|
||||||
|
lower_mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||||
|
lower_index = min(range(len(controls)), key=lambda i: sum(
|
||||||
|
(controls[i][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||||
|
path = controls[:lower_index + 1]
|
||||||
|
if len(path) < 2:
|
||||||
|
raise RuntimeError(f"{passage['stable_id']} has no mouth-to-mouth route")
|
||||||
|
if reverse:
|
||||||
|
path.reverse()
|
||||||
|
lengths = [math.sqrt(sum((b[0][axis] - a[0][axis]) ** 2 for axis in range(3)))
|
||||||
|
for a, b in zip(path, path[1:])]
|
||||||
|
total = sum(lengths)
|
||||||
|
upper, upper_radius, upper_tangent, upper_side = route_frame(path, 0.0)
|
||||||
|
lower, lower_radius, lower_tangent, lower_side = route_frame(path, total)
|
||||||
|
middle_distance = max(12.0, min(total - 12.0, total * 0.4)) if total > 24.0 else total * 0.4
|
||||||
|
entry_distance = min(6.0, total * 0.25)
|
||||||
|
entry, entry_radius, entry_tangent, entry_side = route_frame(path, entry_distance)
|
||||||
|
middle, middle_radius, middle_tangent, middle_side = route_frame(path, middle_distance)
|
||||||
|
|
||||||
|
def exterior_view(prefix, center, radius, outward, target):
|
||||||
|
horizontal_outward = norm((outward[0], outward[1], 0.0))
|
||||||
|
eye_height = center[2] - radius + 1.6
|
||||||
|
eye = (center[0] + horizontal_outward[0] * 5.5,
|
||||||
|
center[1] + horizontal_outward[1] * 5.5,
|
||||||
|
eye_height)
|
||||||
|
yaw, pitch = camera_angles(eye, target)
|
||||||
|
return {"name": prefix, "camera": eye, "yaw": yaw, "pitch": pitch,
|
||||||
|
"target": target, "clip": None, "camera_xy": eye[:2],
|
||||||
|
"focus": center, "mouth": center, "eye_height_z": eye_height}
|
||||||
|
|
||||||
|
entrance, entrance_radius, entrance_tangent = upper, upper_radius, upper_tangent
|
||||||
|
exit_point, exit_radius, exit_tangent = lower, lower_radius, lower_tangent
|
||||||
|
ground_target_pos, ground_target_radius = interpolate_route(path, min(total, 1.0))
|
||||||
|
ground = exterior_view("ground-mouth", entrance, entrance_radius,
|
||||||
|
mul(entrance_tangent, -1.0), (ground_target_pos[0], ground_target_pos[1],
|
||||||
|
ground_target_pos[2] - ground_target_radius + 1.0))
|
||||||
|
ground["focus"] = entrance
|
||||||
|
|
||||||
|
def cutaway_view(prefix, center, radius, tangent, side, distance, target_distance):
|
||||||
|
camera = (center[0] - side[0] * 7.0, center[1] - side[1] * 7.0,
|
||||||
|
center[2])
|
||||||
|
target_pos, target_radius = interpolate_route(path, target_distance)
|
||||||
|
target = (target_pos[0], target_pos[1], target_pos[2])
|
||||||
|
yaw, pitch = camera_angles(camera, target)
|
||||||
|
plane = (side[0], side[1], 0.0,
|
||||||
|
-(side[0] * center[0] + side[1] * center[1]))
|
||||||
|
return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch,
|
||||||
|
"target": target, "clip": plane, "camera_xy": camera[:2],
|
||||||
|
"focus": center, "mouth": center}
|
||||||
|
|
||||||
|
# The lower-SW fixture's terminal volume has no LOD0 solid surface to cap.
|
||||||
|
# Use the far-side, fully meshed section for a comparable capped arrival view.
|
||||||
|
if passage["stable_id"] == "passage/1-2/0":
|
||||||
|
arrival_distance = total * 0.45
|
||||||
|
elif passage["stable_id"] == "passage/0-1/0" and reverse:
|
||||||
|
# Keep this SurfaceWorld arrival view near the endpoint while avoiding
|
||||||
|
# the tile boundary that left an open contour at the final 14 m.
|
||||||
|
arrival_distance = total * 0.80
|
||||||
|
else:
|
||||||
|
arrival_distance = max(0.0, total - 14.0)
|
||||||
|
arrival_center, arrival_radius, arrival_tangent, arrival_side = route_frame(
|
||||||
|
path, arrival_distance)
|
||||||
|
arrival = cutaway_view("arrival", arrival_center, arrival_radius,
|
||||||
|
arrival_tangent, arrival_side, arrival_distance,
|
||||||
|
min(total, arrival_distance + 12.0))
|
||||||
|
arrival_camera, _ = interpolate_route(path, max(0.0, arrival_distance - 6.0))
|
||||||
|
arrival["camera"] = arrival_camera
|
||||||
|
arrival_cut_point = (arrival_center[0] - arrival_side[0] * 2.5,
|
||||||
|
arrival_center[1] - arrival_side[1] * 2.5, arrival_center[2])
|
||||||
|
arrival["clip"] = (*arrival_side,
|
||||||
|
-sum(arrival_side[axis] * arrival_cut_point[axis] for axis in range(3)))
|
||||||
|
arrival["camera_location"] = "passage-air"
|
||||||
|
arrival["focus_radius"] = 20.0
|
||||||
|
arrival["yaw"], arrival["pitch"] = camera_angles(
|
||||||
|
arrival["camera"], arrival["target"])
|
||||||
|
|
||||||
|
entry_camera_distance = min(5.5, total * 0.1)
|
||||||
|
entry_center, entry_radius, entry_tangent, entry_side = route_frame(
|
||||||
|
path, entry_camera_distance)
|
||||||
|
# Exterior oblique view at eye height, aimed along the portal blend.
|
||||||
|
outward_horizontal = norm((-upper_tangent[0], -upper_tangent[1], 0.0))
|
||||||
|
entry_camera = (entrance[0] + outward_horizontal[0] * 4.5 - upper_side[0] * 2.5,
|
||||||
|
entrance[1] + outward_horizontal[1] * 4.5 - upper_side[1] * 2.5,
|
||||||
|
entrance[2] - upper_radius + 1.6)
|
||||||
|
entry_target_pos, entry_target_radius = interpolate_route(
|
||||||
|
path, min(total, 2.0))
|
||||||
|
entry_target = (entry_target_pos[0], entry_target_pos[1],
|
||||||
|
entry_target_pos[2] - entry_target_radius + 2.5)
|
||||||
|
entry_yaw, entry_pitch = camera_angles(entry_camera, entry_target)
|
||||||
|
entry_view = {"name": "rock-entry", "camera": entry_camera,
|
||||||
|
"yaw": entry_yaw, "pitch": entry_pitch, "target": entry_target,
|
||||||
|
"clip": None, "camera_xy": entry_camera[:2],
|
||||||
|
"focus": entry_center, "mouth": entrance, "outside_camera": True,
|
||||||
|
"camera_location": "exterior", "focus_radius": None,
|
||||||
|
"eye_height_z": entry_camera[2]}
|
||||||
|
middle_view = cutaway_view("mid-tunnel", middle, middle_radius, middle_tangent,
|
||||||
|
middle_side, middle_distance, min(total, middle_distance + 2.0))
|
||||||
|
ground["focus_radius"] = 20.0
|
||||||
|
middle_view["focus_radius"] = 20.0
|
||||||
|
middle_camera, _ = interpolate_route(path, max(0.0, middle_distance - 6.0))
|
||||||
|
middle_view["camera"] = middle_camera
|
||||||
|
middle_target_pos, _ = interpolate_route(path, min(total, middle_distance + 9.0))
|
||||||
|
middle_view["target"] = (middle_target_pos[0], middle_target_pos[1], middle[2])
|
||||||
|
# The camera sits in the passage air. Remove the near wall to open a
|
||||||
|
# longitudinal section and cap the remaining rock at that cut.
|
||||||
|
middle_cut_point = (middle[0] - middle_side[0] * 2.5,
|
||||||
|
middle[1] - middle_side[1] * 2.5, middle[2])
|
||||||
|
middle_view["clip"] = (*middle_side,
|
||||||
|
-sum(middle_side[axis] * middle_cut_point[axis] for axis in range(3)))
|
||||||
|
middle_view["camera_location"] = "passage-air"
|
||||||
|
middle_view["yaw"], middle_view["pitch"] = camera_angles(
|
||||||
|
middle_view["camera"], middle_view["target"])
|
||||||
|
return (ground, entry_view, middle_view, arrival), total, lower_index
|
||||||
|
|
||||||
|
|
||||||
|
for scene_name, source, recipe, mouth_generator, preferred_side in RECIPE_INPUTS:
|
||||||
|
if OPTIONS.scene and scene_name != OPTIONS.scene:
|
||||||
|
continue
|
||||||
|
layout = load_layout(scene_name, source, recipe)
|
||||||
|
passage, reverse = select_passage(layout, mouth_generator, preferred_side)
|
||||||
|
views, route_length, lower_index = make_views(layout, passage, reverse)
|
||||||
|
print(f"[L34StandardCaptureSet] scene={scene_name} recipe={recipe} passage={passage['stable_id']} "
|
||||||
|
f"route_m={route_length:.3f} control_points={lower_index + 1} fov_deg=80 "
|
||||||
|
"ground_camera_outside_m=5.500 entry_camera_outside_m=5.148 camera_eye_m=1.600", flush=True)
|
||||||
|
for view in views:
|
||||||
|
if OPTIONS.view and view["name"] != OPTIONS.view:
|
||||||
|
continue
|
||||||
|
stem = f"{scene_name}-{view['name']}"
|
||||||
|
mesh = OUT / f"{stem}.vfmesh"
|
||||||
|
png = PNG_DIR / f"{stem}.png"
|
||||||
|
jpg = CAPTURES / f"{stem}.jpg"
|
||||||
|
yaw, pitch = view["yaw"], view["pitch"]
|
||||||
|
camera = view["camera"]
|
||||||
|
camera_spec = (*camera, yaw, pitch)
|
||||||
|
export_camera = view.get("focus", camera)
|
||||||
|
export_clip_radius = 3 if scene_name == "owner-fi" and view["name"] == "mid-tunnel" else 2
|
||||||
|
run(f"{stem}-export", [HEADLESS, source, "--workers=12",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", f"--clip-radius={export_clip_radius}",
|
||||||
|
"--max-clip-level=0", "--max-level=0",
|
||||||
|
"--render-distance-chunks=0", f"--recipe={recipe}",
|
||||||
|
f"--camera={fmt((*export_camera, yaw, pitch))}", f"--mesh-export={mesh}"])
|
||||||
|
render_clip = view["clip"]
|
||||||
|
if view.get("outside_camera", False):
|
||||||
|
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||||
|
ground_source = "terrain-mesh"
|
||||||
|
if surface is None:
|
||||||
|
camera = (view["camera_xy"][0], view["camera_xy"][1],
|
||||||
|
view["eye_height_z"])
|
||||||
|
ground_source = "mouth-eye-height-fallback"
|
||||||
|
else:
|
||||||
|
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||||
|
elif view["clip"] is not None:
|
||||||
|
# The clip plane removes the rock half-space containing this side camera.
|
||||||
|
# Keep its height at the passage eye line so the cut section stays centered.
|
||||||
|
surface = None
|
||||||
|
camera = view["camera"]
|
||||||
|
ground_source = "clipped-passage-side-camera"
|
||||||
|
else:
|
||||||
|
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||||
|
ground_source = "terrain-mesh"
|
||||||
|
if surface is None:
|
||||||
|
surface_z = view.get("eye_height_z", view["target"][2] + 0.4)
|
||||||
|
camera = (view["camera_xy"][0], view["camera_xy"][1], surface_z)
|
||||||
|
ground_source = "mouth-eye-height-fallback"
|
||||||
|
else:
|
||||||
|
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||||
|
yaw, pitch = camera_angles(camera, view["target"])
|
||||||
|
mouth_position = view["mouth"][0:2]
|
||||||
|
camera_mouth_distance = math.dist(camera, view["mouth"])
|
||||||
|
ground_height = surface[2] if surface is not None else float("nan")
|
||||||
|
surface_offset = (surface[2] - view["target"][2]) if surface is not None else float("nan")
|
||||||
|
print(f"[L34CaptureCamera] view={stem} ground_source={ground_source} "
|
||||||
|
f"ground_z_m={ground_height:.3f} "
|
||||||
|
f"surface_target_delta_m={surface_offset:.3f} "
|
||||||
|
f"eye_z_m={camera[2]:.3f} "
|
||||||
|
f"camera_location={view.get('camera_location', 'exterior')} "
|
||||||
|
f"camera_to_local_mouth_m={camera_mouth_distance:.3f} "
|
||||||
|
f"camera_m=({fmt(camera)}) target_m=({fmt(view['target'])}) "
|
||||||
|
f"clip={fmt(render_clip) if render_clip is not None else 'none'} "
|
||||||
|
f"fov_deg=80", flush=True)
|
||||||
|
render = [RENDERER, mesh, png, "--size=1600x900", "--fov-deg=80",
|
||||||
|
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={view['target'][2]:.6f}",
|
||||||
|
"--height-range-m=8", "--tile-edges=off"]
|
||||||
|
if view.get("focus_radius", 24.0) is not None:
|
||||||
|
render.append(f"--focus-region={fmt((*view['focus'], view.get('focus_radius', 24.0)))}")
|
||||||
|
if render_clip is not None:
|
||||||
|
render.append(f"--clip-plane={fmt(render_clip)}")
|
||||||
|
run(f"{stem}-render", render)
|
||||||
|
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||||
|
"-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg])
|
||||||
|
size = jpg.stat().st_size if jpg.exists() else 0
|
||||||
|
print(f"[L34CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||||
|
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||||
|
if not 0 < size <= 300_000:
|
||||||
|
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB")
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# VoxelForge L34 report
|
||||||
|
|
||||||
|
## Verdict and fix
|
||||||
|
|
||||||
|
The 0.125 m floor and wall profiles found no 0.5 m flat run followed by a step of at least 0.1 m in any of the 24 style, recipe, fixture, and compiler runs. The L23-style negative-control profile (2.0 m flat run, then a 0.25 m step) was detected. The visible bands are cutaway/voxel surface relief, not L23-style terraces in the passage density. No support or tread layer, portal blend step, or carve-boundary distance error was found, so the owner geometry and hashes did not need to change.
|
||||||
|
|
||||||
|
The offscreen renderer has no depth-of-field or blur pass; the old soft image came from the capture framing. The reusable capture script uses 80° FOV, 1.6 m eye height, outside mouth cameras 5.15–6.27 m from the route mouth, LOD0 meshes, and tile skirts off. Inside views use clip planes with cap triangles. Export radius is 3 only for owner-FI mid-tunnel (which reduced its nearest cap gap to 0.058 m); other views use radius 2.
|
||||||
|
|
||||||
|
## Audits and captures
|
||||||
|
|
||||||
|
- Clang and GCC builds passed. Recipe-stamped passage, walkability, binding, strict validator, capsule, open-air, naturalness, bridge, column, and mouth-profile audits: 24/24 PASS. The matrix covers all four styles for both owner recipes and the upper-SW fixture, with both compilers. L24 and L26 negative controls also triggered.
|
||||||
|
- Interface/transition invariants: 16/16 PASS, with zero hole columns and zero air samples. L13/L9: 4/4 PASS. Determinism: 30/30 PASS (L25, the 24 m crossing, and upper-SW fixture at W1/W8/W12 on both compilers). Validator controls rejected 10/10 invalid cases.
|
||||||
|
- Standard set: 12 sharp 1600×900 JPEGs, all 23.5–45.4 KB, in `captures/`: owner-FI, owner-SW, and upper-SW fixture, each with ground-mouth, rock-entry, mid-tunnel, and arrival views. All six inside renders produced cap triangles and report cap status PASS; some have residual open contours, recorded in their render logs. SHA-256 values are in `captures/SHA256SUMS.txt`.
|
||||||
|
- Owner W12 hashes match L33. Recipe/evaluator versions remain 2/20; no Unreal files changed.
|
||||||
|
|
||||||
|
| Recipe | Density | Mesh | Payload | Topology |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| SurfaceWorld | `953eb1df1705f277` | `d8006cb43f44aa5b` | `9217bff766970de5` | `83e3aa6f3a063e60` |
|
||||||
|
| FloatingIslands | `806537cdd98f8afb` | `a4b06dd4d260943d` | `763a99aa04a73eae` | `baa37d80d3620d87` |
|
||||||
|
|
||||||
|
- Cost: five interleaved L33/L34 pairs per recipe. Median whole-view-ready delta was +0.06% for SurfaceWorld and +0.52% for FloatingIslands, within run noise. All 20 paired runs passed.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- The first fixture determinism runner used a nonexistent L34 fixture path: its 24 owner rows passed, six fixture rows failed; correcting the path to the L33 fixture directory made all six pass.
|
||||||
|
- The first L13/L9 SurfaceWorld-Clang run added a camera override and failed route parity; its summary parser also raised a `TypeError`. Removing the override and fixing the parser produced the final 4/4 pass.
|
||||||
|
- The initial style-2 upper-SW lateral-lane probe reported a false step around a tight bend. The supplemental mesh-profile probes (`audit-passage-mouth-mesh-profiles.log`, r2–r7) also had incomplete coverage on several capture exports. The official density-field profile uses the passage centerline for floor steps and both sides for wall steps; it passed the full matrix and the L23 control.
|
||||||
|
- Capture renderer exit-3 retries were recorded in `run-captures.log`, `run-captures-r5`–`r8`, `r10`, `r13`, `r18`, `run-captures-final-r1`, and `run-capture-standard-set.log`. They came from clip planes with no closed cap loop on the initial owner-SW ground/entry, owner-FI entry, and arrival placements. A three-value entry clip was rejected; a four-value outward clip had a 5.1 m open gap, and an inside entry clip obscured the passage. Exterior views now use no clip. Owner-SW arrival trials at the far endpoint had no cap, 60 m from the endpoint capped without showing the tunnel, and 90% route distance had a 3.884 m cap gap; the final view uses the exact route camera at 80%. The upper-SW fixture arrival at 60% failed with a 40.786 m gap; 45% passes. A radius-4 cap probe increased open contours, so the final script uses the per-view radii above. The final full capture run passed all 12 views.
|
||||||
|
- Owner-SW entry framing probes using the opposite side, the centerline target, and a 6 m interior target were less readable; floor-height offsets were compared and the final uses a 2.5 m portal target with no clip or focus crop. A direct renderer probe also lacked the repo-local XDG cache setting and warned that `/home/jahni/.cache` was unavailable; the reusable script now points XDG cache to `Saved/Linux/l34`.
|
||||||
|
- The first fixture-arrival full-set render exited 2 before the final 45% route view was set. The later 60% cap probe failed as noted above; the final 45% render passes. No final run failed.
|
||||||
|
- No run exceeded 30 minutes or remained stuck for 15 minutes. All builds and audits used the requested CPU, memory, and job limits. Nothing was committed, pushed, or stashed.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged as tooling: the standard 12-view capture set is sharp and comparable, and the mouth-profile audit with its
|
||||||
|
L23 control is a good addition. The owner world is unchanged (no geometry change), so this is accepted on that basis.
|
||||||
|
- Not accepted as "no stepping": the captures show **regular ribs/ripples along tunnel walls and floors** (owner-SW
|
||||||
|
mid-tunnel, upper-SW arrival) and faint bands on the owner-SW mouth floor. They are periodic and smaller than the
|
||||||
|
0.5 m-flat + 0.1 m-step detector, so the detector can't see them. The owner-FI ground mouth also shows a **flat
|
||||||
|
rectangular slab with a tiled pattern** inside the crater, which reads as artificial. Both go to L35.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run five interleaved W12 whole-view pairs against the L33 binary."""
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"l33": ROOT / "Saved/Linux/l33/bin/VoxelForgeCoreHeadless-clang-l33-final",
|
||||||
|
"l34": OUT / "bin/VoxelForgeCoreHeadless-l34-clang",
|
||||||
|
}
|
||||||
|
ORDERS = (("l33", "l34"), ("l34", "l33"), ("l33", "l34"),
|
||||||
|
("l34", "l33"), ("l33", "l34"))
|
||||||
|
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for pair, order in enumerate(ORDERS, 1):
|
||||||
|
for build in order:
|
||||||
|
name = f"cost-{recipe}-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "900s",
|
||||||
|
str(BINARIES[build]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matches = READY.findall(text)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||||
|
and "hash=off" in text and "cache_hits=0" in text and stamp_ok)
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else float("nan")
|
||||||
|
rows.append((recipe, pair, build, ready, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", valid))
|
||||||
|
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True)
|
||||||
|
failed |= not valid
|
||||||
|
|
||||||
|
summary = OUT / "cost-runs.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for recipe, pair, build, ready, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build]
|
||||||
|
for build in BINARIES}
|
||||||
|
baseline = statistics.median(by_build["l33"])
|
||||||
|
current = statistics.median(by_build["l34"])
|
||||||
|
paired = []
|
||||||
|
for pair in range(1, 6):
|
||||||
|
values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair}
|
||||||
|
paired.append((values["l34"] - values["l33"]) / values["l33"] * 100.0)
|
||||||
|
print(f"[L34CostSummary] recipe={recipe} pairs=5 median_l33_s={baseline:.3f} "
|
||||||
|
f"median_l34_s={current:.3f} median_change_pct={(current/baseline-1)*100.0:+.2f} "
|
||||||
|
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L34Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}",
|
||||||
|
flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 20 else 0)
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run owner L25 hashes, 24 m crossings, and upper-SurfaceWorld fixture determinism."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
L33 = ROOT / "Saved/Linux/l33"
|
||||||
|
FIXTURE = L33 / "fixtures/sw-upper-tunnel-network-style0.json"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l34-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l34-gcc",
|
||||||
|
}
|
||||||
|
HASH_PATTERN = re.compile(
|
||||||
|
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
LAYOUT_PATTERN = re.compile(
|
||||||
|
r"^\[ResolvedLayoutSnapshot\] schema=\d+ stable_identity=([^ ]+) strata=\d+ bytes=\d+ round_trip=PASS$",
|
||||||
|
re.M)
|
||||||
|
PLAN_PATTERN = re.compile(
|
||||||
|
r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*"
|
||||||
|
r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", re.M)
|
||||||
|
ROUTE_PATTERN = re.compile(
|
||||||
|
r"^\[WorldAssemblerRouteAccepted\] arrival_xy_m=\([^)]*\) "
|
||||||
|
r"departure_xy_m=\([^)]*\) endpoint_proximity_cost=\d+ average_grade_q16=\d+$", re.M)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
owner_reference = {}
|
||||||
|
fixture_reference = None
|
||||||
|
|
||||||
|
|
||||||
|
def run(command, logfile):
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
return result, time.monotonic() - started, logfile.read_text(
|
||||||
|
encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
for kind in ("l25", "crossing24"):
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
name = f"{kind}-{compiler}-{recipe}-w{workers}"
|
||||||
|
logfile = OUT / f"determinism-{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s",
|
||||||
|
str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--far-horizon-lod8-step=0",
|
||||||
|
f"--workers={workers}", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
if kind == "crossing24":
|
||||||
|
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||||
|
result, elapsed, text = run(command, logfile)
|
||||||
|
hashes = HASH_PATTERN.findall(text)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
ref = L33 / f"determinism-{kind}-{compiler}-{recipe}-w{workers}.log"
|
||||||
|
reference_hashes = HASH_PATTERN.findall(ref.read_text(
|
||||||
|
encoding="utf-8", errors="replace")) if ref.exists() else []
|
||||||
|
stable_key = (kind, recipe)
|
||||||
|
stable = stable_key not in owner_reference or hashes == owner_reference[stable_key]
|
||||||
|
if stable_key not in owner_reference and hashes:
|
||||||
|
owner_reference[stable_key] = hashes
|
||||||
|
equal_l32 = len(hashes) == 12 and hashes == reference_hashes
|
||||||
|
ok = (result is not None and result.returncode == 0
|
||||||
|
and stamps == [(recipe, recipe, "PASS")]
|
||||||
|
and "hash=on" in text and len(hashes) == 12 and stable and equal_l32)
|
||||||
|
rows.append((kind, recipe, compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s hashes={len(hashes)}/12 "
|
||||||
|
f"same_across_matrix={'yes' if stable else 'no'} "
|
||||||
|
f"same_as_l33={'yes' if equal_l32 else 'no'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
name = f"upper-fixture-{compiler}-w{workers}"
|
||||||
|
logfile = OUT / f"determinism-{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s",
|
||||||
|
str(binary), str(FIXTURE), "--workers=" + str(workers),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
"--max-level=-1", "--recipe=floating-islands", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
result, elapsed, text = run(command, logfile)
|
||||||
|
layouts = LAYOUT_PATTERN.findall(text)
|
||||||
|
plans = PLAN_PATTERN.findall(text)
|
||||||
|
routes = ROUTE_PATTERN.findall(text)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
passage_summary = re.search(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", text, re.M)
|
||||||
|
signature = (tuple(layouts), tuple(plans), tuple(routes))
|
||||||
|
stable = fixture_reference is None or signature == fixture_reference
|
||||||
|
if fixture_reference is None and layouts and plans and routes:
|
||||||
|
fixture_reference = signature
|
||||||
|
ok = (result is not None and result.returncode == 0 and len(layouts) == 1
|
||||||
|
and len(plans) == 1 and len(routes) == 1
|
||||||
|
and stamps == [("floating-islands", "floating-islands", "PASS")]
|
||||||
|
and passage_summary is not None and stable)
|
||||||
|
rows.append(("upper-fixture", "floating-islands", compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s layout={layouts[0] if layouts else 'MISSING'} "
|
||||||
|
f"stable_across_matrix={'yes' if stable else 'no'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
summary = OUT / "determinism-l34.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"kind,recipe,compiler,workers,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{kind},{recipe},{compiler},{workers},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for kind, recipe, compiler, workers, elapsed, code, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L34Determinism] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 30 else 0)
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run L33 route/proof and transition invariants with recipe-stamped logs."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l34-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l34-gcc",
|
||||||
|
}
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, binary, data, recipe, extra, checks):
|
||||||
|
global failed
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "900s",
|
||||||
|
str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}", *extra,
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
checks_ok = all(check(text) for check in checks)
|
||||||
|
ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok
|
||||||
|
rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
owner = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[RouteParityAudit\].*status=PASS$", text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))])
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for mode in ("hard", "gradient", "interleaved"):
|
||||||
|
name = f"invariant-transition-{recipe}-{mode}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||||
|
text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))])
|
||||||
|
|
||||||
|
summary = OUT / "invariant-audits.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"run,recipe,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for name, recipe, elapsed, code, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L33InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 16 else 0)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the L13 route parity and L9 uniform-block proof audits on L34."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l34-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l34-gcc",
|
||||||
|
}
|
||||||
|
RECIPES = ("floating-islands", "surface-world")
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for recipe in RECIPES:
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"legacy-route-proof-{recipe}-{compiler}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "900s", str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--stream-hash", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}", "--audit-uniform-block-proofs", "--audit-route-parity",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamp = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
output, re.M)
|
||||||
|
route = re.findall(r"^\[RouteParityAudit\].*$", output, re.M)
|
||||||
|
proofs = re.findall(r"^\[UniformBlockProofAudit\].*$", output, re.M)
|
||||||
|
stamp_ok = stamp == [(recipe, recipe, "PASS")]
|
||||||
|
route_ok = len(route) == 1 and route[0].endswith("status=PASS")
|
||||||
|
proof_ok = len(proofs) == 1 and bool(
|
||||||
|
re.search(r"violations=0(?:\s|$)", proofs[0]))
|
||||||
|
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||||
|
and route_ok and proof_ok)
|
||||||
|
code = result.returncode if result is not None else "TIMEOUT"
|
||||||
|
rows.append((name, recipe, compiler, elapsed, code, route_ok, bool(proof_ok), ok))
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[L34LegacyAuditRun] case={name} elapsed={elapsed:.1f}s exit={code} "
|
||||||
|
f"recipe_stamp={'PASS' if stamp_ok else 'FAIL'} "
|
||||||
|
f"L13={'PASS' if route_ok else 'FAIL'} L9={'PASS' if proof_ok else 'FAIL'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
summary = OUT / "legacy-audits.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"case,recipe,compiler,elapsed_seconds,exit_code,l13,l9,status\n" +
|
||||||
|
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if l13 else 'FAIL'},{'PASS' if l9 else 'FAIL'},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for name, recipe, compiler, elapsed, code, l13, l9, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L34LegacyAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 4 else 0)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run recipe-stamped passage/profile audits for both compilers and both owner recipes."""
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l34-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l34-gcc",
|
||||||
|
}
|
||||||
|
RUNS = []
|
||||||
|
for recipe, filename in (
|
||||||
|
("floating-islands", "floating-islands"),
|
||||||
|
("surface-world", "surface-world"),
|
||||||
|
):
|
||||||
|
for style in range(4):
|
||||||
|
RUNS.append((f"owner-{filename}-style{style}",
|
||||||
|
ROOT / f"Saved/Linux/linux25-owner-{filename}-style{style}.json", recipe))
|
||||||
|
for style in range(4):
|
||||||
|
RUNS.append((f"upper-sw-fixture-style{style}",
|
||||||
|
OUT.parent / "l33/fixtures" / f"sw-upper-tunnel-network-style{style}.json",
|
||||||
|
"floating-islands"))
|
||||||
|
|
||||||
|
RECIPE = re.compile(r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||||
|
PASSAGE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||||
|
MOUTH = re.compile(r"^\[PassageMouthProfileAudit\].*status=PASS$", re.M)
|
||||||
|
ROUTE = re.compile(r"^\[ResolvedStrateRouteAudit\].*mouth_binding=PASS status=PASS$", re.M)
|
||||||
|
CHECKS = (
|
||||||
|
r"^\[ResolvedLayoutStrict\] accepted=true issues=0 error=$",
|
||||||
|
r"^\[ResolvedLayoutValidatorControls\].*status=PASS$",
|
||||||
|
r"^\[PassageMouthProfileNegativeControl\].*source=L23_terraced_geometry.*detected=true$",
|
||||||
|
r"^\[PassageMouthProfileSummary\].*l23_terrace_negative_control=DETECTED status=PASS$",
|
||||||
|
r"^\[PassageNaturalnessSummary\].*l23_negative_control=DETECTED.*status=PASS$",
|
||||||
|
r"^\[PassageOpenAirAuditSummary\].*violations=0.*l24_negative_control=DETECTED status=PASS$",
|
||||||
|
r"^\[PassageNoLongBridgeAudit\].*status=PASS$",
|
||||||
|
r"^\[PassageRockColumnAuditSummary\].*status=PASS$",
|
||||||
|
r"^\[ResolvedPassageAuditSummary\].*route_binding_failures=0 .*status=PASS$",
|
||||||
|
r"^\[Walkability\] PASS",
|
||||||
|
r"^\[FloorProfileBlend\].*negative_controls=step_detected/slope_detected status=PASS$",
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for name, data, recipe in RUNS:
|
||||||
|
logfile = OUT / f"passage-audit-{name}-{compiler}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s", str(binary), str(data), "--workers=12",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamp_ok = RECIPE.findall(output) == [(recipe, recipe)]
|
||||||
|
passages = len(PASSAGE.findall(output))
|
||||||
|
mouths = len(MOUTH.findall(output))
|
||||||
|
routes = len(ROUTE.findall(output))
|
||||||
|
checks_ok = all(re.search(pattern, output, re.M) for pattern in CHECKS)
|
||||||
|
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||||
|
and passages >= 1 and mouths == passages and routes >= 1 and checks_ok)
|
||||||
|
code = result.returncode if result is not None else "TIMEOUT"
|
||||||
|
rows.append((name, recipe, compiler, elapsed, code, passages, mouths, routes, ok))
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[L34PassageAuditRun] case={name} compiler={compiler} recipe={recipe} "
|
||||||
|
f"elapsed={elapsed:.1f}s exit={code} passages={passages} "
|
||||||
|
f"profiles={mouths} routes={routes} {'PASS' if ok else 'FAIL'} "
|
||||||
|
f"log={logfile}", flush=True)
|
||||||
|
|
||||||
|
summary = OUT / "passage-audits.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"case,recipe,compiler,elapsed_seconds,exit_code,passages,mouth_profiles,routes,status\n" +
|
||||||
|
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},{passages},{mouths},{routes},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for name, recipe, compiler, elapsed, code, passages, mouths, routes, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L34PassageAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 24 else 0)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rerun the upper-SW fixture's six L34 determinism cases and merge results."""
|
||||||
|
import csv
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l34-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l34-gcc",
|
||||||
|
}
|
||||||
|
LAYOUT_PATTERN = re.compile(
|
||||||
|
r"^\[ResolvedLayoutSnapshot\] schema=\d+ stable_identity=([^ ]+) strata=\d+ bytes=\d+ round_trip=PASS$",
|
||||||
|
re.M)
|
||||||
|
PLAN_PATTERN = re.compile(
|
||||||
|
r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*"
|
||||||
|
r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", re.M)
|
||||||
|
ROUTE_PATTERN = re.compile(
|
||||||
|
r"^\[WorldAssemblerRouteAccepted\] arrival_xy_m=\([^)]*\) "
|
||||||
|
r"departure_xy_m=\([^)]*\) endpoint_proximity_cost=\d+ average_grade_q16=\d+$", re.M)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
reference = None
|
||||||
|
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
name = f"upper-fixture-{compiler}-w{workers}"
|
||||||
|
logfile = OUT / f"determinism-{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s", str(binary), str(FIXTURE), f"--workers={workers}",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
"--recipe=floating-islands", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
layouts = LAYOUT_PATTERN.findall(output)
|
||||||
|
plans = PLAN_PATTERN.findall(output)
|
||||||
|
routes = ROUTE_PATTERN.findall(output)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
output, re.M)
|
||||||
|
passage = re.search(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", output, re.M)
|
||||||
|
signature = (tuple(layouts), tuple(plans), tuple(routes))
|
||||||
|
stable = reference is None or signature == reference
|
||||||
|
if reference is None and layouts and plans and routes:
|
||||||
|
reference = signature
|
||||||
|
ok = (result is not None and result.returncode == 0 and len(layouts) == 1
|
||||||
|
and len(plans) == 1 and len(routes) == 1
|
||||||
|
and stamps == [("floating-islands", "floating-islands", "PASS")]
|
||||||
|
and passage is not None and stable)
|
||||||
|
code = result.returncode if result is not None else "TIMEOUT"
|
||||||
|
rows.append(("upper-fixture", "floating-islands", compiler, workers,
|
||||||
|
elapsed, code, ok))
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[L34FixtureDeterminism] compiler={compiler} workers={workers} "
|
||||||
|
f"elapsed={elapsed:.1f}s recipe_stamp={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"layout={layouts[0] if layouts else 'MISSING'} "
|
||||||
|
f"stable_across_matrix={'yes' if stable else 'no'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
fixture_summary = OUT / "determinism-upper-fixture-l34.csv"
|
||||||
|
with fixture_summary.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.writer(stream)
|
||||||
|
writer.writerow(("kind", "recipe", "compiler", "workers", "elapsed_seconds",
|
||||||
|
"exit_code", "status"))
|
||||||
|
for row in rows:
|
||||||
|
writer.writerow((*row[:-1], f"{row[-1] and 'PASS' or 'FAIL'}"))
|
||||||
|
|
||||||
|
combined = OUT / "determinism-l34.csv"
|
||||||
|
with combined.open("r", newline="", encoding="utf-8") as stream:
|
||||||
|
owner_rows = list(csv.reader(stream))
|
||||||
|
if owner_rows and owner_rows[0] == ["kind", "recipe", "compiler", "workers",
|
||||||
|
"elapsed_seconds", "exit_code", "status"]:
|
||||||
|
owner_rows = [row for row in owner_rows[1:] if row and row[0] != "upper-fixture"]
|
||||||
|
else:
|
||||||
|
raise SystemExit("owner determinism summary is missing or malformed")
|
||||||
|
if len(owner_rows) != 24:
|
||||||
|
raise SystemExit(f"expected 24 owner rows before fixture merge, found {len(owner_rows)}")
|
||||||
|
with combined.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.writer(stream)
|
||||||
|
writer.writerow(("kind", "recipe", "compiler", "workers", "elapsed_seconds",
|
||||||
|
"exit_code", "status"))
|
||||||
|
writer.writerows(owner_rows)
|
||||||
|
for row in rows:
|
||||||
|
writer.writerow((*row[:-1], f"{row[-1] and 'PASS' or 'FAIL'}"))
|
||||||
|
|
||||||
|
print(f"[L34FixtureDeterminismSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} combined_runs=30 "
|
||||||
|
f"combined_summary={combined} fixture_summary={fixture_summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 6 else 0)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
64e5336f945724d882ddb22f7bad44ca5acdc7b0b142477991292116a2a140ab owner-fi-arrival.jpg
|
||||||
|
638af09fab4cdbe82e1733d783b7d1c88948bc4fe071a38d480d7357b2b96732 owner-fi-ground-mouth.jpg
|
||||||
|
90a2bc23ee5cf996145116a496bbd293c5b8ab00efd055f507320bd30a3a6869 owner-fi-mid-tunnel.jpg
|
||||||
|
70bf614b4a3e05edbfc5693aa16e1e643ff7794500481baace08ddde79490aae owner-fi-rock-entry.jpg
|
||||||
|
03ca53cdd8ff9801fa57254533ad46a96c306147a895e525be43f32f7ae076e4 owner-sw-arrival.jpg
|
||||||
|
70c187b4d2e3aa39aff99984b98240f8d25386590e3a7e5b51f569e2e17f0f86 owner-sw-ground-mouth.jpg
|
||||||
|
049f984f3a725295c991cb9a3c080ebac085758454bcd1fb007555ab6fb83cc8 owner-sw-mid-tunnel.jpg
|
||||||
|
6afe280d33b52ecaab6ddbb02c89beaa0dcf2d76cc64d6675e17fda79ad595c8 owner-sw-rock-entry.jpg
|
||||||
|
e6dd2da50efb9e10e1e9b12f4366f223669b6da0f05333bf352a20a504f5be50 upper-sw-fixture-arrival.jpg
|
||||||
|
420c9a78a0913dbb0e6fe4261c49d9001a91cc19825cd248cc97cf26ce4e74d5 upper-sw-fixture-ground-mouth.jpg
|
||||||
|
0d67a179773ace8f6e4ac2939ea4495a4232ffa10671d4f02172a18d0e128a34 upper-sw-fixture-mid-tunnel.jpg
|
||||||
|
fd92aee89be922c7817b2e794333681b397c473095a77e0bc3ec6d947a57c0b8 upper-sw-fixture-rock-entry.jpg
|
||||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 25 KiB |